-
-
Notifications
You must be signed in to change notification settings - Fork 63
ADFA-5659: shut the Gradle daemon watcher down with the server #1816
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: stage
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -73,7 +73,10 @@ import kotlin.concurrent.withLock | |
| * | ||
| * @author Akash Yadav | ||
| */ | ||
| internal class ToolingApiServerImpl : IToolingApiServer { | ||
| internal class ToolingApiServerImpl( | ||
| private val newDaemonWatcher: (onStarted: (Int) -> Unit, onExited: (Int) -> Unit) -> GradleDaemonWatcher = | ||
| { onStarted, onExited -> GradleDaemonWatcher(onStarted = onStarted, onExited = onExited) }, | ||
| ) : IToolingApiServer { | ||
| private var client: IToolingApiClient? = null | ||
| private var connector: GradleConnector? = null | ||
| private var connection: ProjectConnection? = null | ||
|
|
@@ -361,12 +364,15 @@ internal class ToolingApiServerImpl : IToolingApiServer { | |
| * Finds the Gradle daemon and reports it to the client, so the memory chart can plot the process | ||
| * that actually holds the build's heap (ADFA-5514). | ||
| */ | ||
| private val daemonWatcher by lazy { | ||
| GradleDaemonWatcher( | ||
| onStarted = { pid -> client?.onGradleDaemonStarted(pid) }, | ||
| onExited = { pid -> client?.onGradleDaemonExited(pid) }, | ||
| ) | ||
| } | ||
| private val lazyDaemonWatcher = | ||
| lazy { | ||
| newDaemonWatcher( | ||
| { pid -> client?.onGradleDaemonStarted(pid) }, | ||
| { pid -> client?.onGradleDaemonExited(pid) }, | ||
| ) | ||
| } | ||
|
|
||
| private val daemonWatcher by lazyDaemonWatcher | ||
|
|
||
| private fun notifyBuildFailure(result: BuildResult) { | ||
| client?.onBuildFailed(result) | ||
|
|
@@ -407,6 +413,31 @@ internal class ToolingApiServerImpl : IToolingApiServer { | |
| buildCancellationToken?.cancel() | ||
| buildCancellationToken = null | ||
|
|
||
| // Early, and deliberately not "late enough to report the daemon's exit". | ||
| // | ||
| // The leaked thread is the defect: unstopped, an in-flight poll chain goes on scanning | ||
| // ProcessHandle.descendants() for up to a minute after the server is gone. Stopping it | ||
| // here ends that at once, and means no later poll can report a daemon into an RPC | ||
| // channel that is being torn down. | ||
| // | ||
| // Delivering the shutdown-time exit was tried and does not work. That report arrives | ||
| // through handle.onExit().thenRun { scheduler.execute { ... } }, and onExit completes | ||
| // on a process-reaper thread only once the OS has reaped the daemon -- strictly after | ||
| // DefaultGradleConnector.close() returns. There is no point in this sequence where the | ||
| // scheduler is still accepting work *and* the daemon has already been reaped, so the | ||
| // report is not deliverable at shutdown whatever the ordering; keeping `client` alive | ||
| // for it only widens the window in which a half-torn-down channel can be written to. | ||
| // The client learns the daemon is gone when it reconnects, not from here. | ||
| // | ||
| // Through the lazy delegate rather than the property: touching the property would | ||
| // construct a watcher, and its scheduler, only to shut it down again on a server that | ||
| // never ran a build. | ||
| if (lazyDaemonWatcher.isInitialized()) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Serialize watcher creation with watcher shutdown.
Use one lifecycle lock or a shutdown state that closes any watcher created after shutdown starts. Add a deterministic concurrent shutdown/build test. 🤖 Prompt for AI Agents |
||
| log.info("Stopping the Gradle daemon watcher...") | ||
| runCatching { daemonWatcher.shutdown() } | ||
| .onFailure { log.warn("Could not stop the Gradle daemon watcher", it) } | ||
| } | ||
|
|
||
| val connection = this.connection | ||
| val connector = this.connector | ||
| this.connection = null | ||
|
|
||
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.
@davidschachterADFA — medium: re-setting the interrupt flag here aborts the rest of the server shutdown.
getOrElsere-sets the flag on the caller's thread and then returns normally intoToolingApiServerImpl.shutdown(), which continues toconnectionCloseFuture.get(). If that future hasn't completed yet,CompletableFuture.get()sees the flag and throwsInterruptedExceptionimmediately — soDefaultGradleConnector.close()is never waited on, the shutdown future completes exceptionally, andMain'sfinallyrunsexitProcess(0)with daemons possibly not stopped. Before this PRshutdown()contained no interruptible wait, so this failure mode is new.Two things compound it:
runCatchingcatchesThrowable, so any failure out ofawaitTerminationsets the flag, not justInterruptedException.ForkJoinPool.commonPoolworker, which then carries a stale interrupt into unrelated tasks.Suggest restricting the re-interrupt to
InterruptedException, and either swallowing the flag here or having the caller clear it before the remaining teardown.