From c8bfc27a096bf6e7a7c4acdfcb8da2c1a01651f7 Mon Sep 17 00:00:00 2001 From: Gavin Bisesi Date: Mon, 2 Nov 2020 12:24:15 -0500 Subject: [PATCH] On jvm IOApp clean exit, optionally print non-daemon threads 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 --- .../cats/effect/internals/IOAppPlatform.scala | 5 +- .../internals/NonDaemonThreadLogger.scala | 67 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 core/jvm/src/main/scala/cats/effect/internals/NonDaemonThreadLogger.scala diff --git a/core/jvm/src/main/scala/cats/effect/internals/IOAppPlatform.scala b/core/jvm/src/main/scala/cats/effect/internals/IOAppPlatform.scala index ed432b9ea1..4f072c019e 100644 --- a/core/jvm/src/main/scala/cats/effect/internals/IOAppPlatform.scala +++ b/core/jvm/src/main/scala/cats/effect/internals/IOAppPlatform.scala @@ -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) } diff --git a/core/jvm/src/main/scala/cats/effect/internals/NonDaemonThreadLogger.scala b/core/jvm/src/main/scala/cats/effect/internals/NonDaemonThreadLogger.scala new file mode 100644 index 0000000000..4f09f60e2b --- /dev/null +++ b/core/jvm/src/main/scala/cats/effect/internals/NonDaemonThreadLogger.scala @@ -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) + } +}