Skip to content

Commit

Permalink
On jvm IOApp clean exit, optionally print non-daemon threads
Browse files Browse the repository at this point in the history
Debugging when a jvm is being held open due to non-daemon threads is a royal
pain. Try to help users here (who might not even know this can happen) by
giving the option to debug, which we can put into a FAQ entry

I opted for making it configurable here since this isn't a problem for most
people, and there's no reason to slow down the shutdown for it unless debugging
  • Loading branch information
Daenyth committed Nov 12, 2020
1 parent 0201f87 commit c8bfc27
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ private[effect] object IOAppPlatform {
// Return naturally from main. This allows any non-daemon
// threads to gracefully complete their work, and managed
// environments to execute their own shutdown hooks.
()
if (NonDaemonThreadLogger.isEnabled())
new NonDaemonThreadLogger().start()
else
()
} else {
sys.exit(code)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2017-2019 The Typelevel Cats-effect Project Developers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package cats.effect.internals

import cats.syntax.all._
import scala.jdk.CollectionConverters._

private[internals] object NonDaemonThreadLogger {

/** Whether or not we should check for non-daemon threads on jvm exit */
def isEnabled(): Boolean =
Option(System.getProperty("cats.effect.logNonDaemonThreadsOnExit")).map(_.toLowerCase()) match {
case Some(value) => value.equalsIgnoreCase("true")
case None => true // default to enabled
}

/** Time to sleep between checking for non-daemon threads present */
def sleepIntervalMillis: Long =
Option(System.getProperty("cats.effect.logNonDaemonThreads.sleepIntervalMillis"))
.flatMap(time => Either.catchOnly[NumberFormatException](time.toLong).toOption)
.getOrElse(1000L)
}

private[internals] class NonDaemonThreadLogger extends Thread("cats-effect-nondaemon-thread-logger") {

setDaemon(true)

override def run(): Unit = {
var done = false
while (!done) {
Thread.sleep(NonDaemonThreadLogger.sleepIntervalMillis)

val runningThreads = detectThreads()
if (runningThreads.isEmpty) {
// This is probably impossible, as we are supposed to be a *daemon* thread, so the jvm should exit by itself
done = true
} else {
printThreads(runningThreads)
}

}
}
private[this] def detectThreads(): List[String] = {
val threads = Thread.getAllStackTraces().keySet()
val daemonThreads = threads.asScala.filterNot(_.isDaemon).map(t => s" - ${t.getId}: ${t}")
daemonThreads.toList
}

private[this] def printThreads(threads: List[String]) = {
val msg = threads.mkString("Non-daemon threads currently preventing JVM termination:", "\n - ", "")
System.err.println(msg)
}
}

0 comments on commit c8bfc27

Please sign in to comment.