New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add file watching to bloop #125
Conversation
This removes `prependCompile` from the test command -- unlike before, where we were reusing the `Interpreter` to make sure we compiled the project before running tests, this time we need to compile and test in a every watch cycle, so the logic needs to be handled by the test implementation in `Interpreter`.
This makes it clear that this is the default logger, while `Logger` is just the interface.
Per build loggers allow bloop to log project specific logs into the console or a possible file backing without having interferences from other projects being compiled by bloop. This change is instrumental for more fundamental improvements to bloop in the near future.
`CommonOptions` is required in the state so that we can modify the logger every time the common options change. This can happen when users pass in command redirecting input and output, or when bloop is run by a nailgun server. This is also extremely important for testing. The reconfiguration of logging is not done in this PR, but will be done in a follow up.
Every time we change the common options in `State`, via an action, the loggers have to update their backings so that logs are redirected to those streams. This is especially important because of Nailgun. Until this PR, we were changing the common options in the `Cli` entrypoint but we were not updating the loggers with those options which meant that we were subject to losing logs. I think this fixes the problem that @Duhemm has been experiencing several times with Nailgun, but on top of that it allows users to redirect output streams whenever they want (though it's a feature that should not be used very frequently). As a plus, we get a nice way to test bloop: we can always redirect output streams at the test call site and see what the complete output of it is.
Fixes #7, by the way. |
Now that we can programmatically get the output of bloop, our test for file watching the compilation of a project works. This PR also removes scalog as a library dependency.
32f8759
to
5da69d8
Compare
() | ||
} | ||
|
||
type LoggerUpdateKey = (BloopLogger, OutputStream) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's not used anywhere?
logger.debug(s"Update of out ($out) in logger ${logger.name} is cached.") | ||
case _ => swapAndCache | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if I understood everything completely here. You're keeping track of what output stream each logger uses, right? And before changing the output stream, you're checking whether the new output stream is different from the current output stream? I've been a bit confused by the name updateCache
. That's just a suggestion, but I'd have called that currentOutputStreams
or loggerToStream
maybe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Amazing work cleaning up this L4J madness!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's just a suggestion, but I'd have called that currentOutputStreams or loggerToStream maybe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestion!
@@ -41,7 +40,8 @@ class HotBloopBenchmark { | |||
val configDir = Files.createDirectory(tempDir.resolve(".bloop-config")) | |||
|
|||
val path = Paths.get(s"../frontend/src/test/resources/projects") | |||
val state = ProjectHelpers.loadTestProject(path, project, Logger.get) | |||
val logger = BloopLogger(path.toRealPath().toAbsolutePath().toString) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that this logger is not used anywhere
scalacstyle: Boolean = false, | ||
@ExtraName("w") | ||
@HelpMessage("If set, run a command whenever projects' source files change.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
run this command
maybe?
def logAndTime[T](cliOptions: CliOptions, action: => T): T = | ||
logger.verboseIf(cliOptions.verbose) { timed(state.logger) { action } } | ||
logger.verboseIf(cliOptions.verbose) { timed(state0.logger) { action } } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
timed(logger)
, no?
logger.verboseIf(cliOptions.verbose) { timed(state0.logger) { action } } | ||
def updateState(state: State, commonOptions: CommonOptions): State = { | ||
State.updateLogger(state.logger, commonOptions) | ||
val logger = BloopLogger(state.logger.name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need to create a new logger? If I use state.logger
again, it won't have the right output stream?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need a new logger because log4j forces us to call the logger again to get the changes we just made, that involves calling LogManager.getLogger (which is done in the constructor of BloopLogger).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should think of the logger as immutable, and that's why it's important to import it always from the current state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do it because we need to call LogManager.getLogger
every time we modify it (because apparentely changes are not propagated by log4j).
action match { | ||
case Exit(exitStatus) if state.status.isOk => state.copy(status = exitStatus) | ||
case Exit(exitStatus) => state | ||
case Exit(exitStatus) if state0.status.isOk => state0.copy(status = exitStatus) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not use mergeStatus
?
val jars = Paths.getCacheDirectory("scala-jars") | ||
singleCompilerCache = new CompilerCache(provider, jars, logger) | ||
singleCompilerCache | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So in the end you may still get a CompilerCache
that has a different Logger
than the one that's passed to getCompilerCache
?
The compilerCache
needs a Logger
only if we're asking for a compiler for which we don't have a compiler bridge yet (IIRC). Maybe that we could do something here to avoid this weird situation with the compiler cache and the logger, but I'm not sure what.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is a tricky part, the compiler cache is not owned by a build in particular, but we still need a logger. As I wanted the first build to trigger the compilation to register the logs of the compiler cache, I did it this way. I agree it's not ideal, but the other solution is to create a global bloop logger that we pass around.
However, this will complicate things for not much in return and has some design issues. For example, for every compile command that the server receive we need to swap the output streams. This can cause race conditions and misbehaviours.
import io.methvin.watcher.DirectoryChangeEvent.EventType | ||
import io.methvin.watcher.{DirectoryChangeEvent, DirectoryWatcher} | ||
|
||
final class SourceWatcher(paths0: Seq[Path], logger: Logger) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do all the paths have to be directories? Maybe rename to watchedDirectories
, if that's the case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I assume so. I mimicked the name that DirectoryWatcher
uses, but it looks like it only accepts directories.
val cliOptions = defaultCli.copy(common = defaultCli.common.copy(out = newOut)) | ||
val action = Run(Commands.Projects(dotGraph = true, cliOptions), Exit(ExitStatus.Ok)) | ||
defaultCli.copy(common = state.commonOptions.copy(out = newOut)) -> inMemory | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
There could be more deduplication, but there's a good balance.
class FileWatchingSpec { | ||
@scala.annotation.tailrec | ||
final def readCompilingLines(target: Int, msg: String, out: ByteArrayOutputStream): Int = { | ||
Thread.sleep(100) // Wait 10ms for the OS's file system |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
100ms?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, to avoid reading all the time until it appears (since it happens concurrently).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I keep being unclear. The code says 100ms, the comment says 10.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll make an effort 💃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hahaha, makes sense, removing it 😄
|
||
class FileWatchingSpec { | ||
@scala.annotation.tailrec | ||
final def readCompilingLines(target: Int, msg: String, out: ByteArrayOutputStream): Int = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rename readCompilingLines
to outputContains
, and target
to times
or expected
. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds goooooooood.
This is more in-line with the result of doing `reset`, plus it fixes an issue with compilation tasks complaining about the results cache being un-initialized.
0bd7e38
to
9152eeb
Compare
Latest nailgun has a fix wrt reading that may fix #92. Let's try to see if it does.
case Exit(exitStatus) if state.status.isOk => state.copy(status = exitStatus) | ||
case Exit(exitStatus) => state | ||
case Exit(exitStatus) if state0.status.isOk => state0.mergeStatus(exitStatus) | ||
case Exit(exitStatus) => state0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't we just have case Exit(exitStatus) => state0.mergeStatus(exitStatus)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, because the implementation of mergeStatus is very naive at this point and has to improve. When there are two errors, it's best to at least report the first one and not a weird mix for two. I plan to improve this in a future PR
This is often used to stop file watching. Before, we were not making sure that we persisted all the states. Now we do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two small comments and then let's !
implicit val context = state.executionContext | ||
val testFuture = scala.concurrent.Future { testAction(ctx, workerThread) } | ||
try Await.ready(testFuture, duration.Duration(15, duration.SECONDS)) | ||
catch { case t: Throwable => println(s"LOGS WERE ${bloopOut.toString("UTF-8")}"); throw t } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the println
a leftover from debugging?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it's something that should help us debug in case these tests fail 😄.
|
||
def update(logger: BloopLogger, out: OutputStream): Unit = { | ||
def swapAndCache: Unit = { | ||
logger.debug(s"Logger ${logger.name} has not been updated to use $out.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The message reads a bit like a failure, rather than an announcement of what bloop is about to do. Maybe say logger has not been updated yet
or updating logger to use
?
Done, check the last commit 😄. |
…into topic/file-watching
Merge upstream changes
For information on why it's done the way it is, commit messages are rich in details.
This PR adds file watching to bloop and also happens to do a much needed rework of the logger infrastructure to make it more modular, solid and flexible to our needs.
It is still a WIP since I'd like to add one or two more file watching tests, but the one it is right now proves that it works (it's kind of cool we can programmatically test this without emulating a file system of our own).