From a7a540d22811a1ddc489a6a0c0e16f07f0b5978d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Fri, 27 Mar 2026 11:10:58 +0100 Subject: [PATCH 1/3] JDK 17+ compatibility: JCTools 4.x, deprecation suppressions, CI matrix expansion - Upgrade JCTools from 3.3.0 to 4.0.5 - Add Java 17, 21, 25 to CI build matrix - Add @nowarn("msg=deprecated") for Thread.getId (deprecated since JDK 19) in Platform, DynamicWorkerThreadFactory, StandardWorkerThreadFactory, ThreadFactoryBuilder - Add @nowarn("msg=deprecated") for sun.misc.Unsafe usage in FromMessagePassingQueue and FromCircularQueue - Add -Wconf:cat=unused-nowarn:s to suppress unused @nowarn on JDK 11 --- .github/workflows/build.yml | 2 +- build.sbt | 12 +++++++----- .../scala/monix/execution/internal/Platform.scala | 3 +++ .../collection/queues/FromCircularQueue.scala | 3 +++ .../collection/queues/FromMessagePassingQueue.scala | 3 +++ .../forkJoin/DynamicWorkerThreadFactory.scala | 7 ++++--- .../forkJoin/StandardWorkerThreadFactory.scala | 7 +++++-- .../execution/schedulers/ThreadFactoryBuilder.scala | 6 +++++- 8 files changed, 31 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 97c471e1f..466df9346 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - java: [ 11 ] + java: [ 11, 17, 21, 25 ] # WARN: build.sbt depends on this key path, as scalaVersion and # crossScalaVersions is determined from it scala: [ 2.13.18, 3.3.7 ] diff --git a/build.sbt b/build.sbt index 85631e914..6e2e02ed5 100644 --- a/build.sbt +++ b/build.sbt @@ -1,9 +1,9 @@ +import MonixBuildUtils.* +import org.typelevel.scalacoptions.ScalacOptions import sbt.Keys.version -import sbt.{ Def, Global, Tags } +import sbt.{Def, Global, Tags} import scala.collection.immutable.SortedSet -import MonixBuildUtils._ -import org.typelevel.scalacoptions.ScalacOptions val benchmarkProjects = List( "benchmarksPrev", @@ -27,7 +27,7 @@ addCommandAlias("ci-release", ";+publishSigned ;sonatypeBundleRelease") val cats_Version = "2.7.0" val catsEffect_Version = "2.5.5" val fs2_Version = "2.5.11" -val jcTools_Version = "3.3.0" +val jcTools_Version = "4.0.5" val reactiveStreams_Version = "1.0.4" val macrotaskExecutor_Version = "1.0.0" val minitest_Version = "2.9.6" @@ -191,7 +191,7 @@ lazy val sharedSettings = pgpSettings ++ Seq( "-Ywarn-unused:params", "-Wunused:params", "-Xlint:infer-any", - "-Wnonunit-statement" + "-Wnonunit-statement", ), // Disabled from tpolecat for test compilation: // -Wunused:patvars triggers on for-comprehension loop vars in tests (pre-existing pattern) @@ -205,6 +205,8 @@ lazy val sharedSettings = pgpSettings ++ Seq( }, // Turning off fatal warnings for doc generation Compile / doc / tpolecatExcludeOptions ++= ScalacOptions.defaultConsoleExclude, + // Silence "unused @nowarn" —Thread#getId on JDK11 + Compile / scalacOptions ++= Seq("-Wconf:cat=unused-nowarn:s"), // Silence everything in auto-generated files scalacOptions ++= { if (isDotty.value) diff --git a/monix-execution/jvm/src/main/scala/monix/execution/internal/Platform.scala b/monix-execution/jvm/src/main/scala/monix/execution/internal/Platform.scala index a008f28cb..4c957888e 100644 --- a/monix-execution/jvm/src/main/scala/monix/execution/internal/Platform.scala +++ b/monix-execution/jvm/src/main/scala/monix/execution/internal/Platform.scala @@ -18,6 +18,8 @@ package monix.execution.internal import monix.execution.schedulers.CanBlock + +import scala.annotation.nowarn import scala.concurrent.{Await, Awaitable} import scala.concurrent.duration.Duration import scala.util.Try @@ -175,6 +177,7 @@ private[monix] object Platform { * To be used for multi-threading optimizations. Note that * in JavaScript this always returns the same value. */ + @nowarn("msg=deprecated") def currentThreadId(): Long = { Thread.currentThread().getId } diff --git a/monix-execution/jvm/src/main/scala/monix/execution/internal/collection/queues/FromCircularQueue.scala b/monix-execution/jvm/src/main/scala/monix/execution/internal/collection/queues/FromCircularQueue.scala index 6a51f952e..c6d97d201 100644 --- a/monix-execution/jvm/src/main/scala/monix/execution/internal/collection/queues/FromCircularQueue.scala +++ b/monix-execution/jvm/src/main/scala/monix/execution/internal/collection/queues/FromCircularQueue.scala @@ -23,6 +23,8 @@ import monix.execution.internal.atomic.UnsafeAccess import monix.execution.internal.collection.LowLevelConcurrentQueue import monix.execution.internal.jctools.queues.MessagePassingQueue import sun.misc.Unsafe + +import scala.annotation.nowarn import scala.collection.mutable private[internal] abstract class FromCircularQueue[A](queue: MessagePassingQueue[A]) @@ -50,6 +52,7 @@ private[internal] abstract class FromCircularQueue[A](queue: MessagePassingQueue } } +@nowarn("msg=deprecated") private[internal] object FromCircularQueue { /** * Builds a [[FromCircularQueue]] instance. diff --git a/monix-execution/jvm/src/main/scala/monix/execution/internal/collection/queues/FromMessagePassingQueue.scala b/monix-execution/jvm/src/main/scala/monix/execution/internal/collection/queues/FromMessagePassingQueue.scala index c0fd9a1ff..847272e75 100644 --- a/monix-execution/jvm/src/main/scala/monix/execution/internal/collection/queues/FromMessagePassingQueue.scala +++ b/monix-execution/jvm/src/main/scala/monix/execution/internal/collection/queues/FromMessagePassingQueue.scala @@ -23,6 +23,8 @@ import monix.execution.internal.atomic.UnsafeAccess import monix.execution.internal.collection.LowLevelConcurrentQueue import monix.execution.internal.jctools.queues.MessagePassingQueue import sun.misc.Unsafe + +import scala.annotation.nowarn import scala.collection.mutable private[internal] abstract class FromMessagePassingQueue[A](queue: MessagePassingQueue[A]) @@ -47,6 +49,7 @@ private[internal] abstract class FromMessagePassingQueue[A](queue: MessagePassin } } +@nowarn("msg=deprecated") private[internal] object FromMessagePassingQueue { /** * Builds a [[FromMessagePassingQueue]] instance. diff --git a/monix-execution/jvm/src/main/scala/monix/execution/internal/forkJoin/DynamicWorkerThreadFactory.scala b/monix-execution/jvm/src/main/scala/monix/execution/internal/forkJoin/DynamicWorkerThreadFactory.scala index 584183fce..8e297e2c6 100644 --- a/monix-execution/jvm/src/main/scala/monix/execution/internal/forkJoin/DynamicWorkerThreadFactory.scala +++ b/monix-execution/jvm/src/main/scala/monix/execution/internal/forkJoin/DynamicWorkerThreadFactory.scala @@ -19,20 +19,21 @@ package monix.execution.internal.forkJoin import java.util.concurrent.ForkJoinPool.{ForkJoinWorkerThreadFactory, ManagedBlocker} import java.util.concurrent.{ForkJoinPool, ForkJoinWorkerThread, ThreadFactory} - import monix.execution.internal.forkJoin.DynamicWorkerThreadFactory.EmptyBlockContext +import scala.annotation.nowarn import scala.concurrent.{BlockContext, CanAwait} // Implement BlockContext on FJP threads private[monix] final class DynamicWorkerThreadFactory( prefix: String, uncaught: Thread.UncaughtExceptionHandler, - daemonic: Boolean) - extends ThreadFactory with ForkJoinWorkerThreadFactory { + daemonic: Boolean +) extends ThreadFactory with ForkJoinWorkerThreadFactory { require(prefix ne null, "DefaultWorkerThreadFactory.prefix must be non null") + @nowarn("msg=deprecated") def wire[T <: Thread](thread: T): T = { thread.setDaemon(daemonic) thread.setUncaughtExceptionHandler(uncaught) diff --git a/monix-execution/jvm/src/main/scala/monix/execution/internal/forkJoin/StandardWorkerThreadFactory.scala b/monix-execution/jvm/src/main/scala/monix/execution/internal/forkJoin/StandardWorkerThreadFactory.scala index 7d6d885e2..2edcfc0f5 100644 --- a/monix-execution/jvm/src/main/scala/monix/execution/internal/forkJoin/StandardWorkerThreadFactory.scala +++ b/monix-execution/jvm/src/main/scala/monix/execution/internal/forkJoin/StandardWorkerThreadFactory.scala @@ -20,12 +20,15 @@ package monix.execution.internal.forkJoin import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory import java.util.concurrent.{ForkJoinPool, ForkJoinWorkerThread, ThreadFactory} +import scala.annotation.nowarn + private[monix] final class StandardWorkerThreadFactory( prefix: String, uncaught: Thread.UncaughtExceptionHandler, - daemonic: Boolean) - extends ThreadFactory with ForkJoinWorkerThreadFactory { + daemonic: Boolean +) extends ThreadFactory with ForkJoinWorkerThreadFactory { + @nowarn("msg=deprecated") def wire[T <: Thread](thread: T): T = { thread.setDaemon(daemonic) thread.setUncaughtExceptionHandler(uncaught) diff --git a/monix-execution/jvm/src/main/scala/monix/execution/schedulers/ThreadFactoryBuilder.scala b/monix-execution/jvm/src/main/scala/monix/execution/schedulers/ThreadFactoryBuilder.scala index d22978783..4a0b2b4f2 100644 --- a/monix-execution/jvm/src/main/scala/monix/execution/schedulers/ThreadFactoryBuilder.scala +++ b/monix-execution/jvm/src/main/scala/monix/execution/schedulers/ThreadFactoryBuilder.scala @@ -17,9 +17,11 @@ package monix.execution.schedulers -import java.util.concurrent.ThreadFactory import monix.execution.UncaughtExceptionReporter +import java.util.concurrent.ThreadFactory +import scala.annotation.nowarn + private[schedulers] object ThreadFactoryBuilder { /** Constructs a ThreadFactory using the provided name prefix and appending * with a unique incrementing thread identifier. @@ -28,8 +30,10 @@ private[schedulers] object ThreadFactoryBuilder { * @param daemonic specifies whether the created threads should be daemonic * (non-daemonic threads are blocking the JVM process on exit). */ + def apply(name: String, reporter: UncaughtExceptionReporter, daemonic: Boolean): ThreadFactory = { new ThreadFactory { + @nowarn("msg=deprecated") def newThread(r: Runnable) = { val thread = new Thread(r) thread.setName(name + "-" + thread.getId) From 61732f36e5e3051249cef2813ad51481d97a2808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Fri, 27 Mar 2026 14:44:22 +0100 Subject: [PATCH 2/3] fix: handle ForkJoinPool scheduling behavior in ExecutorScheduler Ensure reliable scheduling by using Monix's ScheduledExecutorService for ForkJoinPool, addressing differences in exception handling and task scheduling. --- .../monix/execution/schedulers/ExecutorScheduler.scala | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/monix-execution/jvm/src/main/scala/monix/execution/schedulers/ExecutorScheduler.scala b/monix-execution/jvm/src/main/scala/monix/execution/schedulers/ExecutorScheduler.scala index 51ea65143..df72ccaa3 100644 --- a/monix-execution/jvm/src/main/scala/monix/execution/schedulers/ExecutorScheduler.scala +++ b/monix-execution/jvm/src/main/scala/monix/execution/schedulers/ExecutorScheduler.scala @@ -100,6 +100,13 @@ object ExecutorScheduler { // Implementations will inherit BatchingScheduler, so this is guaranteed val ft = features + Scheduler.BATCHING service match { + case _: ForkJoinPool => + // ForkJoinPool implements ScheduledExecutorService since JDK 25, + // but its scheduling behavior differs from ScheduledThreadPoolExecutor + // (e.g. exception handling in scheduled tasks). Use the simple executor + // path with Monix's own ScheduledExecutorService for reliable scheduling. + val s = Defaults.scheduledExecutor + new FromSimpleExecutor(s, service, reporter, executionModel, ft) case ref: ScheduledExecutorService => new FromScheduledExecutor(ref, reporter, executionModel, ft) case _ => From 6e8a4009ec63c05a45f5c793d27e1fb6e5798ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Fri, 27 Mar 2026 15:56:56 +0100 Subject: [PATCH 3/3] Revert "fix: handle ForkJoinPool scheduling behavior in ExecutorScheduler" This reverts commit 61732f36e5e3051249cef2813ad51481d97a2808. --- .../monix/execution/schedulers/ExecutorScheduler.scala | 7 ------- 1 file changed, 7 deletions(-) diff --git a/monix-execution/jvm/src/main/scala/monix/execution/schedulers/ExecutorScheduler.scala b/monix-execution/jvm/src/main/scala/monix/execution/schedulers/ExecutorScheduler.scala index df72ccaa3..51ea65143 100644 --- a/monix-execution/jvm/src/main/scala/monix/execution/schedulers/ExecutorScheduler.scala +++ b/monix-execution/jvm/src/main/scala/monix/execution/schedulers/ExecutorScheduler.scala @@ -100,13 +100,6 @@ object ExecutorScheduler { // Implementations will inherit BatchingScheduler, so this is guaranteed val ft = features + Scheduler.BATCHING service match { - case _: ForkJoinPool => - // ForkJoinPool implements ScheduledExecutorService since JDK 25, - // but its scheduling behavior differs from ScheduledThreadPoolExecutor - // (e.g. exception handling in scheduled tasks). Use the simple executor - // path with Monix's own ScheduledExecutorService for reliable scheduling. - val s = Defaults.scheduledExecutor - new FromSimpleExecutor(s, service, reporter, executionModel, ft) case ref: ScheduledExecutorService => new FromScheduledExecutor(ref, reporter, executionModel, ft) case _ =>