Skip to content
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

Merged
merged 26 commits into from Dec 12, 2017
Merged

Add file watching to bloop #125

merged 26 commits into from Dec 12, 2017

Conversation

jvican
Copy link
Contributor

@jvican jvican commented Dec 9, 2017

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).

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.
@jvican jvican added the feature label Dec 9, 2017
@jvican
Copy link
Contributor Author

jvican commented Dec 9, 2017

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.
()
}

type LoggerUpdateKey = (BloopLogger, OutputStream)
Copy link
Collaborator

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
}
}
Copy link
Collaborator

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.

Copy link
Collaborator

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!

Copy link
Contributor Author

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.

Copy link
Contributor Author

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)
Copy link
Collaborator

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.")
Copy link
Collaborator

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 } }
Copy link
Collaborator

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)
Copy link
Collaborator

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?

Copy link
Contributor Author

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).

Copy link
Contributor Author

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.

Copy link
Contributor Author

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)
Copy link
Collaborator

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
}
Copy link
Collaborator

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.

Copy link
Contributor Author

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) {
Copy link
Collaborator

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.

Copy link
Contributor Author

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
}
Copy link
Collaborator

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

100ms?

Copy link
Contributor Author

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).

Copy link
Collaborator

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.

Copy link
Collaborator

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 💃

Copy link
Contributor Author

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 = {
Copy link
Collaborator

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds goooooooood.

jvican and others added 3 commits December 11, 2017 17:03
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.
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
Copy link
Collaborator

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) ?

Copy link
Contributor Author

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

@jvican jvican added this to the Bloop 0.1 milestone Dec 12, 2017
Copy link
Collaborator

@Duhemm Duhemm left a 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 :shipit: !

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 }
Copy link
Collaborator

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?

Copy link
Contributor Author

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.")
Copy link
Collaborator

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?

@jvican jvican modified the milestone: Bloop 0.1 Dec 12, 2017
@jvican
Copy link
Contributor Author

jvican commented Dec 12, 2017

Done, check the last commit 😄.

@Duhemm Duhemm merged commit eeca151 into master Dec 12, 2017
@Duhemm Duhemm deleted the topic/file-watching branch December 29, 2017 10:39
tgodzik pushed a commit to tgodzik/bloop that referenced this pull request Jul 5, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants