-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Correctly shutdown current active LoggerConfigurator on app (re)load in DEV mode #10939
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
base: main
Are you sure you want to change the base?
Correctly shutdown current active LoggerConfigurator on app (re)load in DEV mode #10939
Conversation
Just before an app gets build the first time
Just before the app that will be build configures its own
| * Initialize the Logger when there's no application ClassLoader available. | ||
| */ | ||
| def init(rootPath: java.io.File, mode: Mode): Unit = { | ||
| initializedWithoutApp = true |
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.
As written in the javadocs, the init method gets called when there is no app - like that is the case in DevServer before an app gets loaded. Therefore that init method does not call the setApplicationMode method.
| if (!initializedWithoutApp) { | ||
| // Unset the global application mode for logging | ||
| play.api.Logger.unsetApplicationMode() | ||
| } |
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.
This if check is needed, because when init was called, it means there was no app and therefore no app mode was set, therefore we now also don't want to unset an app mode (otherwise the _appsRunning counter would become negative)
|
@Mergifyio backport 2.8.x |
☑️ Nothing to doDetails
|
| // to shut down that old app's LoggerConfigurator, just before the app that will be build configures its own | ||
| lastState.foreach(app => LoggerConfigurator(app.classloader).foreach(lc => lc.shutdown())) | ||
| // FYI: initialLoggerConfigurator and lastState will never both be set at the same time, so in the lines above at most only one shutdown() gets called | ||
|
|
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 put this code just before loader.load(...) builds the new application.
|
One more thing: Would it also make sense to shut down the LoggerConfigurator in the various test helpers Plays provides? E.g. playframework/testkit/play-test/src/main/java/play/test/WithApplication.java Lines 52 to 58 in 44886b0
|
marcospereira
left a comment
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.
Some comments. :-)
| // _before_ an app gets build. In that case the init(...) method below gets called. We track that call with this boolean flag | ||
| // to avoid unsetting the app mode when the LoggerConfigurator gets shut down. | ||
| // Also see play.core.server.DevServerStart (where loggerConfigurator.init(...) gets called) | ||
| private var initializedWithoutApp = false |
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.
Hum, it looks like to me that since this is a special case for dev mode, this state should not be internal to the LoggerConfigurator, but instead, it should be handled there, in DevServerStart. A good reason not to track state here is that it is possible to have a LoggerConfigurator that is not LogbackLoggerConfigurator.
|
|
||
| // Before building a new app _the first time_, we make sure to shutdown the (Logback)LoggerConfigurator created initally by the dev server above, | ||
| // because the app that will be build configures its own LoggerConfigurator later (but then by using the app's classloader) | ||
| initialLoggerConfigurator.foreach(_.shutdown()) | ||
| initialLoggerConfigurator = None | ||
| // However, if we build an app _not the first time_, but there was an app running before (= lastState is success), we make sure | ||
| // to shut down that old app's LoggerConfigurator, just before the app that will be build configures its own | ||
| lastState.foreach(app => LoggerConfigurator(app.classloader).foreach(lc => lc.shutdown())) | ||
| // FYI: initialLoggerConfigurator and lastState will never both be set at the same time, so in the lines above at most only one shutdown() gets called |
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 wonder if it would be better to make this all happen using newApplication's CoordinatedShutdown.
This makes sense to me. |
|
It's a Logback best practice, even:
|
|
Hello @mkurz, I see a lot of work done here and this might be probably stopped due to other priority things. I am thinking to work on this PR. Could you please let me know if it is ok to work on this? |
|
@janani-reddy9 sorry I must have missed your message totally. If you are still interested feel free to work in this issue. |
|
When looking into this issue here one should also check: Maybe those two issues got resolved along the way, but not sure yet. |
(IMHO this PR fixes a potential memory leak in dev mode)
Before I come to the problem this PR fixes, let's first have a look at how, in a default Play app, the
LoggerConfiguratorgets set up:When an app gets build via Guice, at some point the
injectorgets created. For this theapplicationModule()is needed:playframework/core/play-guice/src/main/scala/play/api/inject/guice/GuiceInjectorBuilder.scala
Lines 193 to 200 in 44886b0
The
applicationModule()method now configures theLoggerFactory:playframework/core/play-guice/src/main/scala/play/api/inject/guice/GuiceApplicationBuilder.scala
Lines 108 to 112 in 44886b0
And this is done by "configuring" the
LoggerConfigurator(lc.configure(...)):playframework/core/play-guice/src/main/scala/play/api/inject/guice/GuiceApplicationBuilder.scala
Lines 134 to 140 in 44886b0
Even when not using Guice, we tell people they should call
lc.configure(...)themselves. See here, here, here and here.OK, so now we know that each time we create a new app, a
LoggerConfiguratorgets set up by callinglc.configure(...).Now, let's have a look at PROD mode:
In prod mode, a single app gets created and a server (netty or akka-http) gets started afterwards. Now when shutting down the "application" (usually the os process), it means the http server and the app get shutdown together.
Now, when that shutdown happens, basically the last thing that happens is that also the
LoggerConfigurator(that was configured during bootstrapping the app) gets shut down by the http server (at the point this happens in the belowstopmethod the app was shut down already):playframework/transport/server/play-server/src/main/scala/play/core/server/Server.scala
Lines 48 to 52 in 44886b0
So great, the app and http-server get started, a
LoggerConfiguratorgets configured during bootstrapping the app, and when the whole thing gets shut down, theLoggerConfiguratorwill be shut down as well. On re-start, the same happens again. So no problem here.But now, let's have a look at DEV mode:
Dev mode is different. In dev mode, unlike prod, an http server get's started first, and afterwards an app gets build, and usually (if you change a file), that app gets shut down, a new app will be build again, and so forth. Only when you quit dev mode, when you hit the
returnkey on your keyboard, the current running app gets shut down and finally also the http server will be shut down as well.The problems now are:
LoggerConfiguratornever gets shutdown. Even worse, every time a new app gets created, of course, a newLoggerConfiguratorgets configured... Only at the very end, when you hit return and the http-server gets shutdown, theLoggerConfiguratorof the last app finally gets shut down by the http-server'sstopmethod (like described above in prod mode). AnyLoggerConfigurators set up before however never cleaned up their resources...LoggerConfiguratorbefore the first app gets build. And again, also that specialLoggerConfiguratornever gets shutdown... When the first app gets started, that first app then just goes ahead and configures its ownLoggerConfigurator... The specialLoggerConfiguratorset up is done here:playframework/transport/server/play-server/src/main/scala/play/core/server/DevServerStart.scala
Lines 150 to 159 in 44886b0
So you see, in dev mode the current active
LoggerConfiguratornever cleanly gets shutdown, which could lead to memory leaks or other problems with the logging infrastructure.And there is even one more unfortunate side effect because of this behaviour:
The
LogbackLoggerConfigurator(which is the default) sets the application mode when it gets configured:playframework/core/play-logback/src/main/scala/play/api/libs/logback/LogbackLoggerConfigurator.scala
Lines 71 to 72 in 44886b0
and unsets it again on shut down:
playframework/core/play-logback/src/main/scala/play/api/libs/logback/LogbackLoggerConfigurator.scala
Lines 144 to 145 in 44886b0
Now look what those methods do:
playframework/core/play/src/main/scala/play/api/Logger.scala
Lines 275 to 303 in 44886b0
If in dev mode, when many
LoggerConfigurators will be set up (but never shut down) because of reload cycles, the_appsRunningcounter in this code increases more and more, but never decreases anymore. Actually, in normal dev mode there is always just one running app at a time, even though different apps run one after another, but here the_appsRunningcounter just increases. That also means, that when the http-server gets shut down and finally theunsetApplicationModemethod gets called once, the counter is way to high, and_modewill never be set toNone, although there is no app running anymore. Also in between reload cycles, when briefly no app is running,_modewill not be set toNone.With this PR all of the above problems are fixed. I did a lot of testing by e.g. adding breakpoints to these
[unset]setApplicationModemethods and also to theshutdown,initandconfiguremethods of theLogbackLoggerConfiguratorand can confirm that now there is always just one activeLoggerConfiguratorthat cleanly gets shut down before the nextLoggerConfiguratorgets configured. Also the app counter increases to 1 and gets set back to 0 when an app shuts down and then again 1 -> 0 -> 1 -> 0 ,etc. which means also_modegets set toNonecorrectly when no app is running.