diff --git a/core/jvm/src/main/scala/cats/effect/IOApp.scala b/core/jvm/src/main/scala/cats/effect/IOApp.scala index 6f1a0857af5..5b69c0e61b1 100644 --- a/core/jvm/src/main/scala/cats/effect/IOApp.scala +++ b/core/jvm/src/main/scala/cats/effect/IOApp.scala @@ -314,6 +314,29 @@ trait IOApp { protected def onCpuStarvationWarn(metrics: CpuStarvationWarningMetrics): IO[Unit] = CpuStarvationCheck.logWarning(metrics) + /** + * Defines what to do when IOApp detects that `main` is being invoked on a `Thread` which + * isn't the main process thread. This condition can happen when we are running inside of an + * `sbt run` with `fork := false` + */ + private def onNonMainThreadDetected(): Unit = { + val shouldPrint = + Option(System.getProperty("cats.effect.warnOnNonMainThreadDetected")) + .map(_.equalsIgnoreCase("true")) + .getOrElse(true) + if (shouldPrint) + System + .err + .println( + """|Warning: IOApp `main` is running on a thread other than the main thread. + |This may prevent correct resource cleanup after `main` completes. + |This condition could be caused by executing `sbt run` with `fork := false`. + |Set `Compile / run / fork := true` in this project to resolve this. + |""".stripMargin + ) + else () + } + /** * The entry point for your application. Will be called by the runtime when the process is * started. If the underlying runtime supports it, any arguments passed to the process will be @@ -333,6 +356,7 @@ trait IOApp { final def main(args: Array[String]): Unit = { // checked in openjdk 8-17; this attempts to detect when we're running under artificial environments, like sbt val isForked = Thread.currentThread().getId() == 1 + if (!isForked) onNonMainThreadDetected() val installed = if (runtime == null) { import unsafe.IORuntime diff --git a/docs/core/io-runtime-config.md b/docs/core/io-runtime-config.md index 0a48199b48b..39828ad4293 100644 --- a/docs/core/io-runtime-config.md +++ b/docs/core/io-runtime-config.md @@ -28,7 +28,8 @@ This can be done for example with the [EnvironmentPlugin for Webpack](https://we | `cats.effect.detectBlockedThreads`
N/A | `Boolean` (`false`) | Whether or not we should detect blocked threads. | | `cats.effect.logNonDaemonThreadsOnExit`
N/A | `Boolean` (`true`) | Whether or not we should check for non-daemon threads on JVM exit. | | `cats.effect.logNonDaemonThreads.sleepIntervalMillis`
N/A | `Long` (`10000L`) | Time to sleep between checking for presence of non-daemon threads. | -| `cats.effect.cancelation.check.threshold `
`CATS_EFFECT_CANCELATION_CHECK_THRESHOLD` | `Int` (`512`) | Configure how often cancellation is checked. By default, every 512 iterations of the run loop. | +| `cats.effect.warnOnNonMainThreadDetected`
N/A | `Boolean` (`true`) | Print a warning message when IOApp `main` runs on a non-main thread | +| `cats.effect.cancelation.check.threshold`
`CATS_EFFECT_CANCELATION_CHECK_THRESHOLD` | `Int` (`512`) | Configure how often cancellation is checked. By default, every 512 iterations of the run loop. | | `cats.effect.auto.yield.threshold.multiplier`
`CATS_EFFECT_AUTO_YIELD_THRESHOLD_MULTIPLIER` | `Int` (`2`) | `autoYieldThreshold = autoYieldThresholdMultiplier x cancelationCheckThreshold`. See [thread model](../thread-model.md). | | `cats.effect.tracing.exceptions.enhanced`
`CATS_EFFECT_TRACING_EXCEPTIONS_ENHANCED` | `Boolean` (`true`) | Augment the stack traces of caught exceptions to include frames from the asynchronous stack traces. See [tracing](../tracing.md). | | `cats.effect.tracing.buffer.size`
`CATS_EFFECT_TRACING_BUFFER_SIZE` | `Int` (`16`) | Number of stack frames retained in the tracing buffer. Will be rounded up to next power of two. |