From 6a21e49012bf5abfbbf1806c263bceacf0bb0cd5 Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Thu, 24 Nov 2022 12:22:41 -0700 Subject: [PATCH 001/429] Initial sketch of `PollingSystem` --- .../benchmarks/WorkStealingBenchmark.scala | 1 + .../src/main/scala/cats/effect/IOApp.scala | 5 ++- .../unsafe/IORuntimeCompanionPlatform.scala | 3 +- .../cats/effect/unsafe/PollingSystem.scala | 38 ++++++++++++++++++ .../cats/effect/unsafe/SleepSystem.scala | 40 +++++++++++++++++++ .../unsafe/WorkStealingThreadPool.scala | 7 ++++ .../cats/effect/unsafe/WorkerThread.scala | 17 +++++--- 7 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 core/jvm/src/main/scala/cats/effect/unsafe/PollingSystem.scala create mode 100644 core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala diff --git a/benchmarks/src/main/scala/cats/effect/benchmarks/WorkStealingBenchmark.scala b/benchmarks/src/main/scala/cats/effect/benchmarks/WorkStealingBenchmark.scala index 785831badf..a472d5bd0d 100644 --- a/benchmarks/src/main/scala/cats/effect/benchmarks/WorkStealingBenchmark.scala +++ b/benchmarks/src/main/scala/cats/effect/benchmarks/WorkStealingBenchmark.scala @@ -170,6 +170,7 @@ class WorkStealingBenchmark { "io-compute", "io-blocker", 60.seconds, + SleepSystem, _.printStackTrace()) val cancelationCheckThreshold = diff --git a/core/jvm/src/main/scala/cats/effect/IOApp.scala b/core/jvm/src/main/scala/cats/effect/IOApp.scala index 6d223914de..5e7fd715ce 100644 --- a/core/jvm/src/main/scala/cats/effect/IOApp.scala +++ b/core/jvm/src/main/scala/cats/effect/IOApp.scala @@ -165,6 +165,8 @@ trait IOApp { */ protected def runtimeConfig: unsafe.IORuntimeConfig = unsafe.IORuntimeConfig() + protected def pollingSystem: unsafe.PollingSystem = unsafe.SleepSystem + /** * Controls the number of worker threads which will be allocated to the compute pool in the * underlying runtime. In general, this should be no ''greater'' than the number of physical @@ -317,7 +319,8 @@ trait IOApp { val (compute, compDown) = IORuntime.createWorkStealingComputeThreadPool( threads = computeWorkerThreadCount, - reportFailure = t => reportFailure(t).unsafeRunAndForgetWithoutCallback()(runtime)) + reportFailure = t => reportFailure(t).unsafeRunAndForgetWithoutCallback()(runtime), + pollingSystem = pollingSystem) val (blocking, blockDown) = IORuntime.createDefaultBlockingExecutionContext() diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala index 9799d8ffe7..c5db996b56 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala @@ -33,12 +33,12 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type private[this] final val DefaultBlockerPrefix = "io-compute-blocker" - // The default compute thread pool on the JVM is now a work stealing thread pool. def createWorkStealingComputeThreadPool( threads: Int = Math.max(2, Runtime.getRuntime().availableProcessors()), threadPrefix: String = "io-compute", blockerThreadPrefix: String = DefaultBlockerPrefix, runtimeBlockingExpiration: Duration = 60.seconds, + pollingSystem: PollingSystem = SleepSystem, reportFailure: Throwable => Unit = _.printStackTrace()) : (WorkStealingThreadPool, () => Unit) = { val threadPool = @@ -47,6 +47,7 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type threadPrefix, blockerThreadPrefix, runtimeBlockingExpiration, + pollingSystem, reportFailure) val unregisterMBeans = diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/PollingSystem.scala new file mode 100644 index 0000000000..56379eea87 --- /dev/null +++ b/core/jvm/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -0,0 +1,38 @@ +/* + * Copyright 2020-2022 Typelevel + * + * 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 +package unsafe + +import scala.reflect.ClassTag + +abstract class PollingSystem { + + type Poller <: AbstractPoller + + def apply(): Poller + + final def local()(implicit tag: ClassTag[Poller]): Option[Poller] = + Thread.currentThread() match { + case t: WorkerThread => tag.unapply(t.poller()) + case _ => None + } + + protected abstract class AbstractPoller { + def poll(nanos: Long): Unit + def interrupt(target: Thread): Unit + } +} diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala new file mode 100644 index 0000000000..25ba436178 --- /dev/null +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -0,0 +1,40 @@ +/* + * Copyright 2020-2022 Typelevel + * + * 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 +package unsafe + +import java.util.concurrent.locks.LockSupport + +object SleepSystem extends PollingSystem { + + def apply(): Poller = new Poller() + + final class Poller extends AbstractPoller { + + def poll(nanos: Long): Unit = { + if (nanos < 0) + LockSupport.park() + else if (nanos > 0) + LockSupport.parkNanos(nanos) + else + () + } + + def interrupt(target: Thread): Unit = + LockSupport.unpark(target) + } +} diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index 22b2ae07f9..5da30dd9ba 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -64,6 +64,7 @@ private[effect] final class WorkStealingThreadPool( private[unsafe] val threadPrefix: String, // prefix for the name of worker threads private[unsafe] val blockerThreadPrefix: String, // prefix for the name of worker threads currently in a blocking region private[unsafe] val runtimeBlockingExpiration: Duration, + system: PollingSystem, reportFailure0: Throwable => Unit ) extends ExecutionContextExecutor with Scheduler { @@ -79,6 +80,7 @@ private[effect] final class WorkStealingThreadPool( private[unsafe] val parkedSignals: Array[AtomicBoolean] = new Array(threadCount) private[unsafe] val fiberBags: Array[WeakBag[Runnable]] = new Array(threadCount) private[unsafe] val sleepersQueues: Array[SleepersQueue] = new Array(threadCount) + private[unsafe] val pollers: Array[AnyRef] = new Array(threadCount) /** * Atomic variable for used for publishing changes to the references in the `workerThreads` @@ -124,6 +126,9 @@ private[effect] final class WorkStealingThreadPool( fiberBags(i) = fiberBag val sleepersQueue = SleepersQueue.empty sleepersQueues(i) = sleepersQueue + val poller = system() + pollers(i) = poller + val thread = new WorkerThread( index, @@ -132,7 +137,9 @@ private[effect] final class WorkStealingThreadPool( externalQueue, fiberBag, sleepersQueue, + poller, this) + workerThreads(i) = thread i += 1 } diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala index 4a86ff29d0..70276732d6 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala @@ -28,7 +28,6 @@ import scala.util.control.NonFatal import java.util.concurrent.{ArrayBlockingQueue, ThreadLocalRandom} import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.locks.LockSupport /** * Implementation of the worker thread at the heart of the [[WorkStealingThreadPool]]. @@ -53,8 +52,9 @@ private final class WorkerThread( // A worker-thread-local weak bag for tracking suspended fibers. private[this] var fiberBag: WeakBag[Runnable], private[this] var sleepersQueue: SleepersQueue, + private[this] var _poller: PollingSystem#Poller, // Reference to the `WorkStealingThreadPool` in which this thread operates. - private[this] val pool: WorkStealingThreadPool) + pool: WorkStealingThreadPool) extends Thread with BlockContext { @@ -111,6 +111,8 @@ private final class WorkerThread( setName(s"$prefix-$nameIndex") } + private[unsafe] def poller(): PollingSystem#Poller = _poller + /** * Schedules the fiber for execution at the back of the local queue and notifies the work * stealing pool of newly available work. @@ -316,7 +318,7 @@ private final class WorkerThread( var cont = true while (cont && !done.get()) { // Park the thread until further notice. - LockSupport.park(pool) + _poller.poll(-1) // the only way we can be interrupted here is if it happened *externally* (probably sbt) if (isInterrupted()) @@ -332,7 +334,7 @@ private final class WorkerThread( val now = System.nanoTime() val head = sleepersQueue.head() val nanos = head.triggerTime - now - LockSupport.parkNanos(pool, nanos) + _poller.poll(nanos) if (parked.getAndSet(false)) { pool.doneSleeping() @@ -353,6 +355,7 @@ private final class WorkerThread( parked = null fiberBag = null sleepersQueue = null + _poller = null.asInstanceOf[PollingSystem#Poller] // Add this thread to the cached threads data structure, to be picked up // by another thread in the future. @@ -415,6 +418,9 @@ private final class WorkerThread( ((state & ExternalQueueTicksMask): @switch) match { case 0 => + // give the polling system a chance to discover events + _poller.poll(0) + // Obtain a fiber or batch of fibers from the external queue. val element = external.poll(rnd) if (element.isInstanceOf[Array[Runnable]]) { @@ -714,7 +720,7 @@ private final class WorkerThread( // for unparking. val idx = index val clone = - new WorkerThread(idx, queue, parked, external, fiberBag, sleepersQueue, pool) + new WorkerThread(idx, queue, parked, external, fiberBag, sleepersQueue, _poller, pool) pool.replaceWorker(idx, clone) pool.blockedWorkerThreadCounter.incrementAndGet() clone.start() @@ -730,6 +736,7 @@ private final class WorkerThread( parked = pool.parkedSignals(newIdx) fiberBag = pool.fiberBags(newIdx) sleepersQueue = pool.sleepersQueues(newIdx) + _poller = pool.pollers(newIdx).asInstanceOf[PollingSystem#Poller] // Reset the name of the thread to the regular prefix. val prefix = pool.threadPrefix From cca9cafd4632c25f3c3defac25229c9b39d83bb7 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 1 Dec 2022 04:55:32 +0000 Subject: [PATCH 002/429] Iterate polling system sketch --- .../scala/cats/effect/unsafe/EventLoop.scala | 78 +++++++++++++++++++ .../cats/effect/unsafe/PollingSystem.scala | 9 +-- .../cats/effect/unsafe/SleepSystem.scala | 6 +- .../unsafe/WorkStealingThreadPool.scala | 20 ++++- 4 files changed, 103 insertions(+), 10 deletions(-) create mode 100644 core/jvm/src/main/scala/cats/effect/unsafe/EventLoop.scala diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/EventLoop.scala b/core/jvm/src/main/scala/cats/effect/unsafe/EventLoop.scala new file mode 100644 index 0000000000..88011a519f --- /dev/null +++ b/core/jvm/src/main/scala/cats/effect/unsafe/EventLoop.scala @@ -0,0 +1,78 @@ +/* + * Copyright 2020-2022 Typelevel + * + * 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 +package unsafe + +import scala.concurrent.{ExecutionContext, ExecutionContextExecutor} +import scala.reflect.ClassTag + +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.atomic.AtomicBoolean + +trait EventLoop[+Registrar] extends ExecutionContext { + + protected def registrarTag: ClassTag[_ <: Registrar] + + def registrar(): Registrar + +} + +object EventLoop { + def unapply[R](loop: EventLoop[Any])(ct: ClassTag[R]): Option[EventLoop[R]] = + if (ct.runtimeClass.isAssignableFrom(loop.registrarTag.runtimeClass)) + Some(loop.asInstanceOf[EventLoop[R]]) + else + None + + def fromPollingSystem( + name: String, + system: PollingSystem): (EventLoop[system.Poller], () => Unit) = { + + val done = new AtomicBoolean(false) + val poller = system.makePoller() + + val loop = new Thread(name) with EventLoop[system.Poller] with ExecutionContextExecutor { + + val queue = new LinkedBlockingQueue[Runnable] + + def registrarTag: ClassTag[_ <: system.Poller] = system.pollerTag + + def registrar(): system.Poller = poller + + def execute(command: Runnable): Unit = { + queue.put(command) + poller.interrupt(this) + } + + def reportFailure(cause: Throwable): Unit = cause.printStackTrace() + + override def run(): Unit = { + while (!done.get()) { + while (!queue.isEmpty()) queue.poll().run() + poller.poll(-1) + } + } + } + + val cleanup = () => { + done.set(true) + poller.interrupt(loop) + } + + (loop, cleanup) + } +} diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/PollingSystem.scala index 56379eea87..2638a8c78c 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -22,14 +22,9 @@ import scala.reflect.ClassTag abstract class PollingSystem { type Poller <: AbstractPoller + def pollerTag: ClassTag[Poller] - def apply(): Poller - - final def local()(implicit tag: ClassTag[Poller]): Option[Poller] = - Thread.currentThread() match { - case t: WorkerThread => tag.unapply(t.poller()) - case _ => None - } + def makePoller(): Poller protected abstract class AbstractPoller { def poll(nanos: Long): Unit diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala index 25ba436178..7ed45abf01 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -17,11 +17,15 @@ package cats.effect package unsafe +import scala.reflect.ClassTag + import java.util.concurrent.locks.LockSupport object SleepSystem extends PollingSystem { - def apply(): Poller = new Poller() + def pollerTag: ClassTag[Poller] = ClassTag(classOf[Poller]) + + def makePoller(): Poller = new Poller() final class Poller extends AbstractPoller { diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index 5da30dd9ba..5ee0aef7e5 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -43,6 +43,7 @@ import java.util.Comparator import java.util.concurrent.{ConcurrentSkipListSet, ThreadLocalRandom} import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger, AtomicReference} import java.util.concurrent.locks.LockSupport +import scala.reflect.ClassTag /** * Work-stealing thread pool which manages a pool of [[WorkerThread]] s for the specific purpose @@ -67,7 +68,8 @@ private[effect] final class WorkStealingThreadPool( system: PollingSystem, reportFailure0: Throwable => Unit ) extends ExecutionContextExecutor - with Scheduler { + with Scheduler + with EventLoop[Any] { import TracingConstants._ import WorkStealingThreadPoolConstants._ @@ -126,7 +128,7 @@ private[effect] final class WorkStealingThreadPool( fiberBags(i) = fiberBag val sleepersQueue = SleepersQueue.empty sleepersQueues(i) = sleepersQueue - val poller = system() + val poller = system.makePoller() pollers(i) = poller val thread = @@ -585,6 +587,20 @@ private[effect] final class WorkStealingThreadPool( } } + def registrar(): Any = { + val pool = this + val thread = Thread.currentThread() + + if (thread.isInstanceOf[WorkerThread]) { + val worker = thread.asInstanceOf[WorkerThread] + if (worker.isOwnedBy(pool)) return worker.poller() + } + + throw new RuntimeException("Invoked from outside the WSTP") + } + + protected def registrarTag: ClassTag[?] = system.pollerTag + /** * Shut down the thread pool and clean up the pool state. Calling this method after the pool * has been shut down has no effect. From 6d23310373e7750296967110b661abee65f10c2f Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 11 Dec 2022 05:42:20 +0000 Subject: [PATCH 003/429] Extract `PollingSystem` abstraction on Native --- build.sbt | 8 + .../cats/effect/IOCompanionPlatform.scala | 10 ++ .../scala/cats/effect/unsafe/EventLoop.scala | 26 +++ .../unsafe/EventLoopExecutorScheduler.scala | 150 ++++++++++++++++++ .../unsafe/IORuntimeCompanionPlatform.scala | 4 +- .../unsafe/PollingExecutorScheduler.scala | 130 +++------------ .../cats/effect/unsafe/PollingSystem.scala | 41 +++++ .../unsafe/SchedulerCompanionPlatform.scala | 3 +- ...cutorScheduler.scala => SleepSystem.scala} | 18 ++- 9 files changed, 270 insertions(+), 120 deletions(-) create mode 100644 core/native/src/main/scala/cats/effect/unsafe/EventLoop.scala create mode 100644 core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala create mode 100644 core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala rename core/native/src/main/scala/cats/effect/unsafe/{QueueExecutorScheduler.scala => SleepSystem.scala} (66%) diff --git a/build.sbt b/build.sbt index a9a53ab154..bdb8f1a855 100644 --- a/build.sbt +++ b/build.sbt @@ -772,6 +772,14 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) } else Seq() } ) + .nativeSettings( + mimaBinaryIssueFilters ++= Seq( + ProblemFilters.exclude[MissingClassProblem]( + "cats.effect.unsafe.PollingExecutorScheduler$SleepTask"), + ProblemFilters.exclude[MissingClassProblem]("cats.effect.unsafe.QueueExecutorScheduler"), + ProblemFilters.exclude[MissingClassProblem]("cats.effect.unsafe.QueueExecutorScheduler$") + ) + ) /** * Test support for the core project, providing various helpful instances like ScalaCheck diff --git a/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala b/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala index 71e71c7003..fb4e2a1875 100644 --- a/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala +++ b/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala @@ -17,6 +17,9 @@ package cats.effect import cats.effect.std.Console +import cats.effect.unsafe.EventLoop + +import scala.reflect.ClassTag import java.time.Instant @@ -62,4 +65,11 @@ private[effect] abstract class IOCompanionPlatform { this: IO.type => */ def readLine: IO[String] = Console[IO].readLine + + def eventLoop[Poller](implicit ct: ClassTag[Poller]): IO[Option[EventLoop[Poller]]] = + IO.executionContext.map { + case loop: EventLoop[_] if ct.runtimeClass.isInstance(loop.poller()) => + Some(loop.asInstanceOf[EventLoop[Poller]]) + case _ => None + } } diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoop.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoop.scala new file mode 100644 index 0000000000..181c74ea07 --- /dev/null +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoop.scala @@ -0,0 +1,26 @@ +/* + * Copyright 2020-2022 Typelevel + * + * 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 +package unsafe + +import scala.concurrent.ExecutionContext + +trait EventLoop[Poller] extends ExecutionContext { + + def poller(): Poller + +} diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala new file mode 100644 index 0000000000..acf22401e4 --- /dev/null +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala @@ -0,0 +1,150 @@ +/* + * Copyright 2020-2022 Typelevel + * + * 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 +package unsafe + +import scala.concurrent.{ExecutionContext, ExecutionContextExecutor} +import scala.concurrent.duration._ +import scala.scalanative.libc.errno +import scala.scalanative.meta.LinktimeInfo +import scala.scalanative.unsafe._ +import scala.util.control.NonFatal + +import java.util.{ArrayDeque, PriorityQueue} + +private final class EventLoopExecutorScheduler(pollEvery: Int, system: PollingSystem) + extends EventLoop[Any] + with ExecutionContextExecutor + with Scheduler { + + private[this] val _poller = system.makePoller() + + private[this] var needsReschedule: Boolean = true + + private[this] val executeQueue: ArrayDeque[Runnable] = new ArrayDeque + private[this] val sleepQueue: PriorityQueue[SleepTask] = new PriorityQueue + + private[this] val noop: Runnable = () => () + + private[this] def scheduleIfNeeded(): Unit = if (needsReschedule) { + ExecutionContext.global.execute(() => loop()) + needsReschedule = false + } + + final def execute(runnable: Runnable): Unit = { + scheduleIfNeeded() + executeQueue.addLast(runnable) + } + + final def sleep(delay: FiniteDuration, task: Runnable): Runnable = + if (delay <= Duration.Zero) { + execute(task) + noop + } else { + scheduleIfNeeded() + val now = monotonicNanos() + val sleepTask = new SleepTask(now + delay.toNanos, task) + sleepQueue.offer(sleepTask) + sleepTask + } + + def reportFailure(t: Throwable): Unit = t.printStackTrace() + + def nowMillis() = System.currentTimeMillis() + + override def nowMicros(): Long = + if (LinktimeInfo.isFreeBSD || LinktimeInfo.isLinux || LinktimeInfo.isMac) { + import scala.scalanative.posix.time._ + import scala.scalanative.posix.timeOps._ + val ts = stackalloc[timespec]() + if (clock_gettime(CLOCK_REALTIME, ts) != 0) + throw new RuntimeException(s"clock_gettime: ${errno.errno}") + ts.tv_sec * 1000000 + ts.tv_nsec / 1000 + } else { + super.nowMicros() + } + + def monotonicNanos() = System.nanoTime() + + def poller(): Any = _poller + + private[this] def loop(): Unit = { + needsReschedule = false + + var continue = true + + while (continue) { + // execute the timers + val now = monotonicNanos() + while (!sleepQueue.isEmpty() && sleepQueue.peek().at <= now) { + val task = sleepQueue.poll() + try task.runnable.run() + catch { + case t if NonFatal(t) => reportFailure(t) + case t: Throwable => IOFiber.onFatalFailure(t) + } + } + + // do up to pollEvery tasks + var i = 0 + while (i < pollEvery && !executeQueue.isEmpty()) { + val runnable = executeQueue.poll() + try runnable.run() + catch { + case t if NonFatal(t) => reportFailure(t) + case t: Throwable => IOFiber.onFatalFailure(t) + } + i += 1 + } + + // finally we poll + val timeout = + if (!executeQueue.isEmpty()) + 0 + else if (!sleepQueue.isEmpty()) + Math.max(sleepQueue.peek().at - monotonicNanos(), 0) + else + -1 + + val needsPoll = system.poll(_poller, timeout) + + continue = needsPoll || !executeQueue.isEmpty() || !sleepQueue.isEmpty() + } + + needsReschedule = true + } + + private[this] final class SleepTask( + val at: Long, + val runnable: Runnable + ) extends Runnable + with Comparable[SleepTask] { + + def run(): Unit = { + sleepQueue.remove(this) + () + } + + def compareTo(that: SleepTask): Int = + java.lang.Long.compare(this.at, that.at) + } + +} + +private object EventLoopExecutorScheduler { + lazy val global = new EventLoopExecutorScheduler(64, SleepSystem) +} diff --git a/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala index 42c0d19b1c..99c4d303a0 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala @@ -20,9 +20,9 @@ import scala.concurrent.ExecutionContext private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type => - def defaultComputeExecutionContext: ExecutionContext = QueueExecutorScheduler + def defaultComputeExecutionContext: ExecutionContext = EventLoopExecutorScheduler.global - def defaultScheduler: Scheduler = QueueExecutorScheduler + def defaultScheduler: Scheduler = EventLoopExecutorScheduler.global private[this] var _global: IORuntime = null diff --git a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala index d111ce950c..d8db322fa9 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala @@ -17,65 +17,38 @@ package cats.effect package unsafe -import scala.concurrent.{ExecutionContext, ExecutionContextExecutor} +import scala.concurrent.ExecutionContextExecutor import scala.concurrent.duration._ -import scala.scalanative.libc.errno -import scala.scalanative.meta.LinktimeInfo -import scala.scalanative.unsafe._ -import scala.util.control.NonFatal - -import java.util.{ArrayDeque, PriorityQueue} +@deprecated("Use default runtime with a custom PollingSystem", "3.5.0") abstract class PollingExecutorScheduler(pollEvery: Int) extends ExecutionContextExecutor - with Scheduler { - - private[this] var needsReschedule: Boolean = true - - private[this] val executeQueue: ArrayDeque[Runnable] = new ArrayDeque - private[this] val sleepQueue: PriorityQueue[SleepTask] = new PriorityQueue - - private[this] val noop: Runnable = () => () - - private[this] def scheduleIfNeeded(): Unit = if (needsReschedule) { - ExecutionContext.global.execute(() => loop()) - needsReschedule = false - } + with Scheduler { outer => + + private[this] val loop = new EventLoopExecutorScheduler( + pollEvery, + new PollingSystem { + type Poller = outer.type + def makePoller(): Poller = outer + def close(poller: Poller): Unit = () + def poll(poller: Poller, nanos: Long): Boolean = + if (nanos == -1) outer.poll(Duration.Inf) else outer.poll(nanos.nanos) + } + ) - final def execute(runnable: Runnable): Unit = { - scheduleIfNeeded() - executeQueue.addLast(runnable) - } + final def execute(runnable: Runnable): Unit = + loop.execute(runnable) final def sleep(delay: FiniteDuration, task: Runnable): Runnable = - if (delay <= Duration.Zero) { - execute(task) - noop - } else { - scheduleIfNeeded() - val now = monotonicNanos() - val sleepTask = new SleepTask(now + delay.toNanos, task) - sleepQueue.offer(sleepTask) - sleepTask - } + loop.sleep(delay, task) - def reportFailure(t: Throwable): Unit = t.printStackTrace() + def reportFailure(t: Throwable): Unit = loop.reportFailure(t) - def nowMillis() = System.currentTimeMillis() + def nowMillis() = loop.nowMillis() - override def nowMicros(): Long = - if (LinktimeInfo.isFreeBSD || LinktimeInfo.isLinux || LinktimeInfo.isMac) { - import scala.scalanative.posix.time._ - import scala.scalanative.posix.timeOps._ - val ts = stackalloc[timespec]() - if (clock_gettime(CLOCK_REALTIME, ts) != 0) - throw new RuntimeException(s"clock_gettime: ${errno.errno}") - ts.tv_sec * 1000000 + ts.tv_nsec / 1000 - } else { - super.nowMicros() - } + override def nowMicros(): Long = loop.nowMicros() - def monotonicNanos() = System.nanoTime() + def monotonicNanos() = loop.monotonicNanos() /** * @param timeout @@ -90,65 +63,4 @@ abstract class PollingExecutorScheduler(pollEvery: Int) */ protected def poll(timeout: Duration): Boolean - private[this] def loop(): Unit = { - needsReschedule = false - - var continue = true - - while (continue) { - // execute the timers - val now = monotonicNanos() - while (!sleepQueue.isEmpty() && sleepQueue.peek().at <= now) { - val task = sleepQueue.poll() - try task.runnable.run() - catch { - case t if NonFatal(t) => reportFailure(t) - case t: Throwable => IOFiber.onFatalFailure(t) - } - } - - // do up to pollEvery tasks - var i = 0 - while (i < pollEvery && !executeQueue.isEmpty()) { - val runnable = executeQueue.poll() - try runnable.run() - catch { - case t if NonFatal(t) => reportFailure(t) - case t: Throwable => IOFiber.onFatalFailure(t) - } - i += 1 - } - - // finally we poll - val timeout = - if (!executeQueue.isEmpty()) - Duration.Zero - else if (!sleepQueue.isEmpty()) - Math.max(sleepQueue.peek().at - monotonicNanos(), 0).nanos - else - Duration.Inf - - val needsPoll = poll(timeout) - - continue = needsPoll || !executeQueue.isEmpty() || !sleepQueue.isEmpty() - } - - needsReschedule = true - } - - private[this] final class SleepTask( - val at: Long, - val runnable: Runnable - ) extends Runnable - with Comparable[SleepTask] { - - def run(): Unit = { - sleepQueue.remove(this) - () - } - - def compareTo(that: SleepTask): Int = - java.lang.Long.compare(this.at, that.at) - } - } diff --git a/core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala new file mode 100644 index 0000000000..e4a53b5eb6 --- /dev/null +++ b/core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -0,0 +1,41 @@ +/* + * Copyright 2020-2022 Typelevel + * + * 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 +package unsafe + +abstract class PollingSystem { + + type Poller + + def makePoller(): Poller + + def close(poller: Poller): Unit + + /** + * @param nanos + * the maximum duration for which to block, where `nanos == -1` indicates to block + * indefinitely. ''However'', if `timeout == -1` and there are no remaining events to poll + * for, this method should return `false` immediately. This is unfortunate but necessary so + * that the `EventLoop` can yield to the Scala Native global `ExecutionContext` which is + * currently hard-coded into every test framework, including JUnit, MUnit, and specs2. + * + * @return + * whether poll should be called again (i.e., there are more events to be polled) + */ + def poll(poller: Poller, nanos: Long): Boolean + +} diff --git a/core/native/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala b/core/native/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala index f6e4964808..e6fe2d258c 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala @@ -18,6 +18,7 @@ package cats.effect.unsafe private[unsafe] abstract class SchedulerCompanionPlatform { this: Scheduler.type => - def createDefaultScheduler(): (Scheduler, () => Unit) = (QueueExecutorScheduler, () => ()) + def createDefaultScheduler(): (Scheduler, () => Unit) = + (EventLoopExecutorScheduler.global, () => ()) } diff --git a/core/native/src/main/scala/cats/effect/unsafe/QueueExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala similarity index 66% rename from core/native/src/main/scala/cats/effect/unsafe/QueueExecutorScheduler.scala rename to core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala index c53036b5dc..247321cae1 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/QueueExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -14,18 +14,20 @@ * limitations under the License. */ -package cats.effect.unsafe +package cats.effect +package unsafe -import scala.concurrent.duration._ +object SleepSystem extends PollingSystem { -// JVM WSTP sets ExternalQueueTicks = 64 so we steal it here -private[effect] object QueueExecutorScheduler extends PollingExecutorScheduler(64) { + type Poller = Unit - def poll(timeout: Duration): Boolean = { - if (timeout != Duration.Zero && timeout.isFinite) { - val nanos = timeout.toNanos + def makePoller(): Poller = () + + def close(poller: Poller): Unit = () + + def poll(poller: Poller, nanos: Long): Boolean = { + if (nanos > 0) Thread.sleep(nanos / 1000000, (nanos % 1000000).toInt) - } false } From 727bfe891c7c50986413c2ba5fb453b97cc45b15 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 11 Dec 2022 05:55:18 +0000 Subject: [PATCH 004/429] Add `FileDescriptorPoller` abstraction --- .../effect/unsafe/FileDescriptorPoller.scala | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 core/native/src/main/scala/cats/effect/unsafe/FileDescriptorPoller.scala diff --git a/core/native/src/main/scala/cats/effect/unsafe/FileDescriptorPoller.scala b/core/native/src/main/scala/cats/effect/unsafe/FileDescriptorPoller.scala new file mode 100644 index 0000000000..4765116954 --- /dev/null +++ b/core/native/src/main/scala/cats/effect/unsafe/FileDescriptorPoller.scala @@ -0,0 +1,52 @@ +/* + * Copyright 2020-2022 Typelevel + * + * 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 +package unsafe + +trait FileDescriptorPoller { + + /** + * Registers a callback to be notified of read- and write-ready events on a file descriptor. + * Produces a runnable which unregisters the file descriptor. + * + * 1. It is the responsibility of the caller to set the file descriptor to non-blocking + * mode. + * 1. It is the responsibility of the caller to unregister the file descriptor when they are + * done. + * 1. A file descriptor should be registered at most once. To modify a registration, you + * must unregister and re-register the file descriptor. + * 1. The callback may be invoked "spuriously" claiming that a file descriptor is read- or + * write-ready when in fact it is not. You should be prepared to handle this. + * 1. The callback will be invoked at least once when the file descriptor transitions from + * blocked to read- or write-ready. You may additionally receive zero or more reminders + * of its readiness. However, you should not rely on any further callbacks until after + * the file descriptor has become blocked again. + */ + def registerFileDescriptor( + fileDescriptor: Int, + readReadyEvents: Boolean, + writeReadyEvents: Boolean)( + cb: FileDescriptorPoller.Callback + ): Runnable + +} + +object FileDescriptorPoller { + trait Callback { + def apply(readReady: Boolean, writeReady: Boolean): Unit + } +} From 9a8f7edfd5908f2f4e096f3d1093ee635d036e68 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 11 Dec 2022 08:22:16 +0000 Subject: [PATCH 005/429] Add `EpollSystem` and `KqueueSystem` Co-authored-by: Lee Tibbert --- .../cats/effect/unsafe/EpollSystem.scala | 159 ++++++++++++ .../scala/cats/effect/unsafe/EventLoop.scala | 2 +- .../unsafe/EventLoopExecutorScheduler.scala | 13 +- .../effect/unsafe/FileDescriptorPoller.scala | 14 +- .../cats/effect/unsafe/KqueueSystem.scala | 240 ++++++++++++++++++ .../unsafe/PollingExecutorScheduler.scala | 2 +- .../cats/effect/unsafe/PollingSystem.scala | 2 +- .../cats/effect/unsafe/SleepSystem.scala | 2 +- 8 files changed, 427 insertions(+), 7 deletions(-) create mode 100644 core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala create mode 100644 core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala new file mode 100644 index 0000000000..e3728a94df --- /dev/null +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -0,0 +1,159 @@ +/* + * Copyright 2020-2022 Typelevel + * + * 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 +package unsafe + +import scala.scalanative.libc.errno._ +import scala.scalanative.posix.string._ +import scala.scalanative.posix.unistd +import scala.scalanative.unsafe._ +import scala.scalanative.unsigned._ +import scala.util.control.NonFatal + +import java.io.IOException +import java.util.{Collections, IdentityHashMap, Set} + +import EpollSystem.epoll._ +import EpollSystem.epollImplicits._ + +final class EpollSystem private (maxEvents: Int) extends PollingSystem { + + def makePoller(): Poller = { + val fd = epoll_create1(0) + if (fd == -1) + throw new IOException(fromCString(strerror(errno))) + new Poller(fd, maxEvents) + } + + def close(poller: Poller): Unit = poller.close() + + def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = + poller.poll(nanos, reportFailure) + + final class Poller private[EpollSystem] (private[EpollSystem] val epfd: Int, maxEvents: Int) + extends FileDescriptorPoller { + + private[this] val callbacks: Set[FileDescriptorPoller.Callback] = + Collections.newSetFromMap(new IdentityHashMap) + + private[EpollSystem] def close(): Unit = + if (unistd.close(epfd) != 0) + throw new IOException(fromCString(strerror(errno))) + + private[EpollSystem] def poll(timeout: Long, reportFailure: Throwable => Unit): Boolean = { + val noCallbacks = callbacks.isEmpty() + + if (timeout <= 0 && noCallbacks) + false // nothing to do here + else { + val timeoutMillis = if (timeout == -1) -1 else (timeout / 1000000).toInt + + val events = stackalloc[epoll_event](maxEvents.toUInt) + + val triggeredEvents = epoll_wait(epfd, events, maxEvents, timeoutMillis) + + if (triggeredEvents >= 0) { + var i = 0 + while (i < triggeredEvents) { + val event = events + i.toLong + val cb = FileDescriptorPoller.Callback.fromPtr(event.data) + try { + val e = event.events.toInt + val readReady = (e & EPOLLIN) != 0 + val writeReady = (e & EPOLLOUT) != 0 + cb.notifyFileDescriptorEvents(readReady, writeReady) + } catch { + case ex if NonFatal(ex) => reportFailure(ex) + } + i += 1 + } + } else { + throw new IOException(fromCString(strerror(errno))) + } + + !callbacks.isEmpty() + } + } + + def registerFileDescriptor(fd: Int, reads: Boolean, writes: Boolean)( + cb: FileDescriptorPoller.Callback): Runnable = { + val event = stackalloc[epoll_event]() + event.events = + (EPOLLET | (if (reads) EPOLLIN else 0) | (if (writes) EPOLLOUT else 0)).toUInt + event.data = FileDescriptorPoller.Callback.toPtr(cb) + + if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, event) != 0) + throw new IOException(fromCString(strerror(errno))) + callbacks.add(cb) + + () => { + callbacks.remove(cb) + if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, null) != 0) + throw new IOException(fromCString(strerror(errno))) + } + } + } + +} + +object EpollSystem { + def apply(maxEvents: Int): EpollSystem = new EpollSystem(maxEvents) + + @extern + private[unsafe] object epoll { + + final val EPOLL_CTL_ADD = 1 + final val EPOLL_CTL_DEL = 2 + final val EPOLL_CTL_MOD = 3 + + final val EPOLLIN = 0x001 + final val EPOLLOUT = 0x004 + final val EPOLLONESHOT = 1 << 30 + final val EPOLLET = 1 << 31 + + type epoll_event + type epoll_data_t = Ptr[Byte] + + def epoll_create1(flags: Int): Int = extern + + def epoll_ctl(epfd: Int, op: Int, fd: Int, event: Ptr[epoll_event]): Int = extern + + def epoll_wait(epfd: Int, events: Ptr[epoll_event], maxevents: Int, timeout: Int): Int = + extern + + } + + private[unsafe] object epollImplicits { + + implicit final class epoll_eventOps(epoll_event: Ptr[epoll_event]) { + def events: CUnsignedInt = !(epoll_event.asInstanceOf[Ptr[CUnsignedInt]]) + def events_=(events: CUnsignedInt): Unit = + !(epoll_event.asInstanceOf[Ptr[CUnsignedInt]]) = events + + def data: epoll_data_t = + !((epoll_event.asInstanceOf[Ptr[Byte]] + sizeof[CUnsignedInt]) + .asInstanceOf[Ptr[epoll_data_t]]) + def data_=(data: epoll_data_t): Unit = + !((epoll_event.asInstanceOf[Ptr[Byte]] + sizeof[CUnsignedInt]) + .asInstanceOf[Ptr[epoll_data_t]]) = data + } + + implicit val epoll_eventTag: Tag[epoll_event] = + Tag.materializeCArrayTag[Byte, Nat.Digit2[Nat._1, Nat._2]].asInstanceOf[Tag[epoll_event]] + + } +} diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoop.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoop.scala index 181c74ea07..78cd33cee6 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EventLoop.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoop.scala @@ -20,7 +20,7 @@ package unsafe import scala.concurrent.ExecutionContext trait EventLoop[Poller] extends ExecutionContext { - + def poller(): Poller } diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala index acf22401e4..9b1b55b271 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala @@ -120,7 +120,7 @@ private final class EventLoopExecutorScheduler(pollEvery: Int, system: PollingSy else -1 - val needsPoll = system.poll(_poller, timeout) + val needsPoll = system.poll(_poller, timeout, reportFailure) continue = needsPoll || !executeQueue.isEmpty() || !sleepQueue.isEmpty() } @@ -146,5 +146,14 @@ private final class EventLoopExecutorScheduler(pollEvery: Int, system: PollingSy } private object EventLoopExecutorScheduler { - lazy val global = new EventLoopExecutorScheduler(64, SleepSystem) + lazy val global = { + val system = + if (LinktimeInfo.isLinux) + EpollSystem(64) + else if (LinktimeInfo.isMac) + KqueueSystem(64) + else + SleepSystem + new EventLoopExecutorScheduler(64, system) + } } diff --git a/core/native/src/main/scala/cats/effect/unsafe/FileDescriptorPoller.scala b/core/native/src/main/scala/cats/effect/unsafe/FileDescriptorPoller.scala index 4765116954..df8a0e3e00 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/FileDescriptorPoller.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/FileDescriptorPoller.scala @@ -17,6 +17,10 @@ package cats.effect package unsafe +import scala.scalanative.annotation.alwaysinline +import scala.scalanative.runtime._ +import scala.scalanative.unsafe._ + trait FileDescriptorPoller { /** @@ -47,6 +51,14 @@ trait FileDescriptorPoller { object FileDescriptorPoller { trait Callback { - def apply(readReady: Boolean, writeReady: Boolean): Unit + def notifyFileDescriptorEvents(readReady: Boolean, writeReady: Boolean): Unit + } + + object Callback { + @alwaysinline private[unsafe] def toPtr(cb: Callback): Ptr[Byte] = + fromRawPtr(Intrinsics.castObjectToRawPtr(cb)) + + @alwaysinline private[unsafe] def fromPtr[A](ptr: Ptr[Byte]): Callback = + Intrinsics.castRawPtrToObject(toRawPtr(ptr)).asInstanceOf[Callback] } } diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala new file mode 100644 index 0000000000..4b40ff8bb7 --- /dev/null +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -0,0 +1,240 @@ +/* + * Copyright 2020-2022 Typelevel + * + * 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 +package unsafe + +import scala.collection.mutable.LongMap +import scala.scalanative.libc.errno._ +import scala.scalanative.posix.string._ +import scala.scalanative.posix.time._ +import scala.scalanative.posix.timeOps._ +import scala.scalanative.posix.unistd +import scala.scalanative.unsafe._ +import scala.scalanative.unsigned._ +import scala.util.control.NonFatal + +import java.io.IOException +import java.util.ArrayDeque + +import KqueueSystem.EvAdd +import KqueueSystem.event._ +import KqueueSystem.eventImplicits._ + +final class KqueueSystem private (maxEvents: Int) extends PollingSystem { + + def makePoller(): Poller = { + val fd = kqueue() + if (fd == -1) + throw new IOException(fromCString(strerror(errno))) + new Poller(fd, maxEvents) + } + + def close(poller: Poller): Unit = poller.close() + + def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = + poller.poll(nanos, reportFailure) + + final class Poller private[KqueueSystem] (private[KqueueSystem] val kqfd: Int, maxEvents: Int) + extends FileDescriptorPoller { + + private[this] val changes: ArrayDeque[EvAdd] = new ArrayDeque + private[this] val callbacks: LongMap[FileDescriptorPoller.Callback] = new LongMap + + private[KqueueSystem] def close(): Unit = + if (unistd.close(kqfd) != 0) + throw new IOException(fromCString(strerror(errno))) + + private[KqueueSystem] def poll(timeout: Long, reportFailure: Throwable => Unit): Boolean = { + val noCallbacks = callbacks.isEmpty + + // pre-process the changes to filter canceled ones + val changelist = stackalloc[kevent64_s](changes.size().toLong) + var change = changelist + var changeCount = 0 + while (!changes.isEmpty()) { + val evAdd = changes.poll() + if (!evAdd.canceled) { + change.ident = evAdd.fd.toULong + change.filter = evAdd.filter + change.flags = (EV_ADD | EV_CLEAR).toUShort + change.udata = FileDescriptorPoller.Callback.toPtr(evAdd.cb) + change += 1 + changeCount += 1 + } + } + + if (timeout <= 0 && noCallbacks && changeCount == 0) + false // nothing to do here + else { + + val timeoutSpec = + if (timeout <= 0) null + else { + val ts = stackalloc[timespec]() + ts.tv_sec = timeout / 1000000000 + ts.tv_nsec = timeout % 1000000000 + ts + } + + val eventlist = stackalloc[kevent64_s](maxEvents.toLong) + val flags = (if (timeout == 0) KEVENT_FLAG_IMMEDIATE else KEVENT_FLAG_NONE).toUInt + val triggeredEvents = + kevent64(kqfd, changelist, changeCount, eventlist, maxEvents, flags, timeoutSpec) + + if (triggeredEvents >= 0) { + var i = 0 + var event = eventlist + while (i < triggeredEvents) { + if ((event.flags.toLong & EV_ERROR) != 0) { + + // TODO it would be interesting to propagate this failure via the callback + reportFailure( + new RuntimeException( + s"kevent64: flags=${event.flags.toHexString} errno=${event.data}" + ) + ) + + } else if (callbacks.contains(event.ident.toLong)) { + val filter = event.filter + val cb = FileDescriptorPoller.Callback.fromPtr(event.udata) + + try { + cb.notifyFileDescriptorEvents(filter == EVFILT_READ, filter == EVFILT_WRITE) + } catch { + case NonFatal(ex) => + reportFailure(ex) + } + } + + i += 1 + event += 1 + } + } else { + throw new IOException(fromCString(strerror(errno))) + } + + !changes.isEmpty() || callbacks.nonEmpty + } + } + + def registerFileDescriptor(fd: Int, reads: Boolean, writes: Boolean)( + cb: FileDescriptorPoller.Callback): Runnable = { + + val readEvent = + if (reads) + new EvAdd(fd, EVFILT_READ, cb) + else null + + val writeEvent = + if (writes) + new EvAdd(fd, EVFILT_WRITE, cb) + else null + + if (readEvent != null) + changes.add(readEvent) + if (writeEvent != null) + changes.add(writeEvent) + + callbacks(fd.toLong) = cb + + () => { + // we do not need to explicitly unregister the fd with the kqueue, + // b/c it will be unregistered automatically when the fd is closed + + // release the callback, so it can be GCed + callbacks.remove(fd.toLong) + + // cancel the events, such that if they are currently pending in the + // changes queue awaiting registration, they will not be registered + if (readEvent != null) readEvent.cancel() + if (writeEvent != null) writeEvent.cancel() + } + } + + } + +} + +object KqueueSystem { + def apply(maxEvents: Int): KqueueSystem = new KqueueSystem(maxEvents) + + private final class EvAdd( + val fd: Int, + val filter: Short, + val cb: FileDescriptorPoller.Callback + ) { + var canceled = false + def cancel() = canceled = true + } + + @extern + private[unsafe] object event { + // Derived from https://opensource.apple.com/source/xnu/xnu-7195.81.3/bsd/sys/event.h.auto.html + + final val EVFILT_READ = -1 + final val EVFILT_WRITE = -2 + + final val KEVENT_FLAG_NONE = 0x000000 + final val KEVENT_FLAG_IMMEDIATE = 0x000001 + + final val EV_ADD = 0x0001 + final val EV_DELETE = 0x0002 + final val EV_CLEAR = 0x0020 + final val EV_ERROR = 0x4000 + + type kevent64_s + + def kqueue(): CInt = extern + + def kevent64( + kq: CInt, + changelist: Ptr[kevent64_s], + nchanges: CInt, + eventlist: Ptr[kevent64_s], + nevents: CInt, + flags: CUnsignedInt, + timeout: Ptr[timespec] + ): CInt = extern + + } + + private[unsafe] object eventImplicits { + + implicit final class kevent64_sOps(kevent64_s: Ptr[kevent64_s]) { + def ident: CUnsignedLongInt = !(kevent64_s.asInstanceOf[Ptr[CUnsignedLongInt]]) + def ident_=(ident: CUnsignedLongInt): Unit = + !(kevent64_s.asInstanceOf[Ptr[CUnsignedLongInt]]) = ident + + def filter: CShort = !(kevent64_s.asInstanceOf[Ptr[CShort]] + 4) + def filter_=(filter: CShort): Unit = + !(kevent64_s.asInstanceOf[Ptr[CShort]] + 4) = filter + + def flags: CUnsignedShort = !(kevent64_s.asInstanceOf[Ptr[CUnsignedShort]] + 5) + def flags_=(flags: CUnsignedShort): Unit = + !(kevent64_s.asInstanceOf[Ptr[CUnsignedShort]] + 5) = flags + + def data: CLong = !(kevent64_s.asInstanceOf[Ptr[CLong]] + 2) + + def udata: Ptr[Byte] = !(kevent64_s.asInstanceOf[Ptr[Ptr[Byte]]] + 3) + def udata_=(udata: Ptr[Byte]): Unit = + !(kevent64_s.asInstanceOf[Ptr[Ptr[Byte]]] + 3) = udata + } + + implicit val kevent64_sTag: Tag[kevent64_s] = + Tag.materializeCArrayTag[Byte, Nat.Digit2[Nat._4, Nat._8]].asInstanceOf[Tag[kevent64_s]] + } +} diff --git a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala index d8db322fa9..f4eaef586f 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala @@ -31,7 +31,7 @@ abstract class PollingExecutorScheduler(pollEvery: Int) type Poller = outer.type def makePoller(): Poller = outer def close(poller: Poller): Unit = () - def poll(poller: Poller, nanos: Long): Boolean = + def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = if (nanos == -1) outer.poll(Duration.Inf) else outer.poll(nanos.nanos) } ) diff --git a/core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index e4a53b5eb6..6f1cdef6ae 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -36,6 +36,6 @@ abstract class PollingSystem { * @return * whether poll should be called again (i.e., there are more events to be polled) */ - def poll(poller: Poller, nanos: Long): Boolean + def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean } diff --git a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala index 247321cae1..d1e0b1c399 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -25,7 +25,7 @@ object SleepSystem extends PollingSystem { def close(poller: Poller): Unit = () - def poll(poller: Poller, nanos: Long): Boolean = { + def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = { if (nanos > 0) Thread.sleep(nanos / 1000000, (nanos % 1000000).toInt) false From 08894264c1ddbd6ec4e74dc3edc0656200266d68 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 11 Dec 2022 08:51:04 +0000 Subject: [PATCH 006/429] Add test for `Scheduler#sleep` --- .../src/test/scala/cats/effect/unsafe/SchedulerSpec.scala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/native/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala b/tests/native/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala index c55d919868..9031819c19 100644 --- a/tests/native/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala +++ b/tests/native/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala @@ -17,6 +17,8 @@ package cats.effect package unsafe +import scala.concurrent.duration._ + class SchedulerSpec extends BaseSpec { "Default scheduler" should { @@ -27,12 +29,18 @@ class SchedulerSpec extends BaseSpec { deltas = times.map(_ - start) } yield deltas.exists(_.toMicros % 1000 != 0) } + "correctly calculate real time" in real { IO.realTime.product(IO(System.currentTimeMillis())).map { case (realTime, currentTime) => (realTime.toMillis - currentTime) should be_<=(1L) } } + + "sleep for correct duration" in real { + val duration = 1500.millis + IO.sleep(duration).timed.map(_._1 should be_>=(duration)) + } } } From 4250049131bc98efa945b2817945758b0d2158a3 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 11 Dec 2022 10:03:07 +0000 Subject: [PATCH 007/429] Add `FileDescriptorPollerSpec` Co-authored-by: Lorenzo Gabriele --- .../unsafe/FileDescriptorPollerSpec.scala | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala diff --git a/tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala b/tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala new file mode 100644 index 0000000000..503fea32b4 --- /dev/null +++ b/tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala @@ -0,0 +1,88 @@ +/* + * Copyright 2020-2022 Typelevel + * + * 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 +package unsafe + +import cats.effect.std.{Dispatcher, Queue} +import cats.syntax.all._ + +import scala.scalanative.libc.errno._ +import scala.scalanative.posix.string._ +import scala.scalanative.posix.unistd._ +import scala.scalanative.unsafe._ +import scala.scalanative.unsigned._ + +import java.io.IOException + +class FileDescriptorPollerSpec extends BaseSpec { + + def mkPipe: Resource[IO, (Int, Int)] = + Resource.make { + IO { + val fd = stackalloc[CInt](2) + if (pipe(fd) != 0) + throw new IOException(fromCString(strerror(errno))) + else + (fd(0), fd(1)) + } + } { + case (fd0, fd1) => + IO { + close(fd0) + close(fd1) + () + } + } + + def onRead(poller: FileDescriptorPoller, fd: Int, cb: IO[Unit]): Resource[IO, Unit] = + Dispatcher + .sequential[IO] + .flatMap { dispatcher => + Resource.make { + IO { + poller.registerFileDescriptor(fd, true, false) { (readReady, _) => + dispatcher.unsafeRunAndForget(cb.whenA(readReady)) + } + } + }(unregister => IO(unregister.run())) + } + .void + + "FileDescriptorPoller" should { + "notify read-ready events" in real { + mkPipe.use { + case (readFd, writeFd) => + IO.eventLoop[FileDescriptorPoller].map(_.get.poller()).flatMap { poller => + Queue.unbounded[IO, Unit].flatMap { queue => + onRead(poller, readFd, queue.offer(())).surround { + for { + buf <- IO(new Array[Byte](4)) + _ <- IO(write(writeFd, Array[Byte](1, 2, 3).at(0), 3.toULong)) + _ <- queue.take + _ <- IO(read(readFd, buf.at(0), 3.toULong)) + _ <- IO(write(writeFd, Array[Byte](42).at(0), 1.toULong)) + _ <- queue.take + _ <- IO(read(readFd, buf.at(3), 1.toULong)) + } yield buf.toList must be_==(List[Byte](1, 2, 3, 42)) + } + } + } + } + } + } + +} From 0b9ac0223edcb1f5ee32fd6895dbbf391b19f4ed Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 11 Dec 2022 21:03:22 +0000 Subject: [PATCH 008/429] Consistent error-handling in `KqueueSystem` --- .../src/main/scala/cats/effect/unsafe/KqueueSystem.scala | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index 4b40ff8bb7..7b9a31d64c 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -102,11 +102,7 @@ final class KqueueSystem private (maxEvents: Int) extends PollingSystem { if ((event.flags.toLong & EV_ERROR) != 0) { // TODO it would be interesting to propagate this failure via the callback - reportFailure( - new RuntimeException( - s"kevent64: flags=${event.flags.toHexString} errno=${event.data}" - ) - ) + reportFailure(new IOException(fromCString(strerror(event.data.toInt)))) } else if (callbacks.contains(event.ident.toLong)) { val filter = event.filter From 956734ab8cda9346622e6ebe10e43c807aadcb37 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 11 Dec 2022 22:53:52 +0000 Subject: [PATCH 009/429] Make `pollingSystem` configurable in `IOApp` --- .../src/main/scala/cats/effect/IOApp.scala | 24 ++++++++++++++----- .../unsafe/IORuntimeCompanionPlatform.scala | 3 +++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/IOApp.scala b/core/native/src/main/scala/cats/effect/IOApp.scala index 182fc874b9..1623a3ba55 100644 --- a/core/native/src/main/scala/cats/effect/IOApp.scala +++ b/core/native/src/main/scala/cats/effect/IOApp.scala @@ -20,6 +20,7 @@ import cats.effect.metrics.NativeCpuStarvationMetrics import scala.concurrent.CancellationException import scala.concurrent.duration._ +import scala.scalanative.meta.LinktimeInfo /** * The primary entry point to a Cats Effect application. Extend this trait rather than defining @@ -165,6 +166,21 @@ trait IOApp { */ protected def runtimeConfig: unsafe.IORuntimeConfig = unsafe.IORuntimeConfig() + /** + * The [[unsafe.PollingSystem]] used by the [[runtime]] which will evaluate the [[IO]] + * produced by `run`. It is very unlikely that users will need to override this method. + * + * [[unsafe.PollingSystem]] implementors may provide their own flavors of [[IOApp]] that + * override this method. + */ + protected def pollingSystem: unsafe.PollingSystem = + if (LinktimeInfo.isLinux) + unsafe.EpollSystem(64) + else if (LinktimeInfo.isMac) + unsafe.KqueueSystem(64) + else + unsafe.SleepSystem + /** * 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 @@ -186,12 +202,8 @@ trait IOApp { import unsafe.IORuntime val installed = IORuntime installGlobal { - IORuntime( - IORuntime.defaultComputeExecutionContext, - IORuntime.defaultComputeExecutionContext, - IORuntime.defaultScheduler, - () => (), - runtimeConfig) + val loop = IORuntime.createEventLoop(pollingSystem) + IORuntime(loop, loop, loop, () => (), runtimeConfig) } _runtime = IORuntime.global diff --git a/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala index 99c4d303a0..78c1594cfe 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala @@ -24,6 +24,9 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type def defaultScheduler: Scheduler = EventLoopExecutorScheduler.global + def createEventLoop(system: PollingSystem): ExecutionContext with Scheduler = + new EventLoopExecutorScheduler(64, system) + private[this] var _global: IORuntime = null private[effect] def installGlobal(global: => IORuntime): Boolean = { From dda54b7f9dea483d7e2164a6a864aa19cd414dda Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 12 Dec 2022 00:01:20 +0000 Subject: [PATCH 010/429] Nowarn unuseds --- .../native/src/main/scala/cats/effect/unsafe/EpollSystem.scala | 3 +++ .../src/main/scala/cats/effect/unsafe/KqueueSystem.scala | 3 +++ 2 files changed, 6 insertions(+) diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index e3728a94df..db33cc5d5f 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -17,6 +17,8 @@ package cats.effect package unsafe +import org.typelevel.scalaccompat.annotation._ + import scala.scalanative.libc.errno._ import scala.scalanative.posix.string._ import scala.scalanative.posix.unistd @@ -113,6 +115,7 @@ final class EpollSystem private (maxEvents: Int) extends PollingSystem { object EpollSystem { def apply(maxEvents: Int): EpollSystem = new EpollSystem(maxEvents) + @nowarn212 @extern private[unsafe] object epoll { diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index 7b9a31d64c..d0e5f3873b 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -17,6 +17,8 @@ package cats.effect package unsafe +import org.typelevel.scalaccompat.annotation._ + import scala.collection.mutable.LongMap import scala.scalanative.libc.errno._ import scala.scalanative.posix.string._ @@ -177,6 +179,7 @@ object KqueueSystem { def cancel() = canceled = true } + @nowarn212 @extern private[unsafe] object event { // Derived from https://opensource.apple.com/source/xnu/xnu-7195.81.3/bsd/sys/event.h.auto.html From 1206b273c7eaddb335b463b9f32dc43d241035e5 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 12 Dec 2022 19:29:27 +0000 Subject: [PATCH 011/429] Revise the fd poller spec --- .../effect/unsafe/FileDescriptorPollerSpec.scala | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala b/tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala index 503fea32b4..ba9db40356 100644 --- a/tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala +++ b/tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala @@ -48,13 +48,13 @@ class FileDescriptorPollerSpec extends BaseSpec { } } - def onRead(poller: FileDescriptorPoller, fd: Int, cb: IO[Unit]): Resource[IO, Unit] = + def onRead(loop: EventLoop[FileDescriptorPoller], fd: Int, cb: IO[Unit]): Resource[IO, Unit] = Dispatcher .sequential[IO] .flatMap { dispatcher => Resource.make { IO { - poller.registerFileDescriptor(fd, true, false) { (readReady, _) => + loop.poller().registerFileDescriptor(fd, true, false) { (readReady, _) => dispatcher.unsafeRunAndForget(cb.whenA(readReady)) } } @@ -66,17 +66,17 @@ class FileDescriptorPollerSpec extends BaseSpec { "notify read-ready events" in real { mkPipe.use { case (readFd, writeFd) => - IO.eventLoop[FileDescriptorPoller].map(_.get.poller()).flatMap { poller => + IO.eventLoop[FileDescriptorPoller].map(_.get).flatMap { loop => Queue.unbounded[IO, Unit].flatMap { queue => - onRead(poller, readFd, queue.offer(())).surround { + onRead(loop, readFd, queue.offer(())).surround { for { buf <- IO(new Array[Byte](4)) _ <- IO(write(writeFd, Array[Byte](1, 2, 3).at(0), 3.toULong)) - _ <- queue.take - _ <- IO(read(readFd, buf.at(0), 3.toULong)) + .background + .surround(queue.take *> IO(read(readFd, buf.at(0), 3.toULong))) _ <- IO(write(writeFd, Array[Byte](42).at(0), 1.toULong)) - _ <- queue.take - _ <- IO(read(readFd, buf.at(3), 1.toULong)) + .background + .surround(queue.take *> IO(read(readFd, buf.at(3), 1.toULong))) } yield buf.toList must be_==(List[Byte](1, 2, 3, 42)) } } From e0c4ec33962551b2b241562345be8f2010517d70 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 12 Dec 2022 20:05:05 +0000 Subject: [PATCH 012/429] Remove `maxEvents` config from `EpollSystem` --- .../src/main/scala/cats/effect/IOApp.scala | 2 +- .../cats/effect/unsafe/EpollSystem.scala | 75 ++++++++++--------- .../unsafe/EventLoopExecutorScheduler.scala | 2 +- 3 files changed, 43 insertions(+), 36 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/IOApp.scala b/core/native/src/main/scala/cats/effect/IOApp.scala index 1623a3ba55..9e979d8723 100644 --- a/core/native/src/main/scala/cats/effect/IOApp.scala +++ b/core/native/src/main/scala/cats/effect/IOApp.scala @@ -175,7 +175,7 @@ trait IOApp { */ protected def pollingSystem: unsafe.PollingSystem = if (LinktimeInfo.isLinux) - unsafe.EpollSystem(64) + unsafe.EpollSystem else if (LinktimeInfo.isMac) unsafe.KqueueSystem(64) else diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index db33cc5d5f..1e327f303e 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -19,6 +19,7 @@ package unsafe import org.typelevel.scalaccompat.annotation._ +import scala.annotation.tailrec import scala.scalanative.libc.errno._ import scala.scalanative.posix.string._ import scala.scalanative.posix.unistd @@ -29,16 +30,18 @@ import scala.util.control.NonFatal import java.io.IOException import java.util.{Collections, IdentityHashMap, Set} -import EpollSystem.epoll._ -import EpollSystem.epollImplicits._ +object EpollSystem extends PollingSystem { -final class EpollSystem private (maxEvents: Int) extends PollingSystem { + import epoll._ + import epollImplicits._ + + private[this] final val MaxEvents = 64 def makePoller(): Poller = { val fd = epoll_create1(0) if (fd == -1) throw new IOException(fromCString(strerror(errno))) - new Poller(fd, maxEvents) + new Poller(fd) } def close(poller: Poller): Unit = poller.close() @@ -46,8 +49,7 @@ final class EpollSystem private (maxEvents: Int) extends PollingSystem { def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = poller.poll(nanos, reportFailure) - final class Poller private[EpollSystem] (private[EpollSystem] val epfd: Int, maxEvents: Int) - extends FileDescriptorPoller { + final class Poller private[EpollSystem] (epfd: Int) extends FileDescriptorPoller { private[this] val callbacks: Set[FileDescriptorPoller.Callback] = Collections.newSetFromMap(new IdentityHashMap) @@ -62,31 +64,41 @@ final class EpollSystem private (maxEvents: Int) extends PollingSystem { if (timeout <= 0 && noCallbacks) false // nothing to do here else { - val timeoutMillis = if (timeout == -1) -1 else (timeout / 1000000).toInt - - val events = stackalloc[epoll_event](maxEvents.toUInt) - - val triggeredEvents = epoll_wait(epfd, events, maxEvents, timeoutMillis) - - if (triggeredEvents >= 0) { - var i = 0 - while (i < triggeredEvents) { - val event = events + i.toLong - val cb = FileDescriptorPoller.Callback.fromPtr(event.data) - try { - val e = event.events.toInt - val readReady = (e & EPOLLIN) != 0 - val writeReady = (e & EPOLLOUT) != 0 - cb.notifyFileDescriptorEvents(readReady, writeReady) - } catch { - case ex if NonFatal(ex) => reportFailure(ex) + val events = stackalloc[epoll_event](MaxEvents.toLong) + + @tailrec + def processEvents(timeout: Int): Unit = { + + val triggeredEvents = epoll_wait(epfd, events, MaxEvents, timeout) + + if (triggeredEvents >= 0) { + var i = 0 + while (i < triggeredEvents) { + val event = events + i.toLong + val cb = FileDescriptorPoller.Callback.fromPtr(event.data) + try { + val e = event.events.toInt + val readReady = (e & EPOLLIN) != 0 + val writeReady = (e & EPOLLOUT) != 0 + cb.notifyFileDescriptorEvents(readReady, writeReady) + } catch { + case ex if NonFatal(ex) => reportFailure(ex) + } + i += 1 } - i += 1 + } else { + throw new IOException(fromCString(strerror(errno))) } - } else { - throw new IOException(fromCString(strerror(errno))) + + if (triggeredEvents >= MaxEvents) + processEvents(0) // drain the ready list + else + () } + val timeoutMillis = if (timeout == -1) -1 else (timeout / 1000000).toInt + processEvents(timeoutMillis) + !callbacks.isEmpty() } } @@ -110,14 +122,9 @@ final class EpollSystem private (maxEvents: Int) extends PollingSystem { } } -} - -object EpollSystem { - def apply(maxEvents: Int): EpollSystem = new EpollSystem(maxEvents) - @nowarn212 @extern - private[unsafe] object epoll { + private object epoll { final val EPOLL_CTL_ADD = 1 final val EPOLL_CTL_DEL = 2 @@ -140,7 +147,7 @@ object EpollSystem { } - private[unsafe] object epollImplicits { + private object epollImplicits { implicit final class epoll_eventOps(epoll_event: Ptr[epoll_event]) { def events: CUnsignedInt = !(epoll_event.asInstanceOf[Ptr[CUnsignedInt]]) diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala index 9b1b55b271..e968c7e295 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala @@ -149,7 +149,7 @@ private object EventLoopExecutorScheduler { lazy val global = { val system = if (LinktimeInfo.isLinux) - EpollSystem(64) + EpollSystem else if (LinktimeInfo.isMac) KqueueSystem(64) else From eeeb3e65b9baf9030ae1c9361fc02173190c7b53 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 12 Dec 2022 20:26:35 +0000 Subject: [PATCH 013/429] Remove `maxEvents` config from `KqueueSystem` --- .../src/main/scala/cats/effect/IOApp.scala | 2 +- .../unsafe/EventLoopExecutorScheduler.scala | 2 +- .../cats/effect/unsafe/KqueueSystem.scala | 107 ++++++++++-------- 3 files changed, 64 insertions(+), 47 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/IOApp.scala b/core/native/src/main/scala/cats/effect/IOApp.scala index 9e979d8723..1fc67b36e6 100644 --- a/core/native/src/main/scala/cats/effect/IOApp.scala +++ b/core/native/src/main/scala/cats/effect/IOApp.scala @@ -177,7 +177,7 @@ trait IOApp { if (LinktimeInfo.isLinux) unsafe.EpollSystem else if (LinktimeInfo.isMac) - unsafe.KqueueSystem(64) + unsafe.KqueueSystem else unsafe.SleepSystem diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala index e968c7e295..5c7fe6517e 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala @@ -151,7 +151,7 @@ private object EventLoopExecutorScheduler { if (LinktimeInfo.isLinux) EpollSystem else if (LinktimeInfo.isMac) - KqueueSystem(64) + KqueueSystem else SleepSystem new EventLoopExecutorScheduler(64, system) diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index d0e5f3873b..db8dc4e01f 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -19,6 +19,7 @@ package unsafe import org.typelevel.scalaccompat.annotation._ +import scala.annotation.tailrec import scala.collection.mutable.LongMap import scala.scalanative.libc.errno._ import scala.scalanative.posix.string._ @@ -32,17 +33,18 @@ import scala.util.control.NonFatal import java.io.IOException import java.util.ArrayDeque -import KqueueSystem.EvAdd -import KqueueSystem.event._ -import KqueueSystem.eventImplicits._ +final object KqueueSystem extends PollingSystem { -final class KqueueSystem private (maxEvents: Int) extends PollingSystem { + import event._ + import eventImplicits._ + + private final val MaxEvents = 64 def makePoller(): Poller = { val fd = kqueue() if (fd == -1) throw new IOException(fromCString(strerror(errno))) - new Poller(fd, maxEvents) + new Poller(fd) } def close(poller: Poller): Unit = poller.close() @@ -50,8 +52,7 @@ final class KqueueSystem private (maxEvents: Int) extends PollingSystem { def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = poller.poll(nanos, reportFailure) - final class Poller private[KqueueSystem] (private[KqueueSystem] val kqfd: Int, maxEvents: Int) - extends FileDescriptorPoller { + final class Poller private[KqueueSystem] (kqfd: Int) extends FileDescriptorPoller { private[this] val changes: ArrayDeque[EvAdd] = new ArrayDeque private[this] val callbacks: LongMap[FileDescriptorPoller.Callback] = new LongMap @@ -83,6 +84,56 @@ final class KqueueSystem private (maxEvents: Int) extends PollingSystem { false // nothing to do here else { + val eventlist = stackalloc[kevent64_s](MaxEvents.toLong) + + @tailrec + def processEvents(timeout: Ptr[timespec], changeCount: Int, flags: Int): Unit = { + + val triggeredEvents = + kevent64( + kqfd, + changelist, + changeCount, + eventlist, + MaxEvents, + flags.toUInt, + timeout + ) + + if (triggeredEvents >= 0) { + var i = 0 + var event = eventlist + while (i < triggeredEvents) { + if ((event.flags.toLong & EV_ERROR) != 0) { + + // TODO it would be interesting to propagate this failure via the callback + reportFailure(new IOException(fromCString(strerror(event.data.toInt)))) + + } else if (callbacks.contains(event.ident.toLong)) { + val filter = event.filter + val cb = FileDescriptorPoller.Callback.fromPtr(event.udata) + + try { + cb.notifyFileDescriptorEvents(filter == EVFILT_READ, filter == EVFILT_WRITE) + } catch { + case NonFatal(ex) => + reportFailure(ex) + } + } + + i += 1 + event += 1 + } + } else { + throw new IOException(fromCString(strerror(errno))) + } + + if (triggeredEvents >= MaxEvents) + processEvents(null, 0, KEVENT_FLAG_NONE) // drain the ready list + else + () + } + val timeoutSpec = if (timeout <= 0) null else { @@ -92,38 +143,9 @@ final class KqueueSystem private (maxEvents: Int) extends PollingSystem { ts } - val eventlist = stackalloc[kevent64_s](maxEvents.toLong) - val flags = (if (timeout == 0) KEVENT_FLAG_IMMEDIATE else KEVENT_FLAG_NONE).toUInt - val triggeredEvents = - kevent64(kqfd, changelist, changeCount, eventlist, maxEvents, flags, timeoutSpec) - - if (triggeredEvents >= 0) { - var i = 0 - var event = eventlist - while (i < triggeredEvents) { - if ((event.flags.toLong & EV_ERROR) != 0) { - - // TODO it would be interesting to propagate this failure via the callback - reportFailure(new IOException(fromCString(strerror(event.data.toInt)))) - - } else if (callbacks.contains(event.ident.toLong)) { - val filter = event.filter - val cb = FileDescriptorPoller.Callback.fromPtr(event.udata) - - try { - cb.notifyFileDescriptorEvents(filter == EVFILT_READ, filter == EVFILT_WRITE) - } catch { - case NonFatal(ex) => - reportFailure(ex) - } - } + val flags = if (timeout == 0) KEVENT_FLAG_IMMEDIATE else KEVENT_FLAG_NONE - i += 1 - event += 1 - } - } else { - throw new IOException(fromCString(strerror(errno))) - } + processEvents(timeoutSpec, changeCount, flags) !changes.isEmpty() || callbacks.nonEmpty } @@ -165,11 +187,6 @@ final class KqueueSystem private (maxEvents: Int) extends PollingSystem { } -} - -object KqueueSystem { - def apply(maxEvents: Int): KqueueSystem = new KqueueSystem(maxEvents) - private final class EvAdd( val fd: Int, val filter: Short, @@ -181,7 +198,7 @@ object KqueueSystem { @nowarn212 @extern - private[unsafe] object event { + private object event { // Derived from https://opensource.apple.com/source/xnu/xnu-7195.81.3/bsd/sys/event.h.auto.html final val EVFILT_READ = -1 @@ -211,7 +228,7 @@ object KqueueSystem { } - private[unsafe] object eventImplicits { + private object eventImplicits { implicit final class kevent64_sOps(kevent64_s: Ptr[kevent64_s]) { def ident: CUnsignedLongInt = !(kevent64_s.asInstanceOf[Ptr[CUnsignedLongInt]]) From c4a0a163473308c47eab74c12c9206e96b4460ef Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 12 Dec 2022 20:43:20 +0000 Subject: [PATCH 014/429] Add test for many simultaneous events --- .../unsafe/FileDescriptorPollerSpec.scala | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala b/tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala index ba9db40356..2083cb1370 100644 --- a/tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala +++ b/tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala @@ -17,7 +17,7 @@ package cats.effect package unsafe -import cats.effect.std.{Dispatcher, Queue} +import cats.effect.std.{CountDownLatch, Dispatcher, Queue} import cats.syntax.all._ import scala.scalanative.libc.errno._ @@ -30,20 +30,22 @@ import java.io.IOException class FileDescriptorPollerSpec extends BaseSpec { - def mkPipe: Resource[IO, (Int, Int)] = + case class Pipe(readFd: Int, writeFd: Int) + + def mkPipe: Resource[IO, Pipe] = Resource.make { IO { val fd = stackalloc[CInt](2) if (pipe(fd) != 0) throw new IOException(fromCString(strerror(errno))) else - (fd(0), fd(1)) + Pipe(fd(0), fd(1)) } } { - case (fd0, fd1) => + case Pipe(readFd, writeFd) => IO { - close(fd0) - close(fd1) + close(readFd) + close(writeFd) () } } @@ -63,9 +65,10 @@ class FileDescriptorPollerSpec extends BaseSpec { .void "FileDescriptorPoller" should { + "notify read-ready events" in real { mkPipe.use { - case (readFd, writeFd) => + case Pipe(readFd, writeFd) => IO.eventLoop[FileDescriptorPoller].map(_.get).flatMap { loop => Queue.unbounded[IO, Unit].flatMap { queue => onRead(loop, readFd, queue.offer(())).surround { @@ -83,6 +86,20 @@ class FileDescriptorPollerSpec extends BaseSpec { } } } + + "handle lots of simultaneous events" in real { + mkPipe.replicateA(1000).use { pipes => + IO.eventLoop[FileDescriptorPoller].map(_.get).flatMap { loop => + CountDownLatch[IO](1000).flatMap { latch => + pipes.traverse_(pipe => onRead(loop, pipe.readFd, latch.release)).surround { + IO { // trigger all the pipes at once + pipes.foreach(pipe => write(pipe.writeFd, Array[Byte](42).at(0), 1.toULong)) + }.background.surround(latch.await.as(true)) + } + } + } + } + } } } From aae6e972724fcb7893f26db8e9e33e69ec57575c Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 12 Dec 2022 21:24:16 +0000 Subject: [PATCH 015/429] Remove redundant `final` --- .../native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index db8dc4e01f..bf6f660a56 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -33,7 +33,7 @@ import scala.util.control.NonFatal import java.io.IOException import java.util.ArrayDeque -final object KqueueSystem extends PollingSystem { +object KqueueSystem extends PollingSystem { import event._ import eventImplicits._ From 457f89c04735feb79ae9410b1c3e777d18f72ef3 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 12 Dec 2022 23:07:29 +0000 Subject: [PATCH 016/429] Update comment --- .../src/main/scala/cats/effect/unsafe/PollingSystem.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index 6f1cdef6ae..56c0f2a50f 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -28,10 +28,10 @@ abstract class PollingSystem { /** * @param nanos * the maximum duration for which to block, where `nanos == -1` indicates to block - * indefinitely. ''However'', if `timeout == -1` and there are no remaining events to poll + * indefinitely. ''However'', if `nanos == -1` and there are no remaining events to poll * for, this method should return `false` immediately. This is unfortunate but necessary so * that the `EventLoop` can yield to the Scala Native global `ExecutionContext` which is - * currently hard-coded into every test framework, including JUnit, MUnit, and specs2. + * currently hard-coded into every test framework, including MUnit, specs2, and Weaver. * * @return * whether poll should be called again (i.e., there are more events to be polled) From 721c2fc46df3e0bdea1c3b7a08a913c14621150b Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 12 Dec 2022 23:31:29 +0000 Subject: [PATCH 017/429] Add test for pre-existing readiness --- .../effect/unsafe/FileDescriptorPollerSpec.scala | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala b/tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala index 2083cb1370..6439a0fe0b 100644 --- a/tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala +++ b/tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala @@ -100,6 +100,20 @@ class FileDescriptorPollerSpec extends BaseSpec { } } } + + "notify of pre-existing readiness on registration" in real { + mkPipe.use { + case Pipe(readFd, writeFd) => + IO.eventLoop[FileDescriptorPoller].map(_.get).flatMap { loop => + val registerAndWait = IO.deferred[Unit].flatMap { gate => + onRead(loop, readFd, gate.complete(()).void).surround(gate.get) + } + + IO(write(writeFd, Array[Byte](42).at(0), 1.toULong)) *> + registerAndWait *> registerAndWait *> IO.pure(true) + } + } + } } } From 4f9e57b3ee59a9df859f133d1bf59e50473507d9 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 12 Dec 2022 23:48:29 +0000 Subject: [PATCH 018/429] Add test for no readiness --- .../effect/unsafe/FileDescriptorPollerSpec.scala | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala b/tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala index 6439a0fe0b..7145892f4a 100644 --- a/tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala +++ b/tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala @@ -20,6 +20,7 @@ package unsafe import cats.effect.std.{CountDownLatch, Dispatcher, Queue} import cats.syntax.all._ +import scala.concurrent.duration._ import scala.scalanative.libc.errno._ import scala.scalanative.posix.string._ import scala.scalanative.posix.unistd._ @@ -114,6 +115,19 @@ class FileDescriptorPollerSpec extends BaseSpec { } } } + + "not notify if not ready" in real { + mkPipe.use { + case Pipe(readFd, _) => + IO.eventLoop[FileDescriptorPoller].map(_.get).flatMap { loop => + val registerAndWait = IO.deferred[Unit].flatMap { gate => + onRead(loop, readFd, gate.complete(()).void).surround(gate.get) + } + + registerAndWait.as(false).timeoutTo(1.second, IO.pure(true)) + } + } + } } } From a520aee4c0dedebfcb6c9acc57d3ffe051690e3a Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 19 Dec 2022 05:16:48 +0000 Subject: [PATCH 019/429] Reimagine `FileDescriptorPoller` --- .../cats/effect/FileDescriptorPoller.scala | 60 +++++++++++++++++ .../effect/unsafe/FileDescriptorPoller.scala | 64 ------------------- 2 files changed, 60 insertions(+), 64 deletions(-) create mode 100644 core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala delete mode 100644 core/native/src/main/scala/cats/effect/unsafe/FileDescriptorPoller.scala diff --git a/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala b/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala new file mode 100644 index 0000000000..535d2c041b --- /dev/null +++ b/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala @@ -0,0 +1,60 @@ +/* + * Copyright 2020-2022 Typelevel + * + * 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 + +import scala.scalanative.annotation.alwaysinline +import scala.scalanative.runtime._ +import scala.scalanative.unsafe._ + +trait FileDescriptorPoller { + + /** + * Registers a file descriptor with the poller and monitors read- and/or write-ready events. + */ + def registerFileDescriptor( + fileDescriptor: Int, + read: Boolean, + monitorWrites: Boolean + ): Resource[IO, FileDescriptorPollHandle] + +} + +trait FileDescriptorPollHandle { + + /** + * Recursively invokes `f` until it is no longer blocked. Typically `f` will call `read` or + * `recv` on the file descriptor. + * - If `f` fails because the file descriptor is blocked, then it should return `Left[A]`. + * Then `f` will be invoked again with `A` at a later point, when the file handle is ready + * for reading. + * - If `f` is successful, then it should return a `Right[B]`. The `IO` returned from this + * method will complete with `B`. + */ + def pollReadRec[A, B](a: A)(f: A => IO[Either[A, B]]): IO[B] + + /** + * Recursively invokes `f` until it is no longer blocked. Typically `f` will call `write` or + * `send` on the file descriptor. + * - If `f` fails because the file descriptor is blocked, then it should return `Left[A]`. + * Then `f` will be invoked again with `A` at a later point, when the file handle is ready + * for writing. + * - If `f` is successful, then it should return a `Right[B]`. The `IO` returned from this + * method will complete with `B`. + */ + def pollWriteRec[A, B](a: A)(f: A => IO[Either[A, B]]): IO[B] + +} diff --git a/core/native/src/main/scala/cats/effect/unsafe/FileDescriptorPoller.scala b/core/native/src/main/scala/cats/effect/unsafe/FileDescriptorPoller.scala deleted file mode 100644 index df8a0e3e00..0000000000 --- a/core/native/src/main/scala/cats/effect/unsafe/FileDescriptorPoller.scala +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2020-2022 Typelevel - * - * 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 -package unsafe - -import scala.scalanative.annotation.alwaysinline -import scala.scalanative.runtime._ -import scala.scalanative.unsafe._ - -trait FileDescriptorPoller { - - /** - * Registers a callback to be notified of read- and write-ready events on a file descriptor. - * Produces a runnable which unregisters the file descriptor. - * - * 1. It is the responsibility of the caller to set the file descriptor to non-blocking - * mode. - * 1. It is the responsibility of the caller to unregister the file descriptor when they are - * done. - * 1. A file descriptor should be registered at most once. To modify a registration, you - * must unregister and re-register the file descriptor. - * 1. The callback may be invoked "spuriously" claiming that a file descriptor is read- or - * write-ready when in fact it is not. You should be prepared to handle this. - * 1. The callback will be invoked at least once when the file descriptor transitions from - * blocked to read- or write-ready. You may additionally receive zero or more reminders - * of its readiness. However, you should not rely on any further callbacks until after - * the file descriptor has become blocked again. - */ - def registerFileDescriptor( - fileDescriptor: Int, - readReadyEvents: Boolean, - writeReadyEvents: Boolean)( - cb: FileDescriptorPoller.Callback - ): Runnable - -} - -object FileDescriptorPoller { - trait Callback { - def notifyFileDescriptorEvents(readReady: Boolean, writeReady: Boolean): Unit - } - - object Callback { - @alwaysinline private[unsafe] def toPtr(cb: Callback): Ptr[Byte] = - fromRawPtr(Intrinsics.castObjectToRawPtr(cb)) - - @alwaysinline private[unsafe] def fromPtr[A](ptr: Ptr[Byte]): Callback = - Intrinsics.castRawPtrToObject(toRawPtr(ptr)).asInstanceOf[Callback] - } -} From 5f8146b90d4fe535ced9a859788be0fac1d912f1 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 19 Dec 2022 07:05:27 +0000 Subject: [PATCH 020/429] Fix parameter names --- .../src/main/scala/cats/effect/FileDescriptorPoller.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala b/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala index 535d2c041b..d000caef7a 100644 --- a/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala +++ b/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala @@ -27,8 +27,8 @@ trait FileDescriptorPoller { */ def registerFileDescriptor( fileDescriptor: Int, - read: Boolean, - monitorWrites: Boolean + monitorReadReady: Boolean, + monitorWriteReady: Boolean ): Resource[IO, FileDescriptorPollHandle] } From 364060561dce34a701ad268d37919ee13d64f067 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 19 Dec 2022 20:24:18 +0000 Subject: [PATCH 021/429] Refactor/redesign `PollingSystem` ... again ... (: --- .../cats/effect/FileDescriptorPoller.scala | 4 - .../cats/effect/IOCompanionPlatform.scala | 2 +- .../cats/effect/unsafe/EpollSystem.scala | 140 +++++----- .../scala/cats/effect/unsafe/EventLoop.scala | 4 +- .../unsafe/EventLoopExecutorScheduler.scala | 8 +- .../cats/effect/unsafe/KqueueSystem.scala | 248 +++++++++--------- .../unsafe/PollingExecutorScheduler.scala | 12 +- .../cats/effect/unsafe/PollingSystem.scala | 18 +- .../cats/effect/unsafe/SleepSystem.scala | 13 +- 9 files changed, 238 insertions(+), 211 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala b/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala index d000caef7a..72604bbe66 100644 --- a/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala +++ b/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala @@ -16,10 +16,6 @@ package cats.effect -import scala.scalanative.annotation.alwaysinline -import scala.scalanative.runtime._ -import scala.scalanative.unsafe._ - trait FileDescriptorPoller { /** diff --git a/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala b/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala index fb4e2a1875..05070cc214 100644 --- a/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala +++ b/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala @@ -68,7 +68,7 @@ private[effect] abstract class IOCompanionPlatform { this: IO.type => def eventLoop[Poller](implicit ct: ClassTag[Poller]): IO[Option[EventLoop[Poller]]] = IO.executionContext.map { - case loop: EventLoop[_] if ct.runtimeClass.isInstance(loop.poller()) => + case loop: EventLoop[_] if ct.runtimeClass.isInstance(loop.poller) => Some(loop.asInstanceOf[EventLoop[Poller]]) case _ => None } diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index 1e327f303e..7c6cfd3be6 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -20,6 +20,7 @@ package unsafe import org.typelevel.scalaccompat.annotation._ import scala.annotation.tailrec +import scala.concurrent.ExecutionContext import scala.scalanative.libc.errno._ import scala.scalanative.posix.string._ import scala.scalanative.posix.unistd @@ -37,89 +38,94 @@ object EpollSystem extends PollingSystem { private[this] final val MaxEvents = 64 - def makePoller(): Poller = { + def makePoller(ec: ExecutionContext, data: () => PollData): Poller = new Poller + + def makePollData(): PollData = { val fd = epoll_create1(0) if (fd == -1) throw new IOException(fromCString(strerror(errno))) - new Poller(fd) + new PollData(fd) } - def close(poller: Poller): Unit = poller.close() + def closePollData(data: PollData): Unit = data.close() + + def poll(data: PollData, nanos: Long, reportFailure: Throwable => Unit): Boolean = + data.poll(nanos, reportFailure) - def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = - poller.poll(nanos, reportFailure) + final class Poller private[EpollSystem] () - final class Poller private[EpollSystem] (epfd: Int) extends FileDescriptorPoller { + final class PollData private[EpollSystem] (epfd: Int) { - private[this] val callbacks: Set[FileDescriptorPoller.Callback] = - Collections.newSetFromMap(new IdentityHashMap) + // private[this] val callbacks: Set[FileDescriptorPoller.Callback] = + // Collections.newSetFromMap(new IdentityHashMap) private[EpollSystem] def close(): Unit = if (unistd.close(epfd) != 0) throw new IOException(fromCString(strerror(errno))) private[EpollSystem] def poll(timeout: Long, reportFailure: Throwable => Unit): Boolean = { - val noCallbacks = callbacks.isEmpty() - - if (timeout <= 0 && noCallbacks) - false // nothing to do here - else { - val events = stackalloc[epoll_event](MaxEvents.toLong) - - @tailrec - def processEvents(timeout: Int): Unit = { - - val triggeredEvents = epoll_wait(epfd, events, MaxEvents, timeout) - - if (triggeredEvents >= 0) { - var i = 0 - while (i < triggeredEvents) { - val event = events + i.toLong - val cb = FileDescriptorPoller.Callback.fromPtr(event.data) - try { - val e = event.events.toInt - val readReady = (e & EPOLLIN) != 0 - val writeReady = (e & EPOLLOUT) != 0 - cb.notifyFileDescriptorEvents(readReady, writeReady) - } catch { - case ex if NonFatal(ex) => reportFailure(ex) - } - i += 1 - } - } else { - throw new IOException(fromCString(strerror(errno))) - } - - if (triggeredEvents >= MaxEvents) - processEvents(0) // drain the ready list - else - () - } - - val timeoutMillis = if (timeout == -1) -1 else (timeout / 1000000).toInt - processEvents(timeoutMillis) - - !callbacks.isEmpty() - } + // val noCallbacks = callbacks.isEmpty() + + // if (timeout <= 0 && noCallbacks) + // false // nothing to do here + // else { + // val events = stackalloc[epoll_event](MaxEvents.toLong) + + // @tailrec + // def processEvents(timeout: Int): Unit = { + + // val triggeredEvents = epoll_wait(epfd, events, MaxEvents, timeout) + + // if (triggeredEvents >= 0) { + // var i = 0 + // while (i < triggeredEvents) { + // val event = events + i.toLong + // val cb = FileDescriptorPoller.Callback.fromPtr(event.data) + // try { + // val e = event.events.toInt + // val readReady = (e & EPOLLIN) != 0 + // val writeReady = (e & EPOLLOUT) != 0 + // cb.notifyFileDescriptorEvents(readReady, writeReady) + // } catch { + // case ex if NonFatal(ex) => reportFailure(ex) + // } + // i += 1 + // } + // } else { + // throw new IOException(fromCString(strerror(errno))) + // } + + // if (triggeredEvents >= MaxEvents) + // processEvents(0) // drain the ready list + // else + // () + // } + + // val timeoutMillis = if (timeout == -1) -1 else (timeout / 1000000).toInt + // processEvents(timeoutMillis) + + // !callbacks.isEmpty() + // } + ??? } - def registerFileDescriptor(fd: Int, reads: Boolean, writes: Boolean)( - cb: FileDescriptorPoller.Callback): Runnable = { - val event = stackalloc[epoll_event]() - event.events = - (EPOLLET | (if (reads) EPOLLIN else 0) | (if (writes) EPOLLOUT else 0)).toUInt - event.data = FileDescriptorPoller.Callback.toPtr(cb) - - if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, event) != 0) - throw new IOException(fromCString(strerror(errno))) - callbacks.add(cb) - - () => { - callbacks.remove(cb) - if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, null) != 0) - throw new IOException(fromCString(strerror(errno))) - } - } + // def registerFileDescriptor(fd: Int, reads: Boolean, writes: Boolean)( + // cb: FileDescriptorPoller.Callback): Runnable = { + // val event = stackalloc[epoll_event]() + // event.events = + // (EPOLLET | (if (reads) EPOLLIN else 0) | (if (writes) EPOLLOUT else 0)).toUInt + // event.data = FileDescriptorPoller.Callback.toPtr(cb) + + // if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, event) != 0) + // throw new IOException(fromCString(strerror(errno))) + // callbacks.add(cb) + + // () => { + // callbacks.remove(cb) + // if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, null) != 0) + // throw new IOException(fromCString(strerror(errno))) + // } + // } } @nowarn212 diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoop.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoop.scala index 78cd33cee6..23dd70969b 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EventLoop.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoop.scala @@ -19,8 +19,8 @@ package unsafe import scala.concurrent.ExecutionContext -trait EventLoop[Poller] extends ExecutionContext { +trait EventLoop[+Poller] extends ExecutionContext { - def poller(): Poller + def poller: Poller } diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala index 5c7fe6517e..c91dbe7f3f 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala @@ -31,7 +31,9 @@ private final class EventLoopExecutorScheduler(pollEvery: Int, system: PollingSy with ExecutionContextExecutor with Scheduler { - private[this] val _poller = system.makePoller() + private[this] val pollData = system.makePollData() + + val poller: Any = system.makePoller(this, () => pollData) private[this] var needsReschedule: Boolean = true @@ -80,8 +82,6 @@ private final class EventLoopExecutorScheduler(pollEvery: Int, system: PollingSy def monotonicNanos() = System.nanoTime() - def poller(): Any = _poller - private[this] def loop(): Unit = { needsReschedule = false @@ -120,7 +120,7 @@ private final class EventLoopExecutorScheduler(pollEvery: Int, system: PollingSy else -1 - val needsPoll = system.poll(_poller, timeout, reportFailure) + val needsPoll = system.poll(pollData, timeout, reportFailure) continue = needsPoll || !executeQueue.isEmpty() || !sleepQueue.isEmpty() } diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index bf6f660a56..47ddb9019d 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -20,6 +20,7 @@ package unsafe import org.typelevel.scalaccompat.annotation._ import scala.annotation.tailrec +import scala.concurrent.ExecutionContext import scala.collection.mutable.LongMap import scala.scalanative.libc.errno._ import scala.scalanative.posix.string._ @@ -40,157 +41,162 @@ object KqueueSystem extends PollingSystem { private final val MaxEvents = 64 - def makePoller(): Poller = { + def makePoller(ec: ExecutionContext, data: () => PollData): Poller = new Poller + + def makePollData(): PollData = { val fd = kqueue() if (fd == -1) throw new IOException(fromCString(strerror(errno))) - new Poller(fd) + new PollData(fd) } - def close(poller: Poller): Unit = poller.close() + def closePollData(data: PollData): Unit = data.close() + + def poll(data: PollData, nanos: Long, reportFailure: Throwable => Unit): Boolean = + data.poll(nanos, reportFailure) - def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = - poller.poll(nanos, reportFailure) + final class Poller private[KqueueSystem] () - final class Poller private[KqueueSystem] (kqfd: Int) extends FileDescriptorPoller { + final class PollData private[KqueueSystem] (kqfd: Int) { private[this] val changes: ArrayDeque[EvAdd] = new ArrayDeque - private[this] val callbacks: LongMap[FileDescriptorPoller.Callback] = new LongMap + // private[this] val callbacks: LongMap[FileDescriptorPoller.Callback] = new LongMap private[KqueueSystem] def close(): Unit = if (unistd.close(kqfd) != 0) throw new IOException(fromCString(strerror(errno))) private[KqueueSystem] def poll(timeout: Long, reportFailure: Throwable => Unit): Boolean = { - val noCallbacks = callbacks.isEmpty - - // pre-process the changes to filter canceled ones - val changelist = stackalloc[kevent64_s](changes.size().toLong) - var change = changelist - var changeCount = 0 - while (!changes.isEmpty()) { - val evAdd = changes.poll() - if (!evAdd.canceled) { - change.ident = evAdd.fd.toULong - change.filter = evAdd.filter - change.flags = (EV_ADD | EV_CLEAR).toUShort - change.udata = FileDescriptorPoller.Callback.toPtr(evAdd.cb) - change += 1 - changeCount += 1 - } - } - - if (timeout <= 0 && noCallbacks && changeCount == 0) - false // nothing to do here - else { - - val eventlist = stackalloc[kevent64_s](MaxEvents.toLong) - - @tailrec - def processEvents(timeout: Ptr[timespec], changeCount: Int, flags: Int): Unit = { - - val triggeredEvents = - kevent64( - kqfd, - changelist, - changeCount, - eventlist, - MaxEvents, - flags.toUInt, - timeout - ) - - if (triggeredEvents >= 0) { - var i = 0 - var event = eventlist - while (i < triggeredEvents) { - if ((event.flags.toLong & EV_ERROR) != 0) { - - // TODO it would be interesting to propagate this failure via the callback - reportFailure(new IOException(fromCString(strerror(event.data.toInt)))) - - } else if (callbacks.contains(event.ident.toLong)) { - val filter = event.filter - val cb = FileDescriptorPoller.Callback.fromPtr(event.udata) - - try { - cb.notifyFileDescriptorEvents(filter == EVFILT_READ, filter == EVFILT_WRITE) - } catch { - case NonFatal(ex) => - reportFailure(ex) - } - } - - i += 1 - event += 1 - } - } else { - throw new IOException(fromCString(strerror(errno))) - } - - if (triggeredEvents >= MaxEvents) - processEvents(null, 0, KEVENT_FLAG_NONE) // drain the ready list - else - () - } - - val timeoutSpec = - if (timeout <= 0) null - else { - val ts = stackalloc[timespec]() - ts.tv_sec = timeout / 1000000000 - ts.tv_nsec = timeout % 1000000000 - ts - } - - val flags = if (timeout == 0) KEVENT_FLAG_IMMEDIATE else KEVENT_FLAG_NONE - - processEvents(timeoutSpec, changeCount, flags) - - !changes.isEmpty() || callbacks.nonEmpty - } + // val noCallbacks = callbacks.isEmpty + + // // pre-process the changes to filter canceled ones + // val changelist = stackalloc[kevent64_s](changes.size().toLong) + // var change = changelist + // var changeCount = 0 + // while (!changes.isEmpty()) { + // val evAdd = changes.poll() + // if (!evAdd.canceled) { + // change.ident = evAdd.fd.toULong + // change.filter = evAdd.filter + // change.flags = (EV_ADD | EV_CLEAR).toUShort + // change.udata = FileDescriptorPoller.Callback.toPtr(evAdd.cb) + // change += 1 + // changeCount += 1 + // } + // } + + // if (timeout <= 0 && noCallbacks && changeCount == 0) + // false // nothing to do here + // else { + + // val eventlist = stackalloc[kevent64_s](MaxEvents.toLong) + + // @tailrec + // def processEvents(timeout: Ptr[timespec], changeCount: Int, flags: Int): Unit = { + + // val triggeredEvents = + // kevent64( + // kqfd, + // changelist, + // changeCount, + // eventlist, + // MaxEvents, + // flags.toUInt, + // timeout + // ) + + // if (triggeredEvents >= 0) { + // var i = 0 + // var event = eventlist + // while (i < triggeredEvents) { + // if ((event.flags.toLong & EV_ERROR) != 0) { + + // // TODO it would be interesting to propagate this failure via the callback + // reportFailure(new IOException(fromCString(strerror(event.data.toInt)))) + + // } else if (callbacks.contains(event.ident.toLong)) { + // val filter = event.filter + // val cb = FileDescriptorPoller.Callback.fromPtr(event.udata) + + // try { + // cb.notifyFileDescriptorEvents(filter == EVFILT_READ, filter == EVFILT_WRITE) + // } catch { + // case NonFatal(ex) => + // reportFailure(ex) + // } + // } + + // i += 1 + // event += 1 + // } + // } else { + // throw new IOException(fromCString(strerror(errno))) + // } + + // if (triggeredEvents >= MaxEvents) + // processEvents(null, 0, KEVENT_FLAG_NONE) // drain the ready list + // else + // () + // } + + // val timeoutSpec = + // if (timeout <= 0) null + // else { + // val ts = stackalloc[timespec]() + // ts.tv_sec = timeout / 1000000000 + // ts.tv_nsec = timeout % 1000000000 + // ts + // } + + // val flags = if (timeout == 0) KEVENT_FLAG_IMMEDIATE else KEVENT_FLAG_NONE + + // processEvents(timeoutSpec, changeCount, flags) + + // !changes.isEmpty() || callbacks.nonEmpty + // } + ??? } - def registerFileDescriptor(fd: Int, reads: Boolean, writes: Boolean)( - cb: FileDescriptorPoller.Callback): Runnable = { + // def registerFileDescriptor(fd: Int, reads: Boolean, writes: Boolean)( + // cb: FileDescriptorPoller.Callback): Runnable = { - val readEvent = - if (reads) - new EvAdd(fd, EVFILT_READ, cb) - else null + // val readEvent = + // if (reads) + // new EvAdd(fd, EVFILT_READ, cb) + // else null - val writeEvent = - if (writes) - new EvAdd(fd, EVFILT_WRITE, cb) - else null + // val writeEvent = + // if (writes) + // new EvAdd(fd, EVFILT_WRITE, cb) + // else null - if (readEvent != null) - changes.add(readEvent) - if (writeEvent != null) - changes.add(writeEvent) + // if (readEvent != null) + // changes.add(readEvent) + // if (writeEvent != null) + // changes.add(writeEvent) - callbacks(fd.toLong) = cb + // callbacks(fd.toLong) = cb - () => { - // we do not need to explicitly unregister the fd with the kqueue, - // b/c it will be unregistered automatically when the fd is closed + // () => { + // // we do not need to explicitly unregister the fd with the kqueue, + // // b/c it will be unregistered automatically when the fd is closed - // release the callback, so it can be GCed - callbacks.remove(fd.toLong) + // // release the callback, so it can be GCed + // callbacks.remove(fd.toLong) - // cancel the events, such that if they are currently pending in the - // changes queue awaiting registration, they will not be registered - if (readEvent != null) readEvent.cancel() - if (writeEvent != null) writeEvent.cancel() - } - } + // // cancel the events, such that if they are currently pending in the + // // changes queue awaiting registration, they will not be registered + // if (readEvent != null) readEvent.cancel() + // if (writeEvent != null) writeEvent.cancel() + // } + // } } private final class EvAdd( val fd: Int, val filter: Short, - val cb: FileDescriptorPoller.Callback + val cb: Any ) { var canceled = false def cancel() = canceled = true diff --git a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala index f4eaef586f..ddaa239a30 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala @@ -17,7 +17,7 @@ package cats.effect package unsafe -import scala.concurrent.ExecutionContextExecutor +import scala.concurrent.{ExecutionContext, ExecutionContextExecutor} import scala.concurrent.duration._ @deprecated("Use default runtime with a custom PollingSystem", "3.5.0") @@ -29,10 +29,12 @@ abstract class PollingExecutorScheduler(pollEvery: Int) pollEvery, new PollingSystem { type Poller = outer.type - def makePoller(): Poller = outer - def close(poller: Poller): Unit = () - def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = - if (nanos == -1) outer.poll(Duration.Inf) else outer.poll(nanos.nanos) + type PollData = outer.type + def makePoller(ec: ExecutionContext, data: () => PollData): Poller = outer + def makePollData(): PollData = outer + def closePollData(data: PollData): Unit = () + def poll(data: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = + if (nanos == -1) data.poll(Duration.Inf) else data.poll(nanos.nanos) } ) diff --git a/core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index 56c0f2a50f..594d5657e6 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -17,13 +17,25 @@ package cats.effect package unsafe +import scala.concurrent.ExecutionContext + abstract class PollingSystem { + /** + * The user-facing Poller interface. + */ type Poller - def makePoller(): Poller + /** + * The thread-local data structure used for polling. + */ + type PollData + + def makePoller(ec: ExecutionContext, data: () => PollData): Poller + + def makePollData(): PollData - def close(poller: Poller): Unit + def closePollData(poller: PollData): Unit /** * @param nanos @@ -36,6 +48,6 @@ abstract class PollingSystem { * @return * whether poll should be called again (i.e., there are more events to be polled) */ - def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean + def poll(data: PollData, nanos: Long, reportFailure: Throwable => Unit): Boolean } diff --git a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala index d1e0b1c399..47f8c0418c 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -17,15 +17,20 @@ package cats.effect package unsafe +import scala.concurrent.ExecutionContext + object SleepSystem extends PollingSystem { - type Poller = Unit + final class Poller private[SleepSystem] () + final class PollData private[SleepSystem] () + + def makePoller(ec: ExecutionContext, data: () => PollData): Poller = new Poller - def makePoller(): Poller = () + def makePollData(): PollData = new PollData - def close(poller: Poller): Unit = () + def closePollData(poller: PollData): Unit = () - def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = { + def poll(poller: PollData, nanos: Long, reportFailure: Throwable => Unit): Boolean = { if (nanos > 0) Thread.sleep(nanos / 1000000, (nanos % 1000000).toInt) false From 42491da54a7d0dd0394add2f48f12eecdeccc5d9 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 19 Dec 2022 21:42:39 +0000 Subject: [PATCH 022/429] Dump `EventLoop` abstraction --- .../cats/effect/IOCompanionPlatform.scala | 8 +++--- .../scala/cats/effect/unsafe/EventLoop.scala | 26 ------------------- .../unsafe/EventLoopExecutorScheduler.scala | 5 ++-- 3 files changed, 6 insertions(+), 33 deletions(-) delete mode 100644 core/native/src/main/scala/cats/effect/unsafe/EventLoop.scala diff --git a/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala b/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala index 05070cc214..497e5a818d 100644 --- a/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala +++ b/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala @@ -17,7 +17,7 @@ package cats.effect import cats.effect.std.Console -import cats.effect.unsafe.EventLoop +import cats.effect.unsafe.EventLoopExecutorScheduler import scala.reflect.ClassTag @@ -66,10 +66,10 @@ private[effect] abstract class IOCompanionPlatform { this: IO.type => def readLine: IO[String] = Console[IO].readLine - def eventLoop[Poller](implicit ct: ClassTag[Poller]): IO[Option[EventLoop[Poller]]] = + def poller[Poller](implicit ct: ClassTag[Poller]): IO[Option[Poller]] = IO.executionContext.map { - case loop: EventLoop[_] if ct.runtimeClass.isInstance(loop.poller) => - Some(loop.asInstanceOf[EventLoop[Poller]]) + case loop: EventLoopExecutorScheduler if ct.runtimeClass.isInstance(loop.poller) => + Some(loop.poller.asInstanceOf[Poller]) case _ => None } } diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoop.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoop.scala deleted file mode 100644 index 23dd70969b..0000000000 --- a/core/native/src/main/scala/cats/effect/unsafe/EventLoop.scala +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2020-2022 Typelevel - * - * 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 -package unsafe - -import scala.concurrent.ExecutionContext - -trait EventLoop[+Poller] extends ExecutionContext { - - def poller: Poller - -} diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala index c91dbe7f3f..165fde120e 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala @@ -26,9 +26,8 @@ import scala.util.control.NonFatal import java.util.{ArrayDeque, PriorityQueue} -private final class EventLoopExecutorScheduler(pollEvery: Int, system: PollingSystem) - extends EventLoop[Any] - with ExecutionContextExecutor +private[effect] final class EventLoopExecutorScheduler(pollEvery: Int, system: PollingSystem) + extends ExecutionContextExecutor with Scheduler { private[this] val pollData = system.makePollData() From 786127ca2f1d14f8e12ee0e0af9ab67435402f6e Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 19 Dec 2022 22:06:29 +0000 Subject: [PATCH 023/429] Update the `FileDescriptorPollerSpec` --- .../effect/FileDescriptorPollerSpec.scala | 124 ++++++++++++++++ .../unsafe/FileDescriptorPollerSpec.scala | 133 ------------------ 2 files changed, 124 insertions(+), 133 deletions(-) create mode 100644 tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala delete mode 100644 tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala diff --git a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala new file mode 100644 index 0000000000..95d8594fcf --- /dev/null +++ b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala @@ -0,0 +1,124 @@ +/* + * Copyright 2020-2022 Typelevel + * + * 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 + +import cats.effect.std.CountDownLatch +import cats.syntax.all._ + +import scala.concurrent.duration._ +import scala.scalanative.libc.errno._ +import scala.scalanative.posix.errno._ +import scala.scalanative.posix.string._ +import scala.scalanative.posix.unistd +import scala.scalanative.unsafe._ +import scala.scalanative.unsigned._ + +import java.io.IOException + +class FileDescriptorPollerSpec extends BaseSpec { + + final class Pipe( + val readFd: Int, + val writeFd: Int, + val readHandle: FileDescriptorPollHandle, + val writeHandle: FileDescriptorPollHandle + ) { + def read(buf: Array[Byte], offset: Int, length: Int): IO[Unit] = + readHandle + .pollReadRec(()) { _ => IO(guard(unistd.read(readFd, buf.at(offset), length.toULong))) } + .void + + def write(buf: Array[Byte], offset: Int, length: Int): IO[Unit] = + writeHandle + .pollWriteRec(()) { _ => + IO(guard(unistd.write(readFd, buf.at(offset), length.toULong))) + } + .void + + private def guard(thunk: => CInt): Either[Unit, CInt] = { + val rtn = thunk + if (rtn < 0) { + val en = errno + if (en == EAGAIN || en == EWOULDBLOCK) + Left(()) + else + throw new IOException(fromCString(strerror(errno))) + } else + Right(rtn) + } + } + + def mkPipe: Resource[IO, Pipe] = + Resource.make { + IO { + val fd = stackalloc[CInt](2) + if (unistd.pipe(fd) != 0) + throw new IOException(fromCString(strerror(errno))) + else + (fd(0), fd(1)) + } + } { + case (readFd, writeFd) => + IO { + unistd.close(readFd) + unistd.close(writeFd) + () + } + } >>= { + case (readFd, writeFd) => + Resource.eval(IO.poller[FileDescriptorPoller].map(_.get)).flatMap { poller => + ( + poller.registerFileDescriptor(readFd, true, false), + poller.registerFileDescriptor(writeFd, false, true) + ).mapN(new Pipe(readFd, writeFd, _, _)) + } + } + + "FileDescriptorPoller" should { + + "notify read-ready events" in real { + mkPipe.use { pipe => + for { + buf <- IO(new Array[Byte](4)) + _ <- pipe.write(Array[Byte](1, 2, 3), 0, 3).background.surround(pipe.read(buf, 0, 3)) + _ <- pipe.write(Array[Byte](42), 0, 1).background.surround(pipe.read(buf, 3, 1)) + } yield buf.toList must be_==(List[Byte](1, 2, 3, 42)) + } + } + + "handle lots of simultaneous events" in real { + mkPipe.replicateA(1000).use { pipes => + CountDownLatch[IO](1000).flatMap { latch => + pipes.traverse_(pipe => pipe.read(new Array[Byte](1), 0, 1).background).surround { + IO { // trigger all the pipes at once + pipes.foreach { pipe => + unistd.write(pipe.writeFd, Array[Byte](42).at(0), 1.toULong) + } + }.background.surround(latch.await.as(true)) + } + } + } + } + + "hang if never ready" in real { + mkPipe.use { pipe => + pipe.read(new Array[Byte](1), 0, 1).as(false).timeoutTo(1.second, IO.pure(true)) + } + } + } + +} diff --git a/tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala b/tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala deleted file mode 100644 index 7145892f4a..0000000000 --- a/tests/native/src/test/scala/cats/effect/unsafe/FileDescriptorPollerSpec.scala +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2020-2022 Typelevel - * - * 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 -package unsafe - -import cats.effect.std.{CountDownLatch, Dispatcher, Queue} -import cats.syntax.all._ - -import scala.concurrent.duration._ -import scala.scalanative.libc.errno._ -import scala.scalanative.posix.string._ -import scala.scalanative.posix.unistd._ -import scala.scalanative.unsafe._ -import scala.scalanative.unsigned._ - -import java.io.IOException - -class FileDescriptorPollerSpec extends BaseSpec { - - case class Pipe(readFd: Int, writeFd: Int) - - def mkPipe: Resource[IO, Pipe] = - Resource.make { - IO { - val fd = stackalloc[CInt](2) - if (pipe(fd) != 0) - throw new IOException(fromCString(strerror(errno))) - else - Pipe(fd(0), fd(1)) - } - } { - case Pipe(readFd, writeFd) => - IO { - close(readFd) - close(writeFd) - () - } - } - - def onRead(loop: EventLoop[FileDescriptorPoller], fd: Int, cb: IO[Unit]): Resource[IO, Unit] = - Dispatcher - .sequential[IO] - .flatMap { dispatcher => - Resource.make { - IO { - loop.poller().registerFileDescriptor(fd, true, false) { (readReady, _) => - dispatcher.unsafeRunAndForget(cb.whenA(readReady)) - } - } - }(unregister => IO(unregister.run())) - } - .void - - "FileDescriptorPoller" should { - - "notify read-ready events" in real { - mkPipe.use { - case Pipe(readFd, writeFd) => - IO.eventLoop[FileDescriptorPoller].map(_.get).flatMap { loop => - Queue.unbounded[IO, Unit].flatMap { queue => - onRead(loop, readFd, queue.offer(())).surround { - for { - buf <- IO(new Array[Byte](4)) - _ <- IO(write(writeFd, Array[Byte](1, 2, 3).at(0), 3.toULong)) - .background - .surround(queue.take *> IO(read(readFd, buf.at(0), 3.toULong))) - _ <- IO(write(writeFd, Array[Byte](42).at(0), 1.toULong)) - .background - .surround(queue.take *> IO(read(readFd, buf.at(3), 1.toULong))) - } yield buf.toList must be_==(List[Byte](1, 2, 3, 42)) - } - } - } - } - } - - "handle lots of simultaneous events" in real { - mkPipe.replicateA(1000).use { pipes => - IO.eventLoop[FileDescriptorPoller].map(_.get).flatMap { loop => - CountDownLatch[IO](1000).flatMap { latch => - pipes.traverse_(pipe => onRead(loop, pipe.readFd, latch.release)).surround { - IO { // trigger all the pipes at once - pipes.foreach(pipe => write(pipe.writeFd, Array[Byte](42).at(0), 1.toULong)) - }.background.surround(latch.await.as(true)) - } - } - } - } - } - - "notify of pre-existing readiness on registration" in real { - mkPipe.use { - case Pipe(readFd, writeFd) => - IO.eventLoop[FileDescriptorPoller].map(_.get).flatMap { loop => - val registerAndWait = IO.deferred[Unit].flatMap { gate => - onRead(loop, readFd, gate.complete(()).void).surround(gate.get) - } - - IO(write(writeFd, Array[Byte](42).at(0), 1.toULong)) *> - registerAndWait *> registerAndWait *> IO.pure(true) - } - } - } - - "not notify if not ready" in real { - mkPipe.use { - case Pipe(readFd, _) => - IO.eventLoop[FileDescriptorPoller].map(_.get).flatMap { loop => - val registerAndWait = IO.deferred[Unit].flatMap { gate => - onRead(loop, readFd, gate.complete(()).void).surround(gate.get) - } - - registerAndWait.as(false).timeoutTo(1.second, IO.pure(true)) - } - } - } - } - -} From de3eea0fcb957306bded74951454f6a4acb769d8 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 20 Dec 2022 00:36:10 +0000 Subject: [PATCH 024/429] Rework `EpollSystem` --- .../cats/effect/unsafe/EpollSystem.scala | 249 +++++++++++++----- 1 file changed, 182 insertions(+), 67 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index 7c6cfd3be6..314c51f492 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -17,16 +17,20 @@ package cats.effect package unsafe +import cats.effect.std.Semaphore +import cats.syntax.all._ + import org.typelevel.scalaccompat.annotation._ import scala.annotation.tailrec import scala.concurrent.ExecutionContext +import scala.scalanative.annotation.alwaysinline import scala.scalanative.libc.errno._ import scala.scalanative.posix.string._ import scala.scalanative.posix.unistd +import scala.scalanative.runtime._ import scala.scalanative.unsafe._ import scala.scalanative.unsigned._ -import scala.util.control.NonFatal import java.io.IOException import java.util.{Collections, IdentityHashMap, Set} @@ -38,7 +42,8 @@ object EpollSystem extends PollingSystem { private[this] final val MaxEvents = 64 - def makePoller(ec: ExecutionContext, data: () => PollData): Poller = new Poller + def makePoller(ec: ExecutionContext, data: () => PollData): Poller = + new Poller(ec, data) def makePollData(): PollData = { val fd = epoll_create1(0) @@ -50,82 +55,192 @@ object EpollSystem extends PollingSystem { def closePollData(data: PollData): Unit = data.close() def poll(data: PollData, nanos: Long, reportFailure: Throwable => Unit): Boolean = - data.poll(nanos, reportFailure) + data.poll(nanos) + + final class Poller private[EpollSystem] (ec: ExecutionContext, data: () => PollData) + extends FileDescriptorPoller { + + def registerFileDescriptor( + fd: Int, + reads: Boolean, + writes: Boolean + ): Resource[IO, FileDescriptorPollHandle] = + Resource + .make { + (Semaphore[IO](1), Semaphore[IO](1)).flatMapN { (readSemaphore, writeSemaphore) => + IO { + val handle = new PollHandle(readSemaphore, writeSemaphore) + val unregister = data().register(fd, reads, writes, handle) + (handle, unregister) + }.evalOn(ec) + } + }(_._2) + .map(_._1) + + } + + private final class PollHandle( + readSemaphore: Semaphore[IO], + writeSemaphore: Semaphore[IO] + ) extends FileDescriptorPollHandle { + + private[this] var readReadyCounter = 0 + private[this] var readCallback: Either[Throwable, Int] => Unit = null + + private[this] var writeReadyCounter = 0 + private[this] var writeCallback: Either[Throwable, Int] => Unit = null + + def notify(events: Int): Unit = { + if ((events & EPOLLIN) != 0) { + val counter = readReadyCounter + 1 + readReadyCounter = counter + val cb = readCallback + readCallback = null + if (cb ne null) cb(Right(counter)) + } + if ((events & EPOLLOUT) != 0) { + val counter = writeReadyCounter + 1 + writeReadyCounter = counter + val cb = writeCallback + writeCallback = null + if (cb ne null) cb(Right(counter)) + } + } + + def pollReadRec[A, B](a: A)(f: A => IO[Either[A, B]]): IO[B] = + readSemaphore.permit.surround { + def go(a: A, before: Int): IO[B] = + f(a).flatMap { + case Left(a) => + IO(readReadyCounter).flatMap { after => + if (before != after) + // there was a read-ready notification since we started, try again immediately + go(a, after) + else + IO.async[Int] { cb => + IO { + readCallback = cb + // check again before we suspend + val now = readReadyCounter + if (now != before) { + cb(Right(now)) + readCallback = null + None + } else Some(IO(this.readCallback = null)) + } + }.flatMap(go(a, _)) + } + case Right(b) => IO.pure(b) + } + + IO(readReadyCounter).flatMap(go(a, _)) + } + + def pollWriteRec[A, B](a: A)(f: A => IO[Either[A, B]]): IO[B] = + writeSemaphore.permit.surround { + def go(a: A, before: Int): IO[B] = + f(a).flatMap { + case Left(a) => + IO(writeReadyCounter).flatMap { after => + if (before != after) + // there was a write-ready notification since we started, try again immediately + go(a, after) + else + IO.async[Int] { cb => + IO { + writeCallback = cb + // check again before we suspend + val now = writeReadyCounter + if (now != before) { + cb(Right(now)) + writeCallback = null + None + } else Some(IO(this.writeCallback = null)) + } + }.flatMap(go(a, _)) + } + case Right(b) => IO.pure(b) + } + + IO(writeReadyCounter).flatMap(go(a, _)) + } - final class Poller private[EpollSystem] () + } final class PollData private[EpollSystem] (epfd: Int) { - // private[this] val callbacks: Set[FileDescriptorPoller.Callback] = - // Collections.newSetFromMap(new IdentityHashMap) + private[this] val handles: Set[PollHandle] = + Collections.newSetFromMap(new IdentityHashMap) private[EpollSystem] def close(): Unit = if (unistd.close(epfd) != 0) throw new IOException(fromCString(strerror(errno))) - private[EpollSystem] def poll(timeout: Long, reportFailure: Throwable => Unit): Boolean = { - // val noCallbacks = callbacks.isEmpty() - - // if (timeout <= 0 && noCallbacks) - // false // nothing to do here - // else { - // val events = stackalloc[epoll_event](MaxEvents.toLong) - - // @tailrec - // def processEvents(timeout: Int): Unit = { - - // val triggeredEvents = epoll_wait(epfd, events, MaxEvents, timeout) - - // if (triggeredEvents >= 0) { - // var i = 0 - // while (i < triggeredEvents) { - // val event = events + i.toLong - // val cb = FileDescriptorPoller.Callback.fromPtr(event.data) - // try { - // val e = event.events.toInt - // val readReady = (e & EPOLLIN) != 0 - // val writeReady = (e & EPOLLOUT) != 0 - // cb.notifyFileDescriptorEvents(readReady, writeReady) - // } catch { - // case ex if NonFatal(ex) => reportFailure(ex) - // } - // i += 1 - // } - // } else { - // throw new IOException(fromCString(strerror(errno))) - // } - - // if (triggeredEvents >= MaxEvents) - // processEvents(0) // drain the ready list - // else - // () - // } - - // val timeoutMillis = if (timeout == -1) -1 else (timeout / 1000000).toInt - // processEvents(timeoutMillis) - - // !callbacks.isEmpty() - // } - ??? + private[EpollSystem] def poll(timeout: Long): Boolean = { + val noHandles = handles.isEmpty() + + if (timeout <= 0 && noHandles) + false // nothing to do here + else { + val events = stackalloc[epoll_event](MaxEvents.toLong) + + @tailrec + def processEvents(timeout: Int): Unit = { + + val triggeredEvents = epoll_wait(epfd, events, MaxEvents, timeout) + + if (triggeredEvents >= 0) { + var i = 0 + while (i < triggeredEvents) { + val event = events + i.toLong + val handle = fromPtr(event.data) + handle.notify(event.events.toInt) + i += 1 + } + } else { + throw new IOException(fromCString(strerror(errno))) + } + + if (triggeredEvents >= MaxEvents) + processEvents(0) // drain the ready list + else + () + } + + val timeoutMillis = if (timeout == -1) -1 else (timeout / 1000000).toInt + processEvents(timeoutMillis) + + !handles.isEmpty() + } + } + + private[EpollSystem] def register( + fd: Int, + reads: Boolean, + writes: Boolean, + handle: PollHandle + ): IO[Unit] = { + val event = stackalloc[epoll_event]() + event.events = + (EPOLLET | (if (reads) EPOLLIN else 0) | (if (writes) EPOLLOUT else 0)).toUInt + event.data = toPtr(handle) + + if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, event) != 0) + throw new IOException(fromCString(strerror(errno))) + handles.add(handle) + + IO { + handles.remove(handle) + if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, null) != 0) + throw new IOException(fromCString(strerror(errno))) + } } - // def registerFileDescriptor(fd: Int, reads: Boolean, writes: Boolean)( - // cb: FileDescriptorPoller.Callback): Runnable = { - // val event = stackalloc[epoll_event]() - // event.events = - // (EPOLLET | (if (reads) EPOLLIN else 0) | (if (writes) EPOLLOUT else 0)).toUInt - // event.data = FileDescriptorPoller.Callback.toPtr(cb) - - // if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, event) != 0) - // throw new IOException(fromCString(strerror(errno))) - // callbacks.add(cb) - - // () => { - // callbacks.remove(cb) - // if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, null) != 0) - // throw new IOException(fromCString(strerror(errno))) - // } - // } + @alwaysinline private[this] def toPtr(handle: PollHandle): Ptr[Byte] = + fromRawPtr(Intrinsics.castObjectToRawPtr(handle)) + + @alwaysinline private[this] def fromPtr[A](ptr: Ptr[Byte]): PollHandle = + Intrinsics.castRawPtrToObject(toRawPtr(ptr)).asInstanceOf[PollHandle] } @nowarn212 From eb8ba8403f02b5f2df36fba303e34f56ccfe5e05 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 20 Dec 2022 01:14:04 +0000 Subject: [PATCH 025/429] Set pipes to non-blocking mode --- .../effect/FileDescriptorPollerSpec.scala | 54 +++++++++++-------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala index 95d8594fcf..c87ff8880a 100644 --- a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala +++ b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala @@ -63,30 +63,40 @@ class FileDescriptorPollerSpec extends BaseSpec { } def mkPipe: Resource[IO, Pipe] = - Resource.make { - IO { - val fd = stackalloc[CInt](2) - if (unistd.pipe(fd) != 0) - throw new IOException(fromCString(strerror(errno))) - else - (fd(0), fd(1)) - } - } { - case (readFd, writeFd) => + Resource + .make { IO { - unistd.close(readFd) - unistd.close(writeFd) - () - } - } >>= { - case (readFd, writeFd) => - Resource.eval(IO.poller[FileDescriptorPoller].map(_.get)).flatMap { poller => - ( - poller.registerFileDescriptor(readFd, true, false), - poller.registerFileDescriptor(writeFd, false, true) - ).mapN(new Pipe(readFd, writeFd, _, _)) + val fd = stackalloc[CInt](2) + if (unistd.pipe(fd) != 0) + throw new IOException(fromCString(strerror(errno))) + (fd(0), fd(1)) } - } + } { + case (readFd, writeFd) => + IO { + unistd.close(readFd) + unistd.close(writeFd) + () + } + } + .evalTap { + case (readFd, writeFd) => + IO { + if (fcntl(readFd, F_SETFL, O_NONBLOCK) != 0) + throw new IOException(fromCString(strerror(errno))) + if (fcntl(writeFd, F_SETFL, O_NONBLOCK) != 0) + throw new IOException(fromCString(strerror(errno))) + } + } + .flatMap { + case (readFd, writeFd) => + Resource.eval(IO.poller[FileDescriptorPoller].map(_.get)).flatMap { poller => + ( + poller.registerFileDescriptor(readFd, true, false), + poller.registerFileDescriptor(writeFd, false, true) + ).mapN(new Pipe(readFd, writeFd, _, _)) + } + } "FileDescriptorPoller" should { From 0124567afaacbc991e387144854d8b893772d6f7 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 20 Dec 2022 01:14:44 +0000 Subject: [PATCH 026/429] Add fcntl import --- .../src/test/scala/cats/effect/FileDescriptorPollerSpec.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala index c87ff8880a..a998fb6115 100644 --- a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala +++ b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala @@ -22,6 +22,7 @@ import cats.syntax.all._ import scala.concurrent.duration._ import scala.scalanative.libc.errno._ import scala.scalanative.posix.errno._ +import scala.scalanative.posix.fcntl._ import scala.scalanative.posix.string._ import scala.scalanative.posix.unistd import scala.scalanative.unsafe._ From 72b05a78380339e34984769279e1bece3b8f500b Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 20 Dec 2022 01:25:01 +0000 Subject: [PATCH 027/429] Fix bugs in spec --- .../effect/FileDescriptorPollerSpec.scala | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala index a998fb6115..a321c3b5c7 100644 --- a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala +++ b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala @@ -46,7 +46,7 @@ class FileDescriptorPollerSpec extends BaseSpec { def write(buf: Array[Byte], offset: Int, length: Int): IO[Unit] = writeHandle .pollWriteRec(()) { _ => - IO(guard(unistd.write(readFd, buf.at(offset), length.toULong))) + IO(guard(unistd.write(writeFd, buf.at(offset), length.toULong))) } .void @@ -114,13 +114,17 @@ class FileDescriptorPollerSpec extends BaseSpec { "handle lots of simultaneous events" in real { mkPipe.replicateA(1000).use { pipes => CountDownLatch[IO](1000).flatMap { latch => - pipes.traverse_(pipe => pipe.read(new Array[Byte](1), 0, 1).background).surround { - IO { // trigger all the pipes at once - pipes.foreach { pipe => - unistd.write(pipe.writeFd, Array[Byte](42).at(0), 1.toULong) - } - }.background.surround(latch.await.as(true)) - } + pipes + .traverse_ { pipe => + (pipe.read(new Array[Byte](1), 0, 1) *> latch.release).background + } + .surround { + IO { // trigger all the pipes at once + pipes.foreach { pipe => + unistd.write(pipe.writeFd, Array[Byte](42).at(0), 1.toULong) + } + }.background.surround(latch.await.as(true)) + } } } } From d18fa76ddd8137ca4d54073d4e12c4c15ecdf43f Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 20 Dec 2022 04:03:55 +0000 Subject: [PATCH 028/429] Add some uncancelables --- .../cats/effect/unsafe/EpollSystem.scala | 96 ++++++++++--------- 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index 314c51f492..d7243a441c 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -109,58 +109,62 @@ object EpollSystem extends PollingSystem { def pollReadRec[A, B](a: A)(f: A => IO[Either[A, B]]): IO[B] = readSemaphore.permit.surround { - def go(a: A, before: Int): IO[B] = - f(a).flatMap { - case Left(a) => - IO(readReadyCounter).flatMap { after => - if (before != after) - // there was a read-ready notification since we started, try again immediately - go(a, after) - else - IO.async[Int] { cb => - IO { - readCallback = cb - // check again before we suspend - val now = readReadyCounter - if (now != before) { - cb(Right(now)) - readCallback = null - None - } else Some(IO(this.readCallback = null)) - } - }.flatMap(go(a, _)) - } - case Right(b) => IO.pure(b) - } + IO.uncancelable { poll => + def go(a: A, before: Int): IO[B] = + poll(f(a)).flatMap { + case Left(a) => + IO(readReadyCounter).flatMap { after => + if (before != after) + // there was a read-ready notification since we started, try again immediately + go(a, after) + else + poll(IO.async[Int] { cb => + IO { + readCallback = cb + // check again before we suspend + val now = readReadyCounter + if (now != before) { + cb(Right(now)) + readCallback = null + None + } else Some(IO(this.readCallback = null)) + } + }).flatMap(go(a, _)) + } + case Right(b) => IO.pure(b) + } + } IO(readReadyCounter).flatMap(go(a, _)) } def pollWriteRec[A, B](a: A)(f: A => IO[Either[A, B]]): IO[B] = writeSemaphore.permit.surround { - def go(a: A, before: Int): IO[B] = - f(a).flatMap { - case Left(a) => - IO(writeReadyCounter).flatMap { after => - if (before != after) - // there was a write-ready notification since we started, try again immediately - go(a, after) - else - IO.async[Int] { cb => - IO { - writeCallback = cb - // check again before we suspend - val now = writeReadyCounter - if (now != before) { - cb(Right(now)) - writeCallback = null - None - } else Some(IO(this.writeCallback = null)) - } - }.flatMap(go(a, _)) - } - case Right(b) => IO.pure(b) - } + IO.uncancelable { poll => + def go(a: A, before: Int): IO[B] = + poll(f(a)).flatMap { + case Left(a) => + IO(writeReadyCounter).flatMap { after => + if (before != after) + // there was a write-ready notification since we started, try again immediately + go(a, after) + else + poll(IO.async[Int] { cb => + IO { + writeCallback = cb + // check again before we suspend + val now = writeReadyCounter + if (now != before) { + cb(Right(now)) + writeCallback = null + None + } else Some(IO(this.writeCallback = null)) + } + }).flatMap(go(a, _)) + } + case Right(b) => IO.pure(b) + } + } IO(writeReadyCounter).flatMap(go(a, _)) } From 4d3a916fc542a11566395a5ec4469b5d92f4c4ad Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 20 Dec 2022 04:50:16 +0000 Subject: [PATCH 029/429] Revert "Add some uncancelables" This reverts commit d18fa76ddd8137ca4d54073d4e12c4c15ecdf43f. --- .../cats/effect/unsafe/EpollSystem.scala | 96 +++++++++---------- 1 file changed, 46 insertions(+), 50 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index d7243a441c..314c51f492 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -109,62 +109,58 @@ object EpollSystem extends PollingSystem { def pollReadRec[A, B](a: A)(f: A => IO[Either[A, B]]): IO[B] = readSemaphore.permit.surround { - IO.uncancelable { poll => - def go(a: A, before: Int): IO[B] = - poll(f(a)).flatMap { - case Left(a) => - IO(readReadyCounter).flatMap { after => - if (before != after) - // there was a read-ready notification since we started, try again immediately - go(a, after) - else - poll(IO.async[Int] { cb => - IO { - readCallback = cb - // check again before we suspend - val now = readReadyCounter - if (now != before) { - cb(Right(now)) - readCallback = null - None - } else Some(IO(this.readCallback = null)) - } - }).flatMap(go(a, _)) - } - case Right(b) => IO.pure(b) - } - } + def go(a: A, before: Int): IO[B] = + f(a).flatMap { + case Left(a) => + IO(readReadyCounter).flatMap { after => + if (before != after) + // there was a read-ready notification since we started, try again immediately + go(a, after) + else + IO.async[Int] { cb => + IO { + readCallback = cb + // check again before we suspend + val now = readReadyCounter + if (now != before) { + cb(Right(now)) + readCallback = null + None + } else Some(IO(this.readCallback = null)) + } + }.flatMap(go(a, _)) + } + case Right(b) => IO.pure(b) + } IO(readReadyCounter).flatMap(go(a, _)) } def pollWriteRec[A, B](a: A)(f: A => IO[Either[A, B]]): IO[B] = writeSemaphore.permit.surround { - IO.uncancelable { poll => - def go(a: A, before: Int): IO[B] = - poll(f(a)).flatMap { - case Left(a) => - IO(writeReadyCounter).flatMap { after => - if (before != after) - // there was a write-ready notification since we started, try again immediately - go(a, after) - else - poll(IO.async[Int] { cb => - IO { - writeCallback = cb - // check again before we suspend - val now = writeReadyCounter - if (now != before) { - cb(Right(now)) - writeCallback = null - None - } else Some(IO(this.writeCallback = null)) - } - }).flatMap(go(a, _)) - } - case Right(b) => IO.pure(b) - } - } + def go(a: A, before: Int): IO[B] = + f(a).flatMap { + case Left(a) => + IO(writeReadyCounter).flatMap { after => + if (before != after) + // there was a write-ready notification since we started, try again immediately + go(a, after) + else + IO.async[Int] { cb => + IO { + writeCallback = cb + // check again before we suspend + val now = writeReadyCounter + if (now != before) { + cb(Right(now)) + writeCallback = null + None + } else Some(IO(this.writeCallback = null)) + } + }.flatMap(go(a, _)) + } + case Right(b) => IO.pure(b) + } IO(writeReadyCounter).flatMap(go(a, _)) } From 9ba870f3059de3b83696cb8e9f0b60ac715fc44f Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 20 Dec 2022 07:10:25 +0000 Subject: [PATCH 030/429] Rework `KqueueSystem` --- .../cats/effect/unsafe/KqueueSystem.scala | 299 ++++++++++-------- 1 file changed, 162 insertions(+), 137 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index 47ddb9019d..df5e82accc 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -17,11 +17,13 @@ package cats.effect package unsafe +import cats.effect.std.Semaphore +import cats.syntax.all._ + import org.typelevel.scalaccompat.annotation._ import scala.annotation.tailrec import scala.concurrent.ExecutionContext -import scala.collection.mutable.LongMap import scala.scalanative.libc.errno._ import scala.scalanative.posix.string._ import scala.scalanative.posix.time._ @@ -29,10 +31,9 @@ import scala.scalanative.posix.timeOps._ import scala.scalanative.posix.unistd import scala.scalanative.unsafe._ import scala.scalanative.unsigned._ -import scala.util.control.NonFatal import java.io.IOException -import java.util.ArrayDeque +import java.util.HashMap object KqueueSystem extends PollingSystem { @@ -41,7 +42,8 @@ object KqueueSystem extends PollingSystem { private final val MaxEvents = 64 - def makePoller(ec: ExecutionContext, data: () => PollData): Poller = new Poller + def makePoller(ec: ExecutionContext, data: () => PollData): Poller = + new Poller(ec, data) def makePollData(): PollData = { val fd = kqueue() @@ -53,153 +55,175 @@ object KqueueSystem extends PollingSystem { def closePollData(data: PollData): Unit = data.close() def poll(data: PollData, nanos: Long, reportFailure: Throwable => Unit): Boolean = - data.poll(nanos, reportFailure) + data.poll(nanos) + + final class Poller private[KqueueSystem] ( + ec: ExecutionContext, + data: () => PollData + ) extends FileDescriptorPoller { + def registerFileDescriptor( + fd: Int, + reads: Boolean, + writes: Boolean + ): Resource[IO, FileDescriptorPollHandle] = + Resource.eval { + (Semaphore[IO](1), Semaphore[IO](1)).mapN { + new PollHandle(ec, data, fd, _, _) + } + } + } - final class Poller private[KqueueSystem] () + private final class PollHandle( + ec: ExecutionContext, + data: () => PollData, + fd: Int, + readSemaphore: Semaphore[IO], + writeSemaphore: Semaphore[IO] + ) extends FileDescriptorPollHandle { + + private[this] val readEvent = KEvent(fd.toLong, EVFILT_READ) + private[this] val writeEvent = KEvent(fd.toLong, EVFILT_WRITE) + + def pollReadRec[A, B](a: A)(f: A => IO[Either[A, B]]): IO[B] = + readSemaphore.permit.surround { + a.tailRecM { a => + f(a).flatTap { r => + if (r.isRight) + IO.unit + else + IO.async[Unit] { cb => + IO { + val kqueue = data() + kqueue.evSet(readEvent, EV_ADD.toUShort, cb) + Some(IO(kqueue.removeCallback(readEvent))) + } + }.evalOn(ec) + } + } + } + + def pollWriteRec[A, B](a: A)(f: A => IO[Either[A, B]]): IO[B] = + writeSemaphore.permit.surround { + a.tailRecM { a => + f(a).flatTap { r => + if (r.isRight) + IO.unit + else + IO.async[Unit] { cb => + IO { + val kqueue = data() + kqueue.evSet(writeEvent, EV_ADD.toUShort, cb) + Some(IO(kqueue.removeCallback(writeEvent))) + } + }.evalOn(ec) + } + } + } - final class PollData private[KqueueSystem] (kqfd: Int) { + } - private[this] val changes: ArrayDeque[EvAdd] = new ArrayDeque - // private[this] val callbacks: LongMap[FileDescriptorPoller.Callback] = new LongMap + private final case class KEvent(ident: Long, filter: Short) - private[KqueueSystem] def close(): Unit = - if (unistd.close(kqfd) != 0) - throw new IOException(fromCString(strerror(errno))) - - private[KqueueSystem] def poll(timeout: Long, reportFailure: Throwable => Unit): Boolean = { - // val noCallbacks = callbacks.isEmpty - - // // pre-process the changes to filter canceled ones - // val changelist = stackalloc[kevent64_s](changes.size().toLong) - // var change = changelist - // var changeCount = 0 - // while (!changes.isEmpty()) { - // val evAdd = changes.poll() - // if (!evAdd.canceled) { - // change.ident = evAdd.fd.toULong - // change.filter = evAdd.filter - // change.flags = (EV_ADD | EV_CLEAR).toUShort - // change.udata = FileDescriptorPoller.Callback.toPtr(evAdd.cb) - // change += 1 - // changeCount += 1 - // } - // } - - // if (timeout <= 0 && noCallbacks && changeCount == 0) - // false // nothing to do here - // else { - - // val eventlist = stackalloc[kevent64_s](MaxEvents.toLong) - - // @tailrec - // def processEvents(timeout: Ptr[timespec], changeCount: Int, flags: Int): Unit = { - - // val triggeredEvents = - // kevent64( - // kqfd, - // changelist, - // changeCount, - // eventlist, - // MaxEvents, - // flags.toUInt, - // timeout - // ) - - // if (triggeredEvents >= 0) { - // var i = 0 - // var event = eventlist - // while (i < triggeredEvents) { - // if ((event.flags.toLong & EV_ERROR) != 0) { - - // // TODO it would be interesting to propagate this failure via the callback - // reportFailure(new IOException(fromCString(strerror(event.data.toInt)))) - - // } else if (callbacks.contains(event.ident.toLong)) { - // val filter = event.filter - // val cb = FileDescriptorPoller.Callback.fromPtr(event.udata) - - // try { - // cb.notifyFileDescriptorEvents(filter == EVFILT_READ, filter == EVFILT_WRITE) - // } catch { - // case NonFatal(ex) => - // reportFailure(ex) - // } - // } - - // i += 1 - // event += 1 - // } - // } else { - // throw new IOException(fromCString(strerror(errno))) - // } - - // if (triggeredEvents >= MaxEvents) - // processEvents(null, 0, KEVENT_FLAG_NONE) // drain the ready list - // else - // () - // } - - // val timeoutSpec = - // if (timeout <= 0) null - // else { - // val ts = stackalloc[timespec]() - // ts.tv_sec = timeout / 1000000000 - // ts.tv_nsec = timeout % 1000000000 - // ts - // } - - // val flags = if (timeout == 0) KEVENT_FLAG_IMMEDIATE else KEVENT_FLAG_NONE - - // processEvents(timeoutSpec, changeCount, flags) - - // !changes.isEmpty() || callbacks.nonEmpty - // } - ??? - } + final class PollData private[KqueueSystem] (kqfd: Int) { - // def registerFileDescriptor(fd: Int, reads: Boolean, writes: Boolean)( - // cb: FileDescriptorPoller.Callback): Runnable = { + private[this] val changelistArray = new Array[Byte](sizeof[kevent64_s].toInt * MaxEvents) + private[this] val changelist = changelistArray.at(0).asInstanceOf[Ptr[kevent64_s]] + private[this] var changeCount = 0 - // val readEvent = - // if (reads) - // new EvAdd(fd, EVFILT_READ, cb) - // else null + private[this] val callbacks = new HashMap[KEvent, Either[Throwable, Unit] => Unit]() - // val writeEvent = - // if (writes) - // new EvAdd(fd, EVFILT_WRITE, cb) - // else null + private[KqueueSystem] def evSet( + event: KEvent, + flags: CUnsignedShort, + cb: Either[Throwable, Unit] => Unit + ): Unit = { + val change = changelist + changeCount.toLong - // if (readEvent != null) - // changes.add(readEvent) - // if (writeEvent != null) - // changes.add(writeEvent) + change.ident = event.ident.toULong + change.filter = event.filter + change.flags = (flags.toInt | EV_ONESHOT).toUShort - // callbacks(fd.toLong) = cb + callbacks.put(event, cb) - // () => { - // // we do not need to explicitly unregister the fd with the kqueue, - // // b/c it will be unregistered automatically when the fd is closed + changeCount += 1 + } - // // release the callback, so it can be GCed - // callbacks.remove(fd.toLong) + private[KqueueSystem] def removeCallback(event: KEvent): Unit = { + callbacks.remove(event) + () + } - // // cancel the events, such that if they are currently pending in the - // // changes queue awaiting registration, they will not be registered - // if (readEvent != null) readEvent.cancel() - // if (writeEvent != null) writeEvent.cancel() - // } - // } + private[KqueueSystem] def close(): Unit = + if (unistd.close(kqfd) != 0) + throw new IOException(fromCString(strerror(errno))) - } + private[KqueueSystem] def poll(timeout: Long): Boolean = { + val noCallbacks = callbacks.isEmpty + + if (timeout <= 0 && noCallbacks && changeCount == 0) + false // nothing to do here + else { + + val eventlist = stackalloc[kevent64_s](MaxEvents.toLong) + + @tailrec + def processEvents(timeout: Ptr[timespec], changeCount: Int, flags: Int): Unit = { + + val triggeredEvents = + kevent64( + kqfd, + changelist, + changeCount, + eventlist, + MaxEvents, + flags.toUInt, + timeout + ) + + if (triggeredEvents >= 0) { + var i = 0 + var event = eventlist + while (i < triggeredEvents) { + val cb = callbacks.remove(KEvent(event.ident.toLong, event.filter)) + + if (cb ne null) + cb( + if ((event.flags.toLong & EV_ERROR) != 0) + Left(new IOException(fromCString(strerror(event.data.toInt)))) + else Either.unit + ) + + i += 1 + event += 1 + } + } else { + throw new IOException(fromCString(strerror(errno))) + } + + if (triggeredEvents >= MaxEvents) + processEvents(null, 0, KEVENT_FLAG_NONE) // drain the ready list + else + () + } + + val timeoutSpec = + if (timeout <= 0) null + else { + val ts = stackalloc[timespec]() + ts.tv_sec = timeout / 1000000000 + ts.tv_nsec = timeout % 1000000000 + ts + } + + val flags = if (timeout == 0) KEVENT_FLAG_IMMEDIATE else KEVENT_FLAG_NONE + + processEvents(timeoutSpec, changeCount, flags) + changeCount = 0 + + !callbacks.isEmpty() + } + } - private final class EvAdd( - val fd: Int, - val filter: Short, - val cb: Any - ) { - var canceled = false - def cancel() = canceled = true } @nowarn212 @@ -215,6 +239,7 @@ object KqueueSystem extends PollingSystem { final val EV_ADD = 0x0001 final val EV_DELETE = 0x0002 + final val EV_ONESHOT = 0x0010 final val EV_CLEAR = 0x0020 final val EV_ERROR = 0x4000 From 96738832dcfbbbc8ae1d1c707fce1b28b794e389 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 20 Dec 2022 18:37:35 +0000 Subject: [PATCH 031/429] Post-refactor typos --- .../src/main/scala/cats/effect/unsafe/PollingSystem.scala | 2 +- .../src/main/scala/cats/effect/unsafe/SleepSystem.scala | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index 594d5657e6..a4f0a88248 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -35,7 +35,7 @@ abstract class PollingSystem { def makePollData(): PollData - def closePollData(poller: PollData): Unit + def closePollData(data: PollData): Unit /** * @param nanos diff --git a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala index 47f8c0418c..ce4b85cc4b 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -28,9 +28,9 @@ object SleepSystem extends PollingSystem { def makePollData(): PollData = new PollData - def closePollData(poller: PollData): Unit = () + def closePollData(data: PollData): Unit = () - def poll(poller: PollData, nanos: Long, reportFailure: Throwable => Unit): Boolean = { + def poll(data: PollData, nanos: Long, reportFailure: Throwable => Unit): Boolean = { if (nanos > 0) Thread.sleep(nanos / 1000000, (nanos % 1000000).toInt) false From 43b0b0adf9e7c19be490643d66be5d575af4ebe5 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 24 Dec 2022 04:08:32 +0000 Subject: [PATCH 032/429] Scope `.evalOn` even more tightly --- .../src/main/scala/cats/effect/unsafe/KqueueSystem.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index df5e82accc..d4ec798b79 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -96,8 +96,8 @@ object KqueueSystem extends PollingSystem { val kqueue = data() kqueue.evSet(readEvent, EV_ADD.toUShort, cb) Some(IO(kqueue.removeCallback(readEvent))) - } - }.evalOn(ec) + }.evalOn(ec) + } } } } @@ -114,8 +114,8 @@ object KqueueSystem extends PollingSystem { val kqueue = data() kqueue.evSet(writeEvent, EV_ADD.toUShort, cb) Some(IO(kqueue.removeCallback(writeEvent))) - } - }.evalOn(ec) + }.evalOn(ec) + } } } } From e5dd04f5a0a1528a2a9aa7f2e122703a88627833 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 24 Dec 2022 04:37:55 +0000 Subject: [PATCH 033/429] Use `asyncCheckAttempt` --- .../scala/cats/effect/unsafe/EpollSystem.scala | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index 314c51f492..bb8666391f 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -117,16 +117,15 @@ object EpollSystem extends PollingSystem { // there was a read-ready notification since we started, try again immediately go(a, after) else - IO.async[Int] { cb => + IO.asyncCheckAttempt[Int] { cb => IO { readCallback = cb // check again before we suspend val now = readReadyCounter if (now != before) { - cb(Right(now)) readCallback = null - None - } else Some(IO(this.readCallback = null)) + Right(now) + } else Left(Some(IO(this.readCallback = null))) } }.flatMap(go(a, _)) } @@ -146,16 +145,15 @@ object EpollSystem extends PollingSystem { // there was a write-ready notification since we started, try again immediately go(a, after) else - IO.async[Int] { cb => + IO.asyncCheckAttempt[Int] { cb => IO { writeCallback = cb // check again before we suspend val now = writeReadyCounter if (now != before) { - cb(Right(now)) writeCallback = null - None - } else Some(IO(this.writeCallback = null)) + Right(now) + } else Left(Some(IO(this.writeCallback = null))) } }.flatMap(go(a, _)) } From a41a46ec3c59f192d5148c1c62c8113fb3ecb86c Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 25 Dec 2022 21:32:48 +0000 Subject: [PATCH 034/429] Attempt to reconcile jvm/native polling systems --- .../cats/effect/unsafe/PollingSystem.scala | 6 +- .../scala/cats/effect/unsafe/EventLoop.scala | 78 ------------------- .../cats/effect/unsafe/PollingSystem.scala | 33 -------- .../cats/effect/unsafe/SleepSystem.scala | 32 ++++---- .../unsafe/WorkStealingThreadPool.scala | 20 +++-- .../cats/effect/unsafe/WorkerThread.scala | 30 ++++--- .../cats/effect/unsafe/EpollSystem.scala | 2 + .../cats/effect/unsafe/KqueueSystem.scala | 2 + .../unsafe/PollingExecutorScheduler.scala | 1 + .../cats/effect/unsafe/SleepSystem.scala | 2 + 10 files changed, 59 insertions(+), 147 deletions(-) rename core/{native => jvm-native}/src/main/scala/cats/effect/unsafe/PollingSystem.scala (93%) delete mode 100644 core/jvm/src/main/scala/cats/effect/unsafe/EventLoop.scala delete mode 100644 core/jvm/src/main/scala/cats/effect/unsafe/PollingSystem.scala diff --git a/core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala similarity index 93% rename from core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala rename to core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index a4f0a88248..2797d7b615 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -24,12 +24,12 @@ abstract class PollingSystem { /** * The user-facing Poller interface. */ - type Poller + type Poller <: AnyRef /** * The thread-local data structure used for polling. */ - type PollData + type PollData <: AnyRef def makePoller(ec: ExecutionContext, data: () => PollData): Poller @@ -50,4 +50,6 @@ abstract class PollingSystem { */ def poll(data: PollData, nanos: Long, reportFailure: Throwable => Unit): Boolean + def interrupt(targetThread: Thread, targetData: PollData): Unit + } diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/EventLoop.scala b/core/jvm/src/main/scala/cats/effect/unsafe/EventLoop.scala deleted file mode 100644 index 88011a519f..0000000000 --- a/core/jvm/src/main/scala/cats/effect/unsafe/EventLoop.scala +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2020-2022 Typelevel - * - * 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 -package unsafe - -import scala.concurrent.{ExecutionContext, ExecutionContextExecutor} -import scala.reflect.ClassTag - -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.atomic.AtomicBoolean - -trait EventLoop[+Registrar] extends ExecutionContext { - - protected def registrarTag: ClassTag[_ <: Registrar] - - def registrar(): Registrar - -} - -object EventLoop { - def unapply[R](loop: EventLoop[Any])(ct: ClassTag[R]): Option[EventLoop[R]] = - if (ct.runtimeClass.isAssignableFrom(loop.registrarTag.runtimeClass)) - Some(loop.asInstanceOf[EventLoop[R]]) - else - None - - def fromPollingSystem( - name: String, - system: PollingSystem): (EventLoop[system.Poller], () => Unit) = { - - val done = new AtomicBoolean(false) - val poller = system.makePoller() - - val loop = new Thread(name) with EventLoop[system.Poller] with ExecutionContextExecutor { - - val queue = new LinkedBlockingQueue[Runnable] - - def registrarTag: ClassTag[_ <: system.Poller] = system.pollerTag - - def registrar(): system.Poller = poller - - def execute(command: Runnable): Unit = { - queue.put(command) - poller.interrupt(this) - } - - def reportFailure(cause: Throwable): Unit = cause.printStackTrace() - - override def run(): Unit = { - while (!done.get()) { - while (!queue.isEmpty()) queue.poll().run() - poller.poll(-1) - } - } - } - - val cleanup = () => { - done.set(true) - poller.interrupt(loop) - } - - (loop, cleanup) - } -} diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/PollingSystem.scala deleted file mode 100644 index 2638a8c78c..0000000000 --- a/core/jvm/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020-2022 Typelevel - * - * 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 -package unsafe - -import scala.reflect.ClassTag - -abstract class PollingSystem { - - type Poller <: AbstractPoller - def pollerTag: ClassTag[Poller] - - def makePoller(): Poller - - protected abstract class AbstractPoller { - def poll(nanos: Long): Unit - def interrupt(target: Thread): Unit - } -} diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala index 7ed45abf01..745c583811 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -17,28 +17,32 @@ package cats.effect package unsafe -import scala.reflect.ClassTag +import scala.concurrent.ExecutionContext import java.util.concurrent.locks.LockSupport object SleepSystem extends PollingSystem { - def pollerTag: ClassTag[Poller] = ClassTag(classOf[Poller]) + final class Poller private[SleepSystem] () + final class PollData private[SleepSystem] () - def makePoller(): Poller = new Poller() + def makePoller(ec: ExecutionContext, data: () => PollData): Poller = new Poller - final class Poller extends AbstractPoller { + def makePollData(): PollData = new PollData - def poll(nanos: Long): Unit = { - if (nanos < 0) - LockSupport.park() - else if (nanos > 0) - LockSupport.parkNanos(nanos) - else - () - } + def closePollData(data: PollData): Unit = () - def interrupt(target: Thread): Unit = - LockSupport.unpark(target) + def poll(data: PollData, nanos: Long, reportFailure: Throwable => Unit): Boolean = { + if (nanos < 0) + LockSupport.park() + else if (nanos > 0) + LockSupport.parkNanos(nanos) + else + () + false } + + def interrupt(targetThread: Thread, targetData: PollData): Unit = + LockSupport.unpark(targetThread) + } diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index 5ee0aef7e5..b2697cf13e 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -43,7 +43,6 @@ import java.util.Comparator import java.util.concurrent.{ConcurrentSkipListSet, ThreadLocalRandom} import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger, AtomicReference} import java.util.concurrent.locks.LockSupport -import scala.reflect.ClassTag /** * Work-stealing thread pool which manages a pool of [[WorkerThread]] s for the specific purpose @@ -68,8 +67,7 @@ private[effect] final class WorkStealingThreadPool( system: PollingSystem, reportFailure0: Throwable => Unit ) extends ExecutionContextExecutor - with Scheduler - with EventLoop[Any] { + with Scheduler { import TracingConstants._ import WorkStealingThreadPoolConstants._ @@ -82,7 +80,8 @@ private[effect] final class WorkStealingThreadPool( private[unsafe] val parkedSignals: Array[AtomicBoolean] = new Array(threadCount) private[unsafe] val fiberBags: Array[WeakBag[Runnable]] = new Array(threadCount) private[unsafe] val sleepersQueues: Array[SleepersQueue] = new Array(threadCount) - private[unsafe] val pollers: Array[AnyRef] = new Array(threadCount) + private[effect] val poller: Any = system.makePoller(this, () => pollData().asInstanceOf[system.PollData]) + private[unsafe] val pollDatas: Array[AnyRef] = new Array[AnyRef](threadCount) /** * Atomic variable for used for publishing changes to the references in the `workerThreads` @@ -128,8 +127,8 @@ private[effect] final class WorkStealingThreadPool( fiberBags(i) = fiberBag val sleepersQueue = SleepersQueue.empty sleepersQueues(i) = sleepersQueue - val poller = system.makePoller() - pollers(i) = poller + val pollData = system.makePollData() + pollDatas(i) = pollData val thread = new WorkerThread( @@ -139,7 +138,8 @@ private[effect] final class WorkStealingThreadPool( externalQueue, fiberBag, sleepersQueue, - poller, + system, + pollData, this) workerThreads(i) = thread @@ -587,20 +587,18 @@ private[effect] final class WorkStealingThreadPool( } } - def registrar(): Any = { + def pollData(): Any = { val pool = this val thread = Thread.currentThread() if (thread.isInstanceOf[WorkerThread]) { val worker = thread.asInstanceOf[WorkerThread] - if (worker.isOwnedBy(pool)) return worker.poller() + if (worker.isOwnedBy(pool)) return worker.pollData() } throw new RuntimeException("Invoked from outside the WSTP") } - protected def registrarTag: ClassTag[?] = system.pollerTag - /** * Shut down the thread pool and clean up the pool state. Calling this method after the pool * has been shut down has no effect. diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala index 70276732d6..5734769321 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala @@ -52,7 +52,8 @@ private final class WorkerThread( // A worker-thread-local weak bag for tracking suspended fibers. private[this] var fiberBag: WeakBag[Runnable], private[this] var sleepersQueue: SleepersQueue, - private[this] var _poller: PollingSystem#Poller, + private[this] val system: PollingSystem, + private[this] var __pollData: AnyRef, // Reference to the `WorkStealingThreadPool` in which this thread operates. pool: WorkStealingThreadPool) extends Thread @@ -64,6 +65,8 @@ private final class WorkerThread( // Index assigned by the `WorkStealingThreadPool` for identification purposes. private[this] var _index: Int = idx + private[this] var _pollData: system.PollData = __pollData.asInstanceOf[system.PollData] + /** * Uncontented source of randomness. By default, `java.util.Random` is thread safe, which is a * feature we do not need in this class, as the source of randomness is completely isolated to @@ -111,7 +114,7 @@ private final class WorkerThread( setName(s"$prefix-$nameIndex") } - private[unsafe] def poller(): PollingSystem#Poller = _poller + private[unsafe] def pollData(): Any = _pollData /** * Schedules the fiber for execution at the back of the local queue and notifies the work @@ -244,6 +247,7 @@ private final class WorkerThread( random = ThreadLocalRandom.current() val rnd = random val RightUnit = IOFiber.RightUnit + val reportFailure = pool.reportFailure(_) /* * A counter (modulo `ExternalQueueTicks`) which represents the @@ -318,7 +322,7 @@ private final class WorkerThread( var cont = true while (cont && !done.get()) { // Park the thread until further notice. - _poller.poll(-1) + system.poll(_pollData, -1, reportFailure) // the only way we can be interrupted here is if it happened *externally* (probably sbt) if (isInterrupted()) @@ -334,7 +338,7 @@ private final class WorkerThread( val now = System.nanoTime() val head = sleepersQueue.head() val nanos = head.triggerTime - now - _poller.poll(nanos) + system.poll(_pollData, nanos, reportFailure) if (parked.getAndSet(false)) { pool.doneSleeping() @@ -355,7 +359,7 @@ private final class WorkerThread( parked = null fiberBag = null sleepersQueue = null - _poller = null.asInstanceOf[PollingSystem#Poller] + _pollData = null.asInstanceOf[system.PollData] // Add this thread to the cached threads data structure, to be picked up // by another thread in the future. @@ -419,7 +423,7 @@ private final class WorkerThread( ((state & ExternalQueueTicksMask): @switch) match { case 0 => // give the polling system a chance to discover events - _poller.poll(0) + system.poll(_pollData, 0, reportFailure) // Obtain a fiber or batch of fibers from the external queue. val element = external.poll(rnd) @@ -719,8 +723,16 @@ private final class WorkerThread( // therefore, another worker thread would not even see it as a candidate // for unparking. val idx = index - val clone = - new WorkerThread(idx, queue, parked, external, fiberBag, sleepersQueue, _poller, pool) + val clone = new WorkerThread( + idx, + queue, + parked, + external, + fiberBag, + sleepersQueue, + system, + _pollData, + pool) pool.replaceWorker(idx, clone) pool.blockedWorkerThreadCounter.incrementAndGet() clone.start() @@ -736,7 +748,7 @@ private final class WorkerThread( parked = pool.parkedSignals(newIdx) fiberBag = pool.fiberBags(newIdx) sleepersQueue = pool.sleepersQueues(newIdx) - _poller = pool.pollers(newIdx).asInstanceOf[PollingSystem#Poller] + _pollData = pool.pollDatas(newIdx).asInstanceOf[system.PollData] // Reset the name of the thread to the regular prefix. val prefix = pool.threadPrefix diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index bb8666391f..6213310853 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -57,6 +57,8 @@ object EpollSystem extends PollingSystem { def poll(data: PollData, nanos: Long, reportFailure: Throwable => Unit): Boolean = data.poll(nanos) + def interrupt(targetThread: Thread, targetData: PollData): Unit = () + final class Poller private[EpollSystem] (ec: ExecutionContext, data: () => PollData) extends FileDescriptorPoller { diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index d4ec798b79..6cdde15008 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -57,6 +57,8 @@ object KqueueSystem extends PollingSystem { def poll(data: PollData, nanos: Long, reportFailure: Throwable => Unit): Boolean = data.poll(nanos) + def interrupt(targetThread: Thread, targetData: PollData): Unit = () + final class Poller private[KqueueSystem] ( ec: ExecutionContext, data: () => PollData diff --git a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala index ddaa239a30..07f7bd35ce 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala @@ -35,6 +35,7 @@ abstract class PollingExecutorScheduler(pollEvery: Int) def closePollData(data: PollData): Unit = () def poll(data: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = if (nanos == -1) data.poll(Duration.Inf) else data.poll(nanos.nanos) + def interrupt(targetThread: Thread, targetData: PollData): Unit = () } ) diff --git a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala index ce4b85cc4b..90400318c0 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -36,4 +36,6 @@ object SleepSystem extends PollingSystem { false } + def interrupt(targetThread: Thread, targetData: PollData): Unit = () + } From 1da8c70e677a15828b07147ab91ae9c46dfbf5eb Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 25 Dec 2022 22:20:45 +0000 Subject: [PATCH 035/429] Use polling system interruption; cleanup poll data --- .../scala/cats/effect/unsafe/WorkStealingThreadPool.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index b2697cf13e..2d45478b66 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -42,7 +42,6 @@ import java.time.temporal.ChronoField import java.util.Comparator import java.util.concurrent.{ConcurrentSkipListSet, ThreadLocalRandom} import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger, AtomicReference} -import java.util.concurrent.locks.LockSupport /** * Work-stealing thread pool which manages a pool of [[WorkerThread]] s for the specific purpose @@ -256,7 +255,7 @@ private[effect] final class WorkStealingThreadPool( // impossible. workerThreadPublisher.get() val worker = workerThreads(index) - LockSupport.unpark(worker) + system.interrupt(worker, pollDatas(index).asInstanceOf[system.PollData]) return true } @@ -619,6 +618,7 @@ private[effect] final class WorkStealingThreadPool( var i = 0 while (i < threadCount) { workerThreads(i).interrupt() + system.closePollData(pollDatas(i).asInstanceOf[system.PollData]) i += 1 } From e1cc016fd954bd8c1a38bf0c6634a607e6b98ab6 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 26 Dec 2022 04:02:59 +0000 Subject: [PATCH 036/429] First draft `SelectorSystem` --- .../scala/cats/effect/SelectorPoller.scala | 28 ++++ .../cats/effect/unsafe/SelectorSystem.scala | 145 ++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 core/jvm/src/main/scala/cats/effect/SelectorPoller.scala create mode 100644 core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala diff --git a/core/jvm/src/main/scala/cats/effect/SelectorPoller.scala b/core/jvm/src/main/scala/cats/effect/SelectorPoller.scala new file mode 100644 index 0000000000..6752b5f2be --- /dev/null +++ b/core/jvm/src/main/scala/cats/effect/SelectorPoller.scala @@ -0,0 +1,28 @@ +/* + * Copyright 2020-2022 Typelevel + * + * 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 + +import java.nio.channels.SelectableChannel +import java.nio.channels.spi.SelectorProvider + +trait SelectorPoller { + + def provider: SelectorProvider + + def register(ch: SelectableChannel, ops: Int): IO[Int] + +} diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala new file mode 100644 index 0000000000..b93a99ba0f --- /dev/null +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala @@ -0,0 +1,145 @@ +/* + * Copyright 2020-2022 Typelevel + * + * 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 +package unsafe + +import scala.concurrent.ExecutionContext + +import java.nio.channels.SelectableChannel +import java.nio.channels.spi.SelectorProvider +import java.nio.channels.spi.AbstractSelector + +final class SelectorSystem(provider: SelectorProvider) extends PollingSystem { + + def makePoller(ec: ExecutionContext, data: () => PollData): Poller = + new Poller(ec, data, provider) + + def makePollData(): PollData = new PollData(provider.openSelector()) + + def closePollData(data: PollData): Unit = + data.selector.close() + + def poll(data: PollData, nanos: Long, reportFailure: Throwable => Unit): Boolean = { + val millis = if (nanos >= 0) nanos / 1000000 else -1 + val selector = data.selector + + if (millis == 0) selector.selectNow() + else if (millis > 0) selector.select(millis) + else selector.select() + + val ready = selector.selectedKeys().iterator() + while (ready.hasNext()) { + val key = ready.next() + ready.remove() + + val attachment = key.attachment().asInstanceOf[Attachment] + val interest = attachment.interest + val readyOps = key.readyOps() + + if ((interest & readyOps) != 0) { + val value = Right(readyOps) + + var head: CallbackNode = null + var prev: CallbackNode = null + var node = attachment.callbacks + while (node ne null) { + if ((node.interest & readyOps) != 0) { // execute callback and drop this node + val cb = node.callback + if (cb != null) cb(value) + if (prev ne null) prev.next = node.next + } else { // keep this node + prev = node + if (head eq null) + head = node + } + + node = node.next + } + + // reset interest in triggered ops + val newInterest = interest & ~readyOps + attachment.interest = newInterest + attachment.callbacks = head + key.interestOps(newInterest) + } + } + + !selector.keys().isEmpty() + } + + def interrupt(targetThread: Thread, targetData: PollData): Unit = { + targetData.selector.wakeup() + () + } + + final class Poller private[SelectorSystem] ( + ec: ExecutionContext, + data: () => PollData, + val provider: SelectorProvider + ) extends SelectorPoller { + + def register(ch: SelectableChannel, ops: Int): IO[Int] = IO.async { cb => + IO { + val selector = data().selector + val key = ch.register(selector, ops) // this overrides existing ops interest. annoying + val attachment = key.attachment().asInstanceOf[Attachment] + + val node = if (attachment eq null) { // newly registered on this selector + val node = new CallbackNode(ops, cb, null) + key.attach(new Attachment(ops, node)) + node + } else { // existing key + val interest = attachment.interest + val newInterest = interest | ops + if (interest != newInterest) { // need to restore the existing interest + attachment.interest = newInterest + key.interestOps(newInterest) + } + val node = new CallbackNode(ops, cb, attachment.callbacks) + attachment.callbacks = node + node + } + + Some { + IO { + // set all interest bits + node.interest = -1 + // clear for gc + node.callback = null + } + } + }.evalOn(ec) + } + + } + + final class PollData private[SelectorSystem] ( + private[SelectorSystem] val selector: AbstractSelector + ) + + private final class Attachment( + var interest: Int, + var callbacks: CallbackNode + ) + + private final class CallbackNode( + var interest: Int, + var callback: Either[Throwable, Int] => Unit, + var next: CallbackNode + ) + +} From 1c263adfac45ba72b838a98a33fe3289fbe2aa54 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 26 Dec 2022 04:33:13 +0000 Subject: [PATCH 037/429] Install `SelectorSystem` by default --- core/jvm/src/main/scala/cats/effect/IOApp.scala | 2 +- .../scala/cats/effect/IOCompanionPlatform.scala | 10 ++++++++++ .../effect/unsafe/IORuntimeCompanionPlatform.scala | 2 +- .../scala/cats/effect/unsafe/SelectorSystem.scala | 14 ++++++++++++-- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/IOApp.scala b/core/jvm/src/main/scala/cats/effect/IOApp.scala index 5e7fd715ce..d0f6b77ae9 100644 --- a/core/jvm/src/main/scala/cats/effect/IOApp.scala +++ b/core/jvm/src/main/scala/cats/effect/IOApp.scala @@ -165,7 +165,7 @@ trait IOApp { */ protected def runtimeConfig: unsafe.IORuntimeConfig = unsafe.IORuntimeConfig() - protected def pollingSystem: unsafe.PollingSystem = unsafe.SleepSystem + protected def pollingSystem: unsafe.PollingSystem = unsafe.SelectorSystem() /** * Controls the number of worker threads which will be allocated to the compute pool in the diff --git a/core/jvm/src/main/scala/cats/effect/IOCompanionPlatform.scala b/core/jvm/src/main/scala/cats/effect/IOCompanionPlatform.scala index 02278b9874..68dbcb0be8 100644 --- a/core/jvm/src/main/scala/cats/effect/IOCompanionPlatform.scala +++ b/core/jvm/src/main/scala/cats/effect/IOCompanionPlatform.scala @@ -18,6 +18,9 @@ package cats.effect import cats.effect.std.Console import cats.effect.tracing.Tracing +import cats.effect.unsafe.WorkStealingThreadPool + +import scala.reflect.ClassTag import java.time.Instant import java.util.concurrent.{CompletableFuture, CompletionStage} @@ -141,4 +144,11 @@ private[effect] abstract class IOCompanionPlatform { this: IO.type => */ def readLine: IO[String] = Console[IO].readLine + + def poller[Poller](implicit ct: ClassTag[Poller]): IO[Option[Poller]] = + IO.executionContext.map { + case wstp: WorkStealingThreadPool if ct.runtimeClass.isInstance(wstp.poller) => + Some(wstp.poller.asInstanceOf[Poller]) + case _ => None + } } diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala index c5db996b56..68b731a7a8 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala @@ -38,7 +38,7 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type threadPrefix: String = "io-compute", blockerThreadPrefix: String = DefaultBlockerPrefix, runtimeBlockingExpiration: Duration = 60.seconds, - pollingSystem: PollingSystem = SleepSystem, + pollingSystem: PollingSystem = SelectorSystem(), reportFailure: Throwable => Unit = _.printStackTrace()) : (WorkStealingThreadPool, () => Unit) = { val threadPool = diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala index b93a99ba0f..0ff2ac7f01 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala @@ -23,7 +23,9 @@ import java.nio.channels.SelectableChannel import java.nio.channels.spi.SelectorProvider import java.nio.channels.spi.AbstractSelector -final class SelectorSystem(provider: SelectorProvider) extends PollingSystem { +import SelectorSystem._ + +final class SelectorSystem private (provider: SelectorProvider) extends PollingSystem { def makePoller(ec: ExecutionContext, data: () => PollData): Poller = new Poller(ec, data, provider) @@ -131,6 +133,15 @@ final class SelectorSystem(provider: SelectorProvider) extends PollingSystem { private[SelectorSystem] val selector: AbstractSelector ) +} + +object SelectorSystem { + + def apply(provider: SelectorProvider): SelectorSystem = + new SelectorSystem(provider) + + def apply(): SelectorSystem = apply(SelectorProvider.provider()) + private final class Attachment( var interest: Int, var callbacks: CallbackNode @@ -141,5 +152,4 @@ final class SelectorSystem(provider: SelectorProvider) extends PollingSystem { var callback: Either[Throwable, Int] => Unit, var next: CallbackNode ) - } From e44a8023780da347a4e74a0b78ba581847e118eb Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 26 Dec 2022 04:44:36 +0000 Subject: [PATCH 038/429] Fix calculation of sleep duration --- core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala index 5734769321..f3b5a4f9e8 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala @@ -337,7 +337,7 @@ private final class WorkerThread( if (!isInterrupted()) { val now = System.nanoTime() val head = sleepersQueue.head() - val nanos = head.triggerTime - now + val nanos = Math.max(head.triggerTime - now, 0) system.poll(_pollData, nanos, reportFailure) if (parked.getAndSet(false)) { From 720208c167477f1bc9bfbef2db6829d2ff2db34a Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 26 Dec 2022 05:33:27 +0000 Subject: [PATCH 039/429] Add `SelectorPollerSpec` --- .../cats/effect/SelectorPollerSpec.scala | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala diff --git a/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala b/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala new file mode 100644 index 0000000000..0788b113c9 --- /dev/null +++ b/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala @@ -0,0 +1,71 @@ +/* + * Copyright 2020-2022 Typelevel + * + * 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 + +import cats.syntax.all._ + +import java.nio.channels.Pipe +import java.nio.ByteBuffer +import java.nio.channels.SelectionKey._ + +class SelectorPollerSpec extends BaseSpec { + + def mkPipe: Resource[IO, Pipe] = + Resource + .eval(IO.poller[SelectorPoller].map(_.get)) + .flatMap { poller => + Resource.make(IO(poller.provider.openPipe())) { pipe => + IO(pipe.sink().close()).guarantee(IO(pipe.source().close())) + } + } + .evalTap { pipe => + IO { + pipe.sink().configureBlocking(false) + pipe.source().configureBlocking(false) + } + } + + "SelectorPoller" should { + + "notify read-ready events" in real { + mkPipe.use { pipe => + for { + poller <- IO.poller[SelectorPoller].map(_.get) + buf <- IO(ByteBuffer.allocate(4)) + _ <- IO(pipe.sink.write(ByteBuffer.wrap(Array(1, 2, 3)))).background.surround { + poller.register(pipe.source, OP_READ) *> IO(pipe.source.read(buf)) + } + _ <- IO(pipe.sink.write(ByteBuffer.wrap(Array(42)))).background.surround { + poller.register(pipe.source, OP_READ) *> IO(pipe.source.read(buf)) + } + } yield buf.array().toList must be_==(List[Byte](1, 2, 3, 42)) + } + } + + "setup multiple callbacks" in real { + mkPipe.use { pipe => + for { + poller <- IO.poller[SelectorPoller].map(_.get) + _ <- poller.register(pipe.source, OP_READ).parReplicateA_(10) <& + IO(pipe.sink.write(ByteBuffer.wrap(Array(1, 2, 3)))) + } yield ok + } + } + + } + +} From 411eadcb55fb47e9693cb364624852ec4d104310 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 26 Dec 2022 05:40:46 +0000 Subject: [PATCH 040/429] Only iterate ready keys if selector is open --- .../cats/effect/unsafe/SelectorSystem.scala | 68 ++++++++++--------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala index 0ff2ac7f01..2af8543b83 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala @@ -43,44 +43,46 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS else if (millis > 0) selector.select(millis) else selector.select() - val ready = selector.selectedKeys().iterator() - while (ready.hasNext()) { - val key = ready.next() - ready.remove() - - val attachment = key.attachment().asInstanceOf[Attachment] - val interest = attachment.interest - val readyOps = key.readyOps() - - if ((interest & readyOps) != 0) { - val value = Right(readyOps) - - var head: CallbackNode = null - var prev: CallbackNode = null - var node = attachment.callbacks - while (node ne null) { - if ((node.interest & readyOps) != 0) { // execute callback and drop this node - val cb = node.callback - if (cb != null) cb(value) - if (prev ne null) prev.next = node.next - } else { // keep this node - prev = node - if (head eq null) - head = node + if (selector.isOpen()) { // closing selector interrupts select + val ready = selector.selectedKeys().iterator() + while (ready.hasNext()) { + val key = ready.next() + ready.remove() + + val attachment = key.attachment().asInstanceOf[Attachment] + val interest = attachment.interest + val readyOps = key.readyOps() + + if ((interest & readyOps) != 0) { + val value = Right(readyOps) + + var head: CallbackNode = null + var prev: CallbackNode = null + var node = attachment.callbacks + while (node ne null) { + if ((node.interest & readyOps) != 0) { // execute callback and drop this node + val cb = node.callback + if (cb != null) cb(value) + if (prev ne null) prev.next = node.next + } else { // keep this node + prev = node + if (head eq null) + head = node + } + + node = node.next } - node = node.next + // reset interest in triggered ops + val newInterest = interest & ~readyOps + attachment.interest = newInterest + attachment.callbacks = head + key.interestOps(newInterest) } - - // reset interest in triggered ops - val newInterest = interest & ~readyOps - attachment.interest = newInterest - attachment.callbacks = head - key.interestOps(newInterest) } - } - !selector.keys().isEmpty() + !selector.keys().isEmpty() + } else false } def interrupt(targetThread: Thread, targetData: PollData): Unit = { From 029f5a2984aa33c7ef2db6f37ab68075c4a279bb Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 26 Dec 2022 07:01:38 +0000 Subject: [PATCH 041/429] Fixup `SelectorSystem` --- .../cats/effect/unsafe/SelectorSystem.scala | 69 +++++++------------ 1 file changed, 26 insertions(+), 43 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala index 2af8543b83..0ba3214e9f 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala @@ -49,36 +49,29 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS val key = ready.next() ready.remove() - val attachment = key.attachment().asInstanceOf[Attachment] - val interest = attachment.interest val readyOps = key.readyOps() - - if ((interest & readyOps) != 0) { - val value = Right(readyOps) - - var head: CallbackNode = null - var prev: CallbackNode = null - var node = attachment.callbacks - while (node ne null) { - if ((node.interest & readyOps) != 0) { // execute callback and drop this node - val cb = node.callback - if (cb != null) cb(value) - if (prev ne null) prev.next = node.next - } else { // keep this node - prev = node - if (head eq null) - head = node - } - - node = node.next + val value = Right(readyOps) + + var head: CallbackNode = null + var prev: CallbackNode = null + var node = key.attachment().asInstanceOf[CallbackNode] + while (node ne null) { + if ((node.interest & readyOps) != 0) { // execute callback and drop this node + val cb = node.callback + if (cb != null) cb(value) + if (prev ne null) prev.next = node.next + } else { // keep this node + prev = node + if (head eq null) + head = node } - // reset interest in triggered ops - val newInterest = interest & ~readyOps - attachment.interest = newInterest - attachment.callbacks = head - key.interestOps(newInterest) + node = node.next } + + // reset interest in triggered ops + key.interestOps(key.interestOps() & ~readyOps) + key.attach(head) } !selector.keys().isEmpty() @@ -99,22 +92,17 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS def register(ch: SelectableChannel, ops: Int): IO[Int] = IO.async { cb => IO { val selector = data().selector - val key = ch.register(selector, ops) // this overrides existing ops interest. annoying - val attachment = key.attachment().asInstanceOf[Attachment] + val key = ch.keyFor(selector) - val node = if (attachment eq null) { // newly registered on this selector + val node = if (key eq null) { // not yet registered on this selector val node = new CallbackNode(ops, cb, null) - key.attach(new Attachment(ops, node)) + ch.register(selector, ops, node) node } else { // existing key - val interest = attachment.interest - val newInterest = interest | ops - if (interest != newInterest) { // need to restore the existing interest - attachment.interest = newInterest - key.interestOps(newInterest) - } - val node = new CallbackNode(ops, cb, attachment.callbacks) - attachment.callbacks = node + // mixin the new interest + key.interestOps(key.interestOps() | ops) + val node = new CallbackNode(ops, cb, key.attachment().asInstanceOf[CallbackNode]) + key.attach(node) node } @@ -144,11 +132,6 @@ object SelectorSystem { def apply(): SelectorSystem = apply(SelectorProvider.provider()) - private final class Attachment( - var interest: Int, - var callbacks: CallbackNode - ) - private final class CallbackNode( var interest: Int, var callback: Either[Throwable, Int] => Unit, From c80fabfe1de1a942cbc0fc0a60da3a51f7c9062c Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 26 Dec 2022 07:02:54 +0000 Subject: [PATCH 042/429] Simplification --- .../src/main/scala/cats/effect/unsafe/SelectorSystem.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala index 0ba3214e9f..d9f8ba0251 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala @@ -56,17 +56,19 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS var prev: CallbackNode = null var node = key.attachment().asInstanceOf[CallbackNode] while (node ne null) { + val next = node.next + if ((node.interest & readyOps) != 0) { // execute callback and drop this node val cb = node.callback if (cb != null) cb(value) - if (prev ne null) prev.next = node.next + if (prev ne null) prev.next = next } else { // keep this node prev = node if (head eq null) head = node } - node = node.next + node = next } // reset interest in triggered ops From 9687a5cacdcdc48b52ee221d379d31d515d53c6b Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 26 Dec 2022 07:09:07 +0000 Subject: [PATCH 043/429] Add scaladocs to `SelectorPoller`, bikeshed method --- .../src/main/scala/cats/effect/SelectorPoller.scala | 10 +++++++++- .../main/scala/cats/effect/unsafe/SelectorSystem.scala | 2 +- .../test/scala/cats/effect/SelectorPollerSpec.scala | 6 +++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/SelectorPoller.scala b/core/jvm/src/main/scala/cats/effect/SelectorPoller.scala index 6752b5f2be..e3da13c79b 100644 --- a/core/jvm/src/main/scala/cats/effect/SelectorPoller.scala +++ b/core/jvm/src/main/scala/cats/effect/SelectorPoller.scala @@ -21,8 +21,16 @@ import java.nio.channels.spi.SelectorProvider trait SelectorPoller { + /** + * The [[java.nio.channels.spi.SelectorProvider]] that should be used to create + * [[java.nio.channels.SelectableChannel]]s that are compatible with this polling system. + */ def provider: SelectorProvider - def register(ch: SelectableChannel, ops: Int): IO[Int] + /** + * Fiber-block until a [[java.nio.channels.SelectableChannel]] is ready on at least one of the + * designated operations. The returned value will indicate which operations are ready. + */ + def select(ch: SelectableChannel, ops: Int): IO[Int] } diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala index d9f8ba0251..00c4cc5999 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala @@ -91,7 +91,7 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS val provider: SelectorProvider ) extends SelectorPoller { - def register(ch: SelectableChannel, ops: Int): IO[Int] = IO.async { cb => + def select(ch: SelectableChannel, ops: Int): IO[Int] = IO.async { cb => IO { val selector = data().selector val key = ch.keyFor(selector) diff --git a/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala b/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala index 0788b113c9..1ee9bffa79 100644 --- a/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala @@ -47,10 +47,10 @@ class SelectorPollerSpec extends BaseSpec { poller <- IO.poller[SelectorPoller].map(_.get) buf <- IO(ByteBuffer.allocate(4)) _ <- IO(pipe.sink.write(ByteBuffer.wrap(Array(1, 2, 3)))).background.surround { - poller.register(pipe.source, OP_READ) *> IO(pipe.source.read(buf)) + poller.select(pipe.source, OP_READ) *> IO(pipe.source.read(buf)) } _ <- IO(pipe.sink.write(ByteBuffer.wrap(Array(42)))).background.surround { - poller.register(pipe.source, OP_READ) *> IO(pipe.source.read(buf)) + poller.select(pipe.source, OP_READ) *> IO(pipe.source.read(buf)) } } yield buf.array().toList must be_==(List[Byte](1, 2, 3, 42)) } @@ -60,7 +60,7 @@ class SelectorPollerSpec extends BaseSpec { mkPipe.use { pipe => for { poller <- IO.poller[SelectorPoller].map(_.get) - _ <- poller.register(pipe.source, OP_READ).parReplicateA_(10) <& + _ <- poller.select(pipe.source, OP_READ).parReplicateA_(10) <& IO(pipe.sink.write(ByteBuffer.wrap(Array(1, 2, 3)))) } yield ok } From 861d902c98c7d46f60f9cba09d9e7dcc57826e3b Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 26 Dec 2022 07:54:23 +0000 Subject: [PATCH 044/429] scalafmt --- .../main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index 2d45478b66..31c57d5828 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -79,7 +79,8 @@ private[effect] final class WorkStealingThreadPool( private[unsafe] val parkedSignals: Array[AtomicBoolean] = new Array(threadCount) private[unsafe] val fiberBags: Array[WeakBag[Runnable]] = new Array(threadCount) private[unsafe] val sleepersQueues: Array[SleepersQueue] = new Array(threadCount) - private[effect] val poller: Any = system.makePoller(this, () => pollData().asInstanceOf[system.PollData]) + private[effect] val poller: Any = + system.makePoller(this, () => pollData().asInstanceOf[system.PollData]) private[unsafe] val pollDatas: Array[AnyRef] = new Array[AnyRef](threadCount) /** From 3d265d768fc90dfac307b75fa8ace68e7d8c2424 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 26 Dec 2022 14:24:57 +0000 Subject: [PATCH 045/429] Organize imports --- .../jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala | 3 +-- tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala index 00c4cc5999..3bffd5cc03 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala @@ -20,8 +20,7 @@ package unsafe import scala.concurrent.ExecutionContext import java.nio.channels.SelectableChannel -import java.nio.channels.spi.SelectorProvider -import java.nio.channels.spi.AbstractSelector +import java.nio.channels.spi.{AbstractSelector, SelectorProvider} import SelectorSystem._ diff --git a/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala b/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala index 1ee9bffa79..71c938640c 100644 --- a/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala @@ -18,8 +18,8 @@ package cats.effect import cats.syntax.all._ -import java.nio.channels.Pipe import java.nio.ByteBuffer +import java.nio.channels.Pipe import java.nio.channels.SelectionKey._ class SelectorPollerSpec extends BaseSpec { From 01c4a033531df648954ab91d4b83173120b39795 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 26 Dec 2022 15:02:55 +0000 Subject: [PATCH 046/429] Fix bincompat --- .../unsafe/IORuntimeCompanionPlatform.scala | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala index 68b731a7a8..119c03c1f2 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala @@ -38,9 +38,8 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type threadPrefix: String = "io-compute", blockerThreadPrefix: String = DefaultBlockerPrefix, runtimeBlockingExpiration: Duration = 60.seconds, - pollingSystem: PollingSystem = SelectorSystem(), - reportFailure: Throwable => Unit = _.printStackTrace()) - : (WorkStealingThreadPool, () => Unit) = { + reportFailure: Throwable => Unit = _.printStackTrace(), + pollingSystem: PollingSystem = SelectorSystem()): (WorkStealingThreadPool, () => Unit) = { val threadPool = new WorkStealingThreadPool( threads, @@ -115,6 +114,24 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type }) } + @deprecated( + message = "Use overload which accepts a `PollingSystem`", + since = "3.5.0" + ) + def createWorkStealingComputeThreadPool( + threads: Int, + threadPrefix: String, + blockerThreadPrefix: String, + runtimeBlockingExpiration: Duration, + reportFailure: Throwable => Unit): (WorkStealingThreadPool, () => Unit) = + createWorkStealingComputeThreadPool( + threads, + threadPrefix, + blockerThreadPrefix, + runtimeBlockingExpiration, + reportFailure, + SelectorSystem()) + @deprecated( message = "Replaced by the simpler and safer `createWorkStealingComputePool`", since = "3.4.0" From ec6a29cb619875008f881a915fd28edb366c2c5f Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 28 Dec 2022 06:53:41 +0000 Subject: [PATCH 047/429] Add test for using poller after blocking --- .../src/test/scala/cats/effect/SelectorPollerSpec.scala | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala b/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala index 71c938640c..7e7149e3d7 100644 --- a/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala @@ -66,6 +66,15 @@ class SelectorPollerSpec extends BaseSpec { } } + "works after blocking" in real { + mkPipe.use { pipe => + for { + poller <- IO.poller[SelectorPoller].map(_.get) + _ <- IO.blocking(()) + _ <- poller.select(pipe.sink, OP_WRITE) + } yield ok + } + } } } From 6c4a9d1a12d8a7da938ab2cf8d079ce6e52b23b1 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 28 Dec 2022 07:53:08 +0000 Subject: [PATCH 048/429] Use `delayWithData` abstraction, retire `evalOn` --- .../cats/effect/unsafe/PollingSystem.scala | 4 +- .../cats/effect/unsafe/SelectorSystem.scala | 15 ++++---- .../cats/effect/unsafe/SleepSystem.scala | 4 +- .../unsafe/WorkStealingThreadPool.scala | 38 ++++++++++++------- .../cats/effect/unsafe/EpollSystem.scala | 14 +++---- .../unsafe/EventLoopExecutorScheduler.scala | 7 +++- .../cats/effect/unsafe/KqueueSystem.scala | 24 +++++------- .../unsafe/PollingExecutorScheduler.scala | 6 ++- .../cats/effect/unsafe/SleepSystem.scala | 4 +- 9 files changed, 64 insertions(+), 52 deletions(-) diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index 2797d7b615..140d43928e 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -17,7 +17,7 @@ package cats.effect package unsafe -import scala.concurrent.ExecutionContext +import cats.~> abstract class PollingSystem { @@ -31,7 +31,7 @@ abstract class PollingSystem { */ type PollData <: AnyRef - def makePoller(ec: ExecutionContext, data: () => PollData): Poller + def makePoller(delayWithData: (PollData => *) ~> IO): Poller def makePollData(): PollData diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala index 3bffd5cc03..ddc7e0a8c6 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala @@ -17,7 +17,7 @@ package cats.effect package unsafe -import scala.concurrent.ExecutionContext +import cats.~> import java.nio.channels.SelectableChannel import java.nio.channels.spi.{AbstractSelector, SelectorProvider} @@ -26,8 +26,8 @@ import SelectorSystem._ final class SelectorSystem private (provider: SelectorProvider) extends PollingSystem { - def makePoller(ec: ExecutionContext, data: () => PollData): Poller = - new Poller(ec, data, provider) + def makePoller(delayWithData: (PollData => *) ~> IO): Poller = + new Poller(delayWithData, provider) def makePollData(): PollData = new PollData(provider.openSelector()) @@ -85,14 +85,13 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS } final class Poller private[SelectorSystem] ( - ec: ExecutionContext, - data: () => PollData, + delayWithData: (PollData => *) ~> IO, val provider: SelectorProvider ) extends SelectorPoller { def select(ch: SelectableChannel, ops: Int): IO[Int] = IO.async { cb => - IO { - val selector = data().selector + delayWithData { data => + val selector = data.selector val key = ch.keyFor(selector) val node = if (key eq null) { // not yet registered on this selector @@ -115,7 +114,7 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS node.callback = null } } - }.evalOn(ec) + } } } diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala index 745c583811..718e274865 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -17,7 +17,7 @@ package cats.effect package unsafe -import scala.concurrent.ExecutionContext +import cats.~> import java.util.concurrent.locks.LockSupport @@ -26,7 +26,7 @@ object SleepSystem extends PollingSystem { final class Poller private[SleepSystem] () final class PollData private[SleepSystem] () - def makePoller(ec: ExecutionContext, data: () => PollData): Poller = new Poller + def makePoller(delayWithData: (PollData => *) ~> IO): Poller = new Poller def makePollData(): PollData = new PollData diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index 31c57d5828..ec4a5c88b7 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -32,6 +32,7 @@ package unsafe import cats.effect.tracing.Tracing.captureTrace import cats.effect.tracing.TracingConstants +import cats.~> import scala.collection.mutable import scala.concurrent.ExecutionContextExecutor @@ -79,10 +80,31 @@ private[effect] final class WorkStealingThreadPool( private[unsafe] val parkedSignals: Array[AtomicBoolean] = new Array(threadCount) private[unsafe] val fiberBags: Array[WeakBag[Runnable]] = new Array(threadCount) private[unsafe] val sleepersQueues: Array[SleepersQueue] = new Array(threadCount) - private[effect] val poller: Any = - system.makePoller(this, () => pollData().asInstanceOf[system.PollData]) private[unsafe] val pollDatas: Array[AnyRef] = new Array[AnyRef](threadCount) + private[effect] val poller: Any = + system.makePoller(new ((system.PollData => *) ~> IO) { + def apply[A](thunk: system.PollData => A): IO[A] = { + val ioa = IO { // assume we are in the right place + val worker = Thread.currentThread().asInstanceOf[WorkerThread] + thunk(worker.pollData().asInstanceOf[system.PollData]) + } + + // figure out how to get to the right place + IO.defer { + val thread = Thread.currentThread() + val pool = WorkStealingThreadPool.this + + if (thread.isInstanceOf[WorkerThread]) { + val worker = thread.asInstanceOf[WorkerThread] + if (worker.isOwnedBy(pool)) ioa + else IO.cede *> ioa + } else ioa.evalOn(pool) + } + } + + }) + /** * Atomic variable for used for publishing changes to the references in the `workerThreads` * array. Worker threads can be changed whenever blocking code is encountered on the pool. @@ -587,18 +609,6 @@ private[effect] final class WorkStealingThreadPool( } } - def pollData(): Any = { - val pool = this - val thread = Thread.currentThread() - - if (thread.isInstanceOf[WorkerThread]) { - val worker = thread.asInstanceOf[WorkerThread] - if (worker.isOwnedBy(pool)) return worker.pollData() - } - - throw new RuntimeException("Invoked from outside the WSTP") - } - /** * Shut down the thread pool and clean up the pool state. Calling this method after the pool * has been shut down has no effect. diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index 6213310853..d01d90f785 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -19,11 +19,11 @@ package unsafe import cats.effect.std.Semaphore import cats.syntax.all._ +import cats.~> import org.typelevel.scalaccompat.annotation._ import scala.annotation.tailrec -import scala.concurrent.ExecutionContext import scala.scalanative.annotation.alwaysinline import scala.scalanative.libc.errno._ import scala.scalanative.posix.string._ @@ -42,8 +42,8 @@ object EpollSystem extends PollingSystem { private[this] final val MaxEvents = 64 - def makePoller(ec: ExecutionContext, data: () => PollData): Poller = - new Poller(ec, data) + def makePoller(delayWithData: (PollData => *) ~> IO): Poller = + new Poller(delayWithData) def makePollData(): PollData = { val fd = epoll_create1(0) @@ -59,7 +59,7 @@ object EpollSystem extends PollingSystem { def interrupt(targetThread: Thread, targetData: PollData): Unit = () - final class Poller private[EpollSystem] (ec: ExecutionContext, data: () => PollData) + final class Poller private[EpollSystem] (delayWithData: (PollData => *) ~> IO) extends FileDescriptorPoller { def registerFileDescriptor( @@ -70,11 +70,11 @@ object EpollSystem extends PollingSystem { Resource .make { (Semaphore[IO](1), Semaphore[IO](1)).flatMapN { (readSemaphore, writeSemaphore) => - IO { + delayWithData { data => val handle = new PollHandle(readSemaphore, writeSemaphore) - val unregister = data().register(fd, reads, writes, handle) + val unregister = data.register(fd, reads, writes, handle) (handle, unregister) - }.evalOn(ec) + } } }(_._2) .map(_._1) diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala index 165fde120e..9ca363af79 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala @@ -17,6 +17,8 @@ package cats.effect package unsafe +import cats.~> + import scala.concurrent.{ExecutionContext, ExecutionContextExecutor} import scala.concurrent.duration._ import scala.scalanative.libc.errno @@ -32,7 +34,10 @@ private[effect] final class EventLoopExecutorScheduler(pollEvery: Int, system: P private[this] val pollData = system.makePollData() - val poller: Any = system.makePoller(this, () => pollData) + val poller: Any = system.makePoller(new ((system.PollData => *) ~> IO) { + def apply[A](thunk: system.PollData => A): IO[A] = + IO(thunk(pollData)) + }) private[this] var needsReschedule: Boolean = true diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index 6cdde15008..972dd559e3 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -19,11 +19,11 @@ package unsafe import cats.effect.std.Semaphore import cats.syntax.all._ +import cats.~> import org.typelevel.scalaccompat.annotation._ import scala.annotation.tailrec -import scala.concurrent.ExecutionContext import scala.scalanative.libc.errno._ import scala.scalanative.posix.string._ import scala.scalanative.posix.time._ @@ -42,8 +42,8 @@ object KqueueSystem extends PollingSystem { private final val MaxEvents = 64 - def makePoller(ec: ExecutionContext, data: () => PollData): Poller = - new Poller(ec, data) + def makePoller(delayWithData: (PollData => *) ~> IO): Poller = + new Poller(delayWithData) def makePollData(): PollData = { val fd = kqueue() @@ -60,8 +60,7 @@ object KqueueSystem extends PollingSystem { def interrupt(targetThread: Thread, targetData: PollData): Unit = () final class Poller private[KqueueSystem] ( - ec: ExecutionContext, - data: () => PollData + delayWithData: (PollData => *) ~> IO ) extends FileDescriptorPoller { def registerFileDescriptor( fd: Int, @@ -70,14 +69,13 @@ object KqueueSystem extends PollingSystem { ): Resource[IO, FileDescriptorPollHandle] = Resource.eval { (Semaphore[IO](1), Semaphore[IO](1)).mapN { - new PollHandle(ec, data, fd, _, _) + new PollHandle(delayWithData, fd, _, _) } } } private final class PollHandle( - ec: ExecutionContext, - data: () => PollData, + delayWithData: (PollData => *) ~> IO, fd: Int, readSemaphore: Semaphore[IO], writeSemaphore: Semaphore[IO] @@ -94,11 +92,10 @@ object KqueueSystem extends PollingSystem { IO.unit else IO.async[Unit] { cb => - IO { - val kqueue = data() + delayWithData { kqueue => kqueue.evSet(readEvent, EV_ADD.toUShort, cb) Some(IO(kqueue.removeCallback(readEvent))) - }.evalOn(ec) + } } } } @@ -112,11 +109,10 @@ object KqueueSystem extends PollingSystem { IO.unit else IO.async[Unit] { cb => - IO { - val kqueue = data() + delayWithData { kqueue => kqueue.evSet(writeEvent, EV_ADD.toUShort, cb) Some(IO(kqueue.removeCallback(writeEvent))) - }.evalOn(ec) + } } } } diff --git a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala index 07f7bd35ce..608e18a503 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala @@ -17,7 +17,9 @@ package cats.effect package unsafe -import scala.concurrent.{ExecutionContext, ExecutionContextExecutor} +import cats.~> + +import scala.concurrent.ExecutionContextExecutor import scala.concurrent.duration._ @deprecated("Use default runtime with a custom PollingSystem", "3.5.0") @@ -30,7 +32,7 @@ abstract class PollingExecutorScheduler(pollEvery: Int) new PollingSystem { type Poller = outer.type type PollData = outer.type - def makePoller(ec: ExecutionContext, data: () => PollData): Poller = outer + def makePoller(delayWithData: (PollData => *) ~> IO): Poller = outer def makePollData(): PollData = outer def closePollData(data: PollData): Unit = () def poll(data: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = diff --git a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala index 90400318c0..1697492dc0 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -17,14 +17,14 @@ package cats.effect package unsafe -import scala.concurrent.ExecutionContext +import cats.~> object SleepSystem extends PollingSystem { final class Poller private[SleepSystem] () final class PollData private[SleepSystem] () - def makePoller(ec: ExecutionContext, data: () => PollData): Poller = new Poller + def makePoller(delayWithData: (PollData => *) ~> IO): Poller = new Poller def makePollData(): PollData = new PollData From 6581dc4f679e3c356e39338965ea1751f8cfbec9 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 28 Dec 2022 08:00:49 +0000 Subject: [PATCH 049/429] Guard againstmultiple wstps --- .../scala/cats/effect/unsafe/WorkStealingThreadPool.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index ec4a5c88b7..dce69689ae 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -97,8 +97,10 @@ private[effect] final class WorkStealingThreadPool( if (thread.isInstanceOf[WorkerThread]) { val worker = thread.asInstanceOf[WorkerThread] - if (worker.isOwnedBy(pool)) ioa - else IO.cede *> ioa + if (worker.isOwnedBy(pool)) // we're good + ioa + else // possibly a blocking worker thread, possibly on another wstp + IO.cede *> ioa.evalOn(pool) } else ioa.evalOn(pool) } } From 23f20ac2b023b1942487573c9342003f1992e17e Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 28 Jan 2023 22:33:19 +0000 Subject: [PATCH 050/429] Replace `delayWithData` with `register` --- .../cats/effect/unsafe/PollingSystem.scala | 4 +- .../cats/effect/unsafe/SelectorSystem.scala | 45 ++++++++++--------- .../cats/effect/unsafe/SleepSystem.scala | 4 +- .../unsafe/WorkStealingThreadPool.scala | 36 ++++++--------- .../cats/effect/unsafe/EpollSystem.scala | 29 ++++++------ .../unsafe/EventLoopExecutorScheduler.scala | 7 +-- .../cats/effect/unsafe/KqueueSystem.scala | 36 ++++++++------- .../unsafe/PollingExecutorScheduler.scala | 4 +- .../cats/effect/unsafe/SleepSystem.scala | 4 +- 9 files changed, 77 insertions(+), 92 deletions(-) diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index 140d43928e..b1ee5b2079 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -17,8 +17,6 @@ package cats.effect package unsafe -import cats.~> - abstract class PollingSystem { /** @@ -31,7 +29,7 @@ abstract class PollingSystem { */ type PollData <: AnyRef - def makePoller(delayWithData: (PollData => *) ~> IO): Poller + def makePoller(register: (PollData => Unit) => Unit): Poller def makePollData(): PollData diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala index ddc7e0a8c6..5b81916484 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala @@ -17,8 +17,6 @@ package cats.effect package unsafe -import cats.~> - import java.nio.channels.SelectableChannel import java.nio.channels.spi.{AbstractSelector, SelectorProvider} @@ -26,8 +24,8 @@ import SelectorSystem._ final class SelectorSystem private (provider: SelectorProvider) extends PollingSystem { - def makePoller(delayWithData: (PollData => *) ~> IO): Poller = - new Poller(delayWithData, provider) + def makePoller(register: (PollData => Unit) => Unit): Poller = + new Poller(register, provider) def makePollData(): PollData = new PollData(provider.openSelector()) @@ -85,27 +83,32 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS } final class Poller private[SelectorSystem] ( - delayWithData: (PollData => *) ~> IO, + register: (PollData => Unit) => Unit, val provider: SelectorProvider ) extends SelectorPoller { - def select(ch: SelectableChannel, ops: Int): IO[Int] = IO.async { cb => - delayWithData { data => - val selector = data.selector - val key = ch.keyFor(selector) - - val node = if (key eq null) { // not yet registered on this selector - val node = new CallbackNode(ops, cb, null) - ch.register(selector, ops, node) - node - } else { // existing key - // mixin the new interest - key.interestOps(key.interestOps() | ops) - val node = new CallbackNode(ops, cb, key.attachment().asInstanceOf[CallbackNode]) - key.attach(node) - node - } + def select(ch: SelectableChannel, ops: Int): IO[Int] = IO.async { selectCb => + IO.async_[CallbackNode] { cb => + register { data => + val selector = data.selector + val key = ch.keyFor(selector) + + val node = if (key eq null) { // not yet registered on this selector + val node = new CallbackNode(ops, selectCb, null) + ch.register(selector, ops, node) + node + } else { // existing key + // mixin the new interest + key.interestOps(key.interestOps() | ops) + val node = + new CallbackNode(ops, selectCb, key.attachment().asInstanceOf[CallbackNode]) + key.attach(node) + node + } + cb(Right(node)) + } + }.map { node => Some { IO { // set all interest bits diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala index 718e274865..7396443de0 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -17,8 +17,6 @@ package cats.effect package unsafe -import cats.~> - import java.util.concurrent.locks.LockSupport object SleepSystem extends PollingSystem { @@ -26,7 +24,7 @@ object SleepSystem extends PollingSystem { final class Poller private[SleepSystem] () final class PollData private[SleepSystem] () - def makePoller(delayWithData: (PollData => *) ~> IO): Poller = new Poller + def makePoller(register: (PollData => Unit) => Unit): Poller = new Poller def makePollData(): PollData = new PollData diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index dce69689ae..78212174ec 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -32,7 +32,6 @@ package unsafe import cats.effect.tracing.Tracing.captureTrace import cats.effect.tracing.TracingConstants -import cats.~> import scala.collection.mutable import scala.concurrent.ExecutionContextExecutor @@ -82,30 +81,21 @@ private[effect] final class WorkStealingThreadPool( private[unsafe] val sleepersQueues: Array[SleepersQueue] = new Array(threadCount) private[unsafe] val pollDatas: Array[AnyRef] = new Array[AnyRef](threadCount) - private[effect] val poller: Any = - system.makePoller(new ((system.PollData => *) ~> IO) { - def apply[A](thunk: system.PollData => A): IO[A] = { - val ioa = IO { // assume we are in the right place - val worker = Thread.currentThread().asInstanceOf[WorkerThread] - thunk(worker.pollData().asInstanceOf[system.PollData]) - } + private[effect] val poller: Any = system.makePoller(register) - // figure out how to get to the right place - IO.defer { - val thread = Thread.currentThread() - val pool = WorkStealingThreadPool.this - - if (thread.isInstanceOf[WorkerThread]) { - val worker = thread.asInstanceOf[WorkerThread] - if (worker.isOwnedBy(pool)) // we're good - ioa - else // possibly a blocking worker thread, possibly on another wstp - IO.cede *> ioa.evalOn(pool) - } else ioa.evalOn(pool) - } - } + private[this] def register(cb: system.PollData => Unit): Unit = { - }) + // figure out where we are + val thread = Thread.currentThread() + val pool = WorkStealingThreadPool.this + if (thread.isInstanceOf[WorkerThread]) { + val worker = thread.asInstanceOf[WorkerThread] + if (worker.isOwnedBy(pool)) // we're good + cb(worker.pollData().asInstanceOf[system.PollData]) + else // possibly a blocking worker thread, possibly on another wstp + scheduleExternal(() => register(cb)) + } else scheduleExternal(() => register(cb)) + } /** * Atomic variable for used for publishing changes to the references in the `workerThreads` diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index d01d90f785..bd8b1bba1c 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -19,7 +19,6 @@ package unsafe import cats.effect.std.Semaphore import cats.syntax.all._ -import cats.~> import org.typelevel.scalaccompat.annotation._ @@ -42,8 +41,8 @@ object EpollSystem extends PollingSystem { private[this] final val MaxEvents = 64 - def makePoller(delayWithData: (PollData => *) ~> IO): Poller = - new Poller(delayWithData) + def makePoller(register: (PollData => Unit) => Unit): Poller = + new Poller(register) def makePollData(): PollData = { val fd = epoll_create1(0) @@ -59,7 +58,7 @@ object EpollSystem extends PollingSystem { def interrupt(targetThread: Thread, targetData: PollData): Unit = () - final class Poller private[EpollSystem] (delayWithData: (PollData => *) ~> IO) + final class Poller private[EpollSystem] (register: (PollData => Unit) => Unit) extends FileDescriptorPoller { def registerFileDescriptor( @@ -70,10 +69,12 @@ object EpollSystem extends PollingSystem { Resource .make { (Semaphore[IO](1), Semaphore[IO](1)).flatMapN { (readSemaphore, writeSemaphore) => - delayWithData { data => - val handle = new PollHandle(readSemaphore, writeSemaphore) - val unregister = data.register(fd, reads, writes, handle) - (handle, unregister) + IO.async_[(PollHandle, IO[Unit])] { cb => + register { data => + val handle = new PollHandle(readSemaphore, writeSemaphore) + val unregister = data.register(fd, reads, writes, handle) + cb(Right((handle, unregister))) + } } } }(_._2) @@ -271,16 +272,16 @@ object EpollSystem extends PollingSystem { private object epollImplicits { implicit final class epoll_eventOps(epoll_event: Ptr[epoll_event]) { - def events: CUnsignedInt = !(epoll_event.asInstanceOf[Ptr[CUnsignedInt]]) + def events: CUnsignedInt = !epoll_event.asInstanceOf[Ptr[CUnsignedInt]] def events_=(events: CUnsignedInt): Unit = - !(epoll_event.asInstanceOf[Ptr[CUnsignedInt]]) = events + !epoll_event.asInstanceOf[Ptr[CUnsignedInt]] = events def data: epoll_data_t = - !((epoll_event.asInstanceOf[Ptr[Byte]] + sizeof[CUnsignedInt]) - .asInstanceOf[Ptr[epoll_data_t]]) + !(epoll_event.asInstanceOf[Ptr[Byte]] + sizeof[CUnsignedInt]) + .asInstanceOf[Ptr[epoll_data_t]] def data_=(data: epoll_data_t): Unit = - !((epoll_event.asInstanceOf[Ptr[Byte]] + sizeof[CUnsignedInt]) - .asInstanceOf[Ptr[epoll_data_t]]) = data + !(epoll_event.asInstanceOf[Ptr[Byte]] + sizeof[CUnsignedInt]) + .asInstanceOf[Ptr[epoll_data_t]] = data } implicit val epoll_eventTag: Tag[epoll_event] = diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala index 9ca363af79..2fe049807a 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala @@ -17,8 +17,6 @@ package cats.effect package unsafe -import cats.~> - import scala.concurrent.{ExecutionContext, ExecutionContextExecutor} import scala.concurrent.duration._ import scala.scalanative.libc.errno @@ -34,10 +32,7 @@ private[effect] final class EventLoopExecutorScheduler(pollEvery: Int, system: P private[this] val pollData = system.makePollData() - val poller: Any = system.makePoller(new ((system.PollData => *) ~> IO) { - def apply[A](thunk: system.PollData => A): IO[A] = - IO(thunk(pollData)) - }) + val poller: Any = system.makePoller(cb => cb(pollData)) private[this] var needsReschedule: Boolean = true diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index 972dd559e3..60d967f0c6 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -19,7 +19,6 @@ package unsafe import cats.effect.std.Semaphore import cats.syntax.all._ -import cats.~> import org.typelevel.scalaccompat.annotation._ @@ -42,8 +41,8 @@ object KqueueSystem extends PollingSystem { private final val MaxEvents = 64 - def makePoller(delayWithData: (PollData => *) ~> IO): Poller = - new Poller(delayWithData) + def makePoller(register: (PollData => Unit) => Unit): Poller = + new Poller(register) def makePollData(): PollData = { val fd = kqueue() @@ -60,7 +59,7 @@ object KqueueSystem extends PollingSystem { def interrupt(targetThread: Thread, targetData: PollData): Unit = () final class Poller private[KqueueSystem] ( - delayWithData: (PollData => *) ~> IO + register: (PollData => Unit) => Unit ) extends FileDescriptorPoller { def registerFileDescriptor( fd: Int, @@ -69,13 +68,13 @@ object KqueueSystem extends PollingSystem { ): Resource[IO, FileDescriptorPollHandle] = Resource.eval { (Semaphore[IO](1), Semaphore[IO](1)).mapN { - new PollHandle(delayWithData, fd, _, _) + new PollHandle(register, fd, _, _) } } } private final class PollHandle( - delayWithData: (PollData => *) ~> IO, + register: (PollData => Unit) => Unit, fd: Int, readSemaphore: Semaphore[IO], writeSemaphore: Semaphore[IO] @@ -91,11 +90,14 @@ object KqueueSystem extends PollingSystem { if (r.isRight) IO.unit else - IO.async[Unit] { cb => - delayWithData { kqueue => - kqueue.evSet(readEvent, EV_ADD.toUShort, cb) - Some(IO(kqueue.removeCallback(readEvent))) + IO.async[Unit] { kqcb => + IO.async_[Option[IO[Unit]]] { cb => + register { kqueue => + kqueue.evSet(readEvent, EV_ADD.toUShort, kqcb) + cb(Right(Some(IO(kqueue.removeCallback(readEvent))))) + } } + } } } @@ -108,10 +110,12 @@ object KqueueSystem extends PollingSystem { if (r.isRight) IO.unit else - IO.async[Unit] { cb => - delayWithData { kqueue => - kqueue.evSet(writeEvent, EV_ADD.toUShort, cb) - Some(IO(kqueue.removeCallback(writeEvent))) + IO.async[Unit] { kqcb => + IO.async_[Option[IO[Unit]]] { cb => + register { kqueue => + kqueue.evSet(writeEvent, EV_ADD.toUShort, kqcb) + cb(Right(Some(IO(kqueue.removeCallback(writeEvent))))) + } } } } @@ -260,9 +264,9 @@ object KqueueSystem extends PollingSystem { private object eventImplicits { implicit final class kevent64_sOps(kevent64_s: Ptr[kevent64_s]) { - def ident: CUnsignedLongInt = !(kevent64_s.asInstanceOf[Ptr[CUnsignedLongInt]]) + def ident: CUnsignedLongInt = !kevent64_s.asInstanceOf[Ptr[CUnsignedLongInt]] def ident_=(ident: CUnsignedLongInt): Unit = - !(kevent64_s.asInstanceOf[Ptr[CUnsignedLongInt]]) = ident + !kevent64_s.asInstanceOf[Ptr[CUnsignedLongInt]] = ident def filter: CShort = !(kevent64_s.asInstanceOf[Ptr[CShort]] + 4) def filter_=(filter: CShort): Unit = diff --git a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala index 608e18a503..1c59f4ea16 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala @@ -17,8 +17,6 @@ package cats.effect package unsafe -import cats.~> - import scala.concurrent.ExecutionContextExecutor import scala.concurrent.duration._ @@ -32,7 +30,7 @@ abstract class PollingExecutorScheduler(pollEvery: Int) new PollingSystem { type Poller = outer.type type PollData = outer.type - def makePoller(delayWithData: (PollData => *) ~> IO): Poller = outer + def makePoller(register: (PollData => Unit) => Unit): Poller = outer def makePollData(): PollData = outer def closePollData(data: PollData): Unit = () def poll(data: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = diff --git a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala index 1697492dc0..ea14e9d25f 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -17,14 +17,12 @@ package cats.effect package unsafe -import cats.~> - object SleepSystem extends PollingSystem { final class Poller private[SleepSystem] () final class PollData private[SleepSystem] () - def makePoller(delayWithData: (PollData => *) ~> IO): Poller = new Poller + def makePoller(register: (PollData => Unit) => Unit): Poller = new Poller def makePollData(): PollData = new PollData From e848c2bfd523f1fa6d924ae3e88a332a12b6a886 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 28 Jan 2023 23:35:20 +0000 Subject: [PATCH 051/429] `Poller->GlobalPollingState`, `PollData->Poller` --- .../cats/effect/unsafe/PollingSystem.scala | 16 ++++++------- .../cats/effect/IOCompanionPlatform.scala | 5 ++-- .../cats/effect/unsafe/SelectorSystem.scala | 24 +++++++++---------- .../cats/effect/unsafe/SleepSystem.scala | 13 +++++----- .../unsafe/WorkStealingThreadPool.scala | 18 +++++++------- .../cats/effect/unsafe/WorkerThread.scala | 18 +++++++------- .../cats/effect/IOCompanionPlatform.scala | 5 ++-- .../cats/effect/unsafe/EpollSystem.scala | 20 ++++++++-------- .../unsafe/EventLoopExecutorScheduler.scala | 6 ++--- .../cats/effect/unsafe/KqueueSystem.scala | 24 +++++++++---------- .../unsafe/PollingExecutorScheduler.scala | 14 +++++------ .../cats/effect/unsafe/SleepSystem.scala | 13 +++++----- 12 files changed, 90 insertions(+), 86 deletions(-) diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index b1ee5b2079..27a9adc766 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -20,20 +20,20 @@ package unsafe abstract class PollingSystem { /** - * The user-facing Poller interface. + * The user-facing interface. */ - type Poller <: AnyRef + type GlobalPollingState <: AnyRef /** * The thread-local data structure used for polling. */ - type PollData <: AnyRef + type Poller <: AnyRef - def makePoller(register: (PollData => Unit) => Unit): Poller + def makeGlobalPollingState(register: (Poller => Unit) => Unit): GlobalPollingState - def makePollData(): PollData + def makePoller(): Poller - def closePollData(data: PollData): Unit + def closePoller(poller: Poller): Unit /** * @param nanos @@ -46,8 +46,8 @@ abstract class PollingSystem { * @return * whether poll should be called again (i.e., there are more events to be polled) */ - def poll(data: PollData, nanos: Long, reportFailure: Throwable => Unit): Boolean + def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean - def interrupt(targetThread: Thread, targetData: PollData): Unit + def interrupt(targetThread: Thread, targetPoller: Poller): Unit } diff --git a/core/jvm/src/main/scala/cats/effect/IOCompanionPlatform.scala b/core/jvm/src/main/scala/cats/effect/IOCompanionPlatform.scala index 68dbcb0be8..619cdbd9ec 100644 --- a/core/jvm/src/main/scala/cats/effect/IOCompanionPlatform.scala +++ b/core/jvm/src/main/scala/cats/effect/IOCompanionPlatform.scala @@ -147,8 +147,9 @@ private[effect] abstract class IOCompanionPlatform { this: IO.type => def poller[Poller](implicit ct: ClassTag[Poller]): IO[Option[Poller]] = IO.executionContext.map { - case wstp: WorkStealingThreadPool if ct.runtimeClass.isInstance(wstp.poller) => - Some(wstp.poller.asInstanceOf[Poller]) + case wstp: WorkStealingThreadPool + if ct.runtimeClass.isInstance(wstp.globalPollingState) => + Some(wstp.globalPollingState.asInstanceOf[Poller]) case _ => None } } diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala index 5b81916484..bad9d298a9 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala @@ -24,17 +24,17 @@ import SelectorSystem._ final class SelectorSystem private (provider: SelectorProvider) extends PollingSystem { - def makePoller(register: (PollData => Unit) => Unit): Poller = - new Poller(register, provider) + def makeGlobalPollingState(register: (Poller => Unit) => Unit): GlobalPollingState = + new GlobalPollingState(register, provider) - def makePollData(): PollData = new PollData(provider.openSelector()) + def makePoller(): Poller = new Poller(provider.openSelector()) - def closePollData(data: PollData): Unit = - data.selector.close() + def closePoller(poller: Poller): Unit = + poller.selector.close() - def poll(data: PollData, nanos: Long, reportFailure: Throwable => Unit): Boolean = { + def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = { val millis = if (nanos >= 0) nanos / 1000000 else -1 - val selector = data.selector + val selector = poller.selector if (millis == 0) selector.selectNow() else if (millis > 0) selector.select(millis) @@ -77,13 +77,13 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS } else false } - def interrupt(targetThread: Thread, targetData: PollData): Unit = { - targetData.selector.wakeup() + def interrupt(targetThread: Thread, targetPoller: Poller): Unit = { + targetPoller.selector.wakeup() () } - final class Poller private[SelectorSystem] ( - register: (PollData => Unit) => Unit, + final class GlobalPollingState private[SelectorSystem] ( + register: (Poller => Unit) => Unit, val provider: SelectorProvider ) extends SelectorPoller { @@ -122,7 +122,7 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS } - final class PollData private[SelectorSystem] ( + final class Poller private[SelectorSystem] ( private[SelectorSystem] val selector: AbstractSelector ) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala index 7396443de0..4dd855d327 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -21,16 +21,17 @@ import java.util.concurrent.locks.LockSupport object SleepSystem extends PollingSystem { + final class GlobalPollingState private[SleepSystem] () final class Poller private[SleepSystem] () - final class PollData private[SleepSystem] () - def makePoller(register: (PollData => Unit) => Unit): Poller = new Poller + def makeGlobalPollingState(register: (Poller => Unit) => Unit): GlobalPollingState = + new GlobalPollingState - def makePollData(): PollData = new PollData + def makePoller(): Poller = new Poller - def closePollData(data: PollData): Unit = () + def closePoller(Poller: Poller): Unit = () - def poll(data: PollData, nanos: Long, reportFailure: Throwable => Unit): Boolean = { + def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = { if (nanos < 0) LockSupport.park() else if (nanos > 0) @@ -40,7 +41,7 @@ object SleepSystem extends PollingSystem { false } - def interrupt(targetThread: Thread, targetData: PollData): Unit = + def interrupt(targetThread: Thread, targetPoller: Poller): Unit = LockSupport.unpark(targetThread) } diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index 78212174ec..cccd41d87f 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -79,11 +79,11 @@ private[effect] final class WorkStealingThreadPool( private[unsafe] val parkedSignals: Array[AtomicBoolean] = new Array(threadCount) private[unsafe] val fiberBags: Array[WeakBag[Runnable]] = new Array(threadCount) private[unsafe] val sleepersQueues: Array[SleepersQueue] = new Array(threadCount) - private[unsafe] val pollDatas: Array[AnyRef] = new Array[AnyRef](threadCount) + private[unsafe] val pollers: Array[AnyRef] = new Array[AnyRef](threadCount) - private[effect] val poller: Any = system.makePoller(register) + private[effect] val globalPollingState: Any = system.makeGlobalPollingState(register) - private[this] def register(cb: system.PollData => Unit): Unit = { + private[this] def register(cb: system.Poller => Unit): Unit = { // figure out where we are val thread = Thread.currentThread() @@ -91,7 +91,7 @@ private[effect] final class WorkStealingThreadPool( if (thread.isInstanceOf[WorkerThread]) { val worker = thread.asInstanceOf[WorkerThread] if (worker.isOwnedBy(pool)) // we're good - cb(worker.pollData().asInstanceOf[system.PollData]) + cb(worker.poller().asInstanceOf[system.Poller]) else // possibly a blocking worker thread, possibly on another wstp scheduleExternal(() => register(cb)) } else scheduleExternal(() => register(cb)) @@ -141,8 +141,8 @@ private[effect] final class WorkStealingThreadPool( fiberBags(i) = fiberBag val sleepersQueue = SleepersQueue.empty sleepersQueues(i) = sleepersQueue - val pollData = system.makePollData() - pollDatas(i) = pollData + val poller = system.makePoller() + pollers(i) = poller val thread = new WorkerThread( @@ -153,7 +153,7 @@ private[effect] final class WorkStealingThreadPool( fiberBag, sleepersQueue, system, - pollData, + poller, this) workerThreads(i) = thread @@ -270,7 +270,7 @@ private[effect] final class WorkStealingThreadPool( // impossible. workerThreadPublisher.get() val worker = workerThreads(index) - system.interrupt(worker, pollDatas(index).asInstanceOf[system.PollData]) + system.interrupt(worker, pollers(index).asInstanceOf[system.Poller]) return true } @@ -621,7 +621,7 @@ private[effect] final class WorkStealingThreadPool( var i = 0 while (i < threadCount) { workerThreads(i).interrupt() - system.closePollData(pollDatas(i).asInstanceOf[system.PollData]) + system.closePoller(pollers(i).asInstanceOf[system.Poller]) i += 1 } diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala index f3b5a4f9e8..e145592c78 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala @@ -53,7 +53,7 @@ private final class WorkerThread( private[this] var fiberBag: WeakBag[Runnable], private[this] var sleepersQueue: SleepersQueue, private[this] val system: PollingSystem, - private[this] var __pollData: AnyRef, + __poller: AnyRef, // Reference to the `WorkStealingThreadPool` in which this thread operates. pool: WorkStealingThreadPool) extends Thread @@ -65,7 +65,7 @@ private final class WorkerThread( // Index assigned by the `WorkStealingThreadPool` for identification purposes. private[this] var _index: Int = idx - private[this] var _pollData: system.PollData = __pollData.asInstanceOf[system.PollData] + private[this] var _poller: system.Poller = __poller.asInstanceOf[system.Poller] /** * Uncontented source of randomness. By default, `java.util.Random` is thread safe, which is a @@ -114,7 +114,7 @@ private final class WorkerThread( setName(s"$prefix-$nameIndex") } - private[unsafe] def pollData(): Any = _pollData + private[unsafe] def poller(): Any = _poller /** * Schedules the fiber for execution at the back of the local queue and notifies the work @@ -322,7 +322,7 @@ private final class WorkerThread( var cont = true while (cont && !done.get()) { // Park the thread until further notice. - system.poll(_pollData, -1, reportFailure) + system.poll(_poller, -1, reportFailure) // the only way we can be interrupted here is if it happened *externally* (probably sbt) if (isInterrupted()) @@ -338,7 +338,7 @@ private final class WorkerThread( val now = System.nanoTime() val head = sleepersQueue.head() val nanos = Math.max(head.triggerTime - now, 0) - system.poll(_pollData, nanos, reportFailure) + system.poll(_poller, nanos, reportFailure) if (parked.getAndSet(false)) { pool.doneSleeping() @@ -359,7 +359,7 @@ private final class WorkerThread( parked = null fiberBag = null sleepersQueue = null - _pollData = null.asInstanceOf[system.PollData] + _poller = null.asInstanceOf[system.Poller] // Add this thread to the cached threads data structure, to be picked up // by another thread in the future. @@ -423,7 +423,7 @@ private final class WorkerThread( ((state & ExternalQueueTicksMask): @switch) match { case 0 => // give the polling system a chance to discover events - system.poll(_pollData, 0, reportFailure) + system.poll(_poller, 0, reportFailure) // Obtain a fiber or batch of fibers from the external queue. val element = external.poll(rnd) @@ -731,7 +731,7 @@ private final class WorkerThread( fiberBag, sleepersQueue, system, - _pollData, + _poller, pool) pool.replaceWorker(idx, clone) pool.blockedWorkerThreadCounter.incrementAndGet() @@ -748,7 +748,7 @@ private final class WorkerThread( parked = pool.parkedSignals(newIdx) fiberBag = pool.fiberBags(newIdx) sleepersQueue = pool.sleepersQueues(newIdx) - _pollData = pool.pollDatas(newIdx).asInstanceOf[system.PollData] + _poller = pool.pollers(newIdx).asInstanceOf[system.Poller] // Reset the name of the thread to the regular prefix. val prefix = pool.threadPrefix diff --git a/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala b/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala index 497e5a818d..bcd4d81138 100644 --- a/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala +++ b/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala @@ -68,8 +68,9 @@ private[effect] abstract class IOCompanionPlatform { this: IO.type => def poller[Poller](implicit ct: ClassTag[Poller]): IO[Option[Poller]] = IO.executionContext.map { - case loop: EventLoopExecutorScheduler if ct.runtimeClass.isInstance(loop.poller) => - Some(loop.poller.asInstanceOf[Poller]) + case loop: EventLoopExecutorScheduler + if ct.runtimeClass.isInstance(loop.globalPollingState) => + Some(loop.globalPollingState.asInstanceOf[Poller]) case _ => None } } diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index bd8b1bba1c..0d57c517f0 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -41,24 +41,24 @@ object EpollSystem extends PollingSystem { private[this] final val MaxEvents = 64 - def makePoller(register: (PollData => Unit) => Unit): Poller = - new Poller(register) + def makeGlobalPollingState(register: (Poller => Unit) => Unit): GlobalPollingState = + new GlobalPollingState(register) - def makePollData(): PollData = { + def makePoller(): Poller = { val fd = epoll_create1(0) if (fd == -1) throw new IOException(fromCString(strerror(errno))) - new PollData(fd) + new Poller(fd) } - def closePollData(data: PollData): Unit = data.close() + def closePoller(poller: Poller): Unit = poller.close() - def poll(data: PollData, nanos: Long, reportFailure: Throwable => Unit): Boolean = - data.poll(nanos) + def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = + poller.poll(nanos) - def interrupt(targetThread: Thread, targetData: PollData): Unit = () + def interrupt(targetThread: Thread, targetPoller: Poller): Unit = () - final class Poller private[EpollSystem] (register: (PollData => Unit) => Unit) + final class GlobalPollingState private[EpollSystem] (register: (Poller => Unit) => Unit) extends FileDescriptorPoller { def registerFileDescriptor( @@ -168,7 +168,7 @@ object EpollSystem extends PollingSystem { } - final class PollData private[EpollSystem] (epfd: Int) { + final class Poller private[EpollSystem] (epfd: Int) { private[this] val handles: Set[PollHandle] = Collections.newSetFromMap(new IdentityHashMap) diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala index 2fe049807a..40635b31e7 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala @@ -30,9 +30,9 @@ private[effect] final class EventLoopExecutorScheduler(pollEvery: Int, system: P extends ExecutionContextExecutor with Scheduler { - private[this] val pollData = system.makePollData() + private[this] val poller = system.makePoller() - val poller: Any = system.makePoller(cb => cb(pollData)) + val globalPollingState: Any = system.makeGlobalPollingState(cb => cb(poller)) private[this] var needsReschedule: Boolean = true @@ -119,7 +119,7 @@ private[effect] final class EventLoopExecutorScheduler(pollEvery: Int, system: P else -1 - val needsPoll = system.poll(pollData, timeout, reportFailure) + val needsPoll = system.poll(poller, timeout, reportFailure) continue = needsPoll || !executeQueue.isEmpty() || !sleepQueue.isEmpty() } diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index 60d967f0c6..646be74721 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -41,25 +41,25 @@ object KqueueSystem extends PollingSystem { private final val MaxEvents = 64 - def makePoller(register: (PollData => Unit) => Unit): Poller = - new Poller(register) + def makeGlobalPollingState(register: (Poller => Unit) => Unit): GlobalPollingState = + new GlobalPollingState(register) - def makePollData(): PollData = { + def makePoller(): Poller = { val fd = kqueue() if (fd == -1) throw new IOException(fromCString(strerror(errno))) - new PollData(fd) + new Poller(fd) } - def closePollData(data: PollData): Unit = data.close() + def closePoller(poller: Poller): Unit = poller.close() - def poll(data: PollData, nanos: Long, reportFailure: Throwable => Unit): Boolean = - data.poll(nanos) + def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = + poller.poll(nanos) - def interrupt(targetThread: Thread, targetData: PollData): Unit = () + def interrupt(targetThread: Thread, targetPoller: Poller): Unit = () - final class Poller private[KqueueSystem] ( - register: (PollData => Unit) => Unit + final class GlobalPollingState private[KqueueSystem] ( + register: (Poller => Unit) => Unit ) extends FileDescriptorPoller { def registerFileDescriptor( fd: Int, @@ -74,7 +74,7 @@ object KqueueSystem extends PollingSystem { } private final class PollHandle( - register: (PollData => Unit) => Unit, + register: (Poller => Unit) => Unit, fd: Int, readSemaphore: Semaphore[IO], writeSemaphore: Semaphore[IO] @@ -126,7 +126,7 @@ object KqueueSystem extends PollingSystem { private final case class KEvent(ident: Long, filter: Short) - final class PollData private[KqueueSystem] (kqfd: Int) { + final class Poller private[KqueueSystem] (kqfd: Int) { private[this] val changelistArray = new Array[Byte](sizeof[kevent64_s].toInt * MaxEvents) private[this] val changelist = changelistArray.at(0).asInstanceOf[Ptr[kevent64_s]] diff --git a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala index 1c59f4ea16..d0fd3f0487 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala @@ -28,14 +28,14 @@ abstract class PollingExecutorScheduler(pollEvery: Int) private[this] val loop = new EventLoopExecutorScheduler( pollEvery, new PollingSystem { + type GlobalPollingState = outer.type type Poller = outer.type - type PollData = outer.type - def makePoller(register: (PollData => Unit) => Unit): Poller = outer - def makePollData(): PollData = outer - def closePollData(data: PollData): Unit = () - def poll(data: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = - if (nanos == -1) data.poll(Duration.Inf) else data.poll(nanos.nanos) - def interrupt(targetThread: Thread, targetData: PollData): Unit = () + def makeGlobalPollingState(register: (Poller => Unit) => Unit): GlobalPollingState = outer + def makePoller(): Poller = outer + def closePoller(poller: Poller): Unit = () + def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = + if (nanos == -1) poller.poll(Duration.Inf) else poller.poll(nanos.nanos) + def interrupt(targetThread: Thread, targetPoller: Poller): Unit = () } ) diff --git a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala index ea14e9d25f..6bc2d82d1a 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -19,21 +19,22 @@ package unsafe object SleepSystem extends PollingSystem { + final class GlobalPollingState private[SleepSystem] () final class Poller private[SleepSystem] () - final class PollData private[SleepSystem] () - def makePoller(register: (PollData => Unit) => Unit): Poller = new Poller + def makeGlobalPollingState(register: (Poller => Unit) => Unit): GlobalPollingState = + new GlobalPollingState - def makePollData(): PollData = new PollData + def makePoller(): Poller = new Poller - def closePollData(data: PollData): Unit = () + def closePoller(poller: Poller): Unit = () - def poll(data: PollData, nanos: Long, reportFailure: Throwable => Unit): Boolean = { + def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = { if (nanos > 0) Thread.sleep(nanos / 1000000, (nanos % 1000000).toInt) false } - def interrupt(targetThread: Thread, targetData: PollData): Unit = () + def interrupt(targetThread: Thread, targetPoller: Poller): Unit = () } From 144c049bff3b00dfb20b9afe84877f95f6c336a2 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 19 Apr 2023 17:17:50 +0000 Subject: [PATCH 052/429] Update headers --- .../src/main/scala/cats/effect/unsafe/PollingSystem.scala | 2 +- core/jvm/src/main/scala/cats/effect/SelectorPoller.scala | 2 +- core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala | 2 +- core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala | 2 +- .../src/main/scala/cats/effect/FileDescriptorPoller.scala | 2 +- core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala | 2 +- .../scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala | 2 +- .../native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala | 2 +- tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala | 2 +- .../src/test/scala/cats/effect/FileDescriptorPollerSpec.scala | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index 27a9adc766..190dce80e3 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Typelevel + * Copyright 2020-2023 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/jvm/src/main/scala/cats/effect/SelectorPoller.scala b/core/jvm/src/main/scala/cats/effect/SelectorPoller.scala index e3da13c79b..b3f1663782 100644 --- a/core/jvm/src/main/scala/cats/effect/SelectorPoller.scala +++ b/core/jvm/src/main/scala/cats/effect/SelectorPoller.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Typelevel + * Copyright 2020-2023 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala index bad9d298a9..d90bfe6bf6 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Typelevel + * Copyright 2020-2023 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala index 4dd855d327..0866859b8e 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Typelevel + * Copyright 2020-2023 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala b/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala index 72604bbe66..e5e1a13af1 100644 --- a/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala +++ b/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Typelevel + * Copyright 2020-2023 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index 0d57c517f0..e39e7b6580 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Typelevel + * Copyright 2020-2023 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala index 40635b31e7..c8353947fd 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Typelevel + * Copyright 2020-2023 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index 646be74721..7520f0010d 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Typelevel + * Copyright 2020-2023 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala b/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala index 7e7149e3d7..917da3f6c8 100644 --- a/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Typelevel + * Copyright 2020-2023 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala index a321c3b5c7..06c25a4d2c 100644 --- a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala +++ b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 Typelevel + * Copyright 2020-2023 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 33d8ff55d126a2a065874fa802dc07dff4188b96 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 20 Apr 2023 02:26:43 +0000 Subject: [PATCH 053/429] Try to fix polling/parking interaction --- .../cats/effect/unsafe/PollingSystem.scala | 8 +++- .../cats/effect/unsafe/SelectorSystem.scala | 6 +++ .../cats/effect/unsafe/SleepSystem.scala | 2 + .../cats/effect/unsafe/WorkerThread.scala | 42 ++++++++++--------- .../cats/effect/unsafe/EpollSystem.scala | 7 ++++ .../unsafe/EventLoopExecutorScheduler.scala | 4 +- .../cats/effect/unsafe/KqueueSystem.scala | 7 ++++ .../unsafe/PollingExecutorScheduler.scala | 12 +++++- .../cats/effect/unsafe/SleepSystem.scala | 2 + 9 files changed, 66 insertions(+), 24 deletions(-) diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index 190dce80e3..58db64efbf 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -44,10 +44,16 @@ abstract class PollingSystem { * currently hard-coded into every test framework, including MUnit, specs2, and Weaver. * * @return - * whether poll should be called again (i.e., there are more events to be polled) + * whether any events were polled */ def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean + /** + * @return + * whether poll should be called again (i.e., there are more events to be polled) + */ + def needsPoll(poller: Poller): Boolean + def interrupt(targetThread: Thread, targetPoller: Poller): Unit } diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala index d90bfe6bf6..f8ad147b80 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala @@ -41,6 +41,8 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS else selector.select() if (selector.isOpen()) { // closing selector interrupts select + var polled = false + val ready = selector.selectedKeys().iterator() while (ready.hasNext()) { val key = ready.next() @@ -59,6 +61,7 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS val cb = node.callback if (cb != null) cb(value) if (prev ne null) prev.next = next + polled = true } else { // keep this node prev = node if (head eq null) @@ -77,6 +80,9 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS } else false } + def needsPoll(poller: Poller): Boolean = + !poller.selector.keys().isEmpty() + def interrupt(targetThread: Thread, targetPoller: Poller): Unit = { targetPoller.selector.wakeup() () diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala index 0866859b8e..aa2649d430 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -41,6 +41,8 @@ object SleepSystem extends PollingSystem { false } + def needsPoll(poller: Poller): Boolean = false + def interrupt(targetThread: Thread, targetPoller: Poller): Unit = LockSupport.unpark(targetThread) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala index 92377f8a2b..3d48a3de5d 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala @@ -325,14 +325,17 @@ private final class WorkerThread( def park(): Int = { val tt = sleepers.peekFirstTriggerTime() val nextState = if (tt == MIN_VALUE) { // no sleepers - parkLoop() - - // After the worker thread has been unparked, look for work in the - // external queue. - 3 + if (parkLoop()) { + // we polled something, so go straight to local queue stuff + pool.transitionWorkerFromSearching(rnd) + 4 + } else { + // we were interrupted, look for more work in the external queue + 3 + } } else { if (parkUntilNextSleeper()) { - // we made it to the end of our sleeping, so go straight to local queue stuff + // we made it to the end of our sleeping/polling, so go straight to local queue stuff pool.transitionWorkerFromSearching(rnd) 4 } else { @@ -357,22 +360,24 @@ private final class WorkerThread( } } - def parkLoop(): Unit = { - var cont = true - while (cont && !done.get()) { + // returns true if polled event, false if unparked + def parkLoop(): Boolean = { + while (!done.get()) { // Park the thread until further notice. - system.poll(_poller, -1, reportFailure) + val polled = system.poll(_poller, -1, reportFailure) // the only way we can be interrupted here is if it happened *externally* (probably sbt) if (isInterrupted()) pool.shutdown() - else - // Spurious wakeup check. - cont = parked.get() + else if (polled || !parked.get()) // Spurious wakeup check. + return polled + else // loop + () } + false } - // returns true if timed out, false if unparked + // returns true if timed out or polled event, false if unparked @tailrec def parkUntilNextSleeper(): Boolean = { if (done.get()) { @@ -382,22 +387,21 @@ private final class WorkerThread( if (triggerTime == MIN_VALUE) { // no sleeper (it was removed) parkLoop() - true } else { val now = System.nanoTime() val nanos = triggerTime - now if (nanos > 0L) { - system.poll(_poller, nanos, reportFailure) + val polled = system.poll(_poller, nanos, reportFailure) if (isInterrupted()) { pool.shutdown() false // we know `done` is `true` } else { if (parked.get()) { - // we were either awakened spuriously, or we timed out - if (triggerTime - System.nanoTime() <= 0) { - // we timed out + // we were either awakened spuriously, or we timed out or polled an event + if (polled || (triggerTime - System.nanoTime() <= 0)) { + // we timed out or polled an event if (parked.getAndSet(false)) { pool.doneSleeping() } diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index e39e7b6580..8a06de402d 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -56,6 +56,8 @@ object EpollSystem extends PollingSystem { def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = poller.poll(nanos) + def needsPoll(poller: Poller): Boolean = poller.needsPoll() + def interrupt(targetThread: Thread, targetPoller: Poller): Unit = () final class GlobalPollingState private[EpollSystem] (register: (Poller => Unit) => Unit) @@ -184,6 +186,7 @@ object EpollSystem extends PollingSystem { false // nothing to do here else { val events = stackalloc[epoll_event](MaxEvents.toLong) + var polled = false @tailrec def processEvents(timeout: Int): Unit = { @@ -191,6 +194,8 @@ object EpollSystem extends PollingSystem { val triggeredEvents = epoll_wait(epfd, events, MaxEvents, timeout) if (triggeredEvents >= 0) { + polled = true + var i = 0 while (i < triggeredEvents) { val event = events + i.toLong @@ -215,6 +220,8 @@ object EpollSystem extends PollingSystem { } } + private[EpollSystem] def needsPoll(): Boolean = !handles.isEmpty() + private[EpollSystem] def register( fd: Int, reads: Boolean, diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala index c8353947fd..fc78da5491 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala @@ -119,9 +119,9 @@ private[effect] final class EventLoopExecutorScheduler(pollEvery: Int, system: P else -1 - val needsPoll = system.poll(poller, timeout, reportFailure) + system.poll(poller, timeout, reportFailure) - continue = needsPoll || !executeQueue.isEmpty() || !sleepQueue.isEmpty() + continue = !executeQueue.isEmpty() || !sleepQueue.isEmpty() || system.needsPoll(poller) } needsReschedule = true diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index 7520f0010d..1974b11099 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -56,6 +56,9 @@ object KqueueSystem extends PollingSystem { def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = poller.poll(nanos) + def needsPoll(poller: Poller): Boolean = + poller.needsPoll() + def interrupt(targetThread: Thread, targetPoller: Poller): Unit = () final class GlobalPollingState private[KqueueSystem] ( @@ -167,6 +170,7 @@ object KqueueSystem extends PollingSystem { else { val eventlist = stackalloc[kevent64_s](MaxEvents.toLong) + var polled = false @tailrec def processEvents(timeout: Ptr[timespec], changeCount: Int, flags: Int): Unit = { @@ -183,6 +187,8 @@ object KqueueSystem extends PollingSystem { ) if (triggeredEvents >= 0) { + polled = true + var i = 0 var event = eventlist while (i < triggeredEvents) { @@ -226,6 +232,7 @@ object KqueueSystem extends PollingSystem { } } + def needsPoll(): Boolean = !callbacks.isEmpty() } @nowarn212 diff --git a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala index 284b3508de..4c68b04cc9 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala @@ -30,11 +30,19 @@ abstract class PollingExecutorScheduler(pollEvery: Int) new PollingSystem { type GlobalPollingState = outer.type type Poller = outer.type + private[this] var needsPoll = true def makeGlobalPollingState(register: (Poller => Unit) => Unit): GlobalPollingState = outer def makePoller(): Poller = outer def closePoller(poller: Poller): Unit = () - def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = - if (nanos == -1) poller.poll(Duration.Inf) else poller.poll(nanos.nanos) + def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = { + needsPoll = + if (nanos == -1) + poller.poll(Duration.Inf) + else + poller.poll(nanos.nanos) + true + } + def needsPoll(poller: Poller) = needsPoll def interrupt(targetThread: Thread, targetPoller: Poller): Unit = () } ) diff --git a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala index fb9bfd2f5d..d1adf5cd20 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -35,6 +35,8 @@ object SleepSystem extends PollingSystem { false } + def needsPoll(poller: Poller): Boolean = false + def interrupt(targetThread: Thread, targetPoller: Poller): Unit = () } From 1f95fd79aa5272df1c4ab35e2f46b9a955b45af0 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 21 Apr 2023 02:40:56 +0000 Subject: [PATCH 054/429] Fixes --- .../main/scala/cats/effect/unsafe/SelectorSystem.scala | 8 +++++--- .../scala/cats/effect/unsafe/WorkStealingThreadPool.scala | 3 +-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala index f8ad147b80..ab7a2c8d67 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala @@ -59,9 +59,11 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS if ((node.interest & readyOps) != 0) { // execute callback and drop this node val cb = node.callback - if (cb != null) cb(value) + if (cb != null) { + cb(value) + polled = true + } if (prev ne null) prev.next = next - polled = true } else { // keep this node prev = node if (head eq null) @@ -76,7 +78,7 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS key.attach(head) } - !selector.keys().isEmpty() + polled } else false } diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index 0e74a7ff5b..cb13367221 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -42,7 +42,6 @@ import java.time.temporal.ChronoField import java.util.Comparator import java.util.concurrent.{ConcurrentSkipListSet, ThreadLocalRandom} import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger} -import java.util.concurrent.locks.LockSupport /** * Work-stealing thread pool which manages a pool of [[WorkerThread]] s for the specific purpose @@ -339,7 +338,7 @@ private[effect] final class WorkStealingThreadPool( state.getAndAdd(DeltaSearching) workerThreadPublisher.get() val worker = workerThreads(index) - LockSupport.unpark(worker) + system.interrupt(worker, pollers(index).asInstanceOf[system.Poller]) } // else: was already unparked } From 8bc1940b3a7958222a434d3baabf0c743490f660 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 23 Apr 2023 01:55:01 +0000 Subject: [PATCH 055/429] Clear `_active` when removing refs to old data --- core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala index 3d48a3de5d..dd87862bab 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala @@ -438,6 +438,7 @@ private final class WorkerThread( sleepers = null parked = null fiberBag = null + _active = null _poller = null.asInstanceOf[system.Poller] // Add this thread to the cached threads data structure, to be picked up From 3da03b9616450a0208ad08354c3a7438c004a81c Mon Sep 17 00:00:00 2001 From: Daniel Urban Date: Mon, 24 Apr 2023 01:44:06 +0200 Subject: [PATCH 056/429] doneSleeping --- .../src/main/scala/cats/effect/unsafe/WorkerThread.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala index dd87862bab..763994b221 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala @@ -369,9 +369,12 @@ private final class WorkerThread( // the only way we can be interrupted here is if it happened *externally* (probably sbt) if (isInterrupted()) pool.shutdown() - else if (polled || !parked.get()) // Spurious wakeup check. + else if (polled || !parked.get()) { // Spurious wakeup check. + if (parked.getAndSet(false)) { + pool.doneSleeping() + } return polled - else // loop + } else // loop () } false From 875166e7b7b95c098aab7b9af8b909efa1914421 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 27 Apr 2023 04:18:07 +0000 Subject: [PATCH 057/429] Add hanging test for parked threads and polled events --- .../cats/effect/IOPlatformSpecification.scala | 66 ++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala index 970fd0d90f..687df532ea 100644 --- a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala +++ b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala @@ -17,7 +17,13 @@ package cats.effect import cats.effect.std.Semaphore -import cats.effect.unsafe.{IORuntime, IORuntimeConfig, SleepSystem, WorkStealingThreadPool} +import cats.effect.unsafe.{ + IORuntime, + IORuntimeConfig, + PollingSystem, + SleepSystem, + WorkStealingThreadPool +} import cats.syntax.all._ import org.scalacheck.Prop.forAll @@ -33,7 +39,7 @@ import java.util.concurrent.{ Executors, ThreadLocalRandom } -import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger, AtomicLong} +import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger, AtomicLong, AtomicReference} trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => @@ -460,6 +466,62 @@ trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => ok } + + "wake parked thread for polled events" in { + + trait DummyPoller { + def poll: IO[Unit] + } + + object DummySystem extends PollingSystem { + type GlobalPollingState = DummyPoller + type Poller = AtomicReference[List[Either[Throwable, Unit] => Unit]] + + def makePoller() = new AtomicReference(List.empty[Either[Throwable, Unit] => Unit]) + def needsPoll(poller: Poller) = poller.get.nonEmpty + def closePoller(poller: Poller) = () + + def interrupt(targetThread: Thread, targetPoller: Poller) = + SleepSystem.interrupt(targetThread, SleepSystem.makePoller()) + + def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit) = { + poller.getAndSet(Nil) match { + case Nil => + SleepSystem.poll(SleepSystem.makePoller(), nanos, reportFailure) + case cbs => + cbs.foreach(_.apply(Right(()))) + true + } + } + + def makeGlobalPollingState(register: (Poller => Unit) => Unit) = + new DummyPoller { + def poll = IO.async_[Unit] { cb => + register { poller => + poller.getAndUpdate(cb :: _) + () + } + } + } + } + + val (pool, shutdown) = IORuntime.createWorkStealingComputeThreadPool( + threads = 2, + pollingSystem = DummySystem) + + implicit val runtime: IORuntime = IORuntime.builder().setCompute(pool, shutdown).build() + + try { + val test = + IO.poller[DummyPoller].map(_.get).flatMap { poller => + val blockAndPoll = IO.blocking(Thread.sleep(10)) *> poller.poll + blockAndPoll.replicateA(100).as(true) + } + test.unsafeRunSync() must beTrue + } finally { + runtime.shutdown() + } + } } } } From bbb5dc5a1c1da7d202f967b6da155bd8103407c0 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 27 Apr 2023 04:45:55 +0000 Subject: [PATCH 058/429] Notify `doneSleeping` only if `polled` events --- .../main/scala/cats/effect/unsafe/WorkerThread.scala | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala index 763994b221..f26bd521b8 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala @@ -367,13 +367,14 @@ private final class WorkerThread( val polled = system.poll(_poller, -1, reportFailure) // the only way we can be interrupted here is if it happened *externally* (probably sbt) - if (isInterrupted()) + if (isInterrupted()) { pool.shutdown() - else if (polled || !parked.get()) { // Spurious wakeup check. - if (parked.getAndSet(false)) { + } else if (polled) { + if (parked.getAndSet(false)) pool.doneSleeping() - } - return polled + return true + } else if (!parked.get()) { // Spurious wakeup check. + return false } else // loop () } From 91ef606a567fd3c197a51a26a41737952bf283d7 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 27 Apr 2023 23:05:32 +0000 Subject: [PATCH 059/429] Add test for selecting illegal ops --- .../test/scala/cats/effect/SelectorPollerSpec.scala | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala b/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala index 917da3f6c8..a248b922d3 100644 --- a/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala @@ -75,6 +75,17 @@ class SelectorPollerSpec extends BaseSpec { } yield ok } } + + "gracefully handles illegal ops" in real { + mkPipe.use { pipe => + IO.poller[SelectorPoller].map(_.get).flatMap { poller => + poller.select(pipe.sink, OP_READ).attempt.map { + case Left(_: IllegalArgumentException) => true + case _ => false + } + } + } + } } } From 675838e3118212dac200122c8729af08c8529253 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 27 Apr 2023 23:42:22 +0000 Subject: [PATCH 060/429] Try to replicate `CancelledKeyException` --- .../cats/effect/SelectorPollerSpec.scala | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala b/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala index a248b922d3..02f4bd2976 100644 --- a/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala @@ -16,11 +16,13 @@ package cats.effect +import cats.effect.unsafe.IORuntime import cats.syntax.all._ import java.nio.ByteBuffer import java.nio.channels.Pipe import java.nio.channels.SelectionKey._ +import scala.concurrent.duration._ class SelectorPollerSpec extends BaseSpec { @@ -86,6 +88,30 @@ class SelectorPollerSpec extends BaseSpec { } } } + + "handles concurrent close" in { + val (pool, shutdown) = IORuntime.createWorkStealingComputeThreadPool(threads = 2) + implicit val runtime: IORuntime = IORuntime.builder().setCompute(pool, shutdown).build() + + try { + val test = IO + .poller[SelectorPoller] + .map(_.get) + .flatMap { poller => + mkPipe.allocated.flatMap { + case (pipe, close) => + poller.select(pipe.source, OP_READ).background.surround { + IO.sleep(1.millis) *> close + } + } + } + .replicateA_(1000) + .as(true) + test.unsafeRunSync() must beTrue + } finally { + runtime.shutdown() + } + } } } From 0b3be29221c72448229cd78a89e38d25bebd4784 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 27 Apr 2023 23:45:43 +0000 Subject: [PATCH 061/429] Add hanging test for concurrent close --- tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala b/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala index 02f4bd2976..cd2866668d 100644 --- a/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala @@ -90,7 +90,7 @@ class SelectorPollerSpec extends BaseSpec { } "handles concurrent close" in { - val (pool, shutdown) = IORuntime.createWorkStealingComputeThreadPool(threads = 2) + val (pool, shutdown) = IORuntime.createWorkStealingComputeThreadPool(threads = 1) implicit val runtime: IORuntime = IORuntime.builder().setCompute(pool, shutdown).build() try { @@ -101,7 +101,7 @@ class SelectorPollerSpec extends BaseSpec { mkPipe.allocated.flatMap { case (pipe, close) => poller.select(pipe.source, OP_READ).background.surround { - IO.sleep(1.millis) *> close + IO.sleep(1.millis) *> close *> IO.sleep(1.millis) } } } From 35895703deb2dce05b926fbcb6a4163e6479e460 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 28 Apr 2023 00:01:00 +0000 Subject: [PATCH 062/429] Handle `CancelledKeyException` in `SelectorSystem#poll` --- .../cats/effect/unsafe/SelectorSystem.scala | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala index ab7a2c8d67..cbdedd701a 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala @@ -17,7 +17,7 @@ package cats.effect package unsafe -import java.nio.channels.SelectableChannel +import java.nio.channels.{CancelledKeyException, SelectableChannel} import java.nio.channels.spi.{AbstractSelector, SelectorProvider} import SelectorSystem._ @@ -48,8 +48,15 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS val key = ready.next() ready.remove() - val readyOps = key.readyOps() - val value = Right(readyOps) + val value: Either[Throwable, Int] = + try { + val readyOps = key.readyOps() + // reset interest in triggered ops + key.interestOps(key.interestOps() & ~readyOps) + Right(readyOps) + } catch { case ex: CancelledKeyException => Left(ex) } + + val readyOps = value.getOrElse(-1) // interest all waiters if ex var head: CallbackNode = null var prev: CallbackNode = null @@ -73,9 +80,7 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS node = next } - // reset interest in triggered ops - key.interestOps(key.interestOps() & ~readyOps) - key.attach(head) + key.attach(head) // if key was canceled this will null attachment } polled From e1b1d37d9372f74c1170d32af2d6d9cb409c962c Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 28 Apr 2023 00:06:13 +0000 Subject: [PATCH 063/429] Organize imports --- tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala b/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala index cd2866668d..5b5932f6ce 100644 --- a/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala @@ -19,10 +19,11 @@ package cats.effect import cats.effect.unsafe.IORuntime import cats.syntax.all._ +import scala.concurrent.duration._ + import java.nio.ByteBuffer import java.nio.channels.Pipe import java.nio.channels.SelectionKey._ -import scala.concurrent.duration._ class SelectorPollerSpec extends BaseSpec { From 1fd82dd9ec50560ccb0069f4b86d8a733bc3b37e Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Fri, 12 May 2023 08:10:09 +0000 Subject: [PATCH 064/429] Update sbt to 1.8.3 in series/3.x --- project/build.properties | 2 +- scalafix/project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project/build.properties b/project/build.properties index 46e43a97ed..72413de151 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.8.2 +sbt.version=1.8.3 diff --git a/scalafix/project/build.properties b/scalafix/project/build.properties index 46e43a97ed..72413de151 100644 --- a/scalafix/project/build.properties +++ b/scalafix/project/build.properties @@ -1 +1 @@ -sbt.version=1.8.2 +sbt.version=1.8.3 From a85dca91f82bb53269da7b349d937bc48e27d47d Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 13 May 2023 16:07:19 -0700 Subject: [PATCH 065/429] Configure mergify to add :robot: label --- .mergify.yml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.mergify.yml b/.mergify.yml index 899bab69fd..2e090c8a4b 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -1,10 +1,23 @@ pull_request_rules: - - name: label scala-steward's PRs + - name: label typelevel-steward's PRs conditions: - - author=scala-steward + - author=typelevel-steward[bot] + actions: + label: + add: [':robot:'] + - name: label typelevel-steward's update PRs + conditions: + - author=typelevel-steward[bot] + - body~=labels:.*library-update actions: label: add: [dependencies] + - name: label dependabots's PRs + conditions: + - author=dependabot[bot] + actions: + label: + add: [':robot:'] - name: automatically merge dependabot docs PRs conditions: - author=dependabot[bot] From e016e9ed040c7704f7c7a2749c8bd3f8216a7851 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 16 May 2023 07:48:40 +0000 Subject: [PATCH 066/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/bb9774d68fa09d259f490c81546f36ec6774e96a' (2023-05-08) → 'github:typelevel/typelevel-nix/f5f013dbe26aeb40b6690d55ddddf3a4eb915159' (2023-05-15) • Updated input 'typelevel-nix/devshell': 'github:numtide/devshell/fb6673fe9fe4409e3f43ca86968261e970918a83' (2023-04-28) → 'github:numtide/devshell/5143ea68647c4cf5227e4ad2100db6671fc4c369' (2023-05-09) • Removed input 'typelevel-nix/devshell/flake-utils' • Added input 'typelevel-nix/devshell/systems': 'github:nix-systems/default/da67096a3b9bf56a91d16901293e51ba5b49a27e' (2023-04-09) • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/eb751d65225ec53de9cf3d88acbf08d275882389' (2023-05-07) → 'github:nixos/nixpkgs/3007746b3f5bfcb49e102b517bca891822a41b31' (2023-05-14) --- flake.lock | 56 +++++++++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/flake.lock b/flake.lock index 43737aa7c4..3c221672db 100644 --- a/flake.lock +++ b/flake.lock @@ -2,15 +2,15 @@ "nodes": { "devshell": { "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "systems": "systems" }, "locked": { - "lastModified": 1682700442, - "narHash": "sha256-qjaAAcCYgp1pBBG7mY9z95ODUBZMtUpf0Qp3Gt/Wha0=", + "lastModified": 1683635384, + "narHash": "sha256-9goJTd05yOyD/McaMqZ4BUB8JW+mZMnZQJZ7VQ6C/Lw=", "owner": "numtide", "repo": "devshell", - "rev": "fb6673fe9fe4409e3f43ca86968261e970918a83", + "rev": "5143ea68647c4cf5227e4ad2100db6671fc4c369", "type": "github" }, "original": { @@ -20,23 +20,8 @@ } }, "flake-utils": { - "locked": { - "lastModified": 1642700792, - "narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "846b2ae0fc4cc943637d3d1def4454213e203cba", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_2": { "inputs": { - "systems": "systems" + "systems": "systems_2" }, "locked": { "lastModified": 1681202837, @@ -70,11 +55,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1683442750, - "narHash": "sha256-IiJ0WWW6OcCrVFl1ijE+gTaP0ChFfV6dNkJR05yStmw=", + "lastModified": 1684091592, + "narHash": "sha256-GBGqOd/owyHARCM/kz1OGnUUqEWGWEVcj9+zrl3vVlI=", "owner": "nixos", "repo": "nixpkgs", - "rev": "eb751d65225ec53de9cf3d88acbf08d275882389", + "rev": "3007746b3f5bfcb49e102b517bca891822a41b31", "type": "github" }, "original": { @@ -112,18 +97,33 @@ "type": "github" } }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, "typelevel-nix": { "inputs": { "devshell": "devshell", - "flake-utils": "flake-utils_2", + "flake-utils": "flake-utils", "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1683546385, - "narHash": "sha256-QsxQsFlrturrBYY5nEp/J6UYe7NPL4i+QpXg+4HIl9o=", + "lastModified": 1684122828, + "narHash": "sha256-7HEDlQyRIhw5PLaC3NKiNAGTSrOsjWMT7+wGNaeWIQI=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "bb9774d68fa09d259f490c81546f36ec6774e96a", + "rev": "f5f013dbe26aeb40b6690d55ddddf3a4eb915159", "type": "github" }, "original": { From 0bd85254dd1f10f4329eab97844742f8e4796e27 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 23 May 2023 07:48:43 +0000 Subject: [PATCH 067/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/f5f013dbe26aeb40b6690d55ddddf3a4eb915159' (2023-05-15) → 'github:typelevel/typelevel-nix/bcb8b3623229dc58cf080a3cf83aa9b372a95e79' (2023-05-22) • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/3007746b3f5bfcb49e102b517bca891822a41b31' (2023-05-14) → 'github:nixos/nixpkgs/85340996ba67cc02f01ba324e18b1306892ed6f5' (2023-05-21) --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 3c221672db..3aa99732f3 100644 --- a/flake.lock +++ b/flake.lock @@ -55,11 +55,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1684091592, - "narHash": "sha256-GBGqOd/owyHARCM/kz1OGnUUqEWGWEVcj9+zrl3vVlI=", + "lastModified": 1684668519, + "narHash": "sha256-KkVvlXTqdLLwko9Y0p1Xv6KQ9QTcQorrU098cGilb7c=", "owner": "nixos", "repo": "nixpkgs", - "rev": "3007746b3f5bfcb49e102b517bca891822a41b31", + "rev": "85340996ba67cc02f01ba324e18b1306892ed6f5", "type": "github" }, "original": { @@ -119,11 +119,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1684122828, - "narHash": "sha256-7HEDlQyRIhw5PLaC3NKiNAGTSrOsjWMT7+wGNaeWIQI=", + "lastModified": 1684765835, + "narHash": "sha256-/Wmcx12luP1oTWFj0dd5elX4oxxq1UcAv9gEb54Wx4A=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "f5f013dbe26aeb40b6690d55ddddf3a4eb915159", + "rev": "bcb8b3623229dc58cf080a3cf83aa9b372a95e79", "type": "github" }, "original": { From 4661ba342fd6dc96c06c68c2bc7b3d5a870b4e69 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 23 May 2023 19:53:58 +0000 Subject: [PATCH 068/429] Fixup native polling --- .../cats/effect/unsafe/PollingSystem.scala | 5 +- .../cats/effect/unsafe/EpollSystem.scala | 53 ++++---- .../unsafe/EventLoopExecutorScheduler.scala | 9 +- .../cats/effect/unsafe/KqueueSystem.scala | 118 +++++++++--------- .../unsafe/PollingExecutorScheduler.scala | 2 +- 5 files changed, 90 insertions(+), 97 deletions(-) diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index 58db64efbf..734facd605 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -38,10 +38,7 @@ abstract class PollingSystem { /** * @param nanos * the maximum duration for which to block, where `nanos == -1` indicates to block - * indefinitely. ''However'', if `nanos == -1` and there are no remaining events to poll - * for, this method should return `false` immediately. This is unfortunate but necessary so - * that the `EventLoop` can yield to the Scala Native global `ExecutionContext` which is - * currently hard-coded into every test framework, including MUnit, specs2, and Weaver. + * indefinitely. * * @return * whether any events were polled diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index 8a06de402d..18e043de5d 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -180,44 +180,39 @@ object EpollSystem extends PollingSystem { throw new IOException(fromCString(strerror(errno))) private[EpollSystem] def poll(timeout: Long): Boolean = { - val noHandles = handles.isEmpty() - if (timeout <= 0 && noHandles) - false // nothing to do here - else { - val events = stackalloc[epoll_event](MaxEvents.toLong) - var polled = false + val events = stackalloc[epoll_event](MaxEvents.toLong) + var polled = false - @tailrec - def processEvents(timeout: Int): Unit = { + @tailrec + def processEvents(timeout: Int): Unit = { - val triggeredEvents = epoll_wait(epfd, events, MaxEvents, timeout) + val triggeredEvents = epoll_wait(epfd, events, MaxEvents, timeout) - if (triggeredEvents >= 0) { - polled = true + if (triggeredEvents >= 0) { + polled = true - var i = 0 - while (i < triggeredEvents) { - val event = events + i.toLong - val handle = fromPtr(event.data) - handle.notify(event.events.toInt) - i += 1 - } - } else { - throw new IOException(fromCString(strerror(errno))) + var i = 0 + while (i < triggeredEvents) { + val event = events + i.toLong + val handle = fromPtr(event.data) + handle.notify(event.events.toInt) + i += 1 } - - if (triggeredEvents >= MaxEvents) - processEvents(0) // drain the ready list - else - () + } else { + throw new IOException(fromCString(strerror(errno))) } - val timeoutMillis = if (timeout == -1) -1 else (timeout / 1000000).toInt - processEvents(timeoutMillis) - - !handles.isEmpty() + if (triggeredEvents >= MaxEvents) + processEvents(0) // drain the ready list + else + () } + + val timeoutMillis = if (timeout == -1) -1 else (timeout / 1000000).toInt + processEvents(timeoutMillis) + + polled } private[EpollSystem] def needsPoll(): Boolean = !handles.isEmpty() diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala index fc78da5491..b583d5e718 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala @@ -119,7 +119,14 @@ private[effect] final class EventLoopExecutorScheduler(pollEvery: Int, system: P else -1 - system.poll(poller, timeout, reportFailure) + /* + * if `timeout == -1` and there are no remaining events to poll for, we should break the + * loop immediately. This is unfortunate but necessary so that the event loop can yield to + * the Scala Native global `ExecutionContext` which is currently hard-coded into every + * test framework, including MUnit, specs2, and Weaver. + */ + if (system.needsPoll(poller) || timeout != -1) + system.poll(poller, timeout, reportFailure) continue = !executeQueue.isEmpty() || !sleepQueue.isEmpty() || system.needsPoll(poller) } diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index 1974b11099..882177424a 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -163,76 +163,70 @@ object KqueueSystem extends PollingSystem { throw new IOException(fromCString(strerror(errno))) private[KqueueSystem] def poll(timeout: Long): Boolean = { - val noCallbacks = callbacks.isEmpty - - if (timeout <= 0 && noCallbacks && changeCount == 0) - false // nothing to do here - else { - - val eventlist = stackalloc[kevent64_s](MaxEvents.toLong) - var polled = false - - @tailrec - def processEvents(timeout: Ptr[timespec], changeCount: Int, flags: Int): Unit = { - - val triggeredEvents = - kevent64( - kqfd, - changelist, - changeCount, - eventlist, - MaxEvents, - flags.toUInt, - timeout - ) - - if (triggeredEvents >= 0) { - polled = true - - var i = 0 - var event = eventlist - while (i < triggeredEvents) { - val cb = callbacks.remove(KEvent(event.ident.toLong, event.filter)) - - if (cb ne null) - cb( - if ((event.flags.toLong & EV_ERROR) != 0) - Left(new IOException(fromCString(strerror(event.data.toInt)))) - else Either.unit - ) - - i += 1 - event += 1 - } - } else { - throw new IOException(fromCString(strerror(errno))) - } - if (triggeredEvents >= MaxEvents) - processEvents(null, 0, KEVENT_FLAG_NONE) // drain the ready list - else - () + val eventlist = stackalloc[kevent64_s](MaxEvents.toLong) + var polled = false + + @tailrec + def processEvents(timeout: Ptr[timespec], changeCount: Int, flags: Int): Unit = { + + val triggeredEvents = + kevent64( + kqfd, + changelist, + changeCount, + eventlist, + MaxEvents, + flags.toUInt, + timeout + ) + + if (triggeredEvents >= 0) { + polled = true + + var i = 0 + var event = eventlist + while (i < triggeredEvents) { + val cb = callbacks.remove(KEvent(event.ident.toLong, event.filter)) + + if (cb ne null) + cb( + if ((event.flags.toLong & EV_ERROR) != 0) + Left(new IOException(fromCString(strerror(event.data.toInt)))) + else Either.unit + ) + + i += 1 + event += 1 + } + } else { + throw new IOException(fromCString(strerror(errno))) } - val timeoutSpec = - if (timeout <= 0) null - else { - val ts = stackalloc[timespec]() - ts.tv_sec = timeout / 1000000000 - ts.tv_nsec = timeout % 1000000000 - ts - } + if (triggeredEvents >= MaxEvents) + processEvents(null, 0, KEVENT_FLAG_NONE) // drain the ready list + else + () + } - val flags = if (timeout == 0) KEVENT_FLAG_IMMEDIATE else KEVENT_FLAG_NONE + val timeoutSpec = + if (timeout <= 0) null + else { + val ts = stackalloc[timespec]() + ts.tv_sec = timeout / 1000000000 + ts.tv_nsec = timeout % 1000000000 + ts + } - processEvents(timeoutSpec, changeCount, flags) - changeCount = 0 + val flags = if (timeout == 0) KEVENT_FLAG_IMMEDIATE else KEVENT_FLAG_NONE - !callbacks.isEmpty() - } + processEvents(timeoutSpec, changeCount, flags) + changeCount = 0 + + polled } - def needsPoll(): Boolean = !callbacks.isEmpty() + def needsPoll(): Boolean = changeCount > 0 || !callbacks.isEmpty() } @nowarn212 diff --git a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala index 4c68b04cc9..d54172a69e 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala @@ -20,7 +20,7 @@ package unsafe import scala.concurrent.ExecutionContextExecutor import scala.concurrent.duration._ -@deprecated("Use default runtime with a custom PollingSystem", "3.5.0") +@deprecated("Use default runtime with a custom PollingSystem", "3.6.0") abstract class PollingExecutorScheduler(pollEvery: Int) extends ExecutionContextExecutor with Scheduler { outer => From e6403d256a1ed9c26ec25a59d37590a13f40dc02 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 24 May 2023 01:15:24 +0000 Subject: [PATCH 069/429] Add `Poller` type parameter to WSTP --- .../unsafe/WorkStealingThreadPool.scala | 8 +-- .../cats/effect/unsafe/FiberMonitor.scala | 8 +-- .../cats/effect/unsafe/PollingSystem.scala | 6 +++ .../cats/effect/IOCompanionPlatform.scala | 2 +- .../FiberMonitorCompanionPlatform.scala | 4 +- .../unsafe/IORuntimeCompanionPlatform.scala | 13 ++--- .../scala/cats/effect/unsafe/LocalQueue.scala | 6 +-- .../unsafe/WorkStealingThreadPool.scala | 49 ++++++++++--------- .../cats/effect/unsafe/WorkerThread.scala | 16 +++--- .../unsafe/metrics/ComputePoolSampler.scala | 2 +- .../src/main/scala/cats/effect/IOFiber.scala | 16 +++--- .../cats/effect/IOPlatformSpecification.scala | 6 +-- 12 files changed, 71 insertions(+), 65 deletions(-) diff --git a/core/js-native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/js-native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index e8118e1cee..d4ddcf705a 100644 --- a/core/js-native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/js-native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -23,7 +23,7 @@ import scala.concurrent.duration.FiniteDuration // Can you imagine a thread pool on JS? Have fun trying to extend or instantiate // this class. Unfortunately, due to the explicit branching, this type leaks // into the shared source code of IOFiber.scala. -private[effect] sealed abstract class WorkStealingThreadPool private () +private[effect] sealed abstract class WorkStealingThreadPool[Poller] private () extends ExecutionContext { def execute(runnable: Runnable): Unit def reportFailure(cause: Throwable): Unit @@ -38,12 +38,12 @@ private[effect] sealed abstract class WorkStealingThreadPool private () private[effect] def canExecuteBlockingCode(): Boolean private[unsafe] def liveTraces(): ( Map[Runnable, Trace], - Map[WorkerThread, (Thread.State, Option[(Runnable, Trace)], Map[Runnable, Trace])], + Map[WorkerThread[_], (Thread.State, Option[(Runnable, Trace)], Map[Runnable, Trace])], Map[Runnable, Trace]) } -private[unsafe] sealed abstract class WorkerThread private () extends Thread { - private[unsafe] def isOwnedBy(threadPool: WorkStealingThreadPool): Boolean +private[unsafe] sealed abstract class WorkerThread[Poller] private () extends Thread { + private[unsafe] def isOwnedBy(threadPool: WorkStealingThreadPool[_]): Boolean private[unsafe] def monitor(fiber: Runnable): WeakBag.Handle private[unsafe] def index: Int } diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala index 2d3e2ae618..177b328e1b 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala @@ -44,7 +44,7 @@ import java.util.concurrent.ConcurrentLinkedQueue private[effect] sealed class FiberMonitor( // A reference to the compute pool of the `IORuntime` in which this suspended fiber bag // operates. `null` if the compute pool of the `IORuntime` is not a `WorkStealingThreadPool`. - private[this] val compute: WorkStealingThreadPool + private[this] val compute: WorkStealingThreadPool[_] ) extends FiberMonitorShared { private[this] final val BagReferences = @@ -69,8 +69,8 @@ private[effect] sealed class FiberMonitor( */ def monitorSuspended(fiber: IOFiber[_]): WeakBag.Handle = { val thread = Thread.currentThread() - if (thread.isInstanceOf[WorkerThread]) { - val worker = thread.asInstanceOf[WorkerThread] + if (thread.isInstanceOf[WorkerThread[_]]) { + val worker = thread.asInstanceOf[WorkerThread[_]] // Guard against tracking errors when multiple work stealing thread pools exist. if (worker.isOwnedBy(compute)) { worker.monitor(fiber) @@ -116,7 +116,7 @@ private[effect] sealed class FiberMonitor( val externalFibers = external.collect(justFibers) val suspendedFibers = suspended.collect(justFibers) val workersMapping: Map[ - WorkerThread, + WorkerThread[_], (Thread.State, Option[(IOFiber[_], Trace)], Map[IOFiber[_], Trace])] = workers.map { case (thread, (state, opt, set)) => diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index 734facd605..63918715f4 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -54,3 +54,9 @@ abstract class PollingSystem { def interrupt(targetThread: Thread, targetPoller: Poller): Unit } + +private object PollingSystem { + type WithPoller[Poller0] = PollingSystem { + type Poller = Poller0 + } +} diff --git a/core/jvm/src/main/scala/cats/effect/IOCompanionPlatform.scala b/core/jvm/src/main/scala/cats/effect/IOCompanionPlatform.scala index 363bbcbc78..d9418a6c0d 100644 --- a/core/jvm/src/main/scala/cats/effect/IOCompanionPlatform.scala +++ b/core/jvm/src/main/scala/cats/effect/IOCompanionPlatform.scala @@ -147,7 +147,7 @@ private[effect] abstract class IOCompanionPlatform { this: IO.type => def poller[Poller](implicit ct: ClassTag[Poller]): IO[Option[Poller]] = IO.executionContext.map { - case wstp: WorkStealingThreadPool + case wstp: WorkStealingThreadPool[_] if ct.runtimeClass.isInstance(wstp.globalPollingState) => Some(wstp.globalPollingState.asInstanceOf[Poller]) case _ => None diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/FiberMonitorCompanionPlatform.scala b/core/jvm/src/main/scala/cats/effect/unsafe/FiberMonitorCompanionPlatform.scala index 9be52b3dde..ea910919bc 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/FiberMonitorCompanionPlatform.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/FiberMonitorCompanionPlatform.scala @@ -22,8 +22,8 @@ import scala.concurrent.ExecutionContext private[unsafe] trait FiberMonitorCompanionPlatform { def apply(compute: ExecutionContext): FiberMonitor = { - if (TracingConstants.isStackTracing && compute.isInstanceOf[WorkStealingThreadPool]) { - val wstp = compute.asInstanceOf[WorkStealingThreadPool] + if (TracingConstants.isStackTracing && compute.isInstanceOf[WorkStealingThreadPool[_]]) { + val wstp = compute.asInstanceOf[WorkStealingThreadPool[_]] new FiberMonitor(wstp) } else { new FiberMonitor(null) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala index db259b1316..2b877c55e4 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala @@ -40,7 +40,7 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type blockerThreadPrefix: String, runtimeBlockingExpiration: Duration, reportFailure: Throwable => Unit - ): (WorkStealingThreadPool, () => Unit) = createWorkStealingComputeThreadPool( + ): (WorkStealingThreadPool[_], () => Unit) = createWorkStealingComputeThreadPool( threads, threadPrefix, blockerThreadPrefix, @@ -57,7 +57,7 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type runtimeBlockingExpiration: Duration, reportFailure: Throwable => Unit, blockedThreadDetectionEnabled: Boolean - ): (WorkStealingThreadPool, () => Unit) = createWorkStealingComputeThreadPool( + ): (WorkStealingThreadPool[_], () => Unit) = createWorkStealingComputeThreadPool( threads, threadPrefix, blockerThreadPrefix, @@ -75,9 +75,10 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type runtimeBlockingExpiration: Duration = 60.seconds, reportFailure: Throwable => Unit = _.printStackTrace(), blockedThreadDetectionEnabled: Boolean = false, - pollingSystem: PollingSystem = SelectorSystem()): (WorkStealingThreadPool, () => Unit) = { + pollingSystem: PollingSystem = SelectorSystem()) + : (WorkStealingThreadPool[_], () => Unit) = { val threadPool = - new WorkStealingThreadPool( + new WorkStealingThreadPool[pollingSystem.Poller]( threads, threadPrefix, blockerThreadPrefix, @@ -160,14 +161,14 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type threads: Int = Math.max(2, Runtime.getRuntime().availableProcessors()), threadPrefix: String = "io-compute", blockerThreadPrefix: String = DefaultBlockerPrefix) - : (WorkStealingThreadPool, () => Unit) = + : (WorkStealingThreadPool[_], () => Unit) = createWorkStealingComputeThreadPool(threads, threadPrefix, blockerThreadPrefix) @deprecated("bincompat shim for previous default method overload", "3.3.13") def createDefaultComputeThreadPool( self: () => IORuntime, threads: Int, - threadPrefix: String): (WorkStealingThreadPool, () => Unit) = + threadPrefix: String): (WorkStealingThreadPool[_], () => Unit) = createDefaultComputeThreadPool(self(), threads, threadPrefix) def createDefaultBlockingExecutionContext( diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/LocalQueue.scala b/core/jvm/src/main/scala/cats/effect/unsafe/LocalQueue.scala index 7561a9a989..c83c34b5ec 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/LocalQueue.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/LocalQueue.scala @@ -337,7 +337,7 @@ private final class LocalQueue extends LocalQueuePadding { * @return * a fiber to be executed directly */ - def enqueueBatch(batch: Array[Runnable], worker: WorkerThread): Runnable = { + def enqueueBatch(batch: Array[Runnable], worker: WorkerThread[_]): Runnable = { // A plain, unsynchronized load of the tail of the local queue. val tl = tail @@ -410,7 +410,7 @@ private final class LocalQueue extends LocalQueuePadding { * the fiber at the head of the queue, or `null` if the queue is empty (in order to avoid * unnecessary allocations) */ - def dequeue(worker: WorkerThread): Runnable = { + def dequeue(worker: WorkerThread[_]): Runnable = { // A plain, unsynchronized load of the tail of the local queue. val tl = tail @@ -487,7 +487,7 @@ private final class LocalQueue extends LocalQueuePadding { * a reference to the first fiber to be executed by the stealing [[WorkerThread]], or `null` * if the stealing was unsuccessful */ - def stealInto(dst: LocalQueue, dstWorker: WorkerThread): Runnable = { + def stealInto(dst: LocalQueue, dstWorker: WorkerThread[_]): Runnable = { // A plain, unsynchronized load of the tail of the destination queue, owned // by the executing thread. val dstTl = dst.tail diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index 2355ac0a2a..2f2de85b24 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -58,13 +58,13 @@ import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger} * contention. Work stealing is tried using a linear search starting from a random worker thread * index. */ -private[effect] final class WorkStealingThreadPool( +private[effect] final class WorkStealingThreadPool[Poller]( threadCount: Int, // number of worker threads private[unsafe] val threadPrefix: String, // prefix for the name of worker threads private[unsafe] val blockerThreadPrefix: String, // prefix for the name of worker threads currently in a blocking region private[unsafe] val runtimeBlockingExpiration: Duration, private[unsafe] val blockedThreadDetectionEnabled: Boolean, - system: PollingSystem, + system: PollingSystem.WithPoller[Poller], reportFailure0: Throwable => Unit ) extends ExecutionContextExecutor with Scheduler { @@ -75,24 +75,25 @@ private[effect] final class WorkStealingThreadPool( /** * References to worker threads and their local queues. */ - private[this] val workerThreads: Array[WorkerThread] = new Array(threadCount) + private[this] val workerThreads: Array[WorkerThread[Poller]] = new Array(threadCount) private[unsafe] val localQueues: Array[LocalQueue] = new Array(threadCount) private[unsafe] val sleepers: Array[TimerSkipList] = new Array(threadCount) private[unsafe] val parkedSignals: Array[AtomicBoolean] = new Array(threadCount) private[unsafe] val fiberBags: Array[WeakBag[Runnable]] = new Array(threadCount) - private[unsafe] val pollers: Array[AnyRef] = new Array[AnyRef](threadCount) + private[unsafe] val pollers: Array[Poller] = + new Array[AnyRef](threadCount).asInstanceOf[Array[Poller]] private[effect] val globalPollingState: Any = system.makeGlobalPollingState(register) - private[this] def register(cb: system.Poller => Unit): Unit = { + private[this] def register(cb: Poller => Unit): Unit = { // figure out where we are val thread = Thread.currentThread() val pool = WorkStealingThreadPool.this - if (thread.isInstanceOf[WorkerThread]) { - val worker = thread.asInstanceOf[WorkerThread] + if (thread.isInstanceOf[WorkerThread[_]]) { + val worker = thread.asInstanceOf[WorkerThread[Poller]] if (worker.isOwnedBy(pool)) // we're good - cb(worker.poller().asInstanceOf[system.Poller]) + cb(worker.poller()) else // possibly a blocking worker thread, possibly on another wstp scheduleExternal(() => register(cb)) } else scheduleExternal(() => register(cb)) @@ -117,8 +118,8 @@ private[effect] final class WorkStealingThreadPool( */ private[this] val state: AtomicInteger = new AtomicInteger(threadCount << UnparkShift) - private[unsafe] val cachedThreads: ConcurrentSkipListSet[WorkerThread] = - new ConcurrentSkipListSet(Comparator.comparingInt[WorkerThread](_.nameIndex)) + private[unsafe] val cachedThreads: ConcurrentSkipListSet[WorkerThread[Poller]] = + new ConcurrentSkipListSet(Comparator.comparingInt[WorkerThread[Poller]](_.nameIndex)) /** * The shutdown latch of the work stealing thread pool. @@ -172,7 +173,7 @@ private[effect] final class WorkStealingThreadPool( } } - private[unsafe] def getWorkerThreads: Array[WorkerThread] = workerThreads + private[unsafe] def getWorkerThreads: Array[WorkerThread[Poller]] = workerThreads /** * Tries to steal work from other worker threads. This method does a linear search of the @@ -193,7 +194,7 @@ private[effect] final class WorkStealingThreadPool( private[unsafe] def stealFromOtherWorkerThread( dest: Int, random: ThreadLocalRandom, - destWorker: WorkerThread): Runnable = { + destWorker: WorkerThread[Poller]): Runnable = { val destQueue = localQueues(dest) val from = random.nextInt(threadCount) @@ -472,7 +473,7 @@ private[effect] final class WorkStealingThreadPool( * @param newWorker * the new worker thread instance to be installed at the provided index */ - private[unsafe] def replaceWorker(index: Int, newWorker: WorkerThread): Unit = { + private[unsafe] def replaceWorker(index: Int, newWorker: WorkerThread[Poller]): Unit = { workerThreads(index) = newWorker workerThreadPublisher.lazySet(true) } @@ -495,8 +496,8 @@ private[effect] final class WorkStealingThreadPool( val pool = this val thread = Thread.currentThread() - if (thread.isInstanceOf[WorkerThread]) { - val worker = thread.asInstanceOf[WorkerThread] + if (thread.isInstanceOf[WorkerThread[_]]) { + val worker = thread.asInstanceOf[WorkerThread[Poller]] if (worker.isOwnedBy(pool)) { worker.reschedule(runnable) } else { @@ -513,8 +514,8 @@ private[effect] final class WorkStealingThreadPool( */ private[effect] def canExecuteBlockingCode(): Boolean = { val thread = Thread.currentThread() - if (thread.isInstanceOf[WorkerThread]) { - val worker = thread.asInstanceOf[WorkerThread] + if (thread.isInstanceOf[WorkerThread[_]]) { + val worker = thread.asInstanceOf[WorkerThread[Poller]] worker.canExecuteBlockingCodeOn(this) } else { false @@ -545,7 +546,7 @@ private[effect] final class WorkStealingThreadPool( */ private[unsafe] def liveTraces(): ( Map[Runnable, Trace], - Map[WorkerThread, (Thread.State, Option[(Runnable, Trace)], Map[Runnable, Trace])], + Map[WorkerThread[_], (Thread.State, Option[(Runnable, Trace)], Map[Runnable, Trace])], Map[Runnable, Trace]) = { val externalFibers: Map[Runnable, Trace] = externalQueue .snapshot() @@ -560,7 +561,7 @@ private[effect] final class WorkStealingThreadPool( val map = mutable .Map - .empty[WorkerThread, (Thread.State, Option[(Runnable, Trace)], Map[Runnable, Trace])] + .empty[WorkerThread[_], (Thread.State, Option[(Runnable, Trace)], Map[Runnable, Trace])] val suspended = mutable.Map.empty[Runnable, Trace] var i = 0 @@ -599,8 +600,8 @@ private[effect] final class WorkStealingThreadPool( val pool = this val thread = Thread.currentThread() - if (thread.isInstanceOf[WorkerThread]) { - val worker = thread.asInstanceOf[WorkerThread] + if (thread.isInstanceOf[WorkerThread[_]]) { + val worker = thread.asInstanceOf[WorkerThread[Poller]] if (worker.isOwnedBy(pool)) { worker.schedule(runnable) } else { @@ -637,8 +638,8 @@ private[effect] final class WorkStealingThreadPool( */ def sleepInternal(delay: FiniteDuration, callback: Right[Nothing, Unit] => Unit): Runnable = { val thread = Thread.currentThread() - if (thread.isInstanceOf[WorkerThread]) { - val worker = thread.asInstanceOf[WorkerThread] + if (thread.isInstanceOf[WorkerThread[_]]) { + val worker = thread.asInstanceOf[WorkerThread[Poller]] if (worker.isOwnedBy(this)) { worker.sleep(delay, callback) } else { @@ -701,7 +702,7 @@ private[effect] final class WorkStealingThreadPool( // Clear the interrupt flag. Thread.interrupted() - var t: WorkerThread = null + var t: WorkerThread[Poller] = null while ({ t = cachedThreads.pollFirst() t ne null diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala index da7d6a8ac6..03e28d88fa 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala @@ -41,7 +41,7 @@ import java.util.concurrent.atomic.AtomicBoolean * system when compared to a fixed size thread pool whose worker threads all draw tasks from a * single global work queue. */ -private final class WorkerThread( +private final class WorkerThread[Poller]( idx: Int, // Local queue instance with exclusive write access. private[this] var queue: LocalQueue, @@ -53,10 +53,10 @@ private final class WorkerThread( // A worker-thread-local weak bag for tracking suspended fibers. private[this] var fiberBag: WeakBag[Runnable], private[this] var sleepers: TimerSkipList, - private[this] val system: PollingSystem, - __poller: AnyRef, + private[this] val system: PollingSystem.WithPoller[Poller], + private[this] var _poller: Poller, // Reference to the `WorkStealingThreadPool` in which this thread operates. - pool: WorkStealingThreadPool) + pool: WorkStealingThreadPool[Poller]) extends Thread with BlockContext { @@ -66,8 +66,6 @@ private final class WorkerThread( // Index assigned by the `WorkStealingThreadPool` for identification purposes. private[this] var _index: Int = idx - private[this] var _poller: system.Poller = __poller.asInstanceOf[system.Poller] - /** * Uncontented source of randomness. By default, `java.util.Random` is thread safe, which is a * feature we do not need in this class, as the source of randomness is completely isolated to @@ -115,7 +113,7 @@ private final class WorkerThread( setName(s"$prefix-$nameIndex") } - private[unsafe] def poller(): Any = _poller + private[unsafe] def poller(): Poller = _poller /** * Schedules the fiber for execution at the back of the local queue and notifies the work @@ -179,7 +177,7 @@ private final class WorkerThread( * `true` if this worker thread is owned by the provided work stealing thread pool, `false` * otherwise */ - def isOwnedBy(threadPool: WorkStealingThreadPool): Boolean = + def isOwnedBy(threadPool: WorkStealingThreadPool[_]): Boolean = (pool eq threadPool) && !blocking /** @@ -194,7 +192,7 @@ private final class WorkerThread( * `true` if this worker thread is owned by the provided work stealing thread pool, `false` * otherwise */ - def canExecuteBlockingCodeOn(threadPool: WorkStealingThreadPool): Boolean = + def canExecuteBlockingCodeOn(threadPool: WorkStealingThreadPool[Poller]): Boolean = pool eq threadPool /** diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/metrics/ComputePoolSampler.scala b/core/jvm/src/main/scala/cats/effect/unsafe/metrics/ComputePoolSampler.scala index beae6527f2..d15573f2e4 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/metrics/ComputePoolSampler.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/metrics/ComputePoolSampler.scala @@ -24,7 +24,7 @@ package metrics * @param compute * the monitored compute work stealing thread pool */ -private[unsafe] final class ComputePoolSampler(compute: WorkStealingThreadPool) +private[unsafe] final class ComputePoolSampler(compute: WorkStealingThreadPool[_]) extends ComputePoolSamplerMBean { def getWorkerThreadCount(): Int = compute.getWorkerThreadCount() def getActiveThreadCount(): Int = compute.getActiveThreadCount() diff --git a/core/shared/src/main/scala/cats/effect/IOFiber.scala b/core/shared/src/main/scala/cats/effect/IOFiber.scala index 984e70a03f..3c9d47ee27 100644 --- a/core/shared/src/main/scala/cats/effect/IOFiber.scala +++ b/core/shared/src/main/scala/cats/effect/IOFiber.scala @@ -918,8 +918,8 @@ private final class IOFiber[A]( val scheduler = runtime.scheduler val cancel = - if (scheduler.isInstanceOf[WorkStealingThreadPool]) - scheduler.asInstanceOf[WorkStealingThreadPool].sleepInternal(delay, cb) + if (scheduler.isInstanceOf[WorkStealingThreadPool[_]]) + scheduler.asInstanceOf[WorkStealingThreadPool[_]].sleepInternal(delay, cb) else scheduler.sleep(delay, () => cb(RightUnit)) @@ -962,8 +962,8 @@ private final class IOFiber[A]( if (cur.hint eq IOFiber.TypeBlocking) { val ec = currentCtx - if (ec.isInstanceOf[WorkStealingThreadPool]) { - val wstp = ec.asInstanceOf[WorkStealingThreadPool] + if (ec.isInstanceOf[WorkStealingThreadPool[_]]) { + val wstp = ec.asInstanceOf[WorkStealingThreadPool[_]] if (wstp.canExecuteBlockingCode()) { var error: Throwable = null val r = @@ -1285,8 +1285,8 @@ private final class IOFiber[A]( private[this] def rescheduleFiber(ec: ExecutionContext, fiber: IOFiber[_]): Unit = { if (Platform.isJvm) { - if (ec.isInstanceOf[WorkStealingThreadPool]) { - val wstp = ec.asInstanceOf[WorkStealingThreadPool] + if (ec.isInstanceOf[WorkStealingThreadPool[_]]) { + val wstp = ec.asInstanceOf[WorkStealingThreadPool[_]] wstp.reschedule(fiber) } else { scheduleOnForeignEC(ec, fiber) @@ -1298,8 +1298,8 @@ private final class IOFiber[A]( private[this] def scheduleFiber(ec: ExecutionContext, fiber: IOFiber[_]): Unit = { if (Platform.isJvm) { - if (ec.isInstanceOf[WorkStealingThreadPool]) { - val wstp = ec.asInstanceOf[WorkStealingThreadPool] + if (ec.isInstanceOf[WorkStealingThreadPool[_]]) { + val wstp = ec.asInstanceOf[WorkStealingThreadPool[_]] wstp.execute(fiber) } else { scheduleOnForeignEC(ec, fiber) diff --git a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala index 0b6f902837..bfc9d59718 100644 --- a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala +++ b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala @@ -269,7 +269,7 @@ trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => "run a timer which crosses into a blocking region" in realWithRuntime { rt => rt.scheduler match { - case sched: WorkStealingThreadPool => + case sched: WorkStealingThreadPool[_] => // we structure this test by calling the runtime directly to avoid nondeterminism val delay = IO.async[Unit] { cb => IO { @@ -292,7 +292,7 @@ trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => "run timers exactly once when crossing into a blocking region" in realWithRuntime { rt => rt.scheduler match { - case sched: WorkStealingThreadPool => + case sched: WorkStealingThreadPool[_] => IO defer { val ai = new AtomicInteger(0) @@ -310,7 +310,7 @@ trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => "run a timer registered on a blocker" in realWithRuntime { rt => rt.scheduler match { - case sched: WorkStealingThreadPool => + case sched: WorkStealingThreadPool[_] => // we structure this test by calling the runtime directly to avoid nondeterminism val delay = IO.async[Unit] { cb => IO { From 9b77aacb82d4fe2e9a7535e33a705e198ed7f70b Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 24 May 2023 01:31:09 +0000 Subject: [PATCH 070/429] Cleanup casts --- .../scala/cats/effect/unsafe/WorkStealingThreadPool.scala | 6 +++--- .../src/main/scala/cats/effect/unsafe/WorkerThread.scala | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index 2f2de85b24..5879d58e4b 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -319,7 +319,7 @@ private[effect] final class WorkStealingThreadPool[Poller]( // impossible. workerThreadPublisher.get() val worker = workerThreads(index) - system.interrupt(worker, pollers(index).asInstanceOf[system.Poller]) + system.interrupt(worker, pollers(index)) return true } @@ -343,7 +343,7 @@ private[effect] final class WorkStealingThreadPool[Poller]( state.getAndAdd(DeltaSearching) workerThreadPublisher.get() val worker = workerThreads(index) - system.interrupt(worker, pollers(index).asInstanceOf[system.Poller]) + system.interrupt(worker, pollers(index)) } // else: was already unparked } @@ -695,7 +695,7 @@ private[effect] final class WorkStealingThreadPool[Poller]( var i = 0 while (i < threadCount) { workerThreads(i).interrupt() - system.closePoller(pollers(i).asInstanceOf[system.Poller]) + system.closePoller(pollers(i)) i += 1 } diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala index 03e28d88fa..85e34a8451 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala @@ -441,7 +441,7 @@ private final class WorkerThread[Poller]( parked = null fiberBag = null _active = null - _poller = null.asInstanceOf[system.Poller] + _poller = null.asInstanceOf[Poller] // Add this thread to the cached threads data structure, to be picked up // by another thread in the future. @@ -868,7 +868,7 @@ private final class WorkerThread[Poller]( sleepers = pool.sleepers(newIdx) parked = pool.parkedSignals(newIdx) fiberBag = pool.fiberBags(newIdx) - _poller = pool.pollers(newIdx).asInstanceOf[system.Poller] + _poller = pool.pollers(newIdx) // Reset the name of the thread to the regular prefix. val prefix = pool.threadPrefix From c7a57a8017bd9b4d3401314886df5dcc464604ed Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 24 May 2023 01:39:52 +0000 Subject: [PATCH 071/429] Fix 2.12 compile --- .../src/main/scala/cats/effect/unsafe/FiberMonitor.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala index 177b328e1b..d994a3a13a 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala @@ -123,7 +123,7 @@ private[effect] sealed class FiberMonitor( val filteredOpt = opt.collect(justFibers) val filteredSet = set.collect(justFibers) (thread, (state, filteredOpt, filteredSet)) - } + }.toMap (externalFibers, workersMapping, suspendedFibers) } From 35eae3e46b832433aacca25aca0f4050989d3d4a Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 27 May 2023 17:50:02 +0000 Subject: [PATCH 072/429] Expose pollers via `IORuntime` --- .../src/main/scala/cats/effect/IOApp.scala | 6 ++- .../cats/effect/IOCompanionPlatform.scala | 10 ---- .../unsafe/IORuntimeBuilderPlatform.scala | 33 +++++++++++-- .../unsafe/IORuntimeCompanionPlatform.scala | 46 +++++++++++++------ .../unsafe/WorkStealingThreadPool.scala | 4 +- .../cats/effect/unsafe/WorkerThread.scala | 2 +- .../src/main/scala/cats/effect/IOApp.scala | 12 ++--- .../cats/effect/IOCompanionPlatform.scala | 10 ---- .../unsafe/EventLoopExecutorScheduler.scala | 10 ++-- .../unsafe/IORuntimeBuilderPlatform.scala | 31 +++++++++++-- .../unsafe/IORuntimeCompanionPlatform.scala | 26 +++++++---- .../src/main/scala/cats/effect/IO.scala | 10 ++++ .../src/main/scala/cats/effect/IOFiber.scala | 4 ++ .../scala/cats/effect/unsafe/IORuntime.scala | 22 ++++++++- .../cats/effect/unsafe/IORuntimeBuilder.scala | 8 +++- .../cats/effect/IOPlatformSpecification.scala | 10 ++-- .../scala/cats/effect/RunnersPlatform.scala | 3 +- .../cats/effect/SelectorPollerSpec.scala | 22 +++++---- .../effect/unsafe/HelperThreadParkSpec.scala | 2 +- .../effect/unsafe/StripedHashtableSpec.scala | 2 +- .../effect/unsafe/WorkerThreadNameSpec.scala | 2 +- .../effect/FileDescriptorPollerSpec.scala | 5 +- 22 files changed, 189 insertions(+), 91 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/IOApp.scala b/core/jvm/src/main/scala/cats/effect/IOApp.scala index e2940bed83..065f714285 100644 --- a/core/jvm/src/main/scala/cats/effect/IOApp.scala +++ b/core/jvm/src/main/scala/cats/effect/IOApp.scala @@ -165,7 +165,8 @@ trait IOApp { */ protected def runtimeConfig: unsafe.IORuntimeConfig = unsafe.IORuntimeConfig() - protected def pollingSystem: unsafe.PollingSystem = unsafe.SelectorSystem() + protected def pollingSystem: unsafe.PollingSystem = + unsafe.IORuntime.createDefaultPollingSystem() /** * Controls the number of worker threads which will be allocated to the compute pool in the @@ -340,7 +341,7 @@ trait IOApp { import unsafe.IORuntime val installed = IORuntime installGlobal { - val (compute, compDown) = + val (compute, poller, compDown) = IORuntime.createWorkStealingComputeThreadPool( threads = computeWorkerThreadCount, reportFailure = t => reportFailure(t).unsafeRunAndForgetWithoutCallback()(runtime), @@ -355,6 +356,7 @@ trait IOApp { compute, blocking, compute, + List(poller), { () => compDown() blockDown() diff --git a/core/jvm/src/main/scala/cats/effect/IOCompanionPlatform.scala b/core/jvm/src/main/scala/cats/effect/IOCompanionPlatform.scala index d9418a6c0d..a40cce71a4 100644 --- a/core/jvm/src/main/scala/cats/effect/IOCompanionPlatform.scala +++ b/core/jvm/src/main/scala/cats/effect/IOCompanionPlatform.scala @@ -18,9 +18,6 @@ package cats.effect import cats.effect.std.Console import cats.effect.tracing.Tracing -import cats.effect.unsafe.WorkStealingThreadPool - -import scala.reflect.ClassTag import java.time.Instant import java.util.concurrent.{CompletableFuture, CompletionStage} @@ -145,11 +142,4 @@ private[effect] abstract class IOCompanionPlatform { this: IO.type => def readLine: IO[String] = Console[IO].readLine - def poller[Poller](implicit ct: ClassTag[Poller]): IO[Option[Poller]] = - IO.executionContext.map { - case wstp: WorkStealingThreadPool[_] - if ct.runtimeClass.isInstance(wstp.globalPollingState) => - Some(wstp.globalPollingState.asInstanceOf[Poller]) - case _ => None - } } diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeBuilderPlatform.scala b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeBuilderPlatform.scala index 7c5ac1ec4a..2ccc274a51 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeBuilderPlatform.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeBuilderPlatform.scala @@ -18,11 +18,36 @@ package cats.effect.unsafe private[unsafe] abstract class IORuntimeBuilderPlatform { self: IORuntimeBuilder => + protected var customPollingSystem: Option[PollingSystem] = None + + /** + * Override the default [[PollingSystem]] + */ + def setPollingSystem(system: PollingSystem): IORuntimeBuilder = { + if (customPollingSystem.isDefined) { + throw new RuntimeException("Polling system can only be set once") + } + customPollingSystem = Some(system) + this + } + // TODO unify this with the defaults in IORuntime.global and IOApp protected def platformSpecificBuild: IORuntime = { - val (compute, computeShutdown) = - customCompute.getOrElse( - IORuntime.createWorkStealingComputeThreadPool(reportFailure = failureReporter)) + val (compute, poller, computeShutdown) = + customCompute + .map { + case (c, s) => + (c, Nil, s) + } + .getOrElse { + val (c, p, s) = + IORuntime.createWorkStealingComputeThreadPool( + pollingSystem = + customPollingSystem.getOrElse(IORuntime.createDefaultPollingSystem()), + reportFailure = failureReporter + ) + (c, List(p), s) + } val xformedCompute = computeTransform(compute) val (scheduler, schedulerShutdown) = xformedCompute match { @@ -36,6 +61,7 @@ private[unsafe] abstract class IORuntimeBuilderPlatform { self: IORuntimeBuilder computeShutdown() blockingShutdown() schedulerShutdown() + extraPollers.foreach(_._2()) extraShutdownHooks.reverse.foreach(_()) } val runtimeConfig = customConfig.getOrElse(IORuntimeConfig()) @@ -44,6 +70,7 @@ private[unsafe] abstract class IORuntimeBuilderPlatform { self: IORuntimeBuilder computeTransform(compute), blockingTransform(blocking), scheduler, + poller ::: extraPollers.map(_._1), shutdown, runtimeConfig ) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala index 2b877c55e4..0d85644a15 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala @@ -57,16 +57,18 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type runtimeBlockingExpiration: Duration, reportFailure: Throwable => Unit, blockedThreadDetectionEnabled: Boolean - ): (WorkStealingThreadPool[_], () => Unit) = createWorkStealingComputeThreadPool( - threads, - threadPrefix, - blockerThreadPrefix, - runtimeBlockingExpiration, - reportFailure, - false, - SelectorSystem() - ) - + ): (WorkStealingThreadPool[_], () => Unit) = { + val (pool, _, shutdown) = createWorkStealingComputeThreadPool( + threads, + threadPrefix, + blockerThreadPrefix, + runtimeBlockingExpiration, + reportFailure, + false, + SleepSystem + ) + (pool, shutdown) + } // The default compute thread pool on the JVM is now a work stealing thread pool. def createWorkStealingComputeThreadPool( threads: Int = Math.max(2, Runtime.getRuntime().availableProcessors()), @@ -76,7 +78,7 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type reportFailure: Throwable => Unit = _.printStackTrace(), blockedThreadDetectionEnabled: Boolean = false, pollingSystem: PollingSystem = SelectorSystem()) - : (WorkStealingThreadPool[_], () => Unit) = { + : (WorkStealingThreadPool[_], pollingSystem.GlobalPollingState, () => Unit) = { val threadPool = new WorkStealingThreadPool[pollingSystem.Poller]( threads, @@ -146,6 +148,7 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type ( threadPool, + pollingSystem.makeGlobalPollingState(threadPool.register), { () => unregisterMBeans() threadPool.shutdown() @@ -162,7 +165,14 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type threadPrefix: String = "io-compute", blockerThreadPrefix: String = DefaultBlockerPrefix) : (WorkStealingThreadPool[_], () => Unit) = - createWorkStealingComputeThreadPool(threads, threadPrefix, blockerThreadPrefix) + createWorkStealingComputeThreadPool( + threads, + threadPrefix, + blockerThreadPrefix, + 60.seconds, + _.printStackTrace(), + false + ) @deprecated("bincompat shim for previous default method overload", "3.3.13") def createDefaultComputeThreadPool( @@ -197,6 +207,8 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type (Scheduler.fromScheduledExecutor(scheduler), { () => scheduler.shutdown() }) } + def createDefaultPollingSystem(): PollingSystem = SelectorSystem() + @volatile private[this] var _global: IORuntime = null // we don't need to synchronize this with IOApp, because we control the main thread @@ -216,10 +228,16 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type def global: IORuntime = { if (_global == null) { installGlobal { - val (compute, _) = createWorkStealingComputeThreadPool() + val (compute, poller, _) = createWorkStealingComputeThreadPool() val (blocking, _) = createDefaultBlockingExecutionContext() - IORuntime(compute, blocking, compute, () => resetGlobal(), IORuntimeConfig()) + IORuntime( + compute, + blocking, + compute, + List(poller), + () => resetGlobal(), + IORuntimeConfig()) } } diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index 5879d58e4b..5cb79bd4ca 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -83,9 +83,7 @@ private[effect] final class WorkStealingThreadPool[Poller]( private[unsafe] val pollers: Array[Poller] = new Array[AnyRef](threadCount).asInstanceOf[Array[Poller]] - private[effect] val globalPollingState: Any = system.makeGlobalPollingState(register) - - private[this] def register(cb: Poller => Unit): Unit = { + private[unsafe] def register(cb: Poller => Unit): Unit = { // figure out where we are val thread = Thread.currentThread() diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala index 85e34a8451..2b26ca49f7 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala @@ -192,7 +192,7 @@ private final class WorkerThread[Poller]( * `true` if this worker thread is owned by the provided work stealing thread pool, `false` * otherwise */ - def canExecuteBlockingCodeOn(threadPool: WorkStealingThreadPool[Poller]): Boolean = + def canExecuteBlockingCodeOn(threadPool: WorkStealingThreadPool[_]): Boolean = pool eq threadPool /** diff --git a/core/native/src/main/scala/cats/effect/IOApp.scala b/core/native/src/main/scala/cats/effect/IOApp.scala index e542ad1bfb..4ab94ee6d1 100644 --- a/core/native/src/main/scala/cats/effect/IOApp.scala +++ b/core/native/src/main/scala/cats/effect/IOApp.scala @@ -20,7 +20,6 @@ import cats.effect.metrics.{CpuStarvationWarningMetrics, NativeCpuStarvationMetr import scala.concurrent.CancellationException import scala.concurrent.duration._ -import scala.scalanative.meta.LinktimeInfo /** * The primary entry point to a Cats Effect application. Extend this trait rather than defining @@ -181,12 +180,7 @@ trait IOApp { * override this method. */ protected def pollingSystem: unsafe.PollingSystem = - if (LinktimeInfo.isLinux) - unsafe.EpollSystem - else if (LinktimeInfo.isMac) - unsafe.KqueueSystem - else - unsafe.SleepSystem + unsafe.IORuntime.createDefaultPollingSystem() /** * The entry point for your application. Will be called by the runtime when the process is @@ -209,8 +203,8 @@ trait IOApp { import unsafe.IORuntime val installed = IORuntime installGlobal { - val loop = IORuntime.createEventLoop(pollingSystem) - IORuntime(loop, loop, loop, () => IORuntime.resetGlobal(), runtimeConfig) + val (loop, poller) = IORuntime.createEventLoop(pollingSystem) + IORuntime(loop, loop, loop, List(poller), () => IORuntime.resetGlobal(), runtimeConfig) } _runtime = IORuntime.global diff --git a/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala b/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala index 62be293bdd..9b3b3f9e40 100644 --- a/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala +++ b/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala @@ -17,9 +17,6 @@ package cats.effect import cats.effect.std.Console -import cats.effect.unsafe.EventLoopExecutorScheduler - -import scala.reflect.ClassTag import java.time.Instant @@ -66,11 +63,4 @@ private[effect] abstract class IOCompanionPlatform { this: IO.type => def readLine: IO[String] = Console[IO].readLine - def poller[Poller](implicit ct: ClassTag[Poller]): IO[Option[Poller]] = - IO.executionContext.map { - case loop: EventLoopExecutorScheduler - if ct.runtimeClass.isInstance(loop.globalPollingState) => - Some(loop.globalPollingState.asInstanceOf[Poller]) - case _ => None - } } diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala index b583d5e718..5ddf03f797 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala @@ -26,13 +26,13 @@ import scala.util.control.NonFatal import java.util.{ArrayDeque, PriorityQueue} -private[effect] final class EventLoopExecutorScheduler(pollEvery: Int, system: PollingSystem) +private[effect] final class EventLoopExecutorScheduler[Poller]( + pollEvery: Int, + system: PollingSystem.WithPoller[Poller]) extends ExecutionContextExecutor with Scheduler { - private[this] val poller = system.makePoller() - - val globalPollingState: Any = system.makeGlobalPollingState(cb => cb(poller)) + private[unsafe] val poller: Poller = system.makePoller() private[this] var needsReschedule: Boolean = true @@ -160,6 +160,6 @@ private object EventLoopExecutorScheduler { KqueueSystem else SleepSystem - new EventLoopExecutorScheduler(64, system) + new EventLoopExecutorScheduler[system.Poller](64, system) } } diff --git a/core/native/src/main/scala/cats/effect/unsafe/IORuntimeBuilderPlatform.scala b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeBuilderPlatform.scala index e6cba0fa71..5886834743 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/IORuntimeBuilderPlatform.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeBuilderPlatform.scala @@ -18,17 +18,41 @@ package cats.effect.unsafe private[unsafe] abstract class IORuntimeBuilderPlatform { self: IORuntimeBuilder => + protected var customPollingSystem: Option[PollingSystem] = None + + /** + * Override the default [[PollingSystem]] + */ + def setPollingSystem(system: PollingSystem): IORuntimeBuilder = { + if (customPollingSystem.isDefined) { + throw new RuntimeException("Polling system can only be set once") + } + customPollingSystem = Some(system) + this + } + protected def platformSpecificBuild: IORuntime = { val defaultShutdown: () => Unit = () => () - val (compute, computeShutdown) = - customCompute.getOrElse((IORuntime.defaultComputeExecutionContext, defaultShutdown)) + lazy val (loop, poller) = IORuntime.createEventLoop( + customPollingSystem.getOrElse(IORuntime.createDefaultPollingSystem()) + ) + val (compute, pollers, computeShutdown) = + customCompute + .map { case (c, s) => (c, Nil, s) } + .getOrElse( + ( + loop, + List(poller), + defaultShutdown + )) val (blocking, blockingShutdown) = customBlocking.getOrElse((compute, defaultShutdown)) val (scheduler, schedulerShutdown) = - customScheduler.getOrElse((IORuntime.defaultScheduler, defaultShutdown)) + customScheduler.getOrElse((loop, defaultShutdown)) val shutdown = () => { computeShutdown() blockingShutdown() schedulerShutdown() + extraPollers.foreach(_._2()) extraShutdownHooks.reverse.foreach(_()) } val runtimeConfig = customConfig.getOrElse(IORuntimeConfig()) @@ -37,6 +61,7 @@ private[unsafe] abstract class IORuntimeBuilderPlatform { self: IORuntimeBuilder computeTransform(compute), blockingTransform(blocking), scheduler, + pollers ::: extraPollers.map(_._1), shutdown, runtimeConfig ) diff --git a/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala index 66f63ecda8..1d8b8fccb3 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala @@ -17,6 +17,7 @@ package cats.effect.unsafe import scala.concurrent.ExecutionContext +import scala.scalanative.meta.LinktimeInfo private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type => @@ -24,8 +25,21 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type def defaultScheduler: Scheduler = EventLoopExecutorScheduler.global - def createEventLoop(system: PollingSystem): ExecutionContext with Scheduler = - new EventLoopExecutorScheduler(64, system) + def createEventLoop( + system: PollingSystem + ): (ExecutionContext with Scheduler, system.GlobalPollingState) = { + val loop = new EventLoopExecutorScheduler[system.Poller](64, system) + val poller = loop.poller + (loop, system.makeGlobalPollingState(cb => cb(poller))) + } + + def createDefaultPollingSystem(): PollingSystem = + if (LinktimeInfo.isLinux) + EpollSystem + else if (LinktimeInfo.isMac) + KqueueSystem + else + SleepSystem private[this] var _global: IORuntime = null @@ -44,12 +58,8 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type def global: IORuntime = { if (_global == null) { installGlobal { - IORuntime( - defaultComputeExecutionContext, - defaultComputeExecutionContext, - defaultScheduler, - () => resetGlobal(), - IORuntimeConfig()) + val (loop, poller) = createEventLoop(createDefaultPollingSystem()) + IORuntime(loop, loop, loop, List(poller), () => resetGlobal(), IORuntimeConfig()) } } diff --git a/core/shared/src/main/scala/cats/effect/IO.scala b/core/shared/src/main/scala/cats/effect/IO.scala index 372630f7bb..ac29978d36 100644 --- a/core/shared/src/main/scala/cats/effect/IO.scala +++ b/core/shared/src/main/scala/cats/effect/IO.scala @@ -40,6 +40,7 @@ import cats.effect.kernel.CancelScope import cats.effect.kernel.GenTemporal.handleDuration import cats.effect.std.{Backpressure, Console, Env, Supervisor, UUIDGen} import cats.effect.tracing.{Tracing, TracingEvent} +import cats.effect.unsafe.IORuntime import cats.syntax.all._ import scala.annotation.unchecked.uncheckedVariance @@ -1485,6 +1486,11 @@ object IO extends IOCompanionPlatform with IOLowPriorityImplicits { def trace: IO[Trace] = IOTrace + private[effect] def runtime: IO[IORuntime] = ReadRT + + def pollers: IO[List[Any]] = + IO.runtime.map(_.pollers) + def uncancelable[A](body: Poll[IO] => IO[A]): IO[A] = Uncancelable(body, Tracing.calculateTracingEvent(body)) @@ -2087,6 +2093,10 @@ object IO extends IOCompanionPlatform with IOLowPriorityImplicits { def tag = 23 } + private[effect] case object ReadRT extends IO[IORuntime] { + def tag = 24 + } + // INTERNAL, only created by the runloop itself as the terminal state of several operations private[effect] case object EndFiber extends IO[Nothing] { def tag = -1 diff --git a/core/shared/src/main/scala/cats/effect/IOFiber.scala b/core/shared/src/main/scala/cats/effect/IOFiber.scala index 3c9d47ee27..fc8cb7a84b 100644 --- a/core/shared/src/main/scala/cats/effect/IOFiber.scala +++ b/core/shared/src/main/scala/cats/effect/IOFiber.scala @@ -997,6 +997,10 @@ private final class IOFiber[A]( case 23 => runLoop(succeeded(Trace(tracingEvents), 0), nextCancelation, nextAutoCede) + + /* ReadRT */ + case 24 => + runLoop(succeeded(runtime, 0), nextCancelation, nextAutoCede) } } } diff --git a/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala b/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala index 0a585c2178..220088c830 100644 --- a/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala +++ b/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala @@ -38,6 +38,7 @@ final class IORuntime private[unsafe] ( val compute: ExecutionContext, private[effect] val blocking: ExecutionContext, val scheduler: Scheduler, + private[effect] val pollers: List[Any], private[effect] val fiberMonitor: FiberMonitor, val shutdown: () => Unit, val config: IORuntimeConfig @@ -57,10 +58,12 @@ final class IORuntime private[unsafe] ( } object IORuntime extends IORuntimeCompanionPlatform { + def apply( compute: ExecutionContext, blocking: ExecutionContext, scheduler: Scheduler, + pollers: List[Any], shutdown: () => Unit, config: IORuntimeConfig): IORuntime = { val fiberMonitor = FiberMonitor(compute) @@ -71,16 +74,31 @@ object IORuntime extends IORuntimeCompanionPlatform { } val runtime = - new IORuntime(compute, blocking, scheduler, fiberMonitor, unregisterAndShutdown, config) + new IORuntime( + compute, + blocking, + scheduler, + pollers, + fiberMonitor, + unregisterAndShutdown, + config) allRuntimes.put(runtime, runtime.hashCode()) runtime } + def apply( + compute: ExecutionContext, + blocking: ExecutionContext, + scheduler: Scheduler, + shutdown: () => Unit, + config: IORuntimeConfig): IORuntime = + apply(compute, blocking, scheduler, Nil, shutdown, config) + def builder(): IORuntimeBuilder = IORuntimeBuilder() private[effect] def testRuntime(ec: ExecutionContext, scheduler: Scheduler): IORuntime = - new IORuntime(ec, ec, scheduler, new NoOpFiberMonitor(), () => (), IORuntimeConfig()) + new IORuntime(ec, ec, scheduler, Nil, new NoOpFiberMonitor(), () => (), IORuntimeConfig()) private[effect] final val allRuntimes: ThreadSafeHashtable[IORuntime] = new ThreadSafeHashtable(4) diff --git a/core/shared/src/main/scala/cats/effect/unsafe/IORuntimeBuilder.scala b/core/shared/src/main/scala/cats/effect/unsafe/IORuntimeBuilder.scala index 0b084bdcdf..08e0d2dcf1 100644 --- a/core/shared/src/main/scala/cats/effect/unsafe/IORuntimeBuilder.scala +++ b/core/shared/src/main/scala/cats/effect/unsafe/IORuntimeBuilder.scala @@ -32,7 +32,8 @@ final class IORuntimeBuilder protected ( protected var customScheduler: Option[(Scheduler, () => Unit)] = None, protected var extraShutdownHooks: List[() => Unit] = Nil, protected var builderExecuted: Boolean = false, - protected var failureReporter: Throwable => Unit = _.printStackTrace() + protected var failureReporter: Throwable => Unit = _.printStackTrace(), + protected var extraPollers: List[(Any, () => Unit)] = Nil ) extends IORuntimeBuilderPlatform { /** @@ -119,6 +120,11 @@ final class IORuntimeBuilder protected ( this } + def addPoller(poller: Any, shutdown: () => Unit): IORuntimeBuilder = { + extraPollers = (poller, shutdown) :: extraPollers + this + } + def setFailureReporter(f: Throwable => Unit) = { failureReporter = f this diff --git a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala index bfc9d59718..6970eb97bf 100644 --- a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala +++ b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala @@ -330,7 +330,7 @@ trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => } "safely detect hard-blocked threads even while blockers are being created" in { - val (compute, shutdown) = + val (compute, _, shutdown) = IORuntime.createWorkStealingComputeThreadPool(blockedThreadDetectionEnabled = true) implicit val runtime: IORuntime = @@ -350,7 +350,7 @@ trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => // this test ensures that the parkUntilNextSleeper bit works "run a timer when parking thread" in { - val (pool, shutdown) = IORuntime.createWorkStealingComputeThreadPool(threads = 1) + val (pool, _, shutdown) = IORuntime.createWorkStealingComputeThreadPool(threads = 1) implicit val runtime: IORuntime = IORuntime.builder().setCompute(pool, shutdown).build() @@ -365,7 +365,7 @@ trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => // this test ensures that we always see the timer, even when it fires just as we're about to park "run a timer when detecting just prior to park" in { - val (pool, shutdown) = IORuntime.createWorkStealingComputeThreadPool(threads = 1) + val (pool, _, shutdown) = IORuntime.createWorkStealingComputeThreadPool(threads = 1) implicit val runtime: IORuntime = IORuntime.builder().setCompute(pool, shutdown).build() @@ -510,7 +510,7 @@ trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => } } - val (pool, shutdown) = IORuntime.createWorkStealingComputeThreadPool( + val (pool, _, shutdown) = IORuntime.createWorkStealingComputeThreadPool( threads = 2, pollingSystem = DummySystem) @@ -518,7 +518,7 @@ trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => try { val test = - IO.poller[DummyPoller].map(_.get).flatMap { poller => + IO.pollers.map(_.head.asInstanceOf[DummyPoller]).flatMap { poller => val blockAndPoll = IO.blocking(Thread.sleep(10)) *> poller.poll blockAndPoll.replicateA(100).as(true) } diff --git a/tests/jvm/src/test/scala/cats/effect/RunnersPlatform.scala b/tests/jvm/src/test/scala/cats/effect/RunnersPlatform.scala index bd4ea89b5e..a325c4e310 100644 --- a/tests/jvm/src/test/scala/cats/effect/RunnersPlatform.scala +++ b/tests/jvm/src/test/scala/cats/effect/RunnersPlatform.scala @@ -30,7 +30,7 @@ trait RunnersPlatform extends BeforeAfterAll { val (blocking, blockDown) = IORuntime.createDefaultBlockingExecutionContext(threadPrefix = s"io-blocking-${getClass.getName}") - val (compute, compDown) = + val (compute, poller, compDown) = IORuntime.createWorkStealingComputeThreadPool( threadPrefix = s"io-compute-${getClass.getName}", blockerThreadPrefix = s"io-blocker-${getClass.getName}") @@ -39,6 +39,7 @@ trait RunnersPlatform extends BeforeAfterAll { compute, blocking, compute, + List(poller), { () => compDown() blockDown() diff --git a/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala b/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala index 5b5932f6ce..3f7e3a5ad5 100644 --- a/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala @@ -27,9 +27,12 @@ import java.nio.channels.SelectionKey._ class SelectorPollerSpec extends BaseSpec { + def getSelector: IO[SelectorPoller] = + IO.pollers.map(_.collectFirst { case selector: SelectorPoller => selector }).map(_.get) + def mkPipe: Resource[IO, Pipe] = Resource - .eval(IO.poller[SelectorPoller].map(_.get)) + .eval(getSelector) .flatMap { poller => Resource.make(IO(poller.provider.openPipe())) { pipe => IO(pipe.sink().close()).guarantee(IO(pipe.source().close())) @@ -47,7 +50,7 @@ class SelectorPollerSpec extends BaseSpec { "notify read-ready events" in real { mkPipe.use { pipe => for { - poller <- IO.poller[SelectorPoller].map(_.get) + poller <- getSelector buf <- IO(ByteBuffer.allocate(4)) _ <- IO(pipe.sink.write(ByteBuffer.wrap(Array(1, 2, 3)))).background.surround { poller.select(pipe.source, OP_READ) *> IO(pipe.source.read(buf)) @@ -62,7 +65,7 @@ class SelectorPollerSpec extends BaseSpec { "setup multiple callbacks" in real { mkPipe.use { pipe => for { - poller <- IO.poller[SelectorPoller].map(_.get) + poller <- getSelector _ <- poller.select(pipe.source, OP_READ).parReplicateA_(10) <& IO(pipe.sink.write(ByteBuffer.wrap(Array(1, 2, 3)))) } yield ok @@ -72,7 +75,7 @@ class SelectorPollerSpec extends BaseSpec { "works after blocking" in real { mkPipe.use { pipe => for { - poller <- IO.poller[SelectorPoller].map(_.get) + poller <- getSelector _ <- IO.blocking(()) _ <- poller.select(pipe.sink, OP_WRITE) } yield ok @@ -81,7 +84,7 @@ class SelectorPollerSpec extends BaseSpec { "gracefully handles illegal ops" in real { mkPipe.use { pipe => - IO.poller[SelectorPoller].map(_.get).flatMap { poller => + getSelector.flatMap { poller => poller.select(pipe.sink, OP_READ).attempt.map { case Left(_: IllegalArgumentException) => true case _ => false @@ -91,13 +94,12 @@ class SelectorPollerSpec extends BaseSpec { } "handles concurrent close" in { - val (pool, shutdown) = IORuntime.createWorkStealingComputeThreadPool(threads = 1) - implicit val runtime: IORuntime = IORuntime.builder().setCompute(pool, shutdown).build() + val (pool, poller, shutdown) = IORuntime.createWorkStealingComputeThreadPool(threads = 1) + implicit val runtime: IORuntime = + IORuntime.builder().setCompute(pool, shutdown).addPoller(poller, () => ()).build() try { - val test = IO - .poller[SelectorPoller] - .map(_.get) + val test = getSelector .flatMap { poller => mkPipe.allocated.flatMap { case (pipe, close) => diff --git a/tests/jvm/src/test/scala/cats/effect/unsafe/HelperThreadParkSpec.scala b/tests/jvm/src/test/scala/cats/effect/unsafe/HelperThreadParkSpec.scala index 271361cb9e..303fe7689e 100644 --- a/tests/jvm/src/test/scala/cats/effect/unsafe/HelperThreadParkSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/unsafe/HelperThreadParkSpec.scala @@ -33,7 +33,7 @@ class HelperThreadParkSpec extends BaseSpec { s"io-blocking-${getClass.getName}") val (scheduler, schedDown) = IORuntime.createDefaultScheduler(threadPrefix = s"io-scheduler-${getClass.getName}") - val (compute, compDown) = + val (compute, _, compDown) = IORuntime.createWorkStealingComputeThreadPool( threadPrefix = s"io-compute-${getClass.getName}", threads = 2) diff --git a/tests/jvm/src/test/scala/cats/effect/unsafe/StripedHashtableSpec.scala b/tests/jvm/src/test/scala/cats/effect/unsafe/StripedHashtableSpec.scala index 265e1498b0..c1b5cdb375 100644 --- a/tests/jvm/src/test/scala/cats/effect/unsafe/StripedHashtableSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/unsafe/StripedHashtableSpec.scala @@ -32,7 +32,7 @@ class StripedHashtableSpec extends BaseSpec { val (blocking, blockDown) = IORuntime.createDefaultBlockingExecutionContext(threadPrefix = s"io-blocking-${getClass.getName}") - val (compute, compDown) = + val (compute, _, compDown) = IORuntime.createWorkStealingComputeThreadPool( threadPrefix = s"io-compute-${getClass.getName}", blockerThreadPrefix = s"io-blocker-${getClass.getName}") diff --git a/tests/jvm/src/test/scala/cats/effect/unsafe/WorkerThreadNameSpec.scala b/tests/jvm/src/test/scala/cats/effect/unsafe/WorkerThreadNameSpec.scala index ebd19cb893..d8bd033b13 100644 --- a/tests/jvm/src/test/scala/cats/effect/unsafe/WorkerThreadNameSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/unsafe/WorkerThreadNameSpec.scala @@ -29,7 +29,7 @@ class WorkerThreadNameSpec extends BaseSpec with TestInstances { s"io-blocking-${getClass.getName}") val (scheduler, schedDown) = IORuntime.createDefaultScheduler(threadPrefix = s"io-scheduler-${getClass.getName}") - val (compute, compDown) = + val (compute, _, compDown) = IORuntime.createWorkStealingComputeThreadPool( threads = 1, threadPrefix = s"io-compute-${getClass.getName}", diff --git a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala index 06c25a4d2c..2c10700ea9 100644 --- a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala +++ b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala @@ -63,6 +63,9 @@ class FileDescriptorPollerSpec extends BaseSpec { } } + def getFdPoller: IO[FileDescriptorPoller] = + IO.pollers.map(_.collectFirst { case poller: FileDescriptorPoller => poller }).map(_.get) + def mkPipe: Resource[IO, Pipe] = Resource .make { @@ -91,7 +94,7 @@ class FileDescriptorPollerSpec extends BaseSpec { } .flatMap { case (readFd, writeFd) => - Resource.eval(IO.poller[FileDescriptorPoller].map(_.get)).flatMap { poller => + Resource.eval(getFdPoller).flatMap { poller => ( poller.registerFileDescriptor(readFd, true, false), poller.registerFileDescriptor(writeFd, false, true) From 47107697043585c3d12880f046769de41da235a4 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 27 May 2023 18:11:33 +0000 Subject: [PATCH 073/429] Fix test --- .../src/test/scala/cats/effect/IOPlatformSpecification.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala index 6970eb97bf..5aa616b324 100644 --- a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala +++ b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala @@ -510,11 +510,12 @@ trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => } } - val (pool, _, shutdown) = IORuntime.createWorkStealingComputeThreadPool( + val (pool, poller, shutdown) = IORuntime.createWorkStealingComputeThreadPool( threads = 2, pollingSystem = DummySystem) - implicit val runtime: IORuntime = IORuntime.builder().setCompute(pool, shutdown).build() + implicit val runtime: IORuntime = + IORuntime.builder().setCompute(pool, shutdown).addPoller(poller, () => ()).build() try { val test = From e5586dc90687e7e0c22c8aac6c01324c8d5d9c23 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 27 May 2023 19:10:45 +0000 Subject: [PATCH 074/429] Bincompat fixes --- build.sbt | 5 ++++- .../src/main/scala/cats/effect/unsafe/IORuntime.scala | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 3f5a169cf1..3364820a14 100644 --- a/build.sbt +++ b/build.sbt @@ -640,7 +640,10 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) "cats.effect.IOFiberConstants.ExecuteRunnableR"), ProblemFilters.exclude[ReversedMissingMethodProblem]("cats.effect.IOLocal.scope"), ProblemFilters.exclude[DirectMissingMethodProblem]( - "cats.effect.IOFiberConstants.ContStateResult") + "cats.effect.IOFiberConstants.ContStateResult"), + // introduced by #3332, polling system + ProblemFilters.exclude[DirectMissingMethodProblem]( + "cats.effect.unsafe.IORuntimeBuilder.this") ) ++ { if (tlIsScala3.value) { // Scala 3 specific exclusions diff --git a/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala b/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala index 220088c830..b0e96c9c5c 100644 --- a/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala +++ b/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala @@ -94,6 +94,16 @@ object IORuntime extends IORuntimeCompanionPlatform { config: IORuntimeConfig): IORuntime = apply(compute, blocking, scheduler, Nil, shutdown, config) + @deprecated("Preserved for bincompat", "3.6.0") + private[unsafe] def apply( + compute: ExecutionContext, + blocking: ExecutionContext, + scheduler: Scheduler, + fiberMonitor: FiberMonitor, + shutdown: () => Unit, + config: IORuntimeConfig): IORuntime = + new IORuntime(compute, blocking, scheduler, Nil, fiberMonitor, shutdown, config) + def builder(): IORuntimeBuilder = IORuntimeBuilder() From 01426037f13eff7007904cf16a2a5d40026b3119 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 28 May 2023 18:53:59 +0000 Subject: [PATCH 075/429] `GlobalPollingState`->`Api`, `SelectorPoller`->`Selector` --- .../main/scala/cats/effect/unsafe/PollingSystem.scala | 4 ++-- .../src/main/scala/cats/effect/SelectorPoller.scala | 2 +- .../effect/unsafe/IORuntimeCompanionPlatform.scala | 4 ++-- .../main/scala/cats/effect/unsafe/SelectorSystem.scala | 10 ++++++---- .../main/scala/cats/effect/unsafe/SleepSystem.scala | 6 +++--- .../main/scala/cats/effect/unsafe/EpollSystem.scala | 9 ++++++--- .../effect/unsafe/IORuntimeCompanionPlatform.scala | 4 ++-- .../main/scala/cats/effect/unsafe/KqueueSystem.scala | 8 +++++--- .../cats/effect/unsafe/PollingExecutorScheduler.scala | 4 ++-- .../main/scala/cats/effect/unsafe/SleepSystem.scala | 6 +++--- .../scala/cats/effect/IOPlatformSpecification.scala | 4 ++-- .../{SelectorPollerSpec.scala => SelectorSpec.scala} | 6 +++--- 12 files changed, 37 insertions(+), 30 deletions(-) rename tests/jvm/src/test/scala/cats/effect/{SelectorPollerSpec.scala => SelectorSpec.scala} (95%) diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index 63918715f4..e2d50b2a26 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -22,14 +22,14 @@ abstract class PollingSystem { /** * The user-facing interface. */ - type GlobalPollingState <: AnyRef + type Api <: AnyRef /** * The thread-local data structure used for polling. */ type Poller <: AnyRef - def makeGlobalPollingState(register: (Poller => Unit) => Unit): GlobalPollingState + def makeApi(register: (Poller => Unit) => Unit): Api def makePoller(): Poller diff --git a/core/jvm/src/main/scala/cats/effect/SelectorPoller.scala b/core/jvm/src/main/scala/cats/effect/SelectorPoller.scala index b3f1663782..586c448342 100644 --- a/core/jvm/src/main/scala/cats/effect/SelectorPoller.scala +++ b/core/jvm/src/main/scala/cats/effect/SelectorPoller.scala @@ -19,7 +19,7 @@ package cats.effect import java.nio.channels.SelectableChannel import java.nio.channels.spi.SelectorProvider -trait SelectorPoller { +trait Selector { /** * The [[java.nio.channels.spi.SelectorProvider]] that should be used to create diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala index 0d85644a15..37c64741e9 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala @@ -78,7 +78,7 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type reportFailure: Throwable => Unit = _.printStackTrace(), blockedThreadDetectionEnabled: Boolean = false, pollingSystem: PollingSystem = SelectorSystem()) - : (WorkStealingThreadPool[_], pollingSystem.GlobalPollingState, () => Unit) = { + : (WorkStealingThreadPool[_], pollingSystem.Api, () => Unit) = { val threadPool = new WorkStealingThreadPool[pollingSystem.Poller]( threads, @@ -148,7 +148,7 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type ( threadPool, - pollingSystem.makeGlobalPollingState(threadPool.register), + pollingSystem.makeApi(threadPool.register), { () => unregisterMBeans() threadPool.shutdown() diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala index cbdedd701a..6465c325c2 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala @@ -24,8 +24,10 @@ import SelectorSystem._ final class SelectorSystem private (provider: SelectorProvider) extends PollingSystem { - def makeGlobalPollingState(register: (Poller => Unit) => Unit): GlobalPollingState = - new GlobalPollingState(register, provider) + type Api = Selector + + def makeApi(register: (Poller => Unit) => Unit): Selector = + new SelectorImpl(register, provider) def makePoller(): Poller = new Poller(provider.openSelector()) @@ -95,10 +97,10 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS () } - final class GlobalPollingState private[SelectorSystem] ( + final class SelectorImpl private[SelectorSystem] ( register: (Poller => Unit) => Unit, val provider: SelectorProvider - ) extends SelectorPoller { + ) extends Selector { def select(ch: SelectableChannel, ops: Int): IO[Int] = IO.async { selectCb => IO.async_[CallbackNode] { cb => diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala index aa2649d430..ecded54027 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -21,11 +21,11 @@ import java.util.concurrent.locks.LockSupport object SleepSystem extends PollingSystem { - final class GlobalPollingState private[SleepSystem] () + final class Api private[SleepSystem] () final class Poller private[SleepSystem] () - def makeGlobalPollingState(register: (Poller => Unit) => Unit): GlobalPollingState = - new GlobalPollingState + def makeApi(register: (Poller => Unit) => Unit): Api = + new Api def makePoller(): Poller = new Poller diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index 18e043de5d..c8ec2d17eb 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -41,8 +41,10 @@ object EpollSystem extends PollingSystem { private[this] final val MaxEvents = 64 - def makeGlobalPollingState(register: (Poller => Unit) => Unit): GlobalPollingState = - new GlobalPollingState(register) + type Api = FileDescriptorPoller + + def makeApi(register: (Poller => Unit) => Unit): Api = + new FileDescriptorPollerImpl(register) def makePoller(): Poller = { val fd = epoll_create1(0) @@ -60,7 +62,8 @@ object EpollSystem extends PollingSystem { def interrupt(targetThread: Thread, targetPoller: Poller): Unit = () - final class GlobalPollingState private[EpollSystem] (register: (Poller => Unit) => Unit) + private final class FileDescriptorPollerImpl private[EpollSystem] ( + register: (Poller => Unit) => Unit) extends FileDescriptorPoller { def registerFileDescriptor( diff --git a/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala index 1d8b8fccb3..d78e8c2fc7 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala @@ -27,10 +27,10 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type def createEventLoop( system: PollingSystem - ): (ExecutionContext with Scheduler, system.GlobalPollingState) = { + ): (ExecutionContext with Scheduler, system.Api) = { val loop = new EventLoopExecutorScheduler[system.Poller](64, system) val poller = loop.poller - (loop, system.makeGlobalPollingState(cb => cb(poller))) + (loop, system.makeApi(cb => cb(poller))) } def createDefaultPollingSystem(): PollingSystem = diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index 882177424a..4466b0cc1d 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -41,8 +41,10 @@ object KqueueSystem extends PollingSystem { private final val MaxEvents = 64 - def makeGlobalPollingState(register: (Poller => Unit) => Unit): GlobalPollingState = - new GlobalPollingState(register) + type Api = FileDescriptorPoller + + def makeApi(register: (Poller => Unit) => Unit): FileDescriptorPoller = + new FileDescriptorPollerImpl(register) def makePoller(): Poller = { val fd = kqueue() @@ -61,7 +63,7 @@ object KqueueSystem extends PollingSystem { def interrupt(targetThread: Thread, targetPoller: Poller): Unit = () - final class GlobalPollingState private[KqueueSystem] ( + private final class FileDescriptorPollerImpl private[KqueueSystem] ( register: (Poller => Unit) => Unit ) extends FileDescriptorPoller { def registerFileDescriptor( diff --git a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala index d54172a69e..df65982409 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala @@ -28,10 +28,10 @@ abstract class PollingExecutorScheduler(pollEvery: Int) private[this] val loop = new EventLoopExecutorScheduler( pollEvery, new PollingSystem { - type GlobalPollingState = outer.type + type Api = outer.type type Poller = outer.type private[this] var needsPoll = true - def makeGlobalPollingState(register: (Poller => Unit) => Unit): GlobalPollingState = outer + def makeApi(register: (Poller => Unit) => Unit): Api = outer def makePoller(): Poller = outer def closePoller(poller: Poller): Unit = () def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = { diff --git a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala index d1adf5cd20..2e30c7722a 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -19,11 +19,11 @@ package unsafe object SleepSystem extends PollingSystem { - final class GlobalPollingState private[SleepSystem] () + final class Api private[SleepSystem] () final class Poller private[SleepSystem] () - def makeGlobalPollingState(register: (Poller => Unit) => Unit): GlobalPollingState = - new GlobalPollingState + def makeApi(register: (Poller => Unit) => Unit): Api = + new Api def makePoller(): Poller = new Poller diff --git a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala index 5aa616b324..d49efc924b 100644 --- a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala +++ b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala @@ -479,7 +479,7 @@ trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => } object DummySystem extends PollingSystem { - type GlobalPollingState = DummyPoller + type Api = DummyPoller type Poller = AtomicReference[List[Either[Throwable, Unit] => Unit]] def makePoller() = new AtomicReference(List.empty[Either[Throwable, Unit] => Unit]) @@ -499,7 +499,7 @@ trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => } } - def makeGlobalPollingState(register: (Poller => Unit) => Unit) = + def makeApi(register: (Poller => Unit) => Unit) = new DummyPoller { def poll = IO.async_[Unit] { cb => register { poller => diff --git a/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala b/tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala similarity index 95% rename from tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala rename to tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala index 3f7e3a5ad5..3539a281e4 100644 --- a/tests/jvm/src/test/scala/cats/effect/SelectorPollerSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala @@ -25,10 +25,10 @@ import java.nio.ByteBuffer import java.nio.channels.Pipe import java.nio.channels.SelectionKey._ -class SelectorPollerSpec extends BaseSpec { +class SelectorSpec extends BaseSpec { - def getSelector: IO[SelectorPoller] = - IO.pollers.map(_.collectFirst { case selector: SelectorPoller => selector }).map(_.get) + def getSelector: IO[Selector] = + IO.pollers.map(_.collectFirst { case selector: Selector => selector }).map(_.get) def mkPipe: Resource[IO, Pipe] = Resource From 45d16e7fddf6abbea0a3d6edd841fa32465db81e Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 29 May 2023 16:00:09 +0000 Subject: [PATCH 076/429] Restore scala 3 + native + macos intel job --- .github/workflows/ci.yml | 3 --- build.sbt | 4 +--- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1de22711ab..f0379f1664 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,9 +97,6 @@ jobs: - os: macos-latest ci: ciNative scala: 2.12.17 - - os: macos-latest - ci: ciNative - scala: 3.2.2 - os: windows-latest java: graalvm@11 runs-on: ${{ matrix.os }} diff --git a/build.sbt b/build.sbt index 3364820a14..07ff2ca888 100644 --- a/build.sbt +++ b/build.sbt @@ -254,9 +254,7 @@ ThisBuild / githubWorkflowBuildMatrixExclusions := { javaFilters ++ Seq( MatrixExclude(Map("os" -> Windows, "ci" -> ci)), - MatrixExclude(Map("os" -> MacOS, "ci" -> ci, "scala" -> Scala212)), - // keep a native+2.13+macos job - MatrixExclude(Map("os" -> MacOS, "ci" -> ci, "scala" -> Scala3)) + MatrixExclude(Map("os" -> MacOS, "ci" -> ci, "scala" -> Scala212)) ) } From 72fbd79e4f8b4fd232e2e2014b9c399f77ea289b Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 29 May 2023 16:24:48 +0000 Subject: [PATCH 077/429] Try to fix matrix exclusions --- .github/workflows/ci.yml | 4 ++++ build.sbt | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0379f1664..c46531087e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,12 +42,16 @@ jobs: java: graalvm@11 - os: windows-latest scala: 3.2.2 + ci: ciJVM - os: macos-latest scala: 3.2.2 + ci: ciJVM - os: windows-latest scala: 2.12.17 + ci: ciJVM - os: macos-latest scala: 2.12.17 + ci: ciJVM - ci: ciFirefox scala: 3.2.2 - ci: ciChrome diff --git a/build.sbt b/build.sbt index 07ff2ca888..b509adcba5 100644 --- a/build.sbt +++ b/build.sbt @@ -224,8 +224,8 @@ ThisBuild / githubWorkflowBuildMatrixExclusions := { val windowsAndMacScalaFilters = (ThisBuild / githubWorkflowScalaVersions).value.filterNot(Set(Scala213)).flatMap { scala => Seq( - MatrixExclude(Map("os" -> Windows, "scala" -> scala)), - MatrixExclude(Map("os" -> MacOS, "scala" -> scala))) + MatrixExclude(Map("os" -> Windows, "scala" -> scala, "ci" -> CI.JVM.command)), + MatrixExclude(Map("os" -> MacOS, "scala" -> scala, "ci" -> CI.JVM.command))) } val jsScalaFilters = for { From 78ce2c0850f57416c03cc4db79fd0525ce21f5db Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 29 May 2023 16:54:14 +0000 Subject: [PATCH 078/429] Use `Mutex` instead of `Semaphore(1)` --- .../scala/cats/effect/unsafe/EpollSystem.scala | 14 +++++++------- .../scala/cats/effect/unsafe/KqueueSystem.scala | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index c8ec2d17eb..7088e0ed96 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -17,7 +17,7 @@ package cats.effect package unsafe -import cats.effect.std.Semaphore +import cats.effect.std.Mutex import cats.syntax.all._ import org.typelevel.scalaccompat.annotation._ @@ -73,10 +73,10 @@ object EpollSystem extends PollingSystem { ): Resource[IO, FileDescriptorPollHandle] = Resource .make { - (Semaphore[IO](1), Semaphore[IO](1)).flatMapN { (readSemaphore, writeSemaphore) => + (Mutex[IO], Mutex[IO]).flatMapN { (readMutex, writeMutex) => IO.async_[(PollHandle, IO[Unit])] { cb => register { data => - val handle = new PollHandle(readSemaphore, writeSemaphore) + val handle = new PollHandle(readMutex, writeMutex) val unregister = data.register(fd, reads, writes, handle) cb(Right((handle, unregister))) } @@ -88,8 +88,8 @@ object EpollSystem extends PollingSystem { } private final class PollHandle( - readSemaphore: Semaphore[IO], - writeSemaphore: Semaphore[IO] + readMutex: Mutex[IO], + writeMutex: Mutex[IO] ) extends FileDescriptorPollHandle { private[this] var readReadyCounter = 0 @@ -116,7 +116,7 @@ object EpollSystem extends PollingSystem { } def pollReadRec[A, B](a: A)(f: A => IO[Either[A, B]]): IO[B] = - readSemaphore.permit.surround { + readMutex.lock.surround { def go(a: A, before: Int): IO[B] = f(a).flatMap { case Left(a) => @@ -144,7 +144,7 @@ object EpollSystem extends PollingSystem { } def pollWriteRec[A, B](a: A)(f: A => IO[Either[A, B]]): IO[B] = - writeSemaphore.permit.surround { + writeMutex.lock.surround { def go(a: A, before: Int): IO[B] = f(a).flatMap { case Left(a) => diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index 4466b0cc1d..05a97ae525 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -17,7 +17,7 @@ package cats.effect package unsafe -import cats.effect.std.Semaphore +import cats.effect.std.Mutex import cats.syntax.all._ import org.typelevel.scalaccompat.annotation._ @@ -72,7 +72,7 @@ object KqueueSystem extends PollingSystem { writes: Boolean ): Resource[IO, FileDescriptorPollHandle] = Resource.eval { - (Semaphore[IO](1), Semaphore[IO](1)).mapN { + (Mutex[IO], Mutex[IO]).mapN { new PollHandle(register, fd, _, _) } } @@ -81,15 +81,15 @@ object KqueueSystem extends PollingSystem { private final class PollHandle( register: (Poller => Unit) => Unit, fd: Int, - readSemaphore: Semaphore[IO], - writeSemaphore: Semaphore[IO] + readMutex: Mutex[IO], + writeMutex: Mutex[IO] ) extends FileDescriptorPollHandle { private[this] val readEvent = KEvent(fd.toLong, EVFILT_READ) private[this] val writeEvent = KEvent(fd.toLong, EVFILT_WRITE) def pollReadRec[A, B](a: A)(f: A => IO[Either[A, B]]): IO[B] = - readSemaphore.permit.surround { + readMutex.lock.surround { a.tailRecM { a => f(a).flatTap { r => if (r.isRight) @@ -109,7 +109,7 @@ object KqueueSystem extends PollingSystem { } def pollWriteRec[A, B](a: A)(f: A => IO[Either[A, B]]): IO[B] = - writeSemaphore.permit.surround { + writeMutex.lock.surround { a.tailRecM { a => f(a).flatTap { r => if (r.isRight) From 63dc15c061e7a6c6fb390c24f680213260ec6f7c Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 30 May 2023 20:35:53 +0000 Subject: [PATCH 079/429] Fix kqueue ready-queue draining --- .../src/main/scala/cats/effect/unsafe/KqueueSystem.scala | 2 +- .../test/scala/cats/effect/FileDescriptorPollerSpec.scala | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index 05a97ae525..2263398624 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -206,7 +206,7 @@ object KqueueSystem extends PollingSystem { } if (triggeredEvents >= MaxEvents) - processEvents(null, 0, KEVENT_FLAG_NONE) // drain the ready list + processEvents(null, 0, KEVENT_FLAG_IMMEDIATE) // drain the ready list else () } diff --git a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala index 2c10700ea9..06a8084a28 100644 --- a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala +++ b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala @@ -115,8 +115,8 @@ class FileDescriptorPollerSpec extends BaseSpec { } "handle lots of simultaneous events" in real { - mkPipe.replicateA(1000).use { pipes => - CountDownLatch[IO](1000).flatMap { latch => + def test(n: Int) = mkPipe.replicateA(n).use { pipes => + CountDownLatch[IO](n).flatMap { latch => pipes .traverse_ { pipe => (pipe.read(new Array[Byte](1), 0, 1) *> latch.release).background @@ -130,6 +130,10 @@ class FileDescriptorPollerSpec extends BaseSpec { } } } + + // multiples of 64 to excercise ready queue draining logic + test(64) *> test(128) *> + test(1000) // a big, non-64-multiple } "hang if never ready" in real { From cc2f4616e1c386580d8cbdf3a9fe4dc4cd00d9f9 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 08:06:15 +0000 Subject: [PATCH 080/429] Update scalafmt-core to 3.7.4 in series/3.x --- .scalafmt.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index bbca65b234..b0fe1fee57 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = 3.7.3 +version = 3.7.4 runner.dialect = Scala213Source3 fileOverride { From 5f6a7b3075ba9edf871ed3e592fdd8cbe88beef1 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 31 May 2023 14:24:54 +0000 Subject: [PATCH 081/429] Workaround SN bu in kqueue --- .../src/main/scala/cats/effect/unsafe/KqueueSystem.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index 2263398624..501f61ebd5 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -133,7 +133,8 @@ object KqueueSystem extends PollingSystem { final class Poller private[KqueueSystem] (kqfd: Int) { - private[this] val changelistArray = new Array[Byte](sizeof[kevent64_s].toInt * MaxEvents) + private[KqueueSystem] val changelistArray = // private[this] gets GCed in Scala 3. or something ... + new Array[Byte](sizeof[kevent64_s].toInt * MaxEvents) private[this] val changelist = changelistArray.at(0).asInstanceOf[Ptr[kevent64_s]] private[this] var changeCount = 0 From a03b990b1d3e3ad4a5bfaf017b5fea2b8ffcda4b Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 16:10:52 +0000 Subject: [PATCH 082/429] Update scala3-library, ... to 3.3.0 in series/3.x --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 916ce2b5fa..341f60cdf1 100644 --- a/build.sbt +++ b/build.sbt @@ -114,7 +114,7 @@ val MacOS = "macos-latest" val Scala212 = "2.12.17" val Scala213 = "2.13.10" -val Scala3 = "3.2.2" +val Scala3 = "3.3.0" ThisBuild / crossScalaVersions := Seq(Scala3, Scala212, Scala213) ThisBuild / githubWorkflowScalaVersions := crossScalaVersions.value From 4c357ab68e64a3590ca5b5353145dad3466df52d Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 16:11:20 +0000 Subject: [PATCH 083/429] Regenerate GitHub Actions workflow Executed command: sbt githubWorkflowGenerate --- .github/workflows/ci.yml | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1de22711ab..e0442b902f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,11 +28,11 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - scala: [3.2.2, 2.12.17, 2.13.10] + scala: [3.3.0, 2.12.17, 2.13.10] java: [temurin@8, temurin@11, temurin@17, graalvm@11] ci: [ciJVM, ciNative, ciJS, ciFirefox, ciChrome] exclude: - - scala: 3.2.2 + - scala: 3.3.0 java: temurin@11 - scala: 2.12.17 java: temurin@11 @@ -41,17 +41,17 @@ jobs: - scala: 2.12.17 java: graalvm@11 - os: windows-latest - scala: 3.2.2 + scala: 3.3.0 - os: macos-latest - scala: 3.2.2 + scala: 3.3.0 - os: windows-latest scala: 2.12.17 - os: macos-latest scala: 2.12.17 - ci: ciFirefox - scala: 3.2.2 + scala: 3.3.0 - ci: ciChrome - scala: 3.2.2 + scala: 3.3.0 - ci: ciFirefox scala: 2.12.17 - ci: ciChrome @@ -99,7 +99,7 @@ jobs: scala: 2.12.17 - os: macos-latest ci: ciNative - scala: 3.2.2 + scala: 3.3.0 - os: windows-latest java: graalvm@11 runs-on: ${{ matrix.os }} @@ -235,24 +235,24 @@ jobs: run: sbt githubWorkflowCheck - name: Check that scalafix has been run on JVM - if: matrix.ci == 'ciJVM' && matrix.scala != '3.2.2' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' + if: matrix.ci == 'ciJVM' && matrix.scala != '3.3.0' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' shell: bash run: sbt '++ ${{ matrix.scala }}' 'rootJVM/scalafixAll --check' - name: Check that scalafix has been run on JS - if: matrix.ci == 'ciJS' && matrix.scala != '3.2.2' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' + if: matrix.ci == 'ciJS' && matrix.scala != '3.3.0' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' shell: bash run: sbt '++ ${{ matrix.scala }}' 'rootJS/scalafixAll --check' - name: Check that scalafix has been run on Native - if: matrix.ci == 'ciNative' && matrix.scala != '3.2.2' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' + if: matrix.ci == 'ciNative' && matrix.scala != '3.3.0' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' shell: bash run: sbt '++ ${{ matrix.scala }}' 'rootNative/scalafixAll --check' - shell: bash run: sbt '++ ${{ matrix.scala }}' '${{ matrix.ci }}' - - if: (matrix.scala == '2.13.10' || matrix.scala == '3.2.2') && matrix.ci == 'ciJVM' + - if: (matrix.scala == '2.13.10' || matrix.scala == '3.3.0') && matrix.ci == 'ciJVM' shell: bash run: sbt '++ ${{ matrix.scala }}' docs/mdoc @@ -408,32 +408,32 @@ jobs: if: matrix.java == 'graalvm@11' && steps.setup-java-graalvm-11.outputs.cache-hit == 'false' run: sbt '++ ${{ matrix.scala }}' reload +update - - name: Download target directories (3.2.2, ciJVM) + - name: Download target directories (3.3.0, ciJVM) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-3.2.2-ciJVM + name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.0-ciJVM - - name: Inflate target directories (3.2.2, ciJVM) + - name: Inflate target directories (3.3.0, ciJVM) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (3.2.2, ciNative) + - name: Download target directories (3.3.0, ciNative) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-3.2.2-ciNative + name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.0-ciNative - - name: Inflate target directories (3.2.2, ciNative) + - name: Inflate target directories (3.3.0, ciNative) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (3.2.2, ciJS) + - name: Download target directories (3.3.0, ciJS) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-3.2.2-ciJS + name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.0-ciJS - - name: Inflate target directories (3.2.2, ciJS) + - name: Inflate target directories (3.3.0, ciJS) run: | tar xf targets.tar rm targets.tar From 704b8ec5deeb212d4bc52394b3207e2210ce4c25 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 31 May 2023 21:52:07 +0000 Subject: [PATCH 084/429] Tweak the workaround --- .../src/main/scala/cats/effect/unsafe/KqueueSystem.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index 501f61ebd5..0d2034e371 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -133,9 +133,8 @@ object KqueueSystem extends PollingSystem { final class Poller private[KqueueSystem] (kqfd: Int) { - private[KqueueSystem] val changelistArray = // private[this] gets GCed in Scala 3. or something ... - new Array[Byte](sizeof[kevent64_s].toInt * MaxEvents) - private[this] val changelist = changelistArray.at(0).asInstanceOf[Ptr[kevent64_s]] + private[this] val changelistArray = new Array[Byte](sizeof[kevent64_s].toInt * MaxEvents) + @inline private[this] def changelist = changelistArray.at(0).asInstanceOf[Ptr[kevent64_s]] private[this] var changeCount = 0 private[this] val callbacks = new HashMap[KEvent, Either[Throwable, Unit] => Unit]() From 43946fb7c6b3a322ea4171514cf86478fc6977a4 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Fri, 2 Jun 2023 12:08:27 +0000 Subject: [PATCH 085/429] Update sbt to 1.9.0 in series/3.x --- project/build.properties | 2 +- scalafix/project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project/build.properties b/project/build.properties index 72413de151..40b3b8e7b6 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.8.3 +sbt.version=1.9.0 diff --git a/scalafix/project/build.properties b/scalafix/project/build.properties index 72413de151..40b3b8e7b6 100644 --- a/scalafix/project/build.properties +++ b/scalafix/project/build.properties @@ -1 +1 @@ -sbt.version=1.8.3 +sbt.version=1.9.0 From 750ac49ee010cc5785d1f26fb15e3811f8028c77 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Sat, 3 Jun 2023 16:06:15 +0000 Subject: [PATCH 086/429] Update sbt-scalafix to 0.11.0 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index a91199bba8..a977e738a9 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -13,5 +13,5 @@ addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.7") addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.5.0") addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.10.4") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.0") addSbtPlugin("org.scalameta" % "sbt-native-image" % "0.3.4") From 929453ea7dbb4175f74b0380f035104bab940eb1 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 5 Jun 2023 12:07:42 +0000 Subject: [PATCH 087/429] Update sbt-jmh to 0.4.5 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index a977e738a9..0ea6d4005a 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,7 +6,7 @@ addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.1") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.1") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.12") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.1") -addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.4") +addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.5") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.7") From 2096fadfc2ce2c8885b8134f46f86870d29a3ff2 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 6 Jun 2023 02:34:00 +0000 Subject: [PATCH 088/429] Try to fix epoll binding on ARM --- .../cats/effect/unsafe/EpollSystem.scala | 33 +++++++++++++++---- project/plugins.sbt | 2 +- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index 7088e0ed96..5c6b4e4ea3 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -25,6 +25,7 @@ import org.typelevel.scalaccompat.annotation._ import scala.annotation.tailrec import scala.scalanative.annotation.alwaysinline import scala.scalanative.libc.errno._ +import scala.scalanative.meta.LinktimeInfo import scala.scalanative.posix.string._ import scala.scalanative.posix.unistd import scala.scalanative.runtime._ @@ -281,16 +282,34 @@ object EpollSystem extends PollingSystem { def events_=(events: CUnsignedInt): Unit = !epoll_event.asInstanceOf[Ptr[CUnsignedInt]] = events - def data: epoll_data_t = - !(epoll_event.asInstanceOf[Ptr[Byte]] + sizeof[CUnsignedInt]) - .asInstanceOf[Ptr[epoll_data_t]] - def data_=(data: epoll_data_t): Unit = - !(epoll_event.asInstanceOf[Ptr[Byte]] + sizeof[CUnsignedInt]) + def data: epoll_data_t = { + val offset = + if (LinktimeInfo.target.arch == "x86_64") + sizeof[CUnsignedInt] + else + sizeof[Ptr[Byte]] + !(epoll_event.asInstanceOf[Ptr[Byte]] + offset).asInstanceOf[Ptr[epoll_data_t]] + } + + def data_=(data: epoll_data_t): Unit = { + val offset = + if (LinktimeInfo.target.arch == "x86_64") + sizeof[CUnsignedInt] + else + sizeof[Ptr[Byte]] + !(epoll_event.asInstanceOf[Ptr[Byte]] + offset) .asInstanceOf[Ptr[epoll_data_t]] = data + } } implicit val epoll_eventTag: Tag[epoll_event] = - Tag.materializeCArrayTag[Byte, Nat.Digit2[Nat._1, Nat._2]].asInstanceOf[Tag[epoll_event]] - + if (LinktimeInfo.target.arch == "x86_64") + Tag + .materializeCArrayTag[Byte, Nat.Digit2[Nat._1, Nat._2]] + .asInstanceOf[Tag[epoll_event]] + else + Tag + .materializeCArrayTag[Byte, Nat.Digit2[Nat._1, Nat._6]] + .asInstanceOf[Tag[epoll_event]] } } diff --git a/project/plugins.sbt b/project/plugins.sbt index a91199bba8..943fe02da5 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -4,7 +4,7 @@ addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.0-M10") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.1") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.1") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.12") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.13") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.1") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.4") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") From d65ef7171bac46f9ac410cd184aa088bde972eb1 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 6 Jun 2023 02:39:53 +0000 Subject: [PATCH 089/429] Organize imports rule is available out-of-the-box --- project/Common.scala | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/project/Common.scala b/project/Common.scala index b1a76ed88e..fa9578c8a1 100644 --- a/project/Common.scala +++ b/project/Common.scala @@ -29,14 +29,17 @@ object Common extends AutoPlugin { override def requires = plugins.JvmPlugin && TypelevelPlugin && ScalafixPlugin override def trigger = allRequirements + override def buildSettings = + Seq( + semanticdbEnabled := true, + semanticdbVersion := scalafixSemanticdb.revision + ) + override def projectSettings = Seq( headerLicense := Some( HeaderLicense.ALv2(s"${startYear.value.get}-2023", organizationName.value) ), - ThisBuild / scalafixDependencies += "com.github.liancheng" %% "organize-imports" % "0.6.0", - ThisBuild / semanticdbEnabled := !tlIsScala3.value, - ThisBuild / semanticdbVersion := scalafixSemanticdb.revision, tlVersionIntroduced ++= { if (crossProjectPlatform.?.value.contains(NativePlatform)) List("2.12", "2.13", "3").map(_ -> "3.4.0").toMap From 8d0157ac0c8cb0b9438d9254ec3745b755659a49 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 6 Jun 2023 02:42:32 +0000 Subject: [PATCH 090/429] Formatting --- .../native/src/main/scala/cats/effect/unsafe/EpollSystem.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index 5c6b4e4ea3..847120767e 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -297,8 +297,7 @@ object EpollSystem extends PollingSystem { sizeof[CUnsignedInt] else sizeof[Ptr[Byte]] - !(epoll_event.asInstanceOf[Ptr[Byte]] + offset) - .asInstanceOf[Ptr[epoll_data_t]] = data + !(epoll_event.asInstanceOf[Ptr[Byte]] + offset).asInstanceOf[Ptr[epoll_data_t]] = data } } From 959ff798f1c3354b62980a4e7a9632803902429c Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 6 Jun 2023 07:48:58 +0000 Subject: [PATCH 091/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/bcb8b3623229dc58cf080a3cf83aa9b372a95e79' (2023-05-22) → 'github:typelevel/typelevel-nix/15be638bdfec87bc3fd7b4111de0f572bbb29a5a' (2023-06-05) • Updated input 'typelevel-nix/flake-utils': 'github:numtide/flake-utils/cfacdce06f30d2b68473a46042957675eebb3401' (2023-04-11) → 'github:numtide/flake-utils/a1720a10a6cfe8234c0e93907ffe81be440f4cef' (2023-05-31) • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/85340996ba67cc02f01ba324e18b1306892ed6f5' (2023-05-21) → 'github:nixos/nixpkgs/2e56a850786211972d99d2bb39665a9b5a1801d6' (2023-06-04) --- flake.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/flake.lock b/flake.lock index 3aa99732f3..1f68183097 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ "systems": "systems_2" }, "locked": { - "lastModified": 1681202837, - "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", + "lastModified": 1685518550, + "narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=", "owner": "numtide", "repo": "flake-utils", - "rev": "cfacdce06f30d2b68473a46042957675eebb3401", + "rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef", "type": "github" }, "original": { @@ -55,11 +55,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1684668519, - "narHash": "sha256-KkVvlXTqdLLwko9Y0p1Xv6KQ9QTcQorrU098cGilb7c=", + "lastModified": 1685894048, + "narHash": "sha256-QKqv1QS+22k9oxncj1AnAxeqS5jGnQiUW3Jq3B+dI1w=", "owner": "nixos", "repo": "nixpkgs", - "rev": "85340996ba67cc02f01ba324e18b1306892ed6f5", + "rev": "2e56a850786211972d99d2bb39665a9b5a1801d6", "type": "github" }, "original": { @@ -119,11 +119,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1684765835, - "narHash": "sha256-/Wmcx12luP1oTWFj0dd5elX4oxxq1UcAv9gEb54Wx4A=", + "lastModified": 1685970436, + "narHash": "sha256-j+lPMw27GFKlU+r++N3I0Imu38dKhY4t/MW1r23+ZLw=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "bcb8b3623229dc58cf080a3cf83aa9b372a95e79", + "rev": "15be638bdfec87bc3fd7b4111de0f572bbb29a5a", "type": "github" }, "original": { From 3d69d3c965bd3152987988aa46ad750fc19b30f5 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 6 Jun 2023 12:06:07 +0000 Subject: [PATCH 092/429] Update nscplugin, sbt-scala-native, ... to 0.4.14 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 0ea6d4005a..473fb73c45 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -4,7 +4,7 @@ addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.0-M10") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.1") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.1") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.12") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.14") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.1") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.5") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") From 2da38c655d76e87a969ab3e8b315d7cec3f5134a Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Wed, 7 Jun 2023 20:16:49 +0000 Subject: [PATCH 093/429] Update scala-library to 2.12.18 in series/3.x --- .github/workflows/ci.yml | 18 +++++++++--------- build.sbt | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0442b902f..fb66a80ef3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,34 +28,34 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - scala: [3.3.0, 2.12.17, 2.13.10] + scala: [3.3.0, 2.12.18, 2.13.10] java: [temurin@8, temurin@11, temurin@17, graalvm@11] ci: [ciJVM, ciNative, ciJS, ciFirefox, ciChrome] exclude: - scala: 3.3.0 java: temurin@11 - - scala: 2.12.17 + - scala: 2.12.18 java: temurin@11 - - scala: 2.12.17 + - scala: 2.12.18 java: temurin@17 - - scala: 2.12.17 + - scala: 2.12.18 java: graalvm@11 - os: windows-latest scala: 3.3.0 - os: macos-latest scala: 3.3.0 - os: windows-latest - scala: 2.12.17 + scala: 2.12.18 - os: macos-latest - scala: 2.12.17 + scala: 2.12.18 - ci: ciFirefox scala: 3.3.0 - ci: ciChrome scala: 3.3.0 - ci: ciFirefox - scala: 2.12.17 + scala: 2.12.18 - ci: ciChrome - scala: 2.12.17 + scala: 2.12.18 - ci: ciJS java: temurin@11 - ci: ciJS @@ -96,7 +96,7 @@ jobs: ci: ciNative - os: macos-latest ci: ciNative - scala: 2.12.17 + scala: 2.12.18 - os: macos-latest ci: ciNative scala: 3.3.0 diff --git a/build.sbt b/build.sbt index 341f60cdf1..e9a0182407 100644 --- a/build.sbt +++ b/build.sbt @@ -112,7 +112,7 @@ val PrimaryOS = "ubuntu-latest" val Windows = "windows-latest" val MacOS = "macos-latest" -val Scala212 = "2.12.17" +val Scala212 = "2.12.18" val Scala213 = "2.13.10" val Scala3 = "3.3.0" From f5f0bf064ef8becb41b57fd87b985fc44c1956c6 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Wed, 7 Jun 2023 20:17:36 +0000 Subject: [PATCH 094/429] Regenerate GitHub Actions workflow Executed command: sbt githubWorkflowGenerate --- .github/workflows/ci.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb66a80ef3..b4f2427a85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -438,32 +438,32 @@ jobs: tar xf targets.tar rm targets.tar - - name: Download target directories (2.12.17, ciJVM) + - name: Download target directories (2.12.18, ciJVM) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.17-ciJVM + name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.18-ciJVM - - name: Inflate target directories (2.12.17, ciJVM) + - name: Inflate target directories (2.12.18, ciJVM) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.12.17, ciNative) + - name: Download target directories (2.12.18, ciNative) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.17-ciNative + name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.18-ciNative - - name: Inflate target directories (2.12.17, ciNative) + - name: Inflate target directories (2.12.18, ciNative) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.12.17, ciJS) + - name: Download target directories (2.12.18, ciJS) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.17-ciJS + name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.18-ciJS - - name: Inflate target directories (2.12.17, ciJS) + - name: Inflate target directories (2.12.18, ciJS) run: | tar xf targets.tar rm targets.tar From 3851e12a2d65921a610b79a9408580edf7846a11 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Wed, 7 Jun 2023 20:17:43 +0000 Subject: [PATCH 095/429] Update scala-library to 2.13.11 in series/3.x --- .github/workflows/ci.yml | 12 ++++++------ build.sbt | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0442b902f..d01e71a881 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - scala: [3.3.0, 2.12.17, 2.13.10] + scala: [3.3.0, 2.12.17, 2.13.11] java: [temurin@8, temurin@11, temurin@17, graalvm@11] ci: [ciJVM, ciNative, ciJS, ciFirefox, ciChrome] exclude: @@ -252,7 +252,7 @@ jobs: - shell: bash run: sbt '++ ${{ matrix.scala }}' '${{ matrix.ci }}' - - if: (matrix.scala == '2.13.10' || matrix.scala == '3.3.0') && matrix.ci == 'ciJVM' + - if: (matrix.scala == '2.13.11' || matrix.scala == '3.3.0') && matrix.ci == 'ciJVM' shell: bash run: sbt '++ ${{ matrix.scala }}' docs/mdoc @@ -267,7 +267,7 @@ jobs: run: example/test-js.sh ${{ matrix.scala }} - name: Test GraalVM Native Image - if: matrix.scala == '2.13.10' && matrix.java == 'graalvm@11' && matrix.os == 'ubuntu-latest' + if: matrix.scala == '2.13.11' && matrix.java == 'graalvm@11' && matrix.os == 'ubuntu-latest' shell: bash run: sbt '++ ${{ matrix.scala }}' graalVMExample/nativeImage graalVMExample/nativeImageRun @@ -277,7 +277,7 @@ jobs: run: example/test-native.sh ${{ matrix.scala }} - name: Scalafix tests - if: matrix.scala == '2.13.10' && matrix.ci == 'ciJVM' && matrix.os == 'ubuntu-latest' + if: matrix.scala == '2.13.11' && matrix.ci == 'ciJVM' && matrix.os == 'ubuntu-latest' shell: bash run: | cd scalafix @@ -307,7 +307,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.10] + scala: [2.13.11] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -551,7 +551,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.10] + scala: [2.13.11] java: [temurin@8] runs-on: ${{ matrix.os }} steps: diff --git a/build.sbt b/build.sbt index 341f60cdf1..93014795e5 100644 --- a/build.sbt +++ b/build.sbt @@ -113,7 +113,7 @@ val Windows = "windows-latest" val MacOS = "macos-latest" val Scala212 = "2.12.17" -val Scala213 = "2.13.10" +val Scala213 = "2.13.11" val Scala3 = "3.3.0" ThisBuild / crossScalaVersions := Seq(Scala3, Scala212, Scala213) From 4fc0762d4c5f8da6861ef63827a143b03cbcbc58 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Wed, 7 Jun 2023 20:18:17 +0000 Subject: [PATCH 096/429] Regenerate GitHub Actions workflow Executed command: sbt githubWorkflowGenerate --- .github/workflows/ci.yml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d01e71a881..d05bfbd264 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -468,52 +468,52 @@ jobs: tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.10, ciJVM) + - name: Download target directories (2.13.11, ciJVM) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.10-ciJVM + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.11-ciJVM - - name: Inflate target directories (2.13.10, ciJVM) + - name: Inflate target directories (2.13.11, ciJVM) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.10, ciNative) + - name: Download target directories (2.13.11, ciNative) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.10-ciNative + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.11-ciNative - - name: Inflate target directories (2.13.10, ciNative) + - name: Inflate target directories (2.13.11, ciNative) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.10, ciJS) + - name: Download target directories (2.13.11, ciJS) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.10-ciJS + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.11-ciJS - - name: Inflate target directories (2.13.10, ciJS) + - name: Inflate target directories (2.13.11, ciJS) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.10, ciFirefox) + - name: Download target directories (2.13.11, ciFirefox) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.10-ciFirefox + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.11-ciFirefox - - name: Inflate target directories (2.13.10, ciFirefox) + - name: Inflate target directories (2.13.11, ciFirefox) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.10, ciChrome) + - name: Download target directories (2.13.11, ciChrome) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.10-ciChrome + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.11-ciChrome - - name: Inflate target directories (2.13.10, ciChrome) + - name: Inflate target directories (2.13.11, ciChrome) run: | tar xf targets.tar rm targets.tar From 5ddbc66ccef53209da44c4e8f43c5a6b88f2abc9 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Thu, 8 Jun 2023 16:07:44 +0000 Subject: [PATCH 097/429] Revert commit(s) 4fc0762d4, 3851e12a2 --- .github/workflows/ci.yml | 42 ++++++++++++++++++++-------------------- build.sbt | 2 +- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d05bfbd264..e0442b902f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - scala: [3.3.0, 2.12.17, 2.13.11] + scala: [3.3.0, 2.12.17, 2.13.10] java: [temurin@8, temurin@11, temurin@17, graalvm@11] ci: [ciJVM, ciNative, ciJS, ciFirefox, ciChrome] exclude: @@ -252,7 +252,7 @@ jobs: - shell: bash run: sbt '++ ${{ matrix.scala }}' '${{ matrix.ci }}' - - if: (matrix.scala == '2.13.11' || matrix.scala == '3.3.0') && matrix.ci == 'ciJVM' + - if: (matrix.scala == '2.13.10' || matrix.scala == '3.3.0') && matrix.ci == 'ciJVM' shell: bash run: sbt '++ ${{ matrix.scala }}' docs/mdoc @@ -267,7 +267,7 @@ jobs: run: example/test-js.sh ${{ matrix.scala }} - name: Test GraalVM Native Image - if: matrix.scala == '2.13.11' && matrix.java == 'graalvm@11' && matrix.os == 'ubuntu-latest' + if: matrix.scala == '2.13.10' && matrix.java == 'graalvm@11' && matrix.os == 'ubuntu-latest' shell: bash run: sbt '++ ${{ matrix.scala }}' graalVMExample/nativeImage graalVMExample/nativeImageRun @@ -277,7 +277,7 @@ jobs: run: example/test-native.sh ${{ matrix.scala }} - name: Scalafix tests - if: matrix.scala == '2.13.11' && matrix.ci == 'ciJVM' && matrix.os == 'ubuntu-latest' + if: matrix.scala == '2.13.10' && matrix.ci == 'ciJVM' && matrix.os == 'ubuntu-latest' shell: bash run: | cd scalafix @@ -307,7 +307,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.11] + scala: [2.13.10] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -468,52 +468,52 @@ jobs: tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.11, ciJVM) + - name: Download target directories (2.13.10, ciJVM) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.11-ciJVM + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.10-ciJVM - - name: Inflate target directories (2.13.11, ciJVM) + - name: Inflate target directories (2.13.10, ciJVM) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.11, ciNative) + - name: Download target directories (2.13.10, ciNative) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.11-ciNative + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.10-ciNative - - name: Inflate target directories (2.13.11, ciNative) + - name: Inflate target directories (2.13.10, ciNative) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.11, ciJS) + - name: Download target directories (2.13.10, ciJS) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.11-ciJS + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.10-ciJS - - name: Inflate target directories (2.13.11, ciJS) + - name: Inflate target directories (2.13.10, ciJS) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.11, ciFirefox) + - name: Download target directories (2.13.10, ciFirefox) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.11-ciFirefox + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.10-ciFirefox - - name: Inflate target directories (2.13.11, ciFirefox) + - name: Inflate target directories (2.13.10, ciFirefox) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.11, ciChrome) + - name: Download target directories (2.13.10, ciChrome) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.11-ciChrome + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.10-ciChrome - - name: Inflate target directories (2.13.11, ciChrome) + - name: Inflate target directories (2.13.10, ciChrome) run: | tar xf targets.tar rm targets.tar @@ -551,7 +551,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.11] + scala: [2.13.10] java: [temurin@8] runs-on: ${{ matrix.os }} steps: diff --git a/build.sbt b/build.sbt index 93014795e5..341f60cdf1 100644 --- a/build.sbt +++ b/build.sbt @@ -113,7 +113,7 @@ val Windows = "windows-latest" val MacOS = "macos-latest" val Scala212 = "2.12.17" -val Scala213 = "2.13.11" +val Scala213 = "2.13.10" val Scala3 = "3.3.0" ThisBuild / crossScalaVersions := Seq(Scala3, Scala212, Scala213) From fc56dd67072aa530f59d6b0805b6654fb03399de Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Thu, 8 Jun 2023 16:07:47 +0000 Subject: [PATCH 098/429] Update scala-library to 2.13.11 in series/3.x --- .github/workflows/ci.yml | 12 ++++++------ build.sbt | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4f2427a85..ed2744a9e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - scala: [3.3.0, 2.12.18, 2.13.10] + scala: [3.3.0, 2.12.18, 2.13.11] java: [temurin@8, temurin@11, temurin@17, graalvm@11] ci: [ciJVM, ciNative, ciJS, ciFirefox, ciChrome] exclude: @@ -252,7 +252,7 @@ jobs: - shell: bash run: sbt '++ ${{ matrix.scala }}' '${{ matrix.ci }}' - - if: (matrix.scala == '2.13.10' || matrix.scala == '3.3.0') && matrix.ci == 'ciJVM' + - if: (matrix.scala == '2.13.11' || matrix.scala == '3.3.0') && matrix.ci == 'ciJVM' shell: bash run: sbt '++ ${{ matrix.scala }}' docs/mdoc @@ -267,7 +267,7 @@ jobs: run: example/test-js.sh ${{ matrix.scala }} - name: Test GraalVM Native Image - if: matrix.scala == '2.13.10' && matrix.java == 'graalvm@11' && matrix.os == 'ubuntu-latest' + if: matrix.scala == '2.13.11' && matrix.java == 'graalvm@11' && matrix.os == 'ubuntu-latest' shell: bash run: sbt '++ ${{ matrix.scala }}' graalVMExample/nativeImage graalVMExample/nativeImageRun @@ -277,7 +277,7 @@ jobs: run: example/test-native.sh ${{ matrix.scala }} - name: Scalafix tests - if: matrix.scala == '2.13.10' && matrix.ci == 'ciJVM' && matrix.os == 'ubuntu-latest' + if: matrix.scala == '2.13.11' && matrix.ci == 'ciJVM' && matrix.os == 'ubuntu-latest' shell: bash run: | cd scalafix @@ -307,7 +307,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.10] + scala: [2.13.11] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -551,7 +551,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.10] + scala: [2.13.11] java: [temurin@8] runs-on: ${{ matrix.os }} steps: diff --git a/build.sbt b/build.sbt index e9a0182407..0cc3e3dc68 100644 --- a/build.sbt +++ b/build.sbt @@ -113,7 +113,7 @@ val Windows = "windows-latest" val MacOS = "macos-latest" val Scala212 = "2.12.18" -val Scala213 = "2.13.10" +val Scala213 = "2.13.11" val Scala3 = "3.3.0" ThisBuild / crossScalaVersions := Seq(Scala3, Scala212, Scala213) From 4cf3a381ec0fe16742456d55267596819285a0e7 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Thu, 8 Jun 2023 16:08:14 +0000 Subject: [PATCH 099/429] Regenerate GitHub Actions workflow Executed command: sbt githubWorkflowGenerate --- .github/workflows/ci.yml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed2744a9e8..6b532df984 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -468,52 +468,52 @@ jobs: tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.10, ciJVM) + - name: Download target directories (2.13.11, ciJVM) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.10-ciJVM + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.11-ciJVM - - name: Inflate target directories (2.13.10, ciJVM) + - name: Inflate target directories (2.13.11, ciJVM) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.10, ciNative) + - name: Download target directories (2.13.11, ciNative) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.10-ciNative + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.11-ciNative - - name: Inflate target directories (2.13.10, ciNative) + - name: Inflate target directories (2.13.11, ciNative) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.10, ciJS) + - name: Download target directories (2.13.11, ciJS) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.10-ciJS + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.11-ciJS - - name: Inflate target directories (2.13.10, ciJS) + - name: Inflate target directories (2.13.11, ciJS) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.10, ciFirefox) + - name: Download target directories (2.13.11, ciFirefox) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.10-ciFirefox + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.11-ciFirefox - - name: Inflate target directories (2.13.10, ciFirefox) + - name: Inflate target directories (2.13.11, ciFirefox) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.10, ciChrome) + - name: Download target directories (2.13.11, ciChrome) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.10-ciChrome + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.11-ciChrome - - name: Inflate target directories (2.13.10, ciChrome) + - name: Inflate target directories (2.13.11, ciChrome) run: | tar xf targets.tar rm targets.tar From dfbe7c79464cccffbc4ad46740e8bb4c06c2962d Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 8 Jun 2023 16:45:19 +0000 Subject: [PATCH 100/429] `Poller` -> `P` --- .../unsafe/WorkStealingThreadPool.scala | 4 +-- .../cats/effect/unsafe/PollingSystem.scala | 4 +-- .../unsafe/WorkStealingThreadPool.scala | 34 +++++++++---------- .../cats/effect/unsafe/WorkerThread.scala | 12 +++---- .../unsafe/EventLoopExecutorScheduler.scala | 6 ++-- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/core/js-native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/js-native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index d4ddcf705a..cf68563582 100644 --- a/core/js-native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/js-native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -23,7 +23,7 @@ import scala.concurrent.duration.FiniteDuration // Can you imagine a thread pool on JS? Have fun trying to extend or instantiate // this class. Unfortunately, due to the explicit branching, this type leaks // into the shared source code of IOFiber.scala. -private[effect] sealed abstract class WorkStealingThreadPool[Poller] private () +private[effect] sealed abstract class WorkStealingThreadPool[P] private () extends ExecutionContext { def execute(runnable: Runnable): Unit def reportFailure(cause: Throwable): Unit @@ -42,7 +42,7 @@ private[effect] sealed abstract class WorkStealingThreadPool[Poller] private () Map[Runnable, Trace]) } -private[unsafe] sealed abstract class WorkerThread[Poller] private () extends Thread { +private[unsafe] sealed abstract class WorkerThread[P] private () extends Thread { private[unsafe] def isOwnedBy(threadPool: WorkStealingThreadPool[_]): Boolean private[unsafe] def monitor(fiber: Runnable): WeakBag.Handle private[unsafe] def index: Int diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index e2d50b2a26..6888398243 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -56,7 +56,7 @@ abstract class PollingSystem { } private object PollingSystem { - type WithPoller[Poller0] = PollingSystem { - type Poller = Poller0 + type WithPoller[P] = PollingSystem { + type Poller = P } } diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index 5cb79bd4ca..abea86d902 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -58,13 +58,13 @@ import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger} * contention. Work stealing is tried using a linear search starting from a random worker thread * index. */ -private[effect] final class WorkStealingThreadPool[Poller]( +private[effect] final class WorkStealingThreadPool[P]( threadCount: Int, // number of worker threads private[unsafe] val threadPrefix: String, // prefix for the name of worker threads private[unsafe] val blockerThreadPrefix: String, // prefix for the name of worker threads currently in a blocking region private[unsafe] val runtimeBlockingExpiration: Duration, private[unsafe] val blockedThreadDetectionEnabled: Boolean, - system: PollingSystem.WithPoller[Poller], + system: PollingSystem.WithPoller[P], reportFailure0: Throwable => Unit ) extends ExecutionContextExecutor with Scheduler { @@ -75,21 +75,21 @@ private[effect] final class WorkStealingThreadPool[Poller]( /** * References to worker threads and their local queues. */ - private[this] val workerThreads: Array[WorkerThread[Poller]] = new Array(threadCount) + private[this] val workerThreads: Array[WorkerThread[P]] = new Array(threadCount) private[unsafe] val localQueues: Array[LocalQueue] = new Array(threadCount) private[unsafe] val sleepers: Array[TimerSkipList] = new Array(threadCount) private[unsafe] val parkedSignals: Array[AtomicBoolean] = new Array(threadCount) private[unsafe] val fiberBags: Array[WeakBag[Runnable]] = new Array(threadCount) - private[unsafe] val pollers: Array[Poller] = - new Array[AnyRef](threadCount).asInstanceOf[Array[Poller]] + private[unsafe] val pollers: Array[P] = + new Array[AnyRef](threadCount).asInstanceOf[Array[P]] - private[unsafe] def register(cb: Poller => Unit): Unit = { + private[unsafe] def register(cb: P => Unit): Unit = { // figure out where we are val thread = Thread.currentThread() val pool = WorkStealingThreadPool.this if (thread.isInstanceOf[WorkerThread[_]]) { - val worker = thread.asInstanceOf[WorkerThread[Poller]] + val worker = thread.asInstanceOf[WorkerThread[P]] if (worker.isOwnedBy(pool)) // we're good cb(worker.poller()) else // possibly a blocking worker thread, possibly on another wstp @@ -116,8 +116,8 @@ private[effect] final class WorkStealingThreadPool[Poller]( */ private[this] val state: AtomicInteger = new AtomicInteger(threadCount << UnparkShift) - private[unsafe] val cachedThreads: ConcurrentSkipListSet[WorkerThread[Poller]] = - new ConcurrentSkipListSet(Comparator.comparingInt[WorkerThread[Poller]](_.nameIndex)) + private[unsafe] val cachedThreads: ConcurrentSkipListSet[WorkerThread[P]] = + new ConcurrentSkipListSet(Comparator.comparingInt[WorkerThread[P]](_.nameIndex)) /** * The shutdown latch of the work stealing thread pool. @@ -171,7 +171,7 @@ private[effect] final class WorkStealingThreadPool[Poller]( } } - private[unsafe] def getWorkerThreads: Array[WorkerThread[Poller]] = workerThreads + private[unsafe] def getWorkerThreads: Array[WorkerThread[P]] = workerThreads /** * Tries to steal work from other worker threads. This method does a linear search of the @@ -192,7 +192,7 @@ private[effect] final class WorkStealingThreadPool[Poller]( private[unsafe] def stealFromOtherWorkerThread( dest: Int, random: ThreadLocalRandom, - destWorker: WorkerThread[Poller]): Runnable = { + destWorker: WorkerThread[P]): Runnable = { val destQueue = localQueues(dest) val from = random.nextInt(threadCount) @@ -471,7 +471,7 @@ private[effect] final class WorkStealingThreadPool[Poller]( * @param newWorker * the new worker thread instance to be installed at the provided index */ - private[unsafe] def replaceWorker(index: Int, newWorker: WorkerThread[Poller]): Unit = { + private[unsafe] def replaceWorker(index: Int, newWorker: WorkerThread[P]): Unit = { workerThreads(index) = newWorker workerThreadPublisher.lazySet(true) } @@ -495,7 +495,7 @@ private[effect] final class WorkStealingThreadPool[Poller]( val thread = Thread.currentThread() if (thread.isInstanceOf[WorkerThread[_]]) { - val worker = thread.asInstanceOf[WorkerThread[Poller]] + val worker = thread.asInstanceOf[WorkerThread[P]] if (worker.isOwnedBy(pool)) { worker.reschedule(runnable) } else { @@ -513,7 +513,7 @@ private[effect] final class WorkStealingThreadPool[Poller]( private[effect] def canExecuteBlockingCode(): Boolean = { val thread = Thread.currentThread() if (thread.isInstanceOf[WorkerThread[_]]) { - val worker = thread.asInstanceOf[WorkerThread[Poller]] + val worker = thread.asInstanceOf[WorkerThread[P]] worker.canExecuteBlockingCodeOn(this) } else { false @@ -599,7 +599,7 @@ private[effect] final class WorkStealingThreadPool[Poller]( val thread = Thread.currentThread() if (thread.isInstanceOf[WorkerThread[_]]) { - val worker = thread.asInstanceOf[WorkerThread[Poller]] + val worker = thread.asInstanceOf[WorkerThread[P]] if (worker.isOwnedBy(pool)) { worker.schedule(runnable) } else { @@ -637,7 +637,7 @@ private[effect] final class WorkStealingThreadPool[Poller]( def sleepInternal(delay: FiniteDuration, callback: Right[Nothing, Unit] => Unit): Runnable = { val thread = Thread.currentThread() if (thread.isInstanceOf[WorkerThread[_]]) { - val worker = thread.asInstanceOf[WorkerThread[Poller]] + val worker = thread.asInstanceOf[WorkerThread[P]] if (worker.isOwnedBy(this)) { worker.sleep(delay, callback) } else { @@ -700,7 +700,7 @@ private[effect] final class WorkStealingThreadPool[Poller]( // Clear the interrupt flag. Thread.interrupted() - var t: WorkerThread[Poller] = null + var t: WorkerThread[P] = null while ({ t = cachedThreads.pollFirst() t ne null diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala index 2b26ca49f7..b914d62253 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala @@ -41,7 +41,7 @@ import java.util.concurrent.atomic.AtomicBoolean * system when compared to a fixed size thread pool whose worker threads all draw tasks from a * single global work queue. */ -private final class WorkerThread[Poller]( +private final class WorkerThread[P]( idx: Int, // Local queue instance with exclusive write access. private[this] var queue: LocalQueue, @@ -53,10 +53,10 @@ private final class WorkerThread[Poller]( // A worker-thread-local weak bag for tracking suspended fibers. private[this] var fiberBag: WeakBag[Runnable], private[this] var sleepers: TimerSkipList, - private[this] val system: PollingSystem.WithPoller[Poller], - private[this] var _poller: Poller, + private[this] val system: PollingSystem.WithPoller[P], + private[this] var _poller: P, // Reference to the `WorkStealingThreadPool` in which this thread operates. - pool: WorkStealingThreadPool[Poller]) + pool: WorkStealingThreadPool[P]) extends Thread with BlockContext { @@ -113,7 +113,7 @@ private final class WorkerThread[Poller]( setName(s"$prefix-$nameIndex") } - private[unsafe] def poller(): Poller = _poller + private[unsafe] def poller(): P = _poller /** * Schedules the fiber for execution at the back of the local queue and notifies the work @@ -441,7 +441,7 @@ private final class WorkerThread[Poller]( parked = null fiberBag = null _active = null - _poller = null.asInstanceOf[Poller] + _poller = null.asInstanceOf[P] // Add this thread to the cached threads data structure, to be picked up // by another thread in the future. diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala index 5ddf03f797..8a66e1874f 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala @@ -26,13 +26,13 @@ import scala.util.control.NonFatal import java.util.{ArrayDeque, PriorityQueue} -private[effect] final class EventLoopExecutorScheduler[Poller]( +private[effect] final class EventLoopExecutorScheduler[P]( pollEvery: Int, - system: PollingSystem.WithPoller[Poller]) + system: PollingSystem.WithPoller[P]) extends ExecutionContextExecutor with Scheduler { - private[unsafe] val poller: Poller = system.makePoller() + private[unsafe] val poller: P = system.makePoller() private[this] var needsReschedule: Boolean = true From 8d01908d8b84e9c5cc654d370420c9863b236c70 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 8 Jun 2023 16:53:02 +0000 Subject: [PATCH 101/429] Expose poller type in `liveTraces()` --- .../scala/cats/effect/unsafe/WorkStealingThreadPool.scala | 2 +- .../scala/cats/effect/unsafe/WorkStealingThreadPool.scala | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/js-native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/js-native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index cf68563582..6624287181 100644 --- a/core/js-native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/js-native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -38,7 +38,7 @@ private[effect] sealed abstract class WorkStealingThreadPool[P] private () private[effect] def canExecuteBlockingCode(): Boolean private[unsafe] def liveTraces(): ( Map[Runnable, Trace], - Map[WorkerThread[_], (Thread.State, Option[(Runnable, Trace)], Map[Runnable, Trace])], + Map[WorkerThread[P], (Thread.State, Option[(Runnable, Trace)], Map[Runnable, Trace])], Map[Runnable, Trace]) } diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index abea86d902..7e9bdef70f 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -544,7 +544,7 @@ private[effect] final class WorkStealingThreadPool[P]( */ private[unsafe] def liveTraces(): ( Map[Runnable, Trace], - Map[WorkerThread[_], (Thread.State, Option[(Runnable, Trace)], Map[Runnable, Trace])], + Map[WorkerThread[P], (Thread.State, Option[(Runnable, Trace)], Map[Runnable, Trace])], Map[Runnable, Trace]) = { val externalFibers: Map[Runnable, Trace] = externalQueue .snapshot() @@ -559,7 +559,7 @@ private[effect] final class WorkStealingThreadPool[P]( val map = mutable .Map - .empty[WorkerThread[_], (Thread.State, Option[(Runnable, Trace)], Map[Runnable, Trace])] + .empty[WorkerThread[P], (Thread.State, Option[(Runnable, Trace)], Map[Runnable, Trace])] val suspended = mutable.Map.empty[Runnable, Trace] var i = 0 From 3257702403a974af1deb7a3773509c3472f6f97d Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 12 Jun 2023 05:22:34 +0000 Subject: [PATCH 102/429] Fix compile --- std/shared/src/main/scala/cats/effect/std/PQueue.scala | 2 +- .../src/main/scala/cats/effect/std/internal/BinomialHeap.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/std/shared/src/main/scala/cats/effect/std/PQueue.scala b/std/shared/src/main/scala/cats/effect/std/PQueue.scala index 23284c8257..a8f1864950 100644 --- a/std/shared/src/main/scala/cats/effect/std/PQueue.scala +++ b/std/shared/src/main/scala/cats/effect/std/PQueue.scala @@ -59,7 +59,7 @@ object PQueue { assertNonNegative(capacity) F.ref(State.empty[F, A]).map { ref => new PQueueImpl[F, A](ref, capacity) { - implicit val Ord = O + implicit val Ord: Order[A] = O } } } diff --git a/std/shared/src/main/scala/cats/effect/std/internal/BinomialHeap.scala b/std/shared/src/main/scala/cats/effect/std/internal/BinomialHeap.scala index 32102b3cb7..ce61b7e41a 100644 --- a/std/shared/src/main/scala/cats/effect/std/internal/BinomialHeap.scala +++ b/std/shared/src/main/scala/cats/effect/std/internal/BinomialHeap.scala @@ -66,7 +66,7 @@ private[std] object BinomialHeap { def apply[A](trees: List[BinomialTree[A]])(implicit ord: Order[A]) = new BinomialHeap[A](trees) { - implicit val Ord = ord + implicit val Ord: Order[A] = ord } /** From 6442d7a02bb7b39f267825b6e7df80ff0960e2f1 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 12 Jun 2023 05:44:37 +0000 Subject: [PATCH 103/429] Better error reporting for `clock_gettime` --- .../cats/effect/unsafe/EventLoopExecutorScheduler.scala | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala index 8a66e1874f..95a885a06c 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala @@ -19,8 +19,11 @@ package unsafe import scala.concurrent.{ExecutionContext, ExecutionContextExecutor} import scala.concurrent.duration._ -import scala.scalanative.libc.errno +import scala.scalanative.libc.errno._ +import scala.scalanative.libc.string._ import scala.scalanative.meta.LinktimeInfo +import scala.scalanative.posix.time._ +import scala.scalanative.posix.timeOps._ import scala.scalanative.unsafe._ import scala.util.control.NonFatal @@ -69,11 +72,9 @@ private[effect] final class EventLoopExecutorScheduler[P]( override def nowMicros(): Long = if (LinktimeInfo.isFreeBSD || LinktimeInfo.isLinux || LinktimeInfo.isMac) { - import scala.scalanative.posix.time._ - import scala.scalanative.posix.timeOps._ val ts = stackalloc[timespec]() if (clock_gettime(CLOCK_REALTIME, ts) != 0) - throw new RuntimeException(s"clock_gettime: ${errno.errno}") + throw new RuntimeException(fromCString(strerror(errno))) ts.tv_sec * 1000000 + ts.tv_nsec / 1000 } else { super.nowMicros() From b64bcfdedf1fec99681fb6b77ea2e7983921dd70 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 12 Jun 2023 07:10:53 +0000 Subject: [PATCH 104/429] Add `PollingSystem#close` --- .../cats/effect/unsafe/PollingSystem.scala | 2 ++ .../cats/effect/unsafe/SelectorSystem.scala | 2 ++ .../scala/cats/effect/unsafe/SleepSystem.scala | 2 ++ .../effect/unsafe/WorkStealingThreadPool.scala | 2 ++ .../src/main/scala/cats/effect/IOApp.scala | 13 +++++++++++-- .../scala/cats/effect/unsafe/EpollSystem.scala | 2 ++ .../unsafe/EventLoopExecutorScheduler.scala | 2 ++ .../unsafe/IORuntimeBuilderPlatform.scala | 4 ++-- .../unsafe/IORuntimeCompanionPlatform.scala | 17 +++++++++++++---- .../scala/cats/effect/unsafe/KqueueSystem.scala | 2 ++ .../unsafe/PollingExecutorScheduler.scala | 1 + .../scala/cats/effect/unsafe/SleepSystem.scala | 2 ++ .../cats/effect/IOPlatformSpecification.scala | 2 ++ 13 files changed, 45 insertions(+), 8 deletions(-) diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index 6888398243..d28422ce98 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -29,6 +29,8 @@ abstract class PollingSystem { */ type Poller <: AnyRef + def close(): Unit + def makeApi(register: (Poller => Unit) => Unit): Api def makePoller(): Poller diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala index 6465c325c2..ff49f4a217 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala @@ -26,6 +26,8 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS type Api = Selector + def close(): Unit = () + def makeApi(register: (Poller => Unit) => Unit): Selector = new SelectorImpl(register, provider) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala index ecded54027..c5d6379299 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -24,6 +24,8 @@ object SleepSystem extends PollingSystem { final class Api private[SleepSystem] () final class Poller private[SleepSystem] () + def close(): Unit = () + def makeApi(register: (Poller => Unit) => Unit): Api = new Api diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index 7e9bdef70f..090343bd90 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -697,6 +697,8 @@ private[effect] final class WorkStealingThreadPool[P]( i += 1 } + system.close() + // Clear the interrupt flag. Thread.interrupted() diff --git a/core/native/src/main/scala/cats/effect/IOApp.scala b/core/native/src/main/scala/cats/effect/IOApp.scala index 4ab94ee6d1..2c7b19f4b9 100644 --- a/core/native/src/main/scala/cats/effect/IOApp.scala +++ b/core/native/src/main/scala/cats/effect/IOApp.scala @@ -203,8 +203,17 @@ trait IOApp { import unsafe.IORuntime val installed = IORuntime installGlobal { - val (loop, poller) = IORuntime.createEventLoop(pollingSystem) - IORuntime(loop, loop, loop, List(poller), () => IORuntime.resetGlobal(), runtimeConfig) + val (loop, poller, loopDown) = IORuntime.createEventLoop(pollingSystem) + IORuntime( + loop, + loop, + loop, + List(poller), + () => { + loopDown() + IORuntime.resetGlobal() + }, + runtimeConfig) } _runtime = IORuntime.global diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index 847120767e..c38829ab5c 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -44,6 +44,8 @@ object EpollSystem extends PollingSystem { type Api = FileDescriptorPoller + def close(): Unit = () + def makeApi(register: (Poller => Unit) => Unit): Api = new FileDescriptorPollerImpl(register) diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala index 95a885a06c..90793b9495 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala @@ -150,6 +150,8 @@ private[effect] final class EventLoopExecutorScheduler[P]( java.lang.Long.compare(this.at, that.at) } + def shutdown(): Unit = system.close() + } private object EventLoopExecutorScheduler { diff --git a/core/native/src/main/scala/cats/effect/unsafe/IORuntimeBuilderPlatform.scala b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeBuilderPlatform.scala index 5886834743..53bea4f0c6 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/IORuntimeBuilderPlatform.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeBuilderPlatform.scala @@ -33,7 +33,7 @@ private[unsafe] abstract class IORuntimeBuilderPlatform { self: IORuntimeBuilder protected def platformSpecificBuild: IORuntime = { val defaultShutdown: () => Unit = () => () - lazy val (loop, poller) = IORuntime.createEventLoop( + lazy val (loop, poller, loopDown) = IORuntime.createEventLoop( customPollingSystem.getOrElse(IORuntime.createDefaultPollingSystem()) ) val (compute, pollers, computeShutdown) = @@ -43,7 +43,7 @@ private[unsafe] abstract class IORuntimeBuilderPlatform { self: IORuntimeBuilder ( loop, List(poller), - defaultShutdown + loopDown )) val (blocking, blockingShutdown) = customBlocking.getOrElse((compute, defaultShutdown)) val (scheduler, schedulerShutdown) = diff --git a/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala index d78e8c2fc7..7ec06fdf03 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala @@ -27,10 +27,10 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type def createEventLoop( system: PollingSystem - ): (ExecutionContext with Scheduler, system.Api) = { + ): (ExecutionContext with Scheduler, system.Api, () => Unit) = { val loop = new EventLoopExecutorScheduler[system.Poller](64, system) val poller = loop.poller - (loop, system.makeApi(cb => cb(poller))) + (loop, system.makeApi(cb => cb(poller)), () => loop.shutdown()) } def createDefaultPollingSystem(): PollingSystem = @@ -58,8 +58,17 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type def global: IORuntime = { if (_global == null) { installGlobal { - val (loop, poller) = createEventLoop(createDefaultPollingSystem()) - IORuntime(loop, loop, loop, List(poller), () => resetGlobal(), IORuntimeConfig()) + val (loop, poller, loopDown) = createEventLoop(createDefaultPollingSystem()) + IORuntime( + loop, + loop, + loop, + List(poller), + () => { + loopDown() + resetGlobal() + }, + IORuntimeConfig()) } } diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index 0d2034e371..72958bb20d 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -43,6 +43,8 @@ object KqueueSystem extends PollingSystem { type Api = FileDescriptorPoller + def close(): Unit = () + def makeApi(register: (Poller => Unit) => Unit): FileDescriptorPoller = new FileDescriptorPollerImpl(register) diff --git a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala index df65982409..6ca79ad3bd 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala @@ -31,6 +31,7 @@ abstract class PollingExecutorScheduler(pollEvery: Int) type Api = outer.type type Poller = outer.type private[this] var needsPoll = true + def close(): Unit = () def makeApi(register: (Poller => Unit) => Unit): Api = outer def makePoller(): Poller = outer def closePoller(poller: Poller): Unit = () diff --git a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala index 2e30c7722a..136bda7393 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -22,6 +22,8 @@ object SleepSystem extends PollingSystem { final class Api private[SleepSystem] () final class Poller private[SleepSystem] () + def close(): Unit = () + def makeApi(register: (Poller => Unit) => Unit): Api = new Api diff --git a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala index d49efc924b..14a3d88262 100644 --- a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala +++ b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala @@ -482,6 +482,8 @@ trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => type Api = DummyPoller type Poller = AtomicReference[List[Either[Throwable, Unit] => Unit]] + def close() = () + def makePoller() = new AtomicReference(List.empty[Either[Throwable, Unit] => Unit]) def needsPoll(poller: Poller) = poller.get.nonEmpty def closePoller(poller: Poller) = () From 2cf8eeeedff47593d4a95f46a1ee30d20e07804d Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 12 Jun 2023 07:17:07 +0000 Subject: [PATCH 105/429] Fix `SleepSystem` public api --- .../src/main/scala/cats/effect/unsafe/SleepSystem.scala | 9 ++++----- .../src/main/scala/cats/effect/unsafe/SleepSystem.scala | 9 ++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala index c5d6379299..d39a446c7c 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -21,15 +21,14 @@ import java.util.concurrent.locks.LockSupport object SleepSystem extends PollingSystem { - final class Api private[SleepSystem] () - final class Poller private[SleepSystem] () + type Api = AnyRef + type Poller = AnyRef def close(): Unit = () - def makeApi(register: (Poller => Unit) => Unit): Api = - new Api + def makeApi(register: (Poller => Unit) => Unit): Api = this - def makePoller(): Poller = new Poller + def makePoller(): Poller = this def closePoller(Poller: Poller): Unit = () diff --git a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala index 136bda7393..0848e41adb 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -19,15 +19,14 @@ package unsafe object SleepSystem extends PollingSystem { - final class Api private[SleepSystem] () - final class Poller private[SleepSystem] () + type Api = AnyRef + type Poller = AnyRef def close(): Unit = () - def makeApi(register: (Poller => Unit) => Unit): Api = - new Api + def makeApi(register: (Poller => Unit) => Unit): Api = this - def makePoller(): Poller = new Poller + def makePoller(): Poller = this def closePoller(poller: Poller): Unit = () From 0a578ab7f57b7fbe4fdc4d5435bd8b8d138f172e Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 12 Jun 2023 07:42:59 +0000 Subject: [PATCH 106/429] Workaround warning --- .../scala/cats/effect/benchmarks/WorkStealingBenchmark.scala | 2 +- .../src/test/scala/cats/effect/IOPlatformSpecification.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmarks/src/main/scala/cats/effect/benchmarks/WorkStealingBenchmark.scala b/benchmarks/src/main/scala/cats/effect/benchmarks/WorkStealingBenchmark.scala index af32fb6458..7a3e5265f1 100644 --- a/benchmarks/src/main/scala/cats/effect/benchmarks/WorkStealingBenchmark.scala +++ b/benchmarks/src/main/scala/cats/effect/benchmarks/WorkStealingBenchmark.scala @@ -165,7 +165,7 @@ class WorkStealingBenchmark { (ExecutionContext.fromExecutor(executor), () => executor.shutdown()) } - val compute = new WorkStealingThreadPool( + val compute = new WorkStealingThreadPool[AnyRef]( 256, "io-compute", "io-blocker", diff --git a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala index 14a3d88262..5d095460b6 100644 --- a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala +++ b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala @@ -430,7 +430,7 @@ trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => "not lose cedeing threads from the bypass when blocker transitioning" in { // writing this test in terms of IO seems to not reproduce the issue 0.until(5) foreach { _ => - val wstp = new WorkStealingThreadPool( + val wstp = new WorkStealingThreadPool[AnyRef]( threadCount = 2, threadPrefix = "testWorker", blockerThreadPrefix = "testBlocker", From 5460d481c984d788469736aba25128802f7c83b8 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 12 Jun 2023 13:33:59 -0700 Subject: [PATCH 107/429] Use `LongMap` for `KqueueSystem` --- .../cats/effect/unsafe/KqueueSystem.scala | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index 72958bb20d..884bed7068 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -23,6 +23,7 @@ import cats.syntax.all._ import org.typelevel.scalaccompat.annotation._ import scala.annotation.tailrec +import scala.collection.mutable.LongMap import scala.scalanative.libc.errno._ import scala.scalanative.posix.string._ import scala.scalanative.posix.time._ @@ -32,7 +33,6 @@ import scala.scalanative.unsafe._ import scala.scalanative.unsigned._ import java.io.IOException -import java.util.HashMap object KqueueSystem extends PollingSystem { @@ -80,6 +80,10 @@ object KqueueSystem extends PollingSystem { } } + // A kevent is identified by the (ident, filter) pair; there may only be one unique kevent per kqueue + @inline private def encodeKevent(ident: Int, filter: Short): Long = + (filter.toLong << 32) | ident.toLong + private final class PollHandle( register: (Poller => Unit) => Unit, fd: Int, @@ -87,9 +91,6 @@ object KqueueSystem extends PollingSystem { writeMutex: Mutex[IO] ) extends FileDescriptorPollHandle { - private[this] val readEvent = KEvent(fd.toLong, EVFILT_READ) - private[this] val writeEvent = KEvent(fd.toLong, EVFILT_WRITE) - def pollReadRec[A, B](a: A)(f: A => IO[Either[A, B]]): IO[B] = readMutex.lock.surround { a.tailRecM { a => @@ -100,8 +101,8 @@ object KqueueSystem extends PollingSystem { IO.async[Unit] { kqcb => IO.async_[Option[IO[Unit]]] { cb => register { kqueue => - kqueue.evSet(readEvent, EV_ADD.toUShort, kqcb) - cb(Right(Some(IO(kqueue.removeCallback(readEvent))))) + kqueue.evSet(fd, EVFILT_READ, EV_ADD.toUShort, kqcb) + cb(Right(Some(IO(kqueue.removeCallback(fd, EVFILT_READ))))) } } @@ -120,8 +121,8 @@ object KqueueSystem extends PollingSystem { IO.async[Unit] { kqcb => IO.async_[Option[IO[Unit]]] { cb => register { kqueue => - kqueue.evSet(writeEvent, EV_ADD.toUShort, kqcb) - cb(Right(Some(IO(kqueue.removeCallback(writeEvent))))) + kqueue.evSet(fd, EVFILT_WRITE, EV_ADD.toUShort, kqcb) + cb(Right(Some(IO(kqueue.removeCallback(fd, EVFILT_WRITE))))) } } } @@ -131,34 +132,33 @@ object KqueueSystem extends PollingSystem { } - private final case class KEvent(ident: Long, filter: Short) - final class Poller private[KqueueSystem] (kqfd: Int) { private[this] val changelistArray = new Array[Byte](sizeof[kevent64_s].toInt * MaxEvents) @inline private[this] def changelist = changelistArray.at(0).asInstanceOf[Ptr[kevent64_s]] private[this] var changeCount = 0 - private[this] val callbacks = new HashMap[KEvent, Either[Throwable, Unit] => Unit]() + private[this] val callbacks = new LongMap[Either[Throwable, Unit] => Unit]() private[KqueueSystem] def evSet( - event: KEvent, + ident: Int, + filter: Short, flags: CUnsignedShort, cb: Either[Throwable, Unit] => Unit ): Unit = { val change = changelist + changeCount.toLong - change.ident = event.ident.toULong - change.filter = event.filter + change.ident = ident.toULong + change.filter = filter change.flags = (flags.toInt | EV_ONESHOT).toUShort - callbacks.put(event, cb) + callbacks.update(encodeKevent(ident, filter), cb) changeCount += 1 } - private[KqueueSystem] def removeCallback(event: KEvent): Unit = { - callbacks.remove(event) + private[KqueueSystem] def removeCallback(ident: Int, filter: Short): Unit = { + callbacks.subtractOne(encodeKevent(ident, filter)) () } @@ -191,7 +191,9 @@ object KqueueSystem extends PollingSystem { var i = 0 var event = eventlist while (i < triggeredEvents) { - val cb = callbacks.remove(KEvent(event.ident.toLong, event.filter)) + val kevent = encodeKevent(event.ident.toInt, event.filter) + val cb = callbacks.getOrNull(kevent) + callbacks.subtractOne(kevent) if (cb ne null) cb( @@ -230,7 +232,7 @@ object KqueueSystem extends PollingSystem { polled } - def needsPoll(): Boolean = changeCount > 0 || !callbacks.isEmpty() + def needsPoll(): Boolean = changeCount > 0 || callbacks.nonEmpty } @nowarn212 From 8ad5971dd28e09e8f19fc624007b6c2fba264382 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 12 Jun 2023 13:52:49 -0700 Subject: [PATCH 108/429] Fix 2.12 compile --- .../src/main/scala/cats/effect/unsafe/KqueueSystem.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index 884bed7068..da933e3e22 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -158,7 +158,7 @@ object KqueueSystem extends PollingSystem { } private[KqueueSystem] def removeCallback(ident: Int, filter: Short): Unit = { - callbacks.subtractOne(encodeKevent(ident, filter)) + callbacks -= encodeKevent(ident, filter) () } @@ -193,7 +193,7 @@ object KqueueSystem extends PollingSystem { while (i < triggeredEvents) { val kevent = encodeKevent(event.ident.toInt, event.filter) val cb = callbacks.getOrNull(kevent) - callbacks.subtractOne(kevent) + callbacks -= kevent if (cb ne null) cb( From 7c8c36fb90d3ff5661a445db7d55d4fd860d6484 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 12 Jun 2023 13:59:04 -0700 Subject: [PATCH 109/429] Poke ci From 5760cad1783bf053cb425180567e4089a53693ed Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 12 Jun 2023 21:04:04 +0000 Subject: [PATCH 110/429] Fix filename --- .../scala/cats/effect/{SelectorPoller.scala => Selector.scala} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename core/jvm/src/main/scala/cats/effect/{SelectorPoller.scala => Selector.scala} (100%) diff --git a/core/jvm/src/main/scala/cats/effect/SelectorPoller.scala b/core/jvm/src/main/scala/cats/effect/Selector.scala similarity index 100% rename from core/jvm/src/main/scala/cats/effect/SelectorPoller.scala rename to core/jvm/src/main/scala/cats/effect/Selector.scala From f7661263f5c773633e81a9364a8a5f5eaab16ea5 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 12 Jun 2023 21:13:48 +0000 Subject: [PATCH 111/429] Fix JVM global runtime shutdown --- .../effect/unsafe/IORuntimeCompanionPlatform.scala | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala index 37c64741e9..4548bc3fcd 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala @@ -228,15 +228,19 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type def global: IORuntime = { if (_global == null) { installGlobal { - val (compute, poller, _) = createWorkStealingComputeThreadPool() - val (blocking, _) = createDefaultBlockingExecutionContext() + val (compute, poller, computeDown) = createWorkStealingComputeThreadPool() + val (blocking, blockingDown) = createDefaultBlockingExecutionContext() IORuntime( compute, blocking, compute, List(poller), - () => resetGlobal(), + () => { + computeDown() + blockingDown() + resetGlobal() + }, IORuntimeConfig()) } } From e0e361bd3312d5e290e387841b82773df3193d50 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 12 Jun 2023 21:22:18 +0000 Subject: [PATCH 112/429] Update names in `SelectorSpec` --- .../test/scala/cats/effect/SelectorSpec.scala | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala b/tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala index 3539a281e4..3421dc95d8 100644 --- a/tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala @@ -45,18 +45,18 @@ class SelectorSpec extends BaseSpec { } } - "SelectorPoller" should { + "Selector" should { "notify read-ready events" in real { mkPipe.use { pipe => for { - poller <- getSelector + selector <- getSelector buf <- IO(ByteBuffer.allocate(4)) _ <- IO(pipe.sink.write(ByteBuffer.wrap(Array(1, 2, 3)))).background.surround { - poller.select(pipe.source, OP_READ) *> IO(pipe.source.read(buf)) + selector.select(pipe.source, OP_READ) *> IO(pipe.source.read(buf)) } _ <- IO(pipe.sink.write(ByteBuffer.wrap(Array(42)))).background.surround { - poller.select(pipe.source, OP_READ) *> IO(pipe.source.read(buf)) + selector.select(pipe.source, OP_READ) *> IO(pipe.source.read(buf)) } } yield buf.array().toList must be_==(List[Byte](1, 2, 3, 42)) } @@ -65,8 +65,8 @@ class SelectorSpec extends BaseSpec { "setup multiple callbacks" in real { mkPipe.use { pipe => for { - poller <- getSelector - _ <- poller.select(pipe.source, OP_READ).parReplicateA_(10) <& + selector <- getSelector + _ <- selector.select(pipe.source, OP_READ).parReplicateA_(10) <& IO(pipe.sink.write(ByteBuffer.wrap(Array(1, 2, 3)))) } yield ok } @@ -75,17 +75,17 @@ class SelectorSpec extends BaseSpec { "works after blocking" in real { mkPipe.use { pipe => for { - poller <- getSelector + selector <- getSelector _ <- IO.blocking(()) - _ <- poller.select(pipe.sink, OP_WRITE) + _ <- selector.select(pipe.sink, OP_WRITE) } yield ok } } "gracefully handles illegal ops" in real { mkPipe.use { pipe => - getSelector.flatMap { poller => - poller.select(pipe.sink, OP_READ).attempt.map { + getSelector.flatMap { selector => + selector.select(pipe.sink, OP_READ).attempt.map { case Left(_: IllegalArgumentException) => true case _ => false } @@ -100,10 +100,10 @@ class SelectorSpec extends BaseSpec { try { val test = getSelector - .flatMap { poller => + .flatMap { selector => mkPipe.allocated.flatMap { case (pipe, close) => - poller.select(pipe.source, OP_READ).background.surround { + selector.select(pipe.source, OP_READ).background.surround { IO.sleep(1.millis) *> close *> IO.sleep(1.millis) } } From 126dea3a7bca2e15a8de0de93bde4fd443338d84 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 12 Jun 2023 21:51:02 +0000 Subject: [PATCH 113/429] Fix exception handling in `SelectorSystem` --- .../cats/effect/unsafe/SelectorSystem.scala | 36 ++++++++++--------- .../test/scala/cats/effect/SelectorSpec.scala | 3 +- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala index ff49f4a217..256402518d 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala @@ -17,6 +17,8 @@ package cats.effect package unsafe +import scala.util.control.NonFatal + import java.nio.channels.{CancelledKeyException, SelectableChannel} import java.nio.channels.spi.{AbstractSelector, SelectorProvider} @@ -107,23 +109,25 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS def select(ch: SelectableChannel, ops: Int): IO[Int] = IO.async { selectCb => IO.async_[CallbackNode] { cb => register { data => - val selector = data.selector - val key = ch.keyFor(selector) - - val node = if (key eq null) { // not yet registered on this selector - val node = new CallbackNode(ops, selectCb, null) - ch.register(selector, ops, node) - node - } else { // existing key - // mixin the new interest - key.interestOps(key.interestOps() | ops) - val node = - new CallbackNode(ops, selectCb, key.attachment().asInstanceOf[CallbackNode]) - key.attach(node) - node - } + try { + val selector = data.selector + val key = ch.keyFor(selector) + + val node = if (key eq null) { // not yet registered on this selector + val node = new CallbackNode(ops, selectCb, null) + ch.register(selector, ops, node) + node + } else { // existing key + // mixin the new interest + key.interestOps(key.interestOps() | ops) + val node = + new CallbackNode(ops, selectCb, key.attachment().asInstanceOf[CallbackNode]) + key.attach(node) + node + } - cb(Right(node)) + cb(Right(node)) + } catch { case ex if NonFatal(ex) => cb(Left(ex)) } } }.map { node => Some { diff --git a/tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala b/tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala index 3421dc95d8..b88740891b 100644 --- a/tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala @@ -84,7 +84,8 @@ class SelectorSpec extends BaseSpec { "gracefully handles illegal ops" in real { mkPipe.use { pipe => - getSelector.flatMap { selector => + // get off the wstp to test async codepaths + IO.blocking(()) *> getSelector.flatMap { selector => selector.select(pipe.sink, OP_READ).attempt.map { case Left(_: IllegalArgumentException) => true case _ => false From f1a2864c0459f4d88854c33d626bbe2610460d73 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 13 Jun 2023 00:42:16 +0000 Subject: [PATCH 114/429] `poller`->`selector` --- tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala b/tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala index b88740891b..d977c048ba 100644 --- a/tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala @@ -33,8 +33,8 @@ class SelectorSpec extends BaseSpec { def mkPipe: Resource[IO, Pipe] = Resource .eval(getSelector) - .flatMap { poller => - Resource.make(IO(poller.provider.openPipe())) { pipe => + .flatMap { selector => + Resource.make(IO(selector.provider.openPipe())) { pipe => IO(pipe.sink().close()).guarantee(IO(pipe.source().close())) } } From 09f01098dd141aafe29bb45da44ce47ae68068cc Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 13 Jun 2023 01:17:59 +0000 Subject: [PATCH 115/429] Optimize `SelectorSystem#poll` loop --- .../cats/effect/unsafe/SelectorSystem.scala | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala index 256402518d..d8e41f6a4d 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala @@ -19,7 +19,7 @@ package unsafe import scala.util.control.NonFatal -import java.nio.channels.{CancelledKeyException, SelectableChannel} +import java.nio.channels.SelectableChannel import java.nio.channels.spi.{AbstractSelector, SelectorProvider} import SelectorSystem._ @@ -54,15 +54,19 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS val key = ready.next() ready.remove() - val value: Either[Throwable, Int] = - try { - val readyOps = key.readyOps() - // reset interest in triggered ops - key.interestOps(key.interestOps() & ~readyOps) - Right(readyOps) - } catch { case ex: CancelledKeyException => Left(ex) } + var readyOps = 0 + var error: Throwable = null + try { + readyOps = key.readyOps() + // reset interest in triggered ops + key.interestOps(key.interestOps() & ~readyOps) + } catch { + case ex if NonFatal(ex) => + error = ex + readyOps = -1 // interest all waiters + } - val readyOps = value.getOrElse(-1) // interest all waiters if ex + val value = if (error ne null) Left(error) else Right(readyOps) var head: CallbackNode = null var prev: CallbackNode = null From 58f695fbc25a2f7bef5d04427ec5d03e94212e8d Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 13 Jun 2023 01:52:13 +0000 Subject: [PATCH 116/429] Refactor exception handling in `EpollSystem` --- .../cats/effect/unsafe/EpollSystem.scala | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index c38829ab5c..2f5fb13d25 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -74,19 +74,16 @@ object EpollSystem extends PollingSystem { reads: Boolean, writes: Boolean ): Resource[IO, FileDescriptorPollHandle] = - Resource - .make { - (Mutex[IO], Mutex[IO]).flatMapN { (readMutex, writeMutex) => - IO.async_[(PollHandle, IO[Unit])] { cb => - register { data => - val handle = new PollHandle(readMutex, writeMutex) - val unregister = data.register(fd, reads, writes, handle) - cb(Right((handle, unregister))) - } + Resource { + (Mutex[IO], Mutex[IO]).flatMapN { (readMutex, writeMutex) => + IO.async_[(PollHandle, IO[Unit])] { cb => + register { epoll => + val handle = new PollHandle(readMutex, writeMutex) + epoll.register(fd, reads, writes, handle, cb) } } - }(_._2) - .map(_._1) + } + } } @@ -227,22 +224,28 @@ object EpollSystem extends PollingSystem { fd: Int, reads: Boolean, writes: Boolean, - handle: PollHandle - ): IO[Unit] = { + handle: PollHandle, + cb: Either[Throwable, (PollHandle, IO[Unit])] => Unit + ): Unit = { val event = stackalloc[epoll_event]() event.events = (EPOLLET | (if (reads) EPOLLIN else 0) | (if (writes) EPOLLOUT else 0)).toUInt event.data = toPtr(handle) - if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, event) != 0) - throw new IOException(fromCString(strerror(errno))) - handles.add(handle) + val result = + if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, event) != 0) + Left(new IOException(fromCString(strerror(errno)))) + else { + handles.add(handle) + val remove = IO { + handles.remove(handle) + if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, null) != 0) + throw new IOException(fromCString(strerror(errno))) + } + Right((handle, remove)) + } - IO { - handles.remove(handle) - if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, null) != 0) - throw new IOException(fromCString(strerror(errno))) - } + cb(result) } @alwaysinline private[this] def toPtr(handle: PollHandle): Ptr[Byte] = From 237a2ba1edc00cb180e0b356db19012dcab603dd Mon Sep 17 00:00:00 2001 From: "a.muminov" Date: Wed, 14 Jun 2023 17:22:30 +0300 Subject: [PATCH 117/429] evalOnExecutor --- .../src/main/scala/cats/effect/IO.scala | 27 ++++++++++++ .../main/scala/cats/effect/kernel/Async.scala | 43 +++++++++++++++++++ .../effect/kernel/syntax/AsyncSyntax.scala | 13 +++++- 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/core/shared/src/main/scala/cats/effect/IO.scala b/core/shared/src/main/scala/cats/effect/IO.scala index ac29978d36..95f1a19f95 100644 --- a/core/shared/src/main/scala/cats/effect/IO.scala +++ b/core/shared/src/main/scala/cats/effect/IO.scala @@ -359,11 +359,38 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] { */ def evalOn(ec: ExecutionContext): IO[A] = IO.EvalOn(this, ec) + /** + * Shifts the execution of the current IO to the specified [[java.util.concurrent.Executor]]. + * + * @param executor + * @return + */ + def evalOnExecutor(executor: Executor): IO[A] = { + require(executor != null, "Cannot pass undefined Executor as an argument") + executor match { + case ec: ExecutionContext => + evalOn(ec: ExecutionContext) + case executor => + IO.executionContext.flatMap { refEc => + val newEc: ExecutionContext = + ExecutionContext.fromExecutor(executor, refEc.reportFailure) + evalOn(newEc) + } + } + } + def startOn(ec: ExecutionContext): IO[FiberIO[A @uncheckedVariance]] = start.evalOn(ec) + def startOnExecutor(executor: Executor): IO[FiberIO[A @uncheckedVariance]] = + start.evalOnExecutor(executor) + def backgroundOn(ec: ExecutionContext): ResourceIO[IO[OutcomeIO[A @uncheckedVariance]]] = Resource.make(startOn(ec))(_.cancel).map(_.join) + def backgroundOnExecutor( + executor: Executor): ResourceIO[IO[OutcomeIO[A @uncheckedVariance]]] = + Resource.make(startOnExecutor(executor))(_.cancel).map(_.join) + /** * Given an effect which might be [[uncancelable]] and a finalizer, produce an effect which * can be canceled by running the finalizer. This combinator is useful for handling scenarios diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/Async.scala b/kernel/shared/src/main/scala/cats/effect/kernel/Async.scala index b047f591e0..6fcf64bd26 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/Async.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/Async.scala @@ -166,6 +166,23 @@ trait Async[F[_]] extends AsyncPlatform[F] with Sync[F] with Temporal[F] { */ def evalOn[A](fa: F[A], ec: ExecutionContext): F[A] + /** + * [[Async.evalOn]] with provided [[java.util.concurrent.Executor]] + */ + def evalOnExecutor[A](fa: F[A], executor: Executor): F[A] = { + require(executor != null, "Cannot pass undefined Executor as an argument") + executor match { + case ec: ExecutionContext => + evalOn[A](fa, ec: ExecutionContext) + case executor => + flatMap(executionContext) { refEc => + val newEc: ExecutionContext = + ExecutionContext.fromExecutor(executor, refEc.reportFailure) + evalOn[A](fa, newEc) + } + } + } + /** * [[Async.evalOn]] as a natural transformation. */ @@ -174,6 +191,14 @@ trait Async[F[_]] extends AsyncPlatform[F] with Sync[F] with Temporal[F] { def apply[A](fa: F[A]): F[A] = evalOn(fa, ec) } + /** + * [[Async.evalOnExecutor]] as a natural transformation. + */ + def evalOnExecutorK(executor: Executor): F ~> F = + new (F ~> F) { + def apply[A](fa: F[A]): F[A] = evalOnExecutor(fa, executor) + } + /** * Start a new fiber on a different execution context. * @@ -182,6 +207,14 @@ trait Async[F[_]] extends AsyncPlatform[F] with Sync[F] with Temporal[F] { def startOn[A](fa: F[A], ec: ExecutionContext): F[Fiber[F, Throwable, A]] = evalOn(start(fa), ec) + /** + * Start a new fiber on a different executor. + * + * See [[GenSpawn.start]] for more details. + */ + def startOnExecutor[A](fa: F[A], executor: Executor): F[Fiber[F, Throwable, A]] = + evalOnExecutor(start(fa), executor) + /** * Start a new background fiber on a different execution context. * @@ -192,6 +225,16 @@ trait Async[F[_]] extends AsyncPlatform[F] with Sync[F] with Temporal[F] { ec: ExecutionContext): Resource[F, F[Outcome[F, Throwable, A]]] = Resource.make(startOn(fa, ec))(_.cancel)(this).map(_.join) + /** + * Start a new background fiber on a different executor. + * + * See [[GenSpawn.background]] for more details. + */ + def backgroundOnExecutor[A]( + fa: F[A], + executor: Executor): Resource[F, F[Outcome[F, Throwable, A]]] = + Resource.make(startOnExecutor(fa, executor))(_.cancel)(this).map(_.join) + /** * Obtain a reference to the current execution context. */ diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/syntax/AsyncSyntax.scala b/kernel/shared/src/main/scala/cats/effect/kernel/syntax/AsyncSyntax.scala index 18c4bb17c1..7310e077eb 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/syntax/AsyncSyntax.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/syntax/AsyncSyntax.scala @@ -16,8 +16,9 @@ package cats.effect.kernel.syntax -import cats.effect.kernel.{Async, Fiber, Outcome, Resource, Sync} +import cats.effect.kernel._ +import java.util.concurrent.Executor import scala.concurrent.ExecutionContext trait AsyncSyntax { @@ -30,13 +31,23 @@ final class AsyncOps[F[_], A] private[syntax] (private[syntax] val wrapped: F[A] def evalOn(ec: ExecutionContext)(implicit F: Async[F]): F[A] = Async[F].evalOn(wrapped, ec) + def evalOnExecutor(executor: Executor)(implicit F: Async[F]): F[A] = + Async[F].evalOnExecutor(wrapped, executor) + def startOn(ec: ExecutionContext)(implicit F: Async[F]): F[Fiber[F, Throwable, A]] = Async[F].startOn(wrapped, ec) + def startOnExecutor(executor: Executor)(implicit F: Async[F]): F[Fiber[F, Throwable, A]] = + Async[F].startOnExecutor(wrapped, executor) + def backgroundOn(ec: ExecutionContext)( implicit F: Async[F]): Resource[F, F[Outcome[F, Throwable, A]]] = Async[F].backgroundOn(wrapped, ec) + def backgroundOnExecutor(executor: Executor)( + implicit F: Async[F]): Resource[F, F[Outcome[F, Throwable, A]]] = + Async[F].backgroundOnExecutor(wrapped, executor) + def syncStep[G[_]: Sync](limit: Int)(implicit F: Async[F]): G[Either[F[A], A]] = Async[F].syncStep[G, A](wrapped, limit) } From c8814fa60fb67572b74e0201c62c0621814c96c5 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 16 Jun 2023 00:27:46 +0000 Subject: [PATCH 118/429] Refactor `IOAppSpec` --- build.sbt | 79 ++-- ioapp-tests/src/test/scala/IOAppSpec.scala | 329 ++++++++++++++++ project/CI.scala | 6 +- .../scala/catseffect/examplesplatform.scala | 15 - .../scala/catseffect/examplesplatform.scala | 5 - .../test/scala/cats/effect/IOAppSpec.scala | 352 ------------------ 6 files changed, 383 insertions(+), 403 deletions(-) create mode 100644 ioapp-tests/src/test/scala/IOAppSpec.scala delete mode 100644 tests/jvm/src/test/scala/cats/effect/IOAppSpec.scala diff --git a/build.sbt b/build.sbt index d1fdae38b5..4696b0d4d3 100644 --- a/build.sbt +++ b/build.sbt @@ -270,10 +270,6 @@ lazy val useJSEnv = settingKey[JSEnv]("Use Node.js or a headless browser for running Scala.js tests") Global / useJSEnv := NodeJS -lazy val testJSIOApp = - settingKey[Boolean]("Whether to test JVM (false) or Node.js (true) in IOAppSpec") -Global / testJSIOApp := false - ThisBuild / jsEnv := { useJSEnv.value match { case NodeJS => new NodeJSEnv(NodeJSEnv.Config().withSourceMap(true)) @@ -326,7 +322,16 @@ tlReplaceCommandAlias( "; root/clean; +root/headerCreate; root/scalafixAll; scalafmtSbt; +root/scalafmtAll") val jsProjects: Seq[ProjectReference] = - Seq(kernel.js, kernelTestkit.js, laws.js, core.js, testkit.js, testsJS, std.js, example.js) + Seq( + kernel.js, + kernelTestkit.js, + laws.js, + core.js, + testkit.js, + tests.js, + ioAppTestsJS, + std.js, + example.js) val nativeProjects: Seq[ProjectReference] = Seq( @@ -336,6 +341,7 @@ val nativeProjects: Seq[ProjectReference] = core.native, testkit.native, tests.native, + ioAppTestsNative, std.native, example.native) @@ -367,7 +373,7 @@ lazy val rootJVM = project laws.jvm, core.jvm, testkit.jvm, - testsJVM, + tests.jvm, std.jvm, example.jvm, graalVMExample, @@ -857,7 +863,7 @@ lazy val testkit = crossProject(JSPlatform, JVMPlatform, NativePlatform) lazy val tests: CrossProject = crossProject(JSPlatform, JVMPlatform, NativePlatform) .in(file("tests")) .dependsOn(core, laws % Test, kernelTestkit % Test, testkit % Test) - .enablePlugins(BuildInfoPlugin, NoPublishPlugin) + .enablePlugins(NoPublishPlugin) .settings( name := "cats-effect-tests", libraryDependencies ++= Seq( @@ -866,7 +872,6 @@ lazy val tests: CrossProject = crossProject(JSPlatform, JVMPlatform, NativePlatf "org.typelevel" %%% "discipline-specs2" % DisciplineVersion % Test, "org.typelevel" %%% "cats-kernel-laws" % CatsVersion % Test ), - buildInfoPackage := "catseffect", githubWorkflowArtifactUpload := false ) .jsSettings( @@ -876,26 +881,48 @@ lazy val tests: CrossProject = crossProject(JSPlatform, JVMPlatform, NativePlatf scalacOptions ~= { _.filterNot(_.startsWith("-P:scalajs:mapSourceURI")) } ) .jvmSettings( - Test / fork := true, - Test / javaOptions += s"-Dsbt.classpath=${(Test / fullClasspath).value.map(_.data.getAbsolutePath).mkString(File.pathSeparator)}" + Test / fork := true ) -lazy val testsJS = tests.js -lazy val testsJVM = tests - .jvm - .enablePlugins(BuildInfoPlugin) - .settings( - Test / compile := { - if (testJSIOApp.value) - (Test / compile).dependsOn(testsJS / Compile / fastOptJS).value - else - (Test / compile).value - }, - buildInfoPackage := "cats.effect", - buildInfoKeys += testJSIOApp, - buildInfoKeys += - "jsRunner" -> (testsJS / Compile / fastOptJS / artifactPath).value - ) +def configureIOAppTests(p: Project): Project = + p.enablePlugins(NoPublishPlugin, BuildInfoPlugin) + .settings( + Test / unmanagedSourceDirectories += (LocalRootProject / baseDirectory).value / "ioapp-tests" / "src" / "test" / "scala", + libraryDependencies += "org.specs2" %%% "specs2-core" % Specs2Version % Test, + buildInfoPackage := "cats.effect", + buildInfoKeys ++= Seq( + "jsRunner" -> (tests.js / Compile / fastOptJS / artifactPath).value, + "nativeRunner" -> (tests.native / Compile / nativeLink / artifactPath).value + ) + ) + +lazy val ioAppTestsJVM = + project + .in(file("ioapp-tests/.jvm")) + .configure(configureIOAppTests) + .settings( + buildInfoKeys += "platform" -> "jvm", + Test / fork := true, + Test / javaOptions += s"-Dcatseffect.examples.classpath=${(tests.jvm / Compile / fullClasspath).value.map(_.data.getAbsolutePath).mkString(File.pathSeparator)}" + ) + +lazy val ioAppTestsJS = + project + .in(file("ioapp-tests/.js")) + .configure(configureIOAppTests) + .settings( + (Test / test) := (Test / test).dependsOn(tests.js / Compile / fastOptJS).value, + buildInfoKeys += "platform" -> "js" + ) + +lazy val ioAppTestsNative = + project + .in(file("ioapp-tests/.native")) + .configure(configureIOAppTests) + .settings( + (Test / test) := (Test / test).dependsOn(tests.native / Compile / nativeLink).value, + buildInfoKeys += "platform" -> "native" + ) /** * Implementations lof standard functionality (e.g. Semaphore, Console, Queue) purely in terms diff --git a/ioapp-tests/src/test/scala/IOAppSpec.scala b/ioapp-tests/src/test/scala/IOAppSpec.scala new file mode 100644 index 0000000000..e5dd104d0d --- /dev/null +++ b/ioapp-tests/src/test/scala/IOAppSpec.scala @@ -0,0 +1,329 @@ +/* + * Copyright 2020-2023 Typelevel + * + * 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 + +import org.specs2.mutable.Specification + +import scala.io.Source +import scala.sys.process.{BasicIO, Process, ProcessBuilder} + +import java.io.File + +class IOAppSpec extends Specification { + + abstract class Platform(val id: String) { outer => + def builder(proto: String, args: List[String]): ProcessBuilder + def pid(proto: String): Option[Int] + + def dumpSignal: String + + def sendSignal(pid: Int): Unit = { + Runtime.getRuntime().exec(s"kill -$dumpSignal $pid") + () + } + + def apply(proto: String, args: List[String]): Handle = { + val stdoutBuffer = new StringBuffer() + val stderrBuffer = new StringBuffer() + val p = builder(proto, args).run(BasicIO(false, stdoutBuffer, None).withError { in => + val err = Source.fromInputStream(in).getLines().mkString(System.lineSeparator()) + stderrBuffer.append(err) + () + }) + + new Handle { + def awaitStatus() = p.exitValue() + def term() = p.destroy() // TODO probably doesn't work + def stderr() = stderrBuffer.toString + def stdout() = stdoutBuffer.toString + def pid() = outer.pid(proto) + } + } + } + + object JVM extends Platform("jvm") { + val ClassPath = System.getProperty("catseffect.examples.classpath") + + val JavaHome = { + val path = sys.env.get("JAVA_HOME").orElse(sys.props.get("java.home")).get + if (path.endsWith("/jre")) { + // handle JDK 8 installations + path.replace("/jre", "") + } else { + path + } + } + + val dumpSignal = "USR1" + + def builder(proto: String, args: List[String]) = Process( + s"$JavaHome/bin/java", + List("-cp", ClassPath, s"catseffect.examples.$proto") ::: args) + + // scala.sys.process.Process and java.lang.Process lack getting PID support. Java 9+ introduced it but + // whatever because it's very hard to obtain a java.lang.Process from scala.sys.process.Process. + def pid(proto: String): Option[Int] = { + val jpsStdoutBuffer = new StringBuffer() + val jpsProcess = + Process(s"$JavaHome/bin/jps", List.empty).run(BasicIO(false, jpsStdoutBuffer, None)) + jpsProcess.exitValue() + + val output = jpsStdoutBuffer.toString + Source.fromString(output).getLines().find(_.contains(proto)).map(_.split(" ")(0).toInt) + } + + } + + object Node extends Platform("node") { + val dumpSignal = "USR2" + + def builder(proto: String, args: List[String]) = + Process( + s"node", + "--enable-source-maps" :: BuildInfo + .jsRunner + .getAbsolutePath :: s"catseffect.examples.$proto" :: args) + + def pid(proto: String): Option[Int] = { + val stdoutBuffer = new StringBuffer() + val process = + Process("ps", List("aux")).run(BasicIO(false, stdoutBuffer, None)) + process.exitValue() + + val output = stdoutBuffer.toString + Source + .fromString(output) + .getLines() + .find(l => l.contains(BuildInfo.jsRunner.getAbsolutePath) && l.contains(proto)) + .map(_.split(" +")(1).toInt) + } + } + + lazy val platform = BuildInfo.platform match { + case "jvm" => JVM + case "js" => Node + case "native" => ??? + case platform => throw new RuntimeException(s"unknown platform $platform") + } + + lazy val isJava8 = + platform == JVM && sys.props.get("java.version").filter(_.startsWith("1.8")).isDefined + lazy val isWindows = System.getProperty("os.name").toLowerCase.contains("windows") + + s"IOApp (${platform.id})" should { + + if (!isWindows) { // these tests have all been emperically flaky on Windows CI builds, so they're disabled + + "evaluate and print hello world" in { + val h = platform("HelloWorld", Nil) + h.awaitStatus() mustEqual 0 + h.stdout() mustEqual s"Hello, World!${System.lineSeparator()}" + } + + "pass all arguments to child" in { + val expected = List("the", "quick", "brown", "fox jumped", "over") + val h = platform("Arguments", expected) + h.awaitStatus() mustEqual 0 + h.stdout() mustEqual expected.mkString( + "", + System.lineSeparator(), + System.lineSeparator()) + } + + "exit on non-fatal error" in { + val h = platform("NonFatalError", List.empty) + h.awaitStatus() mustEqual 1 + h.stderr() must contain("Boom!") + } + + "exit with leaked fibers" in { + val h = platform("LeakedFiber", List.empty) + h.awaitStatus() mustEqual 0 + } + + "exit on fatal error" in { + val h = platform("FatalError", List.empty) + h.awaitStatus() mustEqual 1 + h.stderr() must contain("Boom!") + h.stdout() must not(contain("sadness")) + } + + "exit on fatal error with other unsafe runs" in { + val h = platform("FatalErrorUnsafeRun", List.empty) + h.awaitStatus() mustEqual 1 + h.stderr() must contain("Boom!") + } + + "warn on global runtime collision" in { + val h = platform("GlobalRacingInit", List.empty) + h.awaitStatus() mustEqual 0 + h.stderr() must contain( + "Cats Effect global runtime already initialized; custom configurations will be ignored") + h.stderr() must not(contain("boom")) + } + + "reset global runtime on shutdown" in { + val h = platform("GlobalShutdown", List.empty) + h.awaitStatus() mustEqual 0 + h.stderr() must not contain + "Cats Effect global runtime already initialized; custom configurations will be ignored" + h.stderr() must not(contain("boom")) + } + + "warn on cpu starvation" in { + val h = platform("CpuStarvation", List.empty) + h.awaitStatus() + val err = h.stderr() + err must not(contain("[WARNING] Failed to register Cats Effect CPU")) + err must contain("[WARNING] Your app's responsiveness") + // we use a regex because time has too many corner cases - a test run at just the wrong + // moment on new year's eve, etc + err must beMatching( + // (?s) allows matching across line breaks + """(?s)^\d{4}-[01]\d-[0-3]\dT[012]\d:[0-6]\d:[0-6]\d(?:\.\d{1,3})?Z \[WARNING\] Your app's responsiveness.*""" + ) + } + + "custom runtime installed as global" in { + val h = platform("CustomRuntime", List.empty) + h.awaitStatus() mustEqual 0 + } + + "abort awaiting shutdown hooks" in { + val h = platform("ShutdownHookImmediateTimeout", List.empty) + h.awaitStatus() mustEqual 0 + } + } + + if (!isWindows) { + // The jvm cannot gracefully terminate processes on Windows, so this + // test cannot be carried out properly. Same for testing IOApp in sbt. + + "run finalizers on TERM" in { + import _root_.java.io.{BufferedReader, FileReader} + + // we have to resort to this convoluted approach because Process#destroy kills listeners before killing the process + val test = File.createTempFile("cats-effect", "finalizer-test") + def readTest(): String = { + val reader = new BufferedReader(new FileReader(test)) + try { + reader.readLine() + } finally { + reader.close() + } + } + + val h = platform("Finalizers", test.getAbsolutePath() :: Nil) + + var i = 0 + while (!h.stdout().contains("Started") && i < 100) { + Thread.sleep(100) + i += 1 + } + + Thread.sleep( + 100 + ) // give thread scheduling just a sec to catch up and get us into the latch.await() + + h.term() + h.awaitStatus() mustEqual 143 + + i = 0 + while (readTest() == null && i < 100) { + i += 1 + } + readTest() must contain("canceled") + } + } + + "exit on fatal error without IOApp" in { + val h = platform("FatalErrorRaw", List.empty) + h.awaitStatus() + h.stdout() must not(contain("sadness")) + h.stderr() must not(contain("Promise already completed")) + } + + "exit on canceled" in { + val h = platform("Canceled", List.empty) + h.awaitStatus() mustEqual 1 + } + + if (!isJava8 && !isWindows) { + // JDK 8 does not have free signals for live fiber snapshots + // cannot observe signals sent to process termination on Windows + "live fiber snapshot" in { + val h = platform("LiveFiberSnapshot", List.empty) + + // wait for the application to fully start before trying to send the signal + while (!h.stdout().contains("ready")) { + Thread.sleep(100L) + } + + val pid = h.pid() + pid must beSome + pid.foreach(platform.sendSignal) + h.awaitStatus() + val stderr = h.stderr() + stderr must contain("cats.effect.IOFiber") + } + } + + if (platform == JVM) { + "shutdown on worker thread interruption" in { + val h = platform("WorkerThreadInterrupt", List.empty) + h.awaitStatus() mustEqual 1 + h.stderr() must contain("java.lang.InterruptedException") + ok + } + + "support main thread evaluation" in { + val h = platform("EvalOnMainThread", List.empty) + h.awaitStatus() mustEqual 0 + } + + "use configurable reportFailure for MainThread" in { + val h = platform("MainThreadReportFailure", List.empty) + h.awaitStatus() mustEqual 0 + } + + "warn on blocked threads" in { + val h = platform("BlockedThreads", List.empty) + h.awaitStatus() + val err = h.stderr() + err must contain( + "[WARNING] A Cats Effect worker thread was detected to be in a blocked state") + } + } + + if (platform == Node) { + "gracefully ignore undefined process.exit" in { + val h = platform("UndefinedProcessExit", List.empty) + h.awaitStatus() mustEqual 0 + } + } + + "make specs2 happy" in ok + } + + trait Handle { + def awaitStatus(): Int + def term(): Unit + def stderr(): String + def stdout(): String + def pid(): Option[Int] + } +} diff --git a/project/CI.scala b/project/CI.scala index 7469822edd..31f2bbdd1d 100644 --- a/project/CI.scala +++ b/project/CI.scala @@ -56,11 +56,7 @@ object CI { command = "ciJS", rootProject = "rootJS", jsEnv = Some(JSEnv.NodeJS), - testCommands = List( - "test", - "set Global / testJSIOApp := true", - "testsJVM/testOnly *.IOAppSpec", - "set Global / testJSIOApp := false"), + testCommands = List("test"), mimaReport = true, suffixCommands = List("exampleJS/compile") ) diff --git a/tests/js/src/main/scala/catseffect/examplesplatform.scala b/tests/js/src/main/scala/catseffect/examplesplatform.scala index 44081e467f..fa6acfbe10 100644 --- a/tests/js/src/main/scala/catseffect/examplesplatform.scala +++ b/tests/js/src/main/scala/catseffect/examplesplatform.scala @@ -106,19 +106,4 @@ package examples { def run(args: List[String]): IO[ExitCode] = IO.pure(ExitCode.Success) } - // stub - object EvalOnMainThread extends IOApp { - def run(args: List[String]): IO[ExitCode] = IO.never - } - - // stub - object MainThreadReportFailure extends IOApp { - def run(args: List[String]): IO[ExitCode] = IO.never - } - - // stub - object BlockedThreads extends IOApp.Simple { - val run = IO.never - } - } diff --git a/tests/jvm/src/main/scala/catseffect/examplesplatform.scala b/tests/jvm/src/main/scala/catseffect/examplesplatform.scala index 2edbe9bcf9..2b68ae0348 100644 --- a/tests/jvm/src/main/scala/catseffect/examplesplatform.scala +++ b/tests/jvm/src/main/scala/catseffect/examplesplatform.scala @@ -64,11 +64,6 @@ package examples { .as(ExitCode.Success) } - // just a stub to satisfy compiler, never run on JVM - object UndefinedProcessExit extends IOApp { - def run(args: List[String]): IO[ExitCode] = IO.never - } - object EvalOnMainThread extends IOApp { def run(args: List[String]): IO[ExitCode] = IO(Thread.currentThread().getId()).evalOn(MainThread) map { diff --git a/tests/jvm/src/test/scala/cats/effect/IOAppSpec.scala b/tests/jvm/src/test/scala/cats/effect/IOAppSpec.scala deleted file mode 100644 index 75688054fd..0000000000 --- a/tests/jvm/src/test/scala/cats/effect/IOAppSpec.scala +++ /dev/null @@ -1,352 +0,0 @@ -/* - * Copyright 2020-2023 Typelevel - * - * 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 - -import org.specs2.mutable.Specification - -import scala.io.Source -import scala.sys.process.{BasicIO, Process, ProcessBuilder} - -import java.io.File - -class IOAppSpec extends Specification { - - abstract class Platform(val id: String) { outer => - def builder(proto: AnyRef, args: List[String]): ProcessBuilder - def pid(proto: AnyRef): Option[Int] - - def dumpSignal: String - - def sendSignal(pid: Int): Unit = { - Runtime.getRuntime().exec(s"kill -$dumpSignal $pid") - () - } - - def apply(proto: AnyRef, args: List[String]): Handle = { - val stdoutBuffer = new StringBuffer() - val stderrBuffer = new StringBuffer() - val p = builder(proto, args).run(BasicIO(false, stdoutBuffer, None).withError { in => - val err = Source.fromInputStream(in).getLines().mkString(System.lineSeparator()) - stderrBuffer.append(err) - () - }) - - new Handle { - def awaitStatus() = p.exitValue() - def term() = p.destroy() // TODO probably doesn't work - def stderr() = stderrBuffer.toString - def stdout() = stdoutBuffer.toString - def pid() = outer.pid(proto) - } - } - } - - object JVM extends Platform("jvm") { - val ClassPath = System.getProperty("sbt.classpath") - - val JavaHome = { - val path = sys.env.get("JAVA_HOME").orElse(sys.props.get("java.home")).get - if (path.endsWith("/jre")) { - // handle JDK 8 installations - path.replace("/jre", "") - } else { - path - } - } - - val dumpSignal = "USR1" - - def builder(proto: AnyRef, args: List[String]) = Process( - s"$JavaHome/bin/java", - List("-cp", ClassPath, proto.getClass.getName.replaceAll("\\$$", "")) ::: args) - - // scala.sys.process.Process and java.lang.Process lack getting PID support. Java 9+ introduced it but - // whatever because it's very hard to obtain a java.lang.Process from scala.sys.process.Process. - def pid(proto: AnyRef): Option[Int] = { - val mainName = proto.getClass.getSimpleName.replace("$", "") - val jpsStdoutBuffer = new StringBuffer() - val jpsProcess = - Process(s"$JavaHome/bin/jps", List.empty).run(BasicIO(false, jpsStdoutBuffer, None)) - jpsProcess.exitValue() - - val output = jpsStdoutBuffer.toString - Source.fromString(output).getLines().find(_.contains(mainName)).map(_.split(" ")(0).toInt) - } - - } - - object Node extends Platform("node") { - val dumpSignal = "USR2" - - def builder(proto: AnyRef, args: List[String]) = - Process( - s"node", - "--enable-source-maps" :: BuildInfo - .jsRunner - .getAbsolutePath :: proto.getClass.getName.init :: args) - - def pid(proto: AnyRef): Option[Int] = { - val mainName = proto.getClass.getName.init - val stdoutBuffer = new StringBuffer() - val process = - Process("ps", List("aux")).run(BasicIO(false, stdoutBuffer, None)) - process.exitValue() - - val output = stdoutBuffer.toString - Source - .fromString(output) - .getLines() - .find(l => l.contains(BuildInfo.jsRunner.getAbsolutePath) && l.contains(mainName)) - .map(_.split(" +")(1).toInt) - } - } - - if (BuildInfo.testJSIOApp) - test(Node) - else - test(JVM) - - def test(platform: Platform): Unit = { - s"IOApp (${platform.id})" should { - import catseffect.examples._ - - val isWindows = System.getProperty("os.name").toLowerCase.contains("windows") - - if (isWindows) { - // these tests have all been emperically flaky on Windows CI builds, so they're disabled - "evaluate and print hello world" in skipped("this test is unreliable on Windows") - "pass all arguments to child" in skipped("this test is unreliable on Windows") - - "exit with leaked fibers" in skipped("this test is unreliable on Windows") - - "exit on non-fatal error" in skipped("this test is unreliable on Windows") - "exit on fatal error" in skipped("this test is unreliable on Windows") - - "exit on fatal error with other unsafe runs" in skipped( - "this test is unreliable on Windows") - - "warn on global runtime collision" in skipped("this test is unreliable on Windows") - "abort awaiting shutdown hooks" in skipped("this test is unreliable on Windows") - - // The jvm cannot gracefully terminate processes on Windows, so this - // test cannot be carried out properly. Same for testing IOApp in sbt. - "run finalizers on TERM" in skipped( - "cannot observe graceful process termination on Windows") - } else { - "evaluate and print hello world" in { - val h = platform(HelloWorld, Nil) - h.awaitStatus() mustEqual 0 - h.stdout() mustEqual s"Hello, World!${System.lineSeparator()}" - } - - "pass all arguments to child" in { - val expected = List("the", "quick", "brown", "fox jumped", "over") - val h = platform(Arguments, expected) - h.awaitStatus() mustEqual 0 - h.stdout() mustEqual expected.mkString( - "", - System.lineSeparator(), - System.lineSeparator()) - } - - "exit on non-fatal error" in { - val h = platform(NonFatalError, List.empty) - h.awaitStatus() mustEqual 1 - h.stderr() must contain("Boom!") - } - - "exit with leaked fibers" in { - val h = platform(LeakedFiber, List.empty) - h.awaitStatus() mustEqual 0 - } - - "exit on fatal error" in { - val h = platform(FatalError, List.empty) - h.awaitStatus() mustEqual 1 - h.stderr() must contain("Boom!") - h.stdout() must not(contain("sadness")) - } - - "exit on fatal error with other unsafe runs" in { - val h = platform(FatalErrorUnsafeRun, List.empty) - h.awaitStatus() mustEqual 1 - h.stderr() must contain("Boom!") - } - - "warn on global runtime collision" in { - val h = platform(GlobalRacingInit, List.empty) - h.awaitStatus() mustEqual 0 - h.stderr() must contain( - "Cats Effect global runtime already initialized; custom configurations will be ignored") - h.stderr() must not(contain("boom")) - } - - "reset global runtime on shutdown" in { - val h = platform(GlobalShutdown, List.empty) - h.awaitStatus() mustEqual 0 - h.stderr() must not contain - "Cats Effect global runtime already initialized; custom configurations will be ignored" - h.stderr() must not(contain("boom")) - } - - "warn on cpu starvation" in { - val h = platform(CpuStarvation, List.empty) - h.awaitStatus() - val err = h.stderr() - err must not(contain("[WARNING] Failed to register Cats Effect CPU")) - err must contain("[WARNING] Your app's responsiveness") - // we use a regex because time has too many corner cases - a test run at just the wrong - // moment on new year's eve, etc - err must beMatching( - // (?s) allows matching across line breaks - """(?s)^\d{4}-[01]\d-[0-3]\dT[012]\d:[0-6]\d:[0-6]\d(?:\.\d{1,3})?Z \[WARNING\] Your app's responsiveness.*""" - ) - } - - "custom runtime installed as global" in { - val h = platform(CustomRuntime, List.empty) - h.awaitStatus() mustEqual 0 - } - - "abort awaiting shutdown hooks" in { - val h = platform(ShutdownHookImmediateTimeout, List.empty) - h.awaitStatus() mustEqual 0 - } - - "run finalizers on TERM" in { - import _root_.java.io.{BufferedReader, FileReader} - - // we have to resort to this convoluted approach because Process#destroy kills listeners before killing the process - val test = File.createTempFile("cats-effect", "finalizer-test") - def readTest(): String = { - val reader = new BufferedReader(new FileReader(test)) - try { - reader.readLine() - } finally { - reader.close() - } - } - - val h = platform(Finalizers, test.getAbsolutePath() :: Nil) - - var i = 0 - while (!h.stdout().contains("Started") && i < 100) { - Thread.sleep(100) - i += 1 - } - - Thread.sleep( - 100 - ) // give thread scheduling just a sec to catch up and get us into the latch.await() - - h.term() - h.awaitStatus() mustEqual 143 - - i = 0 - while (readTest() == null && i < 100) { - i += 1 - } - readTest() must contain("canceled") - } - } - - "exit on fatal error without IOApp" in { - val h = platform(FatalErrorRaw, List.empty) - h.awaitStatus() - h.stdout() must not(contain("sadness")) - h.stderr() must not(contain("Promise already completed")) - } - - "exit on canceled" in { - val h = platform(Canceled, List.empty) - h.awaitStatus() mustEqual 1 - } - - if (BuildInfo.testJSIOApp) { - "gracefully ignore undefined process.exit" in { - val h = platform(UndefinedProcessExit, List.empty) - h.awaitStatus() mustEqual 0 - } - - "support main thread evaluation" in skipped( - "JavaScript is all main thread, all the time") - - } else { - val isJava8 = sys.props.get("java.version").filter(_.startsWith("1.8")).isDefined - - if (isJava8) { - "live fiber snapshot" in skipped( - "JDK 8 does not have free signals for live fiber snapshots") - } else if (isWindows) { - "live fiber snapshot" in skipped( - "cannot observe signals sent to process termination on Windows") - } else { - "live fiber snapshot" in { - val h = platform(LiveFiberSnapshot, List.empty) - - // wait for the application to fully start before trying to send the signal - while (!h.stdout().contains("ready")) { - Thread.sleep(100L) - } - - val pid = h.pid() - pid must beSome - pid.foreach(platform.sendSignal) - h.awaitStatus() - val stderr = h.stderr() - stderr must contain("cats.effect.IOFiber") - } - } - - "shutdown on worker thread interruption" in { - val h = platform(WorkerThreadInterrupt, List.empty) - h.awaitStatus() mustEqual 1 - h.stderr() must contain("java.lang.InterruptedException") - ok - } - - "support main thread evaluation" in { - val h = platform(EvalOnMainThread, List.empty) - h.awaitStatus() mustEqual 0 - } - - "use configurable reportFailure for MainThread" in { - val h = platform(MainThreadReportFailure, List.empty) - h.awaitStatus() mustEqual 0 - } - - "warn on blocked threads" in { - val h = platform(BlockedThreads, List.empty) - h.awaitStatus() - val err = h.stderr() - err must contain( - "[WARNING] A Cats Effect worker thread was detected to be in a blocked state") - } - - } - } - () - } - - trait Handle { - def awaitStatus(): Int - def term(): Unit - def stderr(): String - def stdout(): String - def pid(): Option[Int] - } -} From 23aa24bb57cd778db7d1383445023d4fb467ffbd Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 16 Jun 2023 09:02:34 +0000 Subject: [PATCH 119/429] Setup `IOAppSpec` for Native --- build.sbt | 3 + ioapp-tests/src/test/scala/IOAppSpec.scala | 23 +++++++- .../scala/catseffect/examplesjvmnative.scala | 35 ++++++++++++ .../scala/catseffect/examplesplatform.scala | 14 ----- .../scala/catseffect/examplesplatform.scala | 55 ++++++++++++++++++- 5 files changed, 113 insertions(+), 17 deletions(-) create mode 100644 tests/jvm-native/src/main/scala/catseffect/examplesjvmnative.scala diff --git a/build.sbt b/build.sbt index 4696b0d4d3..2e22d7b01a 100644 --- a/build.sbt +++ b/build.sbt @@ -883,6 +883,9 @@ lazy val tests: CrossProject = crossProject(JSPlatform, JVMPlatform, NativePlatf .jvmSettings( Test / fork := true ) + .nativeSettings( + Compile / mainClass := Some("catseffect.examples.NativeRunner") + ) def configureIOAppTests(p: Project): Project = p.enablePlugins(NoPublishPlugin, BuildInfoPlugin) diff --git a/ioapp-tests/src/test/scala/IOAppSpec.scala b/ioapp-tests/src/test/scala/IOAppSpec.scala index e5dd104d0d..43b86909dc 100644 --- a/ioapp-tests/src/test/scala/IOAppSpec.scala +++ b/ioapp-tests/src/test/scala/IOAppSpec.scala @@ -113,10 +113,31 @@ class IOAppSpec extends Specification { } } + object Native extends Platform("native") { + val dumpSignal = "USR1" + + def builder(proto: String, args: List[String]) = + Process(BuildInfo.nativeRunner.getAbsolutePath, s"catseffect.examples.$proto" :: args) + + def pid(proto: String): Option[Int] = { + val stdoutBuffer = new StringBuffer() + val process = + Process("ps", List("aux")).run(BasicIO(false, stdoutBuffer, None)) + process.exitValue() + + val output = stdoutBuffer.toString + Source + .fromString(output) + .getLines() + .find(l => l.contains(BuildInfo.nativeRunner.getAbsolutePath) && l.contains(proto)) + .map(_.split(" +")(1).toInt) + } + } + lazy val platform = BuildInfo.platform match { case "jvm" => JVM case "js" => Node - case "native" => ??? + case "native" => Native case platform => throw new RuntimeException(s"unknown platform $platform") } diff --git a/tests/jvm-native/src/main/scala/catseffect/examplesjvmnative.scala b/tests/jvm-native/src/main/scala/catseffect/examplesjvmnative.scala new file mode 100644 index 0000000000..6e5b4a5d49 --- /dev/null +++ b/tests/jvm-native/src/main/scala/catseffect/examplesjvmnative.scala @@ -0,0 +1,35 @@ +/* + * Copyright 2020-2023 Typelevel + * + * 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 catseffect + +import cats.effect.{ExitCode, IO, IOApp} + +import java.io.{File, FileWriter} + +package examples { + object Finalizers extends IOApp { + + def writeToFile(string: String, file: File): IO[Unit] = + IO(new FileWriter(file)).bracket { writer => IO(writer.write(string)) }(writer => + IO(writer.close())) + + def run(args: List[String]): IO[ExitCode] = + (IO(println("Started")) >> IO.never) + .onCancel(writeToFile("canceled", new File(args.head))) + .as(ExitCode.Success) + } +} diff --git a/tests/jvm/src/main/scala/catseffect/examplesplatform.scala b/tests/jvm/src/main/scala/catseffect/examplesplatform.scala index 2b68ae0348..db71eca548 100644 --- a/tests/jvm/src/main/scala/catseffect/examplesplatform.scala +++ b/tests/jvm/src/main/scala/catseffect/examplesplatform.scala @@ -22,7 +22,6 @@ import cats.syntax.all._ import scala.concurrent.ExecutionContext import scala.concurrent.duration._ -import java.io.File import java.util.concurrent.atomic.AtomicReference package object examples { @@ -51,19 +50,6 @@ package examples { } yield ExitCode.Success } - object Finalizers extends IOApp { - import java.io.FileWriter - - def writeToFile(string: String, file: File): IO[Unit] = - IO(new FileWriter(file)).bracket { writer => IO(writer.write(string)) }(writer => - IO(writer.close())) - - def run(args: List[String]): IO[ExitCode] = - (IO(println("Started")) >> IO.never) - .onCancel(writeToFile("canceled", new File(args.head))) - .as(ExitCode.Success) - } - object EvalOnMainThread extends IOApp { def run(args: List[String]): IO[ExitCode] = IO(Thread.currentThread().getId()).evalOn(MainThread) map { diff --git a/tests/native/src/main/scala/catseffect/examplesplatform.scala b/tests/native/src/main/scala/catseffect/examplesplatform.scala index 6929aa28c4..f2c8d356b8 100644 --- a/tests/native/src/main/scala/catseffect/examplesplatform.scala +++ b/tests/native/src/main/scala/catseffect/examplesplatform.scala @@ -16,8 +16,59 @@ package catseffect -import scala.concurrent.ExecutionContext +import cats.effect.{ExitCode, IO, IOApp} +import cats.effect.unsafe.IORuntime +import cats.syntax.all._ + +import scala.collection.mutable package object examples { - def exampleExecutionContext = ExecutionContext.global + def exampleExecutionContext = IORuntime.global.compute +} + +package examples { + + object NativeRunner { + val apps = mutable.Map.empty[String, () => IOApp] + def register(app: IOApp): Unit = apps(app.getClass.getName.init) = () => app + def registerLazy(name: String, app: => IOApp): Unit = + apps(name) = () => app + + val rawApps = mutable.Map.empty[String, () => RawApp] + def registerRaw(app: RawApp): Unit = rawApps(app.getClass.getName.init) = () => app + + register(HelloWorld) + register(Arguments) + register(NonFatalError) + register(FatalError) + registerRaw(FatalErrorRaw) + register(Canceled) + registerLazy("catseffect.examples.GlobalRacingInit", GlobalRacingInit) + registerLazy("catseffect.examples.GlobalShutdown", GlobalShutdown) + register(LiveFiberSnapshot) + register(FatalErrorUnsafeRun) + register(Finalizers) + register(LeakedFiber) + register(CustomRuntime) + register(CpuStarvation) + + def main(args: Array[String]): Unit = { + val app = args(0) + apps + .get(app) + .map(_().main(args.tail)) + .orElse(rawApps.get(app).map(_().main(args.tail))) + .get + } + } + + object FatalErrorUnsafeRun extends IOApp { + def run(args: List[String]): IO[ExitCode] = + for { + _ <- (0 until 100).toList.traverse(_ => IO.never.start) + _ <- IO(throw new OutOfMemoryError("Boom!")).start + _ <- IO.never[Unit] + } yield ExitCode.Success + } + } From 5ff59a65a719b70fd6d04c3865d6a6e0c7388fd6 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 16 Jun 2023 09:09:03 +0000 Subject: [PATCH 120/429] Fix example ec --- tests/native/src/main/scala/catseffect/examplesplatform.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/native/src/main/scala/catseffect/examplesplatform.scala b/tests/native/src/main/scala/catseffect/examplesplatform.scala index f2c8d356b8..c74289db06 100644 --- a/tests/native/src/main/scala/catseffect/examplesplatform.scala +++ b/tests/native/src/main/scala/catseffect/examplesplatform.scala @@ -23,7 +23,7 @@ import cats.syntax.all._ import scala.collection.mutable package object examples { - def exampleExecutionContext = IORuntime.global.compute + def exampleExecutionContext = IORuntime.defaultComputeExecutionContext } package examples { From 24c98d9e1abef1c93eabd9ffdbdf10dcb8584f8d Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 16 Jun 2023 09:11:23 +0000 Subject: [PATCH 121/429] Disable shutdown hook test on Native --- ioapp-tests/src/test/scala/IOAppSpec.scala | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ioapp-tests/src/test/scala/IOAppSpec.scala b/ioapp-tests/src/test/scala/IOAppSpec.scala index 43b86909dc..4ce97562b4 100644 --- a/ioapp-tests/src/test/scala/IOAppSpec.scala +++ b/ioapp-tests/src/test/scala/IOAppSpec.scala @@ -224,10 +224,11 @@ class IOAppSpec extends Specification { h.awaitStatus() mustEqual 0 } - "abort awaiting shutdown hooks" in { - val h = platform("ShutdownHookImmediateTimeout", List.empty) - h.awaitStatus() mustEqual 0 - } + if (platform != Native) + "abort awaiting shutdown hooks" in { + val h = platform("ShutdownHookImmediateTimeout", List.empty) + h.awaitStatus() mustEqual 0 + } } if (!isWindows) { From c647d57431d55d310cd0078ea18c0a9913b77fb8 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 16 Jun 2023 20:59:42 +0000 Subject: [PATCH 122/429] Working `SIG{INT,TERM}` cancelation --- .../cats/effect/FileDescriptorPoller.scala | 11 ++ .../src/main/scala/cats/effect/IOApp.scala | 34 ++++- .../src/main/scala/cats/effect/Signal.scala | 125 ++++++++++++++++++ .../cats/effect/unsafe/EpollSystem.scala | 3 +- .../cats/effect/unsafe/KqueueSystem.scala | 3 +- .../effect/FileDescriptorPollerSpec.scala | 5 +- 6 files changed, 168 insertions(+), 13 deletions(-) create mode 100644 core/native/src/main/scala/cats/effect/Signal.scala diff --git a/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala b/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala index e5e1a13af1..28deff9bbf 100644 --- a/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala +++ b/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala @@ -16,6 +16,8 @@ package cats.effect +import cats.syntax.all._ + trait FileDescriptorPoller { /** @@ -29,6 +31,15 @@ trait FileDescriptorPoller { } +object FileDescriptorPoller { + def find: IO[Option[FileDescriptorPoller]] = + IO.pollers.map(_.collectFirst { case poller: FileDescriptorPoller => poller }) + + def get = find.flatMap( + _.liftTo[IO](new RuntimeException("No FileDescriptorPoller installed in this IORuntime")) + ) +} + trait FileDescriptorPollHandle { /** diff --git a/core/native/src/main/scala/cats/effect/IOApp.scala b/core/native/src/main/scala/cats/effect/IOApp.scala index 2c7b19f4b9..7239f2c59c 100644 --- a/core/native/src/main/scala/cats/effect/IOApp.scala +++ b/core/native/src/main/scala/cats/effect/IOApp.scala @@ -20,6 +20,7 @@ import cats.effect.metrics.{CpuStarvationWarningMetrics, NativeCpuStarvationMetr import scala.concurrent.CancellationException import scala.concurrent.duration._ +import scala.scalanative.meta.LinktimeInfo._ /** * The primary entry point to a Cats Effect application. Extend this trait rather than defining @@ -239,20 +240,39 @@ trait IOApp { lazy val keepAlive: IO[Nothing] = IO.sleep(1.hour) >> keepAlive + val awaitInterruptOrStayAlive = + if (isLinux || isMac) + FileDescriptorPoller.find.flatMap { + case Some(poller) => + val interruptOrTerm = + Signal + .awaitInterrupt(poller) + .as(ExitCode(130)) + .race(Signal.awaitTerm(poller).as(ExitCode(143))) + + def hardExit(code: ExitCode) = + IO.sleep(runtime.config.shutdownHookTimeout) *> IO(System.exit(code.code)) + + interruptOrTerm.map(_.merge).flatTap(hardExit(_).start) + case _ => keepAlive + } + else + keepAlive + Spawn[IO] - .raceOutcome[ExitCode, Nothing]( + .raceOutcome[ExitCode, ExitCode]( CpuStarvationCheck .run(runtimeConfig, NativeCpuStarvationMetrics(), onCpuStarvationWarn) .background .surround(run(args.toList)), - keepAlive) + awaitInterruptOrStayAlive + ) + .map(_.merge) .flatMap { - case Left(Outcome.Canceled()) => + case Outcome.Canceled() => IO.raiseError(new CancellationException("IOApp main fiber was canceled")) - case Left(Outcome.Errored(t)) => IO.raiseError(t) - case Left(Outcome.Succeeded(code)) => code - case Right(Outcome.Errored(t)) => IO.raiseError(t) - case Right(_) => sys.error("impossible") + case Outcome.Errored(t) => IO.raiseError(t) + case Outcome.Succeeded(code) => code } .unsafeRunFiber( System.exit(0), diff --git a/core/native/src/main/scala/cats/effect/Signal.scala b/core/native/src/main/scala/cats/effect/Signal.scala new file mode 100644 index 0000000000..4a5a32b414 --- /dev/null +++ b/core/native/src/main/scala/cats/effect/Signal.scala @@ -0,0 +1,125 @@ +/* + * Copyright 2020-2023 Typelevel + * + * 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 + +import cats.syntax.all._ + +import scala.scalanative.libc.errno._ +import scala.scalanative.meta.LinktimeInfo._ +import scala.scalanative.posix.errno._ +import scala.scalanative.posix.fcntl._ +import scala.scalanative.posix.signal._ +import scala.scalanative.posix.signalOps._ +import scala.scalanative.posix.string._ +import scala.scalanative.posix.unistd._ +import scala.scalanative.unsafe._ +import scala.scalanative.unsigned._ + +import java.io.IOException + +private object Signal { + + private[this] def mkPipe() = if (isLinux || isMac) { + val fd = stackalloc[CInt](2) + if (pipe(fd) != 0) + throw new IOException(fromCString(strerror(errno))) + + val readFd = fd(0) + val writeFd = fd(1) + + if (fcntl(readFd, F_SETFL, O_NONBLOCK) != 0) + throw new IOException(fromCString(strerror(errno))) + + if (fcntl(writeFd, F_SETFL, O_NONBLOCK) != 0) + throw new IOException(fromCString(strerror(errno))) + + Array(readFd, writeFd) + } else Array(0, 0) + + private[this] val interruptFds = mkPipe() + private[this] val interruptReadFd = interruptFds(0) + private[this] val interruptWriteFd = interruptFds(1) + + private[this] def onInterrupt(signum: CInt): Unit = { + val _ = signum + val buf = stackalloc[Byte]() + write(interruptWriteFd, buf, 1.toULong) + () + } + + private[this] val termFds = mkPipe() + private[this] val termReadFd = termFds(0) + private[this] val termWriteFd = termFds(1) + + private[this] def onTerm(signum: CInt): Unit = { + val _ = signum + val buf = stackalloc[Byte]() + write(termWriteFd, buf, 1.toULong) + () + } + + private[this] val dumpFds = mkPipe() + private[this] val dumpReadFd = dumpFds(0) + private[this] val dumpWriteFd = dumpFds(1) + + private[this] def onDump(signum: CInt): Unit = { + val _ = signum + val buf = stackalloc[Byte]() + write(dumpWriteFd, buf, 1.toULong) + () + } + + private[this] def installHandler(signum: CInt, handler: CFuncPtr1[CInt, Unit]): Unit = { + val action = stackalloc[sigaction]() + action.sa_handler = handler + sigaddset(action.at2, 13) // mask SIGPIPE + if (sigaction(signum, action, null) != 0) + throw new IOException(fromCString(strerror(errno))) + } + + private[this] final val SIGINT = 2 + private[this] final val SIGTERM = 15 + private[this] final val SIGUSR1 = 10 + private[this] final val SIGINFO = 29 + + if (isLinux || isMac) { + installHandler(SIGINT, onInterrupt(_)) + installHandler(SIGTERM, onTerm(_)) + installHandler(if (isMac) SIGINFO else SIGUSR1, onDump(_)) + } + + def awaitInterrupt(poller: FileDescriptorPoller): IO[Unit] = + registerAndAwaitSignal(poller, interruptReadFd) + + def awaitTerm(poller: FileDescriptorPoller): IO[Unit] = + registerAndAwaitSignal(poller, termReadFd) + + private[this] def registerAndAwaitSignal(poller: FileDescriptorPoller, fd: Int): IO[Unit] = + poller.registerFileDescriptor(fd, true, false).use(awaitSignal(_, fd)) + + private[this] def awaitSignal(handle: FileDescriptorPollHandle, fd: Int): IO[Unit] = + handle.pollReadRec(()) { _ => + IO { + val buf = stackalloc[Byte]() + val rtn = read(fd, buf, 1.toULong) + if (rtn >= 0) Either.unit + else if (errno == EAGAIN) Left(()) + else throw new IOException(fromCString(strerror(errno))) + } + } + +} diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index 2f5fb13d25..37539dfd83 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -26,6 +26,7 @@ import scala.annotation.tailrec import scala.scalanative.annotation.alwaysinline import scala.scalanative.libc.errno._ import scala.scalanative.meta.LinktimeInfo +import scala.scalanative.posix.errno._ import scala.scalanative.posix.string._ import scala.scalanative.posix.unistd import scala.scalanative.runtime._ @@ -202,7 +203,7 @@ object EpollSystem extends PollingSystem { handle.notify(event.events.toInt) i += 1 } - } else { + } else if (errno != EINTR) { // spurious wake-up by signal throw new IOException(fromCString(strerror(errno))) } diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index da933e3e22..9e79e101ab 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -25,6 +25,7 @@ import org.typelevel.scalaccompat.annotation._ import scala.annotation.tailrec import scala.collection.mutable.LongMap import scala.scalanative.libc.errno._ +import scala.scalanative.posix.errno._ import scala.scalanative.posix.string._ import scala.scalanative.posix.time._ import scala.scalanative.posix.timeOps._ @@ -205,7 +206,7 @@ object KqueueSystem extends PollingSystem { i += 1 event += 1 } - } else { + } else if (errno != EINTR) { // spurious wake-up by signal throw new IOException(fromCString(strerror(errno))) } diff --git a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala index 06a8084a28..e2027a4619 100644 --- a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala +++ b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala @@ -63,9 +63,6 @@ class FileDescriptorPollerSpec extends BaseSpec { } } - def getFdPoller: IO[FileDescriptorPoller] = - IO.pollers.map(_.collectFirst { case poller: FileDescriptorPoller => poller }).map(_.get) - def mkPipe: Resource[IO, Pipe] = Resource .make { @@ -94,7 +91,7 @@ class FileDescriptorPollerSpec extends BaseSpec { } .flatMap { case (readFd, writeFd) => - Resource.eval(getFdPoller).flatMap { poller => + Resource.eval(FileDescriptorPoller.get).flatMap { poller => ( poller.registerFileDescriptor(readFd, true, false), poller.registerFileDescriptor(writeFd, false, true) From 5c0b77d0e684b921f520564f725e4faad70587e1 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 16 Jun 2023 21:43:53 +0000 Subject: [PATCH 123/429] Fiber dumps on Native --- .../cats/effect/unsafe/FiberMonitor.scala | 13 +- .../FiberMonitorCompanionPlatform.scala | 32 ----- .../src/main/scala/cats/effect/IOApp.scala | 22 +++- .../src/main/scala/cats/effect/Signal.scala | 5 + .../unsafe/EventLoopExecutorScheduler.scala | 9 ++ .../cats/effect/unsafe/FiberMonitor.scala | 113 ++++++++++++++++++ .../FiberMonitorCompanionPlatform.scala | 26 ---- 7 files changed, 156 insertions(+), 64 deletions(-) rename core/{jvm-native => jvm}/src/main/scala/cats/effect/unsafe/FiberMonitor.scala (95%) delete mode 100644 core/jvm/src/main/scala/cats/effect/unsafe/FiberMonitorCompanionPlatform.scala create mode 100644 core/native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala delete mode 100644 core/native/src/main/scala/cats/effect/unsafe/FiberMonitorCompanionPlatform.scala diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala b/core/jvm/src/main/scala/cats/effect/unsafe/FiberMonitor.scala similarity index 95% rename from core/jvm-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala rename to core/jvm/src/main/scala/cats/effect/unsafe/FiberMonitor.scala index d994a3a13a..c43ec462f3 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/FiberMonitor.scala @@ -20,6 +20,8 @@ package unsafe import cats.effect.tracing.TracingConstants import cats.effect.unsafe.ref.WeakReference +import scala.concurrent.ExecutionContext + import java.util.concurrent.ConcurrentLinkedQueue /** @@ -219,4 +221,13 @@ private[effect] final class NoOpFiberMonitor extends FiberMonitor(null) { override def liveFiberSnapshot(print: String => Unit): Unit = {} } -private[effect] object FiberMonitor extends FiberMonitorCompanionPlatform +private[effect] object FiberMonitor { + def apply(compute: ExecutionContext): FiberMonitor = { + if (TracingConstants.isStackTracing && compute.isInstanceOf[WorkStealingThreadPool[_]]) { + val wstp = compute.asInstanceOf[WorkStealingThreadPool[_]] + new FiberMonitor(wstp) + } else { + new FiberMonitor(null) + } + } +} diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/FiberMonitorCompanionPlatform.scala b/core/jvm/src/main/scala/cats/effect/unsafe/FiberMonitorCompanionPlatform.scala deleted file mode 100644 index ea910919bc..0000000000 --- a/core/jvm/src/main/scala/cats/effect/unsafe/FiberMonitorCompanionPlatform.scala +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2020-2023 Typelevel - * - * 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.unsafe - -import cats.effect.tracing.TracingConstants - -import scala.concurrent.ExecutionContext - -private[unsafe] trait FiberMonitorCompanionPlatform { - def apply(compute: ExecutionContext): FiberMonitor = { - if (TracingConstants.isStackTracing && compute.isInstanceOf[WorkStealingThreadPool[_]]) { - val wstp = compute.asInstanceOf[WorkStealingThreadPool[_]] - new FiberMonitor(wstp) - } else { - new FiberMonitor(null) - } - } -} diff --git a/core/native/src/main/scala/cats/effect/IOApp.scala b/core/native/src/main/scala/cats/effect/IOApp.scala index 7239f2c59c..6268cf9b33 100644 --- a/core/native/src/main/scala/cats/effect/IOApp.scala +++ b/core/native/src/main/scala/cats/effect/IOApp.scala @@ -17,6 +17,7 @@ package cats.effect import cats.effect.metrics.{CpuStarvationWarningMetrics, NativeCpuStarvationMetrics} +import cats.syntax.all._ import scala.concurrent.CancellationException import scala.concurrent.duration._ @@ -254,17 +255,28 @@ trait IOApp { IO.sleep(runtime.config.shutdownHookTimeout) *> IO(System.exit(code.code)) interruptOrTerm.map(_.merge).flatTap(hardExit(_).start) - case _ => keepAlive + case None => keepAlive } else keepAlive + val fiberDumper = + if (isLinux || isMac) + FileDescriptorPoller.find.toResource.flatMap { + case Some(poller) => + val dump = IO.blocking(runtime.fiberMonitor.liveFiberSnapshot(System.err.print(_))) + Signal.foreachDump(poller, dump).background.void + case None => Resource.unit[IO] + } + else Resource.unit[IO] + + val starvationChecker = CpuStarvationCheck + .run(runtimeConfig, NativeCpuStarvationMetrics(), onCpuStarvationWarn) + .background + Spawn[IO] .raceOutcome[ExitCode, ExitCode]( - CpuStarvationCheck - .run(runtimeConfig, NativeCpuStarvationMetrics(), onCpuStarvationWarn) - .background - .surround(run(args.toList)), + (fiberDumper *> starvationChecker).surround(run(args.toList)), awaitInterruptOrStayAlive ) .map(_.merge) diff --git a/core/native/src/main/scala/cats/effect/Signal.scala b/core/native/src/main/scala/cats/effect/Signal.scala index 4a5a32b414..b93f4d6190 100644 --- a/core/native/src/main/scala/cats/effect/Signal.scala +++ b/core/native/src/main/scala/cats/effect/Signal.scala @@ -108,6 +108,11 @@ private object Signal { def awaitTerm(poller: FileDescriptorPoller): IO[Unit] = registerAndAwaitSignal(poller, termReadFd) + def foreachDump(poller: FileDescriptorPoller, action: IO[Unit]): IO[Nothing] = + poller.registerFileDescriptor(dumpReadFd, true, false).use { handle => + (awaitSignal(handle, dumpReadFd) *> action).foreverM + } + private[this] def registerAndAwaitSignal(poller: FileDescriptorPoller, fd: Int): IO[Unit] = poller.registerFileDescriptor(fd, true, false).use(awaitSignal(_, fd)) diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala index 90793b9495..25408fdf9e 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala @@ -152,6 +152,15 @@ private[effect] final class EventLoopExecutorScheduler[P]( def shutdown(): Unit = system.close() + def liveTraces(): Map[IOFiber[_], Trace] = { + val builder = Map.newBuilder[IOFiber[_], Trace] + executeQueue.forEach { + case f: IOFiber[_] => builder += f -> f.captureTrace() + case _ => () + } + builder.result() + } + } private object EventLoopExecutorScheduler { diff --git a/core/native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala b/core/native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala new file mode 100644 index 0000000000..b164829a03 --- /dev/null +++ b/core/native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala @@ -0,0 +1,113 @@ +/* + * Copyright 2020-2023 Typelevel + * + * 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 +package unsafe + +import scala.collection.mutable +import scala.concurrent.ExecutionContext +import scala.scalanative.meta.LinktimeInfo + +private[effect] sealed abstract class FiberMonitor extends FiberMonitorShared { + + /** + * Registers a suspended fiber. + * + * @param fiber + * the suspended fiber to be registered + * @return + * a handle for deregistering the fiber on resumption + */ + def monitorSuspended(fiber: IOFiber[_]): WeakBag.Handle + + /** + * Obtains a snapshot of the fibers currently live on the [[IORuntime]] which this fiber + * monitor instance belongs to. + * + * @return + * a textual representation of the runtime snapshot, `None` if a snapshot cannot be obtained + */ + def liveFiberSnapshot(print: String => Unit): Unit +} + +private final class FiberMonitorImpl( + // A reference to the compute pool of the `IORuntime` in which this suspended fiber bag + // operates. `null` if the compute pool of the `IORuntime` is not a `EventLoopExecutorScheduler`. + private[this] val compute: EventLoopExecutorScheduler[_] +) extends FiberMonitor { + private[this] val bag = new WeakBag[IOFiber[_]]() + + override def monitorSuspended(fiber: IOFiber[_]): WeakBag.Handle = + bag.insert(fiber) + + def foreignTraces(): Map[IOFiber[_], Trace] = { + val foreign = mutable.Map.empty[IOFiber[Any], Trace] + bag.forEach(fiber => + if (!fiber.isDone) foreign += (fiber.asInstanceOf[IOFiber[Any]] -> fiber.captureTrace())) + foreign.toMap + } + + def liveFiberSnapshot(print: String => Unit): Unit = + Option(compute).foreach { compute => + val queued = compute.liveTraces() + val rawForeign = foreignTraces() + + // We trust the sources of data in the following order, ordered from + // most trustworthy to least trustworthy. + // 1. Fibers from the macrotask executor + // 2. Fibers from the foreign fallback weak GC map + + val allForeign = rawForeign -- queued.keys + val (suspended, foreign) = allForeign.partition { case (f, _) => f.get() } + + printFibers(queued, "YIELDING")(print) + printFibers(foreign, "YIELDING")(print) + printFibers(suspended, "WAITING")(print) + + val globalStatus = + s"Global: enqueued ${queued.size + foreign.size}, waiting ${suspended.size}" + + print(doubleNewline) + print(globalStatus) + print(newline) + } + +} + +/** + * A no-op implementation of an unordered bag used for tracking asynchronously suspended fiber + * instances on Scala.js. This is used as a fallback. + */ +private final class NoOpFiberMonitor extends FiberMonitor { + override def monitorSuspended(fiber: IOFiber[_]): WeakBag.Handle = () => () + def liveFiberSnapshot(print: String => Unit): Unit = () +} + +private[effect] object FiberMonitor { + def apply(compute: ExecutionContext): FiberMonitor = { + if (LinktimeInfo.debugMode && LinktimeInfo.isWeakReferenceSupported) { + if (compute.isInstanceOf[EventLoopExecutorScheduler[_]]) { + val loop = compute.asInstanceOf[EventLoopExecutorScheduler[_]] + new FiberMonitorImpl(loop) + } else { + new FiberMonitorImpl(null) + } + } else { + new NoOpFiberMonitor() + } + } + +} diff --git a/core/native/src/main/scala/cats/effect/unsafe/FiberMonitorCompanionPlatform.scala b/core/native/src/main/scala/cats/effect/unsafe/FiberMonitorCompanionPlatform.scala deleted file mode 100644 index 47628974ac..0000000000 --- a/core/native/src/main/scala/cats/effect/unsafe/FiberMonitorCompanionPlatform.scala +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2020-2023 Typelevel - * - * 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.unsafe - -import scala.concurrent.ExecutionContext - -private[unsafe] trait FiberMonitorCompanionPlatform { - def apply(compute: ExecutionContext): FiberMonitor = { - val _ = compute - new NoOpFiberMonitor - } -} From 130113bf8b2e28e45b86e466cba78006d0663298 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 16 Jun 2023 21:53:09 +0000 Subject: [PATCH 124/429] Regenerate workflow --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03537e2711..791ec8efc5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -287,12 +287,12 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) shell: bash - run: mkdir -p benchmarks/target testkit/native/target target stress-tests/target std/jvm/target example/js/target kernel-testkit/jvm/target testkit/js/target rootJS/target core/native/target site-docs/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target graalvm-example/target kernel-testkit/js/target core/jvm/target rootJVM/target rootNative/target example/native/target kernel/native/target example/jvm/target laws/jvm/target std/native/target testkit/jvm/target project/target + run: mkdir -p benchmarks/target testkit/native/target target stress-tests/target std/jvm/target example/js/target kernel-testkit/jvm/target testkit/js/target rootJS/target core/native/target site-docs/target ioapp-tests/.native/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target graalvm-example/target kernel-testkit/js/target core/jvm/target rootJVM/target rootNative/target example/native/target kernel/native/target example/jvm/target laws/jvm/target std/native/target ioapp-tests/.jvm/target testkit/jvm/target ioapp-tests/.js/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) shell: bash - run: tar cf targets.tar benchmarks/target testkit/native/target target stress-tests/target std/jvm/target example/js/target kernel-testkit/jvm/target testkit/js/target rootJS/target core/native/target site-docs/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target graalvm-example/target kernel-testkit/js/target core/jvm/target rootJVM/target rootNative/target example/native/target kernel/native/target example/jvm/target laws/jvm/target std/native/target testkit/jvm/target project/target + run: tar cf targets.tar benchmarks/target testkit/native/target target stress-tests/target std/jvm/target example/js/target kernel-testkit/jvm/target testkit/js/target rootJS/target core/native/target site-docs/target ioapp-tests/.native/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target graalvm-example/target kernel-testkit/js/target core/jvm/target rootJVM/target rootNative/target example/native/target kernel/native/target example/jvm/target laws/jvm/target std/native/target ioapp-tests/.jvm/target testkit/jvm/target ioapp-tests/.js/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) From 6d136bac199e42dd06a7295a1e220dfb8e856e46 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 16 Jun 2023 22:09:13 +0000 Subject: [PATCH 125/429] MiMa filter --- build.sbt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 2e22d7b01a..3c8b817b4d 100644 --- a/build.sbt +++ b/build.sbt @@ -647,7 +647,10 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) "cats.effect.IOFiberConstants.ContStateResult"), // introduced by #3332, polling system ProblemFilters.exclude[DirectMissingMethodProblem]( - "cats.effect.unsafe.IORuntimeBuilder.this") + "cats.effect.unsafe.IORuntimeBuilder.this"), + // introduced by #3695, which enabled fiber dumps on native + ProblemFilters.exclude[MissingClassProblem]( + "cats.effect.unsafe.FiberMonitorCompanionPlatform") ) ++ { if (tlIsScala3.value) { // Scala 3 specific exclusions From 4e7b0397b4757803327f580fe4f6149b9fdf5ae5 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 16 Jun 2023 15:32:33 -0700 Subject: [PATCH 126/429] macOS also has `SIGUSR1` --- core/native/src/main/scala/cats/effect/Signal.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/Signal.scala b/core/native/src/main/scala/cats/effect/Signal.scala index b93f4d6190..2ebd683b2d 100644 --- a/core/native/src/main/scala/cats/effect/Signal.scala +++ b/core/native/src/main/scala/cats/effect/Signal.scala @@ -93,13 +93,16 @@ private object Signal { private[this] final val SIGINT = 2 private[this] final val SIGTERM = 15 - private[this] final val SIGUSR1 = 10 + private[this] final val SIGUSR1Linux = 10 + private[this] final val SIGUSR1Mac = 30 private[this] final val SIGINFO = 29 if (isLinux || isMac) { installHandler(SIGINT, onInterrupt(_)) installHandler(SIGTERM, onTerm(_)) - installHandler(if (isMac) SIGINFO else SIGUSR1, onDump(_)) + if (isLinux) installHandler(SIGUSR1Linux, onDump(_)) + if (isMac) installHandler(SIGUSR1Mac, onDump(_)) + if (isMac) installHandler(SIGINFO, onDump(_)) } def awaitInterrupt(poller: FileDescriptorPoller): IO[Unit] = From 2170e98313ee430e58a8648c9b636362d38acd81 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 17 Jun 2023 00:00:37 +0000 Subject: [PATCH 127/429] Less iterations for mutex test --- tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala b/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala index 8748af8b8e..75c7148d35 100644 --- a/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala @@ -124,7 +124,10 @@ final class MutexSpec extends BaseSpec with DetectPlatform { } "not deadlock when highly contended" in real { - mutex.flatMap(_.lock.use_.parReplicateA_(10)).replicateA_(10000).as(true) + mutex + .flatMap(_.lock.use_.parReplicateA_(10)) + .replicateA_(if (isJVM) 10000 else 100) + .as(true) } "handle cancelled acquire" in real { From ee15a238eeee9b6c5ab6b6f1d6f7856ae7dfb989 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 17 Jun 2023 05:01:12 +0000 Subject: [PATCH 128/429] Revert "Less iterations for mutex test" This reverts commit 2170e98313ee430e58a8648c9b636362d38acd81. --- tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala b/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala index 75c7148d35..8748af8b8e 100644 --- a/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala @@ -124,10 +124,7 @@ final class MutexSpec extends BaseSpec with DetectPlatform { } "not deadlock when highly contended" in real { - mutex - .flatMap(_.lock.use_.parReplicateA_(10)) - .replicateA_(if (isJVM) 10000 else 100) - .as(true) + mutex.flatMap(_.lock.use_.parReplicateA_(10)).replicateA_(10000).as(true) } "handle cancelled acquire" in real { From 2ee3b763e34cd8542f4116978465b260e99f24c1 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 17 Jun 2023 05:11:07 +0000 Subject: [PATCH 129/429] Disable fiber dumps on native for now --- .../src/main/scala/cats/effect/unsafe/FiberMonitor.scala | 5 ++--- ioapp-tests/src/test/scala/IOAppSpec.scala | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala b/core/native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala index b164829a03..f6b00c771e 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala @@ -19,7 +19,6 @@ package unsafe import scala.collection.mutable import scala.concurrent.ExecutionContext -import scala.scalanative.meta.LinktimeInfo private[effect] sealed abstract class FiberMonitor extends FiberMonitorShared { @@ -89,7 +88,7 @@ private final class FiberMonitorImpl( /** * A no-op implementation of an unordered bag used for tracking asynchronously suspended fiber - * instances on Scala.js. This is used as a fallback. + * instances on Scala Native. This is used as a fallback. */ private final class NoOpFiberMonitor extends FiberMonitor { override def monitorSuspended(fiber: IOFiber[_]): WeakBag.Handle = () => () @@ -98,7 +97,7 @@ private final class NoOpFiberMonitor extends FiberMonitor { private[effect] object FiberMonitor { def apply(compute: ExecutionContext): FiberMonitor = { - if (LinktimeInfo.debugMode && LinktimeInfo.isWeakReferenceSupported) { + if (false) { // LinktimeInfo.debugMode && LinktimeInfo.isWeakReferenceSupported if (compute.isInstanceOf[EventLoopExecutorScheduler[_]]) { val loop = compute.asInstanceOf[EventLoopExecutorScheduler[_]] new FiberMonitorImpl(loop) diff --git a/ioapp-tests/src/test/scala/IOAppSpec.scala b/ioapp-tests/src/test/scala/IOAppSpec.scala index 4ce97562b4..cbd4a0e5da 100644 --- a/ioapp-tests/src/test/scala/IOAppSpec.scala +++ b/ioapp-tests/src/test/scala/IOAppSpec.scala @@ -284,7 +284,7 @@ class IOAppSpec extends Specification { h.awaitStatus() mustEqual 1 } - if (!isJava8 && !isWindows) { + if (!isJava8 && !isWindows && platform != Native) { // JDK 8 does not have free signals for live fiber snapshots // cannot observe signals sent to process termination on Windows "live fiber snapshot" in { From fa8245f3ffa1692be97c2617821e9435f99e00a2 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 17 Jun 2023 05:32:10 +0000 Subject: [PATCH 130/429] Run `IOApp` tests in Cirrus --- .cirrus.yml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index e85323d54c..067ede2b9d 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -5,11 +5,11 @@ jvm_highcore_task: memory: 8G matrix: - name: JVM high-core-count 2.12 - script: sbt '++ 2.12' testsJVM/test + script: sbt '++ 2.12' testsJVM/test ioAppTestsJVM/test - name: JVM high-core-count 2.13 - script: sbt '++ 2.13' testsJVM/test stressTests/Jcstress/run + script: sbt '++ 2.13' testsJVM/test ioAppTestsJVM/test stressTests/Jcstress/run - name: JVM high-core-count 3 - script: sbt '++ 3' testsJVM/test + script: sbt '++ 3' testsJVM/test ioAppTestsJVM/test jvm_arm_highcore_task: arm_container: @@ -18,11 +18,11 @@ jvm_arm_highcore_task: memory: 8G matrix: - name: JVM ARM high-core-count 2.12 - script: sbt '++ 2.12' testsJVM/test + script: sbt '++ 2.12' testsJVM/test ioAppTestsJVM/test - name: JVM ARM high-core-count 2.13 - script: sbt '++ 2.13' testsJVM/test stressTests/Jcstress/run + script: sbt '++ 2.13' testsJVM/test ioAppTestsJVM/test stressTests/Jcstress/run - name: JVM ARM high-core-count 3 - script: sbt '++ 3' testsJVM/test + script: sbt '++ 3' testsJVM/test ioAppTestsJVM/test jvm_macos_highcore_task: macos_instance: @@ -33,15 +33,15 @@ jvm_macos_highcore_task: - name: JVM Apple Silicon high-core-count 2.12 script: - brew install sbt - - sbt '++ 2.12' testsJVM/test + - sbt '++ 2.12' testsJVM/test ioAppTestsJVM/test - name: JVM Apple Silicon high-core-count 2.13 script: - brew install sbt - - sbt '++ 2.13' testsJVM/test stressTests/Jcstress/run + - sbt '++ 2.13' testsJVM/test ioAppTestsJVM/test stressTests/Jcstress/run - name: JVM Apple Silicon high-core-count 3 script: - brew install sbt - - sbt '++ 3' testsJVM/test + - sbt '++ 3' testsJVM/test ioAppTestsJVM/test native_arm_task: arm_container: @@ -50,11 +50,11 @@ native_arm_task: memory: 8G matrix: - name: Native ARM 2.12 - script: sbt '++ 2.12' testsNative/test + script: sbt '++ 2.12' testsNative/test ioAppTestsNative/test - name: Native ARM 2.13 - script: sbt '++ 2.13' testsNative/test + script: sbt '++ 2.13' testsNative/test ioAppTestsNative/test - name: Native ARM 3 - script: sbt '++ 3' testsNative/test + script: sbt '++ 3' testsNative/test ioAppTestsNative/test native_macos_task: macos_instance: @@ -65,12 +65,12 @@ native_macos_task: - name: Native Apple Silicon 2.12 script: - brew install sbt - - sbt '++ 2.12' testsNative/test + - sbt '++ 2.12' testsNative/test ioAppTestsNative/test - name: Native Apple Silicon 2.13 script: - brew install sbt - - sbt '++ 2.13' testsNative/test + - sbt '++ 2.13' testsNative/test ioAppTestsNative/test - name: Native Apple Silicon 3 script: - brew install sbt - - sbt '++ 3' testsNative/test + - sbt '++ 3' testsNative/test ioAppTestsNative/test From 115b063fafe0da3ebd427100a79c4b939ef7a787 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 19 Jun 2023 05:42:28 +0000 Subject: [PATCH 131/429] Update docker images --- .cirrus.yml | 4 ++-- .cirrus/Dockerfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index e85323d54c..2824e8b9c4 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,6 +1,6 @@ jvm_highcore_task: container: - image: sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.8.2_3.2.2 + image: sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.0_3.3.0 cpu: 4 memory: 8G matrix: @@ -13,7 +13,7 @@ jvm_highcore_task: jvm_arm_highcore_task: arm_container: - image: sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.8.2_3.2.2 + image: sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.0_3.3.0 cpu: 4 memory: 8G matrix: diff --git a/.cirrus/Dockerfile b/.cirrus/Dockerfile index 72c0c40936..c28b541b7d 100644 --- a/.cirrus/Dockerfile +++ b/.cirrus/Dockerfile @@ -1,3 +1,3 @@ -FROM sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.8.2_3.2.2 +FROM sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.0_3.3.0 RUN apt-get update && apt-get install -y clang From 8636c4f5a893282a7a26bed6cb30327bb919254e Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 19 Jun 2023 18:22:22 +0000 Subject: [PATCH 132/429] Use our own dockerfile --- .cirrus.yml | 4 ++-- .cirrus/Dockerfile | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 2824e8b9c4..a35e31c451 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,6 +1,6 @@ jvm_highcore_task: container: - image: sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.0_3.3.0 + dockerfile: .cirrus/Dockerfile cpu: 4 memory: 8G matrix: @@ -13,7 +13,7 @@ jvm_highcore_task: jvm_arm_highcore_task: arm_container: - image: sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.0_3.3.0 + dockerfile: .cirrus/Dockerfile cpu: 4 memory: 8G matrix: diff --git a/.cirrus/Dockerfile b/.cirrus/Dockerfile index c28b541b7d..9baa5917e8 100644 --- a/.cirrus/Dockerfile +++ b/.cirrus/Dockerfile @@ -1,3 +1,4 @@ -FROM sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.0_3.3.0 - +FROM eclipse-temurin:17 RUN apt-get update && apt-get install -y clang +RUN wget -q https://github.com/sbt/sbt/releases/download/v1.9.0/sbt-1.9.0.tgz && tar xvfz sbt-1.9.0.tgz +ENV PATH="$PATH:/sbt/bin" From 50621fd7828228ee5f3902f25af03495bfaee638 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 22 Jun 2023 05:40:50 +0000 Subject: [PATCH 133/429] Revert "Use our own dockerfile" This reverts commit 8636c4f5a893282a7a26bed6cb30327bb919254e. --- .cirrus.yml | 4 ++-- .cirrus/Dockerfile | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index a35e31c451..2824e8b9c4 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,6 +1,6 @@ jvm_highcore_task: container: - dockerfile: .cirrus/Dockerfile + image: sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.0_3.3.0 cpu: 4 memory: 8G matrix: @@ -13,7 +13,7 @@ jvm_highcore_task: jvm_arm_highcore_task: arm_container: - dockerfile: .cirrus/Dockerfile + image: sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.0_3.3.0 cpu: 4 memory: 8G matrix: diff --git a/.cirrus/Dockerfile b/.cirrus/Dockerfile index 9baa5917e8..c28b541b7d 100644 --- a/.cirrus/Dockerfile +++ b/.cirrus/Dockerfile @@ -1,4 +1,3 @@ -FROM eclipse-temurin:17 +FROM sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.0_3.3.0 + RUN apt-get update && apt-get install -y clang -RUN wget -q https://github.com/sbt/sbt/releases/download/v1.9.0/sbt-1.9.0.tgz && tar xvfz sbt-1.9.0.tgz -ENV PATH="$PATH:/sbt/bin" From 708b6072844d49018c9a6de824f085d04a0779e5 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 22 Jun 2023 16:12:32 -0700 Subject: [PATCH 134/429] Disable ARM jobs for now --- .cirrus.yml | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 2824e8b9c4..768443c3b6 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -11,18 +11,18 @@ jvm_highcore_task: - name: JVM high-core-count 3 script: sbt '++ 3' testsJVM/test -jvm_arm_highcore_task: - arm_container: - image: sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.0_3.3.0 - cpu: 4 - memory: 8G - matrix: - - name: JVM ARM high-core-count 2.12 - script: sbt '++ 2.12' testsJVM/test - - name: JVM ARM high-core-count 2.13 - script: sbt '++ 2.13' testsJVM/test stressTests/Jcstress/run - - name: JVM ARM high-core-count 3 - script: sbt '++ 3' testsJVM/test +# jvm_arm_highcore_task: +# arm_container: +# image: sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.0_3.3.0 +# cpu: 4 +# memory: 8G +# matrix: +# - name: JVM ARM high-core-count 2.12 +# script: sbt '++ 2.12' testsJVM/test +# - name: JVM ARM high-core-count 2.13 +# script: sbt '++ 2.13' testsJVM/test stressTests/Jcstress/run +# - name: JVM ARM high-core-count 3 +# script: sbt '++ 3' testsJVM/test jvm_macos_highcore_task: macos_instance: @@ -43,18 +43,18 @@ jvm_macos_highcore_task: - brew install sbt - sbt '++ 3' testsJVM/test -native_arm_task: - arm_container: - dockerfile: .cirrus/Dockerfile - cpu: 2 - memory: 8G - matrix: - - name: Native ARM 2.12 - script: sbt '++ 2.12' testsNative/test - - name: Native ARM 2.13 - script: sbt '++ 2.13' testsNative/test - - name: Native ARM 3 - script: sbt '++ 3' testsNative/test +# native_arm_task: +# arm_container: +# dockerfile: .cirrus/Dockerfile +# cpu: 2 +# memory: 8G +# matrix: +# - name: Native ARM 2.12 +# script: sbt '++ 2.12' testsNative/test +# - name: Native ARM 2.13 +# script: sbt '++ 2.13' testsNative/test +# - name: Native ARM 3 +# script: sbt '++ 3' testsNative/test native_macos_task: macos_instance: From ca9463a85cba0d7576567870e0fac981173b3eb6 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Fri, 23 Jun 2023 08:06:00 +0000 Subject: [PATCH 135/429] Update sbt-scalajs, scalajs-compiler, ... to 1.13.2 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 473fb73c45..afe1e03800 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,7 +2,7 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.0-M10") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.1") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.1") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.14") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.1") From ba85e5e88d98080180232f2cb941a43a59effaf2 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 26 Jun 2023 22:39:16 +0000 Subject: [PATCH 136/429] Fix compile --- .../scala/cats/effect/unsafe/WorkStealingThreadPool.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index daf3cba18a..2921e7bf30 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -624,8 +624,8 @@ private[effect] final class WorkStealingThreadPool[P]( val back = System.nanoTime() val thread = Thread.currentThread() - if (thread.isInstanceOf[WorkerThread]) { - thread.asInstanceOf[WorkerThread].now = back + if (thread.isInstanceOf[WorkerThread[_]]) { + thread.asInstanceOf[WorkerThread[_]].now = back } back From 3b7467bd5bb546a2285b21dad70584cf9bb58389 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 29 Jun 2023 00:01:52 +0000 Subject: [PATCH 137/429] Share `FiberMonitor` between JS and Native --- build.sbt | 5 +- .../cats/effect/unsafe/FiberExecutor.scala | 25 ++++ .../cats/effect/unsafe/FiberMonitor.scala | 31 +---- .../unsafe/BatchingMacrotaskExecutor.scala | 3 +- .../unsafe/FiberAwareExecutionContext.scala | 45 ------- .../cats/effect/unsafe/FiberMonitor.scala | 124 ------------------ .../effect/unsafe/FiberMonitorPlatform.scala | 45 +++++++ .../unsafe/EventLoopExecutorScheduler.scala | 3 +- .../effect/unsafe/FiberMonitorPlatform.scala | 36 +++++ 9 files changed, 121 insertions(+), 196 deletions(-) create mode 100644 core/js-native/src/main/scala/cats/effect/unsafe/FiberExecutor.scala rename core/{native => js-native}/src/main/scala/cats/effect/unsafe/FiberMonitor.scala (78%) delete mode 100644 core/js/src/main/scala/cats/effect/unsafe/FiberAwareExecutionContext.scala delete mode 100644 core/js/src/main/scala/cats/effect/unsafe/FiberMonitor.scala create mode 100644 core/js/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala create mode 100644 core/native/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala diff --git a/build.sbt b/build.sbt index 3e4f675aa0..be14667038 100644 --- a/build.sbt +++ b/build.sbt @@ -810,7 +810,10 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) ProblemFilters.exclude[IncompatibleTemplateDefProblem]("cats.effect.CallbackStack"), // introduced by #3642, which optimized the BatchingMacrotaskExecutor ProblemFilters.exclude[MissingClassProblem]( - "cats.effect.unsafe.BatchingMacrotaskExecutor$executeBatchTaskRunnable$") + "cats.effect.unsafe.BatchingMacrotaskExecutor$executeBatchTaskRunnable$"), + // introduced by #3695, which ported fiber monitoring to Native + // internal API change + ProblemFilters.exclude[MissingClassProblem]("cats.effect.unsafe.ES2021FiberMonitor") ) }, mimaBinaryIssueFilters ++= { diff --git a/core/js-native/src/main/scala/cats/effect/unsafe/FiberExecutor.scala b/core/js-native/src/main/scala/cats/effect/unsafe/FiberExecutor.scala new file mode 100644 index 0000000000..cfe694de74 --- /dev/null +++ b/core/js-native/src/main/scala/cats/effect/unsafe/FiberExecutor.scala @@ -0,0 +1,25 @@ +/* + * Copyright 2020-2023 Typelevel + * + * 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 +package unsafe + +/** + * An introspectable executor that runs fibers. Useful for fiber dumps. + */ +private[unsafe] trait FiberExecutor { + def liveTraces(): Map[IOFiber[_], Trace] +} diff --git a/core/native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala b/core/js-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala similarity index 78% rename from core/native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala rename to core/js-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala index f6b00c771e..147ea719b4 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala +++ b/core/js-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala @@ -17,9 +17,6 @@ package cats.effect package unsafe -import scala.collection.mutable -import scala.concurrent.ExecutionContext - private[effect] sealed abstract class FiberMonitor extends FiberMonitorShared { /** @@ -44,19 +41,19 @@ private[effect] sealed abstract class FiberMonitor extends FiberMonitorShared { private final class FiberMonitorImpl( // A reference to the compute pool of the `IORuntime` in which this suspended fiber bag - // operates. `null` if the compute pool of the `IORuntime` is not a `EventLoopExecutorScheduler`. - private[this] val compute: EventLoopExecutorScheduler[_] + // operates. `null` if the compute pool of the `IORuntime` is not a `FiberExecutor`. + private[this] val compute: FiberExecutor ) extends FiberMonitor { private[this] val bag = new WeakBag[IOFiber[_]]() override def monitorSuspended(fiber: IOFiber[_]): WeakBag.Handle = bag.insert(fiber) - def foreignTraces(): Map[IOFiber[_], Trace] = { - val foreign = mutable.Map.empty[IOFiber[Any], Trace] + private[this] def foreignTraces(): Map[IOFiber[_], Trace] = { + val foreign = Map.newBuilder[IOFiber[_], Trace] bag.forEach(fiber => if (!fiber.isDone) foreign += (fiber.asInstanceOf[IOFiber[Any]] -> fiber.captureTrace())) - foreign.toMap + foreign.result() } def liveFiberSnapshot(print: String => Unit): Unit = @@ -66,7 +63,7 @@ private final class FiberMonitorImpl( // We trust the sources of data in the following order, ordered from // most trustworthy to least trustworthy. - // 1. Fibers from the macrotask executor + // 1. Fibers from the fiber executor // 2. Fibers from the foreign fallback weak GC map val allForeign = rawForeign -- queued.keys @@ -95,18 +92,4 @@ private final class NoOpFiberMonitor extends FiberMonitor { def liveFiberSnapshot(print: String => Unit): Unit = () } -private[effect] object FiberMonitor { - def apply(compute: ExecutionContext): FiberMonitor = { - if (false) { // LinktimeInfo.debugMode && LinktimeInfo.isWeakReferenceSupported - if (compute.isInstanceOf[EventLoopExecutorScheduler[_]]) { - val loop = compute.asInstanceOf[EventLoopExecutorScheduler[_]] - new FiberMonitorImpl(loop) - } else { - new FiberMonitorImpl(null) - } - } else { - new NoOpFiberMonitor() - } - } - -} +private[effect] object FiberMonitor extends FiberMonitorPlatform diff --git a/core/js/src/main/scala/cats/effect/unsafe/BatchingMacrotaskExecutor.scala b/core/js/src/main/scala/cats/effect/unsafe/BatchingMacrotaskExecutor.scala index d550d2c389..d563ef750d 100644 --- a/core/js/src/main/scala/cats/effect/unsafe/BatchingMacrotaskExecutor.scala +++ b/core/js/src/main/scala/cats/effect/unsafe/BatchingMacrotaskExecutor.scala @@ -40,7 +40,8 @@ import scala.util.control.NonFatal private[effect] final class BatchingMacrotaskExecutor( batchSize: Int, reportFailure0: Throwable => Unit -) extends ExecutionContextExecutor { +) extends ExecutionContextExecutor + with FiberExecutor { private[this] val queueMicrotask: js.Function1[js.Function0[Any], Any] = if (js.typeOf(js.Dynamic.global.queueMicrotask) == "function") diff --git a/core/js/src/main/scala/cats/effect/unsafe/FiberAwareExecutionContext.scala b/core/js/src/main/scala/cats/effect/unsafe/FiberAwareExecutionContext.scala deleted file mode 100644 index 3718b8d4c7..0000000000 --- a/core/js/src/main/scala/cats/effect/unsafe/FiberAwareExecutionContext.scala +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020-2023 Typelevel - * - * 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 -package unsafe - -import scala.collection.mutable -import scala.concurrent.ExecutionContext - -private final class FiberAwareExecutionContext(ec: ExecutionContext) extends ExecutionContext { - - def liveTraces(): Map[IOFiber[_], Trace] = - fiberBag.iterator.filterNot(_.isDone).map(f => f -> f.captureTrace()).toMap - - private[this] val fiberBag = mutable.Set.empty[IOFiber[_]] - - def execute(runnable: Runnable): Unit = runnable match { - case r: IOFiber[_] => - fiberBag += r - ec execute { () => - // We have to remove r _before_ running it, b/c it may be re-enqueued while running - // B/c JS is single-threaded, nobody can observe the bag while it is running anyway - fiberBag -= r - r.run() - } - - case r => r.run() - } - - def reportFailure(cause: Throwable): Unit = ec.reportFailure(cause) - -} diff --git a/core/js/src/main/scala/cats/effect/unsafe/FiberMonitor.scala b/core/js/src/main/scala/cats/effect/unsafe/FiberMonitor.scala deleted file mode 100644 index fe3a3bf653..0000000000 --- a/core/js/src/main/scala/cats/effect/unsafe/FiberMonitor.scala +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2020-2023 Typelevel - * - * 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 -package unsafe - -import scala.collection.mutable -import scala.concurrent.ExecutionContext -import scala.scalajs.{js, LinkingInfo} - -private[effect] sealed abstract class FiberMonitor extends FiberMonitorShared { - - /** - * Registers a suspended fiber. - * - * @param fiber - * the suspended fiber to be registered - * @return - * a handle for deregistering the fiber on resumption - */ - def monitorSuspended(fiber: IOFiber[_]): WeakBag.Handle - - /** - * Obtains a snapshot of the fibers currently live on the [[IORuntime]] which this fiber - * monitor instance belongs to. - * - * @return - * a textual representation of the runtime snapshot, `None` if a snapshot cannot be obtained - */ - def liveFiberSnapshot(print: String => Unit): Unit -} - -/** - * Relies on features *standardized* in ES2021, although already offered in many environments - */ -private final class ES2021FiberMonitor( - // A reference to the compute pool of the `IORuntime` in which this suspended fiber bag - // operates. `null` if the compute pool of the `IORuntime` is not a `BatchingMacrotaskExecutor`. - private[this] val compute: BatchingMacrotaskExecutor -) extends FiberMonitor { - private[this] val bag = new WeakBag[IOFiber[_]]() - - override def monitorSuspended(fiber: IOFiber[_]): WeakBag.Handle = - bag.insert(fiber) - - def foreignTraces(): Map[IOFiber[_], Trace] = { - val foreign = mutable.Map.empty[IOFiber[Any], Trace] - bag.forEach(fiber => - if (!fiber.isDone) foreign += (fiber.asInstanceOf[IOFiber[Any]] -> fiber.captureTrace())) - foreign.toMap - } - - def liveFiberSnapshot(print: String => Unit): Unit = - Option(compute).foreach { compute => - val queued = compute.liveTraces() - val rawForeign = foreignTraces() - - // We trust the sources of data in the following order, ordered from - // most trustworthy to least trustworthy. - // 1. Fibers from the macrotask executor - // 2. Fibers from the foreign fallback weak GC map - - val allForeign = rawForeign -- queued.keys - val (suspended, foreign) = allForeign.partition { case (f, _) => f.get() } - - printFibers(queued, "YIELDING")(print) - printFibers(foreign, "YIELDING")(print) - printFibers(suspended, "WAITING")(print) - - val globalStatus = - s"Global: enqueued ${queued.size + foreign.size}, waiting ${suspended.size}" - - print(doubleNewline) - print(globalStatus) - print(newline) - } - -} - -/** - * A no-op implementation of an unordered bag used for tracking asynchronously suspended fiber - * instances on Scala.js. This is used as a fallback. - */ -private final class NoOpFiberMonitor extends FiberMonitor { - override def monitorSuspended(fiber: IOFiber[_]): WeakBag.Handle = () => () - def liveFiberSnapshot(print: String => Unit): Unit = () -} - -private[effect] object FiberMonitor { - def apply(compute: ExecutionContext): FiberMonitor = { - if (LinkingInfo.developmentMode && weakRefsAvailable) { - if (compute.isInstanceOf[BatchingMacrotaskExecutor]) { - val bmec = compute.asInstanceOf[BatchingMacrotaskExecutor] - new ES2021FiberMonitor(bmec) - } else { - new ES2021FiberMonitor(null) - } - } else { - new NoOpFiberMonitor() - } - } - - private[this] final val Undefined = "undefined" - - /** - * Feature-tests for all the required, well, features :) - */ - private[unsafe] def weakRefsAvailable: Boolean = - js.typeOf(js.Dynamic.global.WeakRef) != Undefined && - js.typeOf(js.Dynamic.global.FinalizationRegistry) != Undefined -} diff --git a/core/js/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala b/core/js/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala new file mode 100644 index 0000000000..0e60f17760 --- /dev/null +++ b/core/js/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala @@ -0,0 +1,45 @@ +/* + * Copyright 2020-2023 Typelevel + * + * 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 +package unsafe + +import scala.concurrent.ExecutionContext +import scala.scalajs.{js, LinkingInfo} + +private[effect] abstract class FiberMonitorPlatform { + def apply(compute: ExecutionContext): FiberMonitor = { + if (LinkingInfo.developmentMode && weakRefsAvailable) { + if (compute.isInstanceOf[BatchingMacrotaskExecutor]) { + val bmec = compute.asInstanceOf[BatchingMacrotaskExecutor] + new FiberMonitorImpl(bmec) + } else { + new FiberMonitorImpl(null) + } + } else { + new NoOpFiberMonitor() + } + } + + private[this] final val Undefined = "undefined" + + /** + * Feature-tests for all the required, well, features :) + */ + private[unsafe] def weakRefsAvailable: Boolean = + js.typeOf(js.Dynamic.global.WeakRef) != Undefined && + js.typeOf(js.Dynamic.global.FinalizationRegistry) != Undefined +} diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala index 25408fdf9e..105c159ed8 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala @@ -33,7 +33,8 @@ private[effect] final class EventLoopExecutorScheduler[P]( pollEvery: Int, system: PollingSystem.WithPoller[P]) extends ExecutionContextExecutor - with Scheduler { + with Scheduler + with FiberExecutor { private[unsafe] val poller: P = system.makePoller() diff --git a/core/native/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala b/core/native/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala new file mode 100644 index 0000000000..f38fb411b7 --- /dev/null +++ b/core/native/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala @@ -0,0 +1,36 @@ +/* + * Copyright 2020-2023 Typelevel + * + * 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 +package unsafe + +import scala.concurrent.ExecutionContext + +private[effect] abstract class FiberMonitorPlatform { + def apply(compute: ExecutionContext): FiberMonitor = { + if (false) { // LinktimeInfo.debugMode && LinktimeInfo.isWeakReferenceSupported + if (compute.isInstanceOf[EventLoopExecutorScheduler[_]]) { + val loop = compute.asInstanceOf[EventLoopExecutorScheduler[_]] + new FiberMonitorImpl(loop) + } else { + new FiberMonitorImpl(null) + } + } else { + new NoOpFiberMonitor() + } + } + +} From 3c3f980b99aa2d9a5d07e2eae46bc720cec081a4 Mon Sep 17 00:00:00 2001 From: "a.muminov" Date: Mon, 3 Jul 2023 17:30:22 +0300 Subject: [PATCH 138/429] scalafixAll --- .../shared/src/main/scala/cats/effect/IO.scala | 18 +++--------------- .../effect/kernel/syntax/AsyncSyntax.scala | 3 ++- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/core/shared/src/main/scala/cats/effect/IO.scala b/core/shared/src/main/scala/cats/effect/IO.scala index 95f1a19f95..0f8a15baac 100644 --- a/core/shared/src/main/scala/cats/effect/IO.scala +++ b/core/shared/src/main/scala/cats/effect/IO.scala @@ -362,22 +362,10 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] { /** * Shifts the execution of the current IO to the specified [[java.util.concurrent.Executor]]. * - * @param executor - * @return + * @see [[evalOn]] */ - def evalOnExecutor(executor: Executor): IO[A] = { - require(executor != null, "Cannot pass undefined Executor as an argument") - executor match { - case ec: ExecutionContext => - evalOn(ec: ExecutionContext) - case executor => - IO.executionContext.flatMap { refEc => - val newEc: ExecutionContext = - ExecutionContext.fromExecutor(executor, refEc.reportFailure) - evalOn(newEc) - } - } - } + def evalOnExecutor(executor: Executor): IO[A] = + IO.asyncForIO.evalOnExecutor(this, executor) def startOn(ec: ExecutionContext): IO[FiberIO[A @uncheckedVariance]] = start.evalOn(ec) diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/syntax/AsyncSyntax.scala b/kernel/shared/src/main/scala/cats/effect/kernel/syntax/AsyncSyntax.scala index 7310e077eb..906c371cac 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/syntax/AsyncSyntax.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/syntax/AsyncSyntax.scala @@ -18,9 +18,10 @@ package cats.effect.kernel.syntax import cats.effect.kernel._ -import java.util.concurrent.Executor import scala.concurrent.ExecutionContext +import java.util.concurrent.Executor + trait AsyncSyntax { implicit def asyncOps[F[_], A](wrapped: F[A]): AsyncOps[F, A] = new AsyncOps(wrapped) From 1298f25b8de3b7179aeb7b6473e037130021f7c4 Mon Sep 17 00:00:00 2001 From: "a.muminov" Date: Tue, 4 Jul 2023 12:32:42 +0300 Subject: [PATCH 139/429] scalafmtAll --- core/shared/src/main/scala/cats/effect/IO.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/shared/src/main/scala/cats/effect/IO.scala b/core/shared/src/main/scala/cats/effect/IO.scala index 0f8a15baac..bee4f5ea17 100644 --- a/core/shared/src/main/scala/cats/effect/IO.scala +++ b/core/shared/src/main/scala/cats/effect/IO.scala @@ -362,7 +362,8 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] { /** * Shifts the execution of the current IO to the specified [[java.util.concurrent.Executor]]. * - * @see [[evalOn]] + * @see + * [[evalOn]] */ def evalOnExecutor(executor: Executor): IO[A] = IO.asyncForIO.evalOnExecutor(this, executor) From 08ec2526ca3fc0ff6c7c8a140107d797dd23e041 Mon Sep 17 00:00:00 2001 From: "a.muminov" Date: Wed, 5 Jul 2023 18:08:04 +0300 Subject: [PATCH 140/429] comments fix --- core/shared/src/main/scala/cats/effect/IO.scala | 4 ++-- kernel/shared/src/main/scala/cats/effect/kernel/Async.scala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/shared/src/main/scala/cats/effect/IO.scala b/core/shared/src/main/scala/cats/effect/IO.scala index bee4f5ea17..967ae534a7 100644 --- a/core/shared/src/main/scala/cats/effect/IO.scala +++ b/core/shared/src/main/scala/cats/effect/IO.scala @@ -371,14 +371,14 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] { def startOn(ec: ExecutionContext): IO[FiberIO[A @uncheckedVariance]] = start.evalOn(ec) def startOnExecutor(executor: Executor): IO[FiberIO[A @uncheckedVariance]] = - start.evalOnExecutor(executor) + IO.asyncForIO.startOnExecutor(this, executor) def backgroundOn(ec: ExecutionContext): ResourceIO[IO[OutcomeIO[A @uncheckedVariance]]] = Resource.make(startOn(ec))(_.cancel).map(_.join) def backgroundOnExecutor( executor: Executor): ResourceIO[IO[OutcomeIO[A @uncheckedVariance]]] = - Resource.make(startOnExecutor(executor))(_.cancel).map(_.join) + IO.asyncForIO.backgroundOnExecutor(this, executor) /** * Given an effect which might be [[uncancelable]] and a finalizer, produce an effect which diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/Async.scala b/kernel/shared/src/main/scala/cats/effect/kernel/Async.scala index 6fcf64bd26..25a1599928 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/Async.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/Async.scala @@ -170,7 +170,7 @@ trait Async[F[_]] extends AsyncPlatform[F] with Sync[F] with Temporal[F] { * [[Async.evalOn]] with provided [[java.util.concurrent.Executor]] */ def evalOnExecutor[A](fa: F[A], executor: Executor): F[A] = { - require(executor != null, "Cannot pass undefined Executor as an argument") + require(executor != null, "Cannot pass null Executor as an argument") executor match { case ec: ExecutionContext => evalOn[A](fa, ec: ExecutionContext) From 71e3c423ebf5dfc87cf8162583cf4f672bc5c10b Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Thu, 13 Jul 2023 00:19:23 +0000 Subject: [PATCH 141/429] Update scalafmt-core to 3.7.9 in series/3.x --- .scalafmt.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index b0fe1fee57..d55ab07e89 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = 3.7.4 +version = 3.7.9 runner.dialect = Scala213Source3 fileOverride { From db6c201aad98cc3d19f67cc688dfa6332e2fb939 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Thu, 13 Jul 2023 00:19:39 +0000 Subject: [PATCH 142/429] Reformat with scalafmt 3.7.9 Executed command: scalafmt --non-interactive --- scripts/data/scastie.sbt | 3 ++- scripts/data/scastie.scala | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/data/scastie.sbt b/scripts/data/scastie.sbt index 0c11d2f069..fbeae236bf 100644 --- a/scripts/data/scastie.sbt +++ b/scripts/data/scastie.sbt @@ -1,6 +1,7 @@ scalacOptions ++= Seq( "-deprecation", - "-encoding", "UTF-8", + "-encoding", + "UTF-8", "-feature", "-unchecked" ) diff --git a/scripts/data/scastie.scala b/scripts/data/scastie.scala index d465972a02..eb33d4e537 100644 --- a/scripts/data/scastie.scala +++ b/scripts/data/scastie.scala @@ -8,8 +8,8 @@ object Hello extends IOApp.Simple { def sleepPrint(word: String, name: String, rand: Random[IO]) = for { delay <- rand.betweenInt(200, 700) - _ <- IO.sleep(delay.millis) - _ <- IO.println(s"$word, $name") + _ <- IO.sleep(delay.millis) + _ <- IO.println(s"$word, $name") } yield () val run = @@ -21,7 +21,7 @@ object Hello extends IOApp.Simple { name <- IO.pure("Daniel") english <- sleepPrint("Hello", name, rand).foreverM.start - french <- sleepPrint("Bonjour", name, rand).foreverM.start + french <- sleepPrint("Bonjour", name, rand).foreverM.start spanish <- sleepPrint("Hola", name, rand).foreverM.start _ <- IO.sleep(5.seconds) From f7da5e84e61e38105a685f90953b220853815e8a Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Thu, 13 Jul 2023 00:19:39 +0000 Subject: [PATCH 143/429] Add 'Reformat with scalafmt 3.7.9' to .git-blame-ignore-revs --- .git-blame-ignore-revs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index c96433bdba..09caafe4da 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -3,3 +3,6 @@ c3404c6577af33d65017aeaca248d51dab770021 # Scala Steward: Reformat with scalafmt 3.7.1 52c851127a918b050f7b1d33ad71f128cb7bc48e + +# Scala Steward: Reformat with scalafmt 3.7.9 +db6c201aad98cc3d19f67cc688dfa6332e2fb939 From 3c820988cfbd8c1d1d1ca8b1f8cf05d1b11b5416 Mon Sep 17 00:00:00 2001 From: antjim1 Date: Sat, 15 Jul 2023 18:36:12 +0200 Subject: [PATCH 144/429] Add Documentation to `PollingSystem` This PR adds documentation to the `PollingSystem` abstract class and its associated methods. --- .../cats/effect/unsafe/PollingSystem.scala | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index d28422ce98..d8b3c43974 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -17,6 +17,23 @@ package cats.effect package unsafe +/** + * Encapsulates the methods required for managing and interacting with a polling system. Polling + * systems are typically used in scenarios such as handling asynchronous I/O or other + * event-driven systems, where one needs to repeatedly check (or "poll") some condition or + * state. + * + * This class abstracts the general components and actions of a polling system, such as: + * - The user-facing interface (API) which interacts with the outside world + * - The thread-local data structure used for polling, which keeps track of the internal state + * of the system and its events + * - The lifecycle management methods, such as creating and closing the polling system and its + * components + * - The interaction methods, such as polling events and interrupting the process + * + * A concrete implementation of `PollingSystem` should provide specific implementations for all + * these components and actions. + */ abstract class PollingSystem { /** @@ -29,19 +46,49 @@ abstract class PollingSystem { */ type Poller <: AnyRef + /** + * Provides the functionality to close the polling system. + */ def close(): Unit + /** + * Creates a new instance of the user-facing interface. + * + * @param register + * callback that takes a function from `Poller` to `Unit` The function is used to register a + * new poller. + * @return + * an instance of the user-facing interface `Api`. + */ def makeApi(register: (Poller => Unit) => Unit): Api + /** + * Creates a new instance of the thread-local data structure used for polling. + * + * @return + * an instance of the poller `Poller`. + */ def makePoller(): Poller + /** + * Provides the functionality to close a specific poller. + * + * @param poller + * the poller to be closed. + */ def closePoller(poller: Poller): Unit /** + * @param poller + * the poller used to poll events. + * * @param nanos * the maximum duration for which to block, where `nanos == -1` indicates to block * indefinitely. * + * @param reportFailure + * callback that handles any failures that occur during polling. + * * @return * whether any events were polled */ @@ -53,11 +100,26 @@ abstract class PollingSystem { */ def needsPoll(poller: Poller): Boolean + /** + * Interrupt a specific target poller running on a specific target thread. + * + * @param targetThread + * is the thread where the target poller is running. + * @param targetPoller + * is the poller to be interrupted. + */ def interrupt(targetThread: Thread, targetPoller: Poller): Unit } private object PollingSystem { + + /** + * Type alias for a `PollingSystem` that has a specified `Poller` type. + * + * @tparam P + * The type of the `Poller` in the `PollingSystem`. + */ type WithPoller[P] = PollingSystem { type Poller = P } From 5beda2ae4af7b3aba2ead523a37b15223b31cb1c Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Sun, 16 Jul 2023 20:06:56 +0000 Subject: [PATCH 145/429] Update scalafmt-core to 3.7.10 in series/3.x --- .scalafmt.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index d55ab07e89..a533d8d278 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = 3.7.9 +version = 3.7.10 runner.dialect = Scala213Source3 fileOverride { From 0ab7918866873ce84937fc3895a3881f376d8259 Mon Sep 17 00:00:00 2001 From: Antonio Jimenez <100201872+antoniojimeneznieto@users.noreply.github.com> Date: Mon, 17 Jul 2023 01:28:23 +0200 Subject: [PATCH 146/429] Update core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala Co-authored-by: Arman Bilge --- .../src/main/scala/cats/effect/unsafe/PollingSystem.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index d8b3c43974..12b882b509 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -19,7 +19,7 @@ package unsafe /** * Encapsulates the methods required for managing and interacting with a polling system. Polling - * systems are typically used in scenarios such as handling asynchronous I/O or other + * systems are typically used in scenarios such as handling multiplexed blocking I/O or other * event-driven systems, where one needs to repeatedly check (or "poll") some condition or * state. * From c5c28a33502ee989cfbc708202c3d26a3ed15248 Mon Sep 17 00:00:00 2001 From: Antonio Jimenez <100201872+antoniojimeneznieto@users.noreply.github.com> Date: Mon, 17 Jul 2023 01:30:04 +0200 Subject: [PATCH 147/429] Update core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala Co-authored-by: Arman Bilge --- .../src/main/scala/cats/effect/unsafe/PollingSystem.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index 12b882b509..f810da8d95 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -21,7 +21,7 @@ package unsafe * Encapsulates the methods required for managing and interacting with a polling system. Polling * systems are typically used in scenarios such as handling multiplexed blocking I/O or other * event-driven systems, where one needs to repeatedly check (or "poll") some condition or - * state. + * state, blocking up to some timeout until it is ready. * * This class abstracts the general components and actions of a polling system, such as: * - The user-facing interface (API) which interacts with the outside world From 1f2d2cbb321c2dc85d81512155d5a6f4ef486be0 Mon Sep 17 00:00:00 2001 From: Antonio Jimenez <100201872+antoniojimeneznieto@users.noreply.github.com> Date: Mon, 17 Jul 2023 01:30:25 +0200 Subject: [PATCH 148/429] Update core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala Co-authored-by: Arman Bilge --- .../src/main/scala/cats/effect/unsafe/PollingSystem.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index f810da8d95..0319d34978 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -47,7 +47,7 @@ abstract class PollingSystem { type Poller <: AnyRef /** - * Provides the functionality to close the polling system. + * Closes the polling system. */ def close(): Unit From 285038b0994843c2eb366c7af7c07d7ec8a7cbaa Mon Sep 17 00:00:00 2001 From: Antonio Jimenez <100201872+antoniojimeneznieto@users.noreply.github.com> Date: Mon, 17 Jul 2023 01:31:20 +0200 Subject: [PATCH 149/429] Update core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala Co-authored-by: Arman Bilge --- .../src/main/scala/cats/effect/unsafe/PollingSystem.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index 0319d34978..8d9fc4c774 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -29,7 +29,7 @@ package unsafe * of the system and its events * - The lifecycle management methods, such as creating and closing the polling system and its * components - * - The interaction methods, such as polling events and interrupting the process + * - The runtime interaction methods, such as polling events and interrupting the process * * A concrete implementation of `PollingSystem` should provide specific implementations for all * these components and actions. From c87bdbbbee5ad84d4d54df5d55f4b7f79dfcb5f4 Mon Sep 17 00:00:00 2001 From: Antonio Jimenez <100201872+antoniojimeneznieto@users.noreply.github.com> Date: Mon, 17 Jul 2023 01:31:47 +0200 Subject: [PATCH 150/429] Update core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala Co-authored-by: Arman Bilge --- .../src/main/scala/cats/effect/unsafe/PollingSystem.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index 8d9fc4c774..29db9afcab 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -90,7 +90,7 @@ abstract class PollingSystem { * callback that handles any failures that occur during polling. * * @return - * whether any events were polled + * whether any events were polled. e.g. if the method returned due to timeout, this should be `false`. */ def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean From 712aa4efb6e03c8d976444375499731691e208ba Mon Sep 17 00:00:00 2001 From: Antonio Jimenez <100201872+antoniojimeneznieto@users.noreply.github.com> Date: Mon, 17 Jul 2023 01:32:07 +0200 Subject: [PATCH 151/429] Update core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala Co-authored-by: Arman Bilge --- .../src/main/scala/cats/effect/unsafe/PollingSystem.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index 29db9afcab..a0a47275be 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -80,7 +80,7 @@ abstract class PollingSystem { /** * @param poller - * the poller used to poll events. + * the thread-local [[Poller]] used to poll events. * * @param nanos * the maximum duration for which to block, where `nanos == -1` indicates to block From 16d46e8ccddc2b6f1bca860eb169bf9061f4a73a Mon Sep 17 00:00:00 2001 From: Antonio Jimenez <100201872+antoniojimeneznieto@users.noreply.github.com> Date: Mon, 17 Jul 2023 01:32:15 +0200 Subject: [PATCH 152/429] Update core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala Co-authored-by: Arman Bilge --- .../src/main/scala/cats/effect/unsafe/PollingSystem.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index a0a47275be..c3972c3f80 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -71,7 +71,7 @@ abstract class PollingSystem { def makePoller(): Poller /** - * Provides the functionality to close a specific poller. + * Close a specific poller. * * @param poller * the poller to be closed. From 8ea9f8b06a3857ee2ca796ace46afe8fe843857c Mon Sep 17 00:00:00 2001 From: Antonio Jimenez <100201872+antoniojimeneznieto@users.noreply.github.com> Date: Tue, 18 Jul 2023 16:32:52 +0200 Subject: [PATCH 153/429] Update core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala Co-authored-by: Arman Bilge --- .../src/main/scala/cats/effect/unsafe/PollingSystem.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index c3972c3f80..ca04091fb1 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -55,8 +55,7 @@ abstract class PollingSystem { * Creates a new instance of the user-facing interface. * * @param register - * callback that takes a function from `Poller` to `Unit` The function is used to register a - * new poller. + * callback to obtain a thread-local `Poller`. * @return * an instance of the user-facing interface `Api`. */ From 45096290eb0ed2c6361da68993a88ac345944847 Mon Sep 17 00:00:00 2001 From: antjim1 Date: Tue, 18 Jul 2023 16:38:32 +0200 Subject: [PATCH 154/429] Review documentation Co-Authored-By: Daniel Spiewak Co-Authored-By: Arman Bilge --- .../scala/cats/effect/unsafe/PollingSystem.scala | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index ca04091fb1..c4aa0d2b2d 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -18,7 +18,7 @@ package cats.effect package unsafe /** - * Encapsulates the methods required for managing and interacting with a polling system. Polling + * Represents a stateful system for managing and interacting with a polling system. Polling * systems are typically used in scenarios such as handling multiplexed blocking I/O or other * event-driven systems, where one needs to repeatedly check (or "poll") some condition or * state, blocking up to some timeout until it is ready. @@ -30,9 +30,6 @@ package unsafe * - The lifecycle management methods, such as creating and closing the polling system and its * components * - The runtime interaction methods, such as polling events and interrupting the process - * - * A concrete implementation of `PollingSystem` should provide specific implementations for all - * these components and actions. */ abstract class PollingSystem { @@ -54,12 +51,12 @@ abstract class PollingSystem { /** * Creates a new instance of the user-facing interface. * - * @param register + * @param access * callback to obtain a thread-local `Poller`. * @return * an instance of the user-facing interface `Api`. */ - def makeApi(register: (Poller => Unit) => Unit): Api + def makeApi(access: (Poller => Unit) => Unit): Api /** * Creates a new instance of the thread-local data structure used for polling. @@ -70,7 +67,7 @@ abstract class PollingSystem { def makePoller(): Poller /** - * Close a specific poller. + * Closes a specific poller. * * @param poller * the poller to be closed. @@ -89,7 +86,8 @@ abstract class PollingSystem { * callback that handles any failures that occur during polling. * * @return - * whether any events were polled. e.g. if the method returned due to timeout, this should be `false`. + * whether any events were polled. e.g. if the method returned due to timeout, this should + * be `false`. */ def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean @@ -100,7 +98,7 @@ abstract class PollingSystem { def needsPoll(poller: Poller): Boolean /** - * Interrupt a specific target poller running on a specific target thread. + * Interrupts a specific target poller running on a specific target thread. * * @param targetThread * is the thread where the target poller is running. From d047237acf323ddbde0657b0046f9bcb3c6fbfbd Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 08:09:42 +0000 Subject: [PATCH 155/429] Update sbt to 1.9.3 in series/3.x --- project/build.properties | 2 +- scalafix/project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project/build.properties b/project/build.properties index 40b3b8e7b6..52413ab79a 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.0 +sbt.version=1.9.3 diff --git a/scalafix/project/build.properties b/scalafix/project/build.properties index 40b3b8e7b6..52413ab79a 100644 --- a/scalafix/project/build.properties +++ b/scalafix/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.0 +sbt.version=1.9.3 From b40fcae377d76102730809dfb5b8686858081a95 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 3 Aug 2023 15:15:46 +0000 Subject: [PATCH 156/429] Update to sbt-typelevel 0.5.0 --- .github/workflows/ci.yml | 207 +++++------------- build.sbt | 9 +- .../cats/effect/unsafe/IORuntimeConfig.scala | 24 +- .../cats/effect/kernel/OutcomeSpec.scala | 4 - project/build.properties | 2 +- project/plugins.sbt | 2 +- 6 files changed, 77 insertions(+), 171 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2c77d4ba8..33ac51d0f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] scala: [3.3.0, 2.12.18, 2.13.11] - java: [temurin@8, temurin@11, temurin@17, graalvm@11] + java: [temurin@8, temurin@11, temurin@17, graalvm@17] ci: [ciJVM, ciNative, ciJS, ciFirefox, ciChrome] exclude: - scala: 3.3.0 @@ -39,7 +39,7 @@ jobs: - scala: 2.12.18 java: temurin@17 - scala: 2.12.18 - java: graalvm@11 + java: graalvm@17 - os: windows-latest scala: 3.3.0 ci: ciJVM @@ -65,7 +65,7 @@ jobs: - ci: ciJS java: temurin@17 - ci: ciJS - java: graalvm@11 + java: graalvm@17 - os: windows-latest ci: ciJS - os: macos-latest @@ -75,7 +75,7 @@ jobs: - ci: ciFirefox java: temurin@17 - ci: ciFirefox - java: graalvm@11 + java: graalvm@17 - os: windows-latest ci: ciFirefox - os: macos-latest @@ -85,7 +85,7 @@ jobs: - ci: ciChrome java: temurin@17 - ci: ciChrome - java: graalvm@11 + java: graalvm@17 - os: windows-latest ci: ciChrome - os: macos-latest @@ -95,14 +95,14 @@ jobs: - ci: ciNative java: temurin@17 - ci: ciNative - java: graalvm@11 + java: graalvm@17 - os: windows-latest ci: ciNative - os: macos-latest ci: ciNative scala: 2.12.18 - os: windows-latest - java: graalvm@11 + java: graalvm@17 runs-on: ${{ matrix.os }} timeout-minutes: 60 steps: @@ -116,22 +116,13 @@ jobs: with: fetch-depth: 0 - - name: Download Java (temurin@8) - id: download-java-temurin-8 - if: matrix.java == 'temurin@8' - uses: typelevel/download-java@v2 - with: - distribution: temurin - java-version: 8 - - name: Setup Java (temurin@8) id: setup-java-temurin-8 if: matrix.java == 'temurin@8' uses: actions/setup-java@v3 with: - distribution: jdkfile + distribution: temurin java-version: 8 - jdkFile: ${{ steps.download-java-temurin-8.outputs.jdkFile }} cache: sbt - name: sbt update @@ -139,22 +130,13 @@ jobs: shell: bash run: sbt '++ ${{ matrix.scala }}' reload +update - - name: Download Java (temurin@11) - id: download-java-temurin-11 - if: matrix.java == 'temurin@11' - uses: typelevel/download-java@v2 - with: - distribution: temurin - java-version: 11 - - name: Setup Java (temurin@11) id: setup-java-temurin-11 if: matrix.java == 'temurin@11' uses: actions/setup-java@v3 with: - distribution: jdkfile + distribution: temurin java-version: 11 - jdkFile: ${{ steps.download-java-temurin-11.outputs.jdkFile }} cache: sbt - name: sbt update @@ -162,22 +144,13 @@ jobs: shell: bash run: sbt '++ ${{ matrix.scala }}' reload +update - - name: Download Java (temurin@17) - id: download-java-temurin-17 - if: matrix.java == 'temurin@17' - uses: typelevel/download-java@v2 - with: - distribution: temurin - java-version: 17 - - name: Setup Java (temurin@17) id: setup-java-temurin-17 if: matrix.java == 'temurin@17' uses: actions/setup-java@v3 with: - distribution: jdkfile + distribution: temurin java-version: 17 - jdkFile: ${{ steps.download-java-temurin-17.outputs.jdkFile }} cache: sbt - name: sbt update @@ -185,26 +158,17 @@ jobs: shell: bash run: sbt '++ ${{ matrix.scala }}' reload +update - - name: Download Java (graalvm@11) - id: download-java-graalvm-11 - if: matrix.java == 'graalvm@11' - uses: typelevel/download-java@v2 + - name: Setup Java (graalvm@17) + id: setup-java-graalvm-17 + if: matrix.java == 'graalvm@17' + uses: graalvm/setup-graalvm@v1 with: distribution: graalvm - java-version: 11 - - - name: Setup Java (graalvm@11) - id: setup-java-graalvm-11 - if: matrix.java == 'graalvm@11' - uses: actions/setup-java@v3 - with: - distribution: jdkfile - java-version: 11 - jdkFile: ${{ steps.download-java-graalvm-11.outputs.jdkFile }} + java-version: 17 cache: sbt - name: sbt update - if: matrix.java == 'graalvm@11' && steps.setup-java-graalvm-11.outputs.cache-hit == 'false' + if: matrix.java == 'graalvm@17' && steps.setup-java-graalvm-17.outputs.cache-hit == 'false' shell: bash run: sbt '++ ${{ matrix.scala }}' reload +update @@ -220,7 +184,7 @@ jobs: run: npm install - name: Install GraalVM Native Image - if: matrix.java == 'graalvm@11' + if: matrix.java == 'graalvm@17' shell: bash run: gu install native-image @@ -269,7 +233,7 @@ jobs: run: example/test-js.sh ${{ matrix.scala }} - name: Test GraalVM Native Image - if: matrix.scala == '2.13.11' && matrix.java == 'graalvm@11' && matrix.os == 'ubuntu-latest' + if: matrix.scala == '2.13.11' && matrix.java == 'graalvm@17' && matrix.os == 'ubuntu-latest' shell: bash run: sbt '++ ${{ matrix.scala }}' graalVMExample/nativeImage graalVMExample/nativeImageRun @@ -288,12 +252,12 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) shell: bash - run: mkdir -p benchmarks/target testkit/native/target target stress-tests/target std/jvm/target example/js/target kernel-testkit/jvm/target testkit/js/target rootJS/target core/native/target site-docs/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target graalvm-example/target kernel-testkit/js/target core/jvm/target rootJVM/target rootNative/target example/native/target kernel/native/target example/jvm/target laws/jvm/target std/native/target testkit/jvm/target project/target + run: mkdir -p testkit/native/target std/jvm/target kernel-testkit/jvm/target testkit/js/target core/native/target site-docs/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target kernel-testkit/js/target core/jvm/target kernel/native/target laws/jvm/target std/native/target testkit/jvm/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) shell: bash - run: tar cf targets.tar benchmarks/target testkit/native/target target stress-tests/target std/jvm/target example/js/target kernel-testkit/jvm/target testkit/js/target rootJS/target core/native/target site-docs/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target graalvm-example/target kernel-testkit/js/target core/jvm/target rootJVM/target rootNative/target example/native/target kernel/native/target example/jvm/target laws/jvm/target std/native/target testkit/jvm/target project/target + run: tar cf targets.tar testkit/native/target std/jvm/target kernel-testkit/jvm/target testkit/js/target core/native/target site-docs/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target kernel-testkit/js/target core/jvm/target kernel/native/target laws/jvm/target std/native/target testkit/jvm/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) @@ -309,7 +273,6 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.11] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -322,93 +285,57 @@ jobs: with: fetch-depth: 0 - - name: Download Java (temurin@8) - id: download-java-temurin-8 - if: matrix.java == 'temurin@8' - uses: typelevel/download-java@v2 - with: - distribution: temurin - java-version: 8 - - name: Setup Java (temurin@8) id: setup-java-temurin-8 if: matrix.java == 'temurin@8' uses: actions/setup-java@v3 with: - distribution: jdkfile + distribution: temurin java-version: 8 - jdkFile: ${{ steps.download-java-temurin-8.outputs.jdkFile }} cache: sbt - name: sbt update if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' - run: sbt '++ ${{ matrix.scala }}' reload +update - - - name: Download Java (temurin@11) - id: download-java-temurin-11 - if: matrix.java == 'temurin@11' - uses: typelevel/download-java@v2 - with: - distribution: temurin - java-version: 11 + run: sbt reload +update - name: Setup Java (temurin@11) id: setup-java-temurin-11 if: matrix.java == 'temurin@11' uses: actions/setup-java@v3 with: - distribution: jdkfile + distribution: temurin java-version: 11 - jdkFile: ${{ steps.download-java-temurin-11.outputs.jdkFile }} cache: sbt - name: sbt update if: matrix.java == 'temurin@11' && steps.setup-java-temurin-11.outputs.cache-hit == 'false' - run: sbt '++ ${{ matrix.scala }}' reload +update - - - name: Download Java (temurin@17) - id: download-java-temurin-17 - if: matrix.java == 'temurin@17' - uses: typelevel/download-java@v2 - with: - distribution: temurin - java-version: 17 + run: sbt reload +update - name: Setup Java (temurin@17) id: setup-java-temurin-17 if: matrix.java == 'temurin@17' uses: actions/setup-java@v3 with: - distribution: jdkfile + distribution: temurin java-version: 17 - jdkFile: ${{ steps.download-java-temurin-17.outputs.jdkFile }} cache: sbt - name: sbt update if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' - run: sbt '++ ${{ matrix.scala }}' reload +update + run: sbt reload +update - - name: Download Java (graalvm@11) - id: download-java-graalvm-11 - if: matrix.java == 'graalvm@11' - uses: typelevel/download-java@v2 + - name: Setup Java (graalvm@17) + id: setup-java-graalvm-17 + if: matrix.java == 'graalvm@17' + uses: graalvm/setup-graalvm@v1 with: distribution: graalvm - java-version: 11 - - - name: Setup Java (graalvm@11) - id: setup-java-graalvm-11 - if: matrix.java == 'graalvm@11' - uses: actions/setup-java@v3 - with: - distribution: jdkfile - java-version: 11 - jdkFile: ${{ steps.download-java-graalvm-11.outputs.jdkFile }} + java-version: 17 cache: sbt - name: sbt update - if: matrix.java == 'graalvm@11' && steps.setup-java-graalvm-11.outputs.cache-hit == 'false' - run: sbt '++ ${{ matrix.scala }}' reload +update + if: matrix.java == 'graalvm@17' && steps.setup-java-graalvm-17.outputs.cache-hit == 'false' + run: sbt reload +update - name: Download target directories (3.3.0, ciJVM) uses: actions/download-artifact@v3 @@ -545,7 +472,7 @@ jobs: SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} SONATYPE_CREDENTIAL_HOST: ${{ secrets.SONATYPE_CREDENTIAL_HOST }} - run: sbt '++ ${{ matrix.scala }}' tlCiRelease + run: sbt tlCiRelease dependency-submission: name: Submit Dependencies @@ -553,7 +480,6 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.11] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -566,93 +492,60 @@ jobs: with: fetch-depth: 0 - - name: Download Java (temurin@8) - id: download-java-temurin-8 - if: matrix.java == 'temurin@8' - uses: typelevel/download-java@v2 - with: - distribution: temurin - java-version: 8 - - name: Setup Java (temurin@8) id: setup-java-temurin-8 if: matrix.java == 'temurin@8' uses: actions/setup-java@v3 with: - distribution: jdkfile + distribution: temurin java-version: 8 - jdkFile: ${{ steps.download-java-temurin-8.outputs.jdkFile }} cache: sbt - name: sbt update if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' - run: sbt '++ ${{ matrix.scala }}' reload +update - - - name: Download Java (temurin@11) - id: download-java-temurin-11 - if: matrix.java == 'temurin@11' - uses: typelevel/download-java@v2 - with: - distribution: temurin - java-version: 11 + run: sbt reload +update - name: Setup Java (temurin@11) id: setup-java-temurin-11 if: matrix.java == 'temurin@11' uses: actions/setup-java@v3 with: - distribution: jdkfile + distribution: temurin java-version: 11 - jdkFile: ${{ steps.download-java-temurin-11.outputs.jdkFile }} cache: sbt - name: sbt update if: matrix.java == 'temurin@11' && steps.setup-java-temurin-11.outputs.cache-hit == 'false' - run: sbt '++ ${{ matrix.scala }}' reload +update - - - name: Download Java (temurin@17) - id: download-java-temurin-17 - if: matrix.java == 'temurin@17' - uses: typelevel/download-java@v2 - with: - distribution: temurin - java-version: 17 + run: sbt reload +update - name: Setup Java (temurin@17) id: setup-java-temurin-17 if: matrix.java == 'temurin@17' uses: actions/setup-java@v3 with: - distribution: jdkfile + distribution: temurin java-version: 17 - jdkFile: ${{ steps.download-java-temurin-17.outputs.jdkFile }} cache: sbt - name: sbt update if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' - run: sbt '++ ${{ matrix.scala }}' reload +update + run: sbt reload +update - - name: Download Java (graalvm@11) - id: download-java-graalvm-11 - if: matrix.java == 'graalvm@11' - uses: typelevel/download-java@v2 + - name: Setup Java (graalvm@17) + id: setup-java-graalvm-17 + if: matrix.java == 'graalvm@17' + uses: graalvm/setup-graalvm@v1 with: distribution: graalvm - java-version: 11 - - - name: Setup Java (graalvm@11) - id: setup-java-graalvm-11 - if: matrix.java == 'graalvm@11' - uses: actions/setup-java@v3 - with: - distribution: jdkfile - java-version: 11 - jdkFile: ${{ steps.download-java-graalvm-11.outputs.jdkFile }} + java-version: 17 cache: sbt - name: sbt update - if: matrix.java == 'graalvm@11' && steps.setup-java-graalvm-11.outputs.cache-hit == 'false' - run: sbt '++ ${{ matrix.scala }}' reload +update + if: matrix.java == 'graalvm@17' && steps.setup-java-graalvm-17.outputs.cache-hit == 'false' + run: sbt reload +update - name: Submit Dependencies uses: scalacenter/sbt-dependency-submission@v2 + with: + modules-ignore: cats-effect-benchmarks_3 cats-effect-benchmarks_2.12 cats-effect-benchmarks_2.13 cats-effect_3 cats-effect_2.12 cats-effect_2.13 cats-effect-stress-tests_3 cats-effect-stress-tests_2.12 cats-effect-stress-tests_2.13 cats-effect-example_sjs1_3 cats-effect-example_sjs1_2.12 cats-effect-example_sjs1_2.13 rootjs_3 rootjs_2.12 rootjs_2.13 cats-effect-graalvm-example_3 cats-effect-graalvm-example_2.12 cats-effect-graalvm-example_2.13 cats-effect-tests_sjs1_3 cats-effect-tests_sjs1_2.12 cats-effect-tests_sjs1_2.13 rootjvm_3 rootjvm_2.12 rootjvm_2.13 rootnative_3 rootnative_2.12 rootnative_2.13 cats-effect-example_native0.4_3 cats-effect-example_native0.4_2.12 cats-effect-example_native0.4_2.13 cats-effect-example_3 cats-effect-example_2.12 cats-effect-example_2.13 cats-effect-tests_3 cats-effect-tests_2.12 cats-effect-tests_2.13 cats-effect-tests_native0.4_3 cats-effect-tests_native0.4_2.12 cats-effect-tests_native0.4_2.13 + configs-ignore: test scala-tool scala-doc-tool test-internal diff --git a/build.sbt b/build.sbt index 706ae7fe80..bb3619999e 100644 --- a/build.sbt +++ b/build.sbt @@ -137,7 +137,7 @@ val LTSJava = JavaSpec.temurin("11") val LatestJava = JavaSpec.temurin("17") val ScalaJSJava = OldGuardJava val ScalaNativeJava = OldGuardJava -val GraalVM = JavaSpec.graalvm("11") +val GraalVM = JavaSpec.graalvm("17") ThisBuild / githubWorkflowJavaVersions := Seq(OldGuardJava, LTSJava, LatestJava, GraalVM) ThisBuild / githubWorkflowOSes := Seq(PrimaryOS, Windows, MacOS) @@ -313,8 +313,6 @@ val CoopVersion = "1.2.0" val MacrotaskExecutorVersion = "1.1.1" -val ScalacCompatVersion = "0.1.0" - tlReplaceCommandAlias("ci", CI.AllCIs.map(_.toString).mkString) addCommandAlias("release", "tlRelease") @@ -420,6 +418,7 @@ lazy val kernelTestkit = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.typelevel" %%% "cats-free" % CatsVersion, "org.scalacheck" %%% "scalacheck" % ScalaCheckVersion, "org.typelevel" %%% "coop" % CoopVersion), + scalacOptions -= "-Xsource:3", // bugged mimaBinaryIssueFilters ++= Seq( ProblemFilters.exclude[DirectMissingMethodProblem]( "cats.effect.kernel.testkit.TestContext.this"), @@ -463,9 +462,6 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) .dependsOn(kernel, std) .settings( name := "cats-effect", - libraryDependencies ++= Seq( - "org.typelevel" %% "scalac-compat-annotation" % ScalacCompatVersion % CompileTime - ), mimaBinaryIssueFilters ++= Seq( // introduced by #1837, removal of package private class ProblemFilters.exclude[MissingClassProblem]("cats.effect.AsyncPropagateCancelation"), @@ -915,7 +911,6 @@ lazy val std = crossProject(JSPlatform, JVMPlatform, NativePlatform) .settings( name := "cats-effect-std", libraryDependencies ++= Seq( - "org.typelevel" %% "scalac-compat-annotation" % ScalacCompatVersion % CompileTime, "org.scalacheck" %%% "scalacheck" % ScalaCheckVersion % Test, "org.specs2" %%% "specs2-scalacheck" % Specs2Version % Test ), diff --git a/core/shared/src/main/scala/cats/effect/unsafe/IORuntimeConfig.scala b/core/shared/src/main/scala/cats/effect/unsafe/IORuntimeConfig.scala index fd670b56ca..a7fc3f9ab6 100644 --- a/core/shared/src/main/scala/cats/effect/unsafe/IORuntimeConfig.scala +++ b/core/shared/src/main/scala/cats/effect/unsafe/IORuntimeConfig.scala @@ -221,7 +221,7 @@ object IORuntimeConfig extends IORuntimeConfigCompanionPlatform { shutdownHookTimeout: Duration, reportUnhandledFiberErrors: Boolean, cpuStarvationCheckInterval: FiniteDuration, - cpuStarvationCheckInitialDelay: FiniteDuration, + cpuStarvationCheckInitialDelay: Duration, cpuStarvationCheckThreshold: Double ): IORuntimeConfig = { if (autoYieldThreshold % cancelationCheckThreshold == 0) @@ -244,4 +244,26 @@ object IORuntimeConfig extends IORuntimeConfigCompanionPlatform { throw new AssertionError( s"Auto yield threshold $autoYieldThreshold must be a multiple of cancelation check threshold $cancelationCheckThreshold") } + + def apply( + cancelationCheckThreshold: Int, + autoYieldThreshold: Int, + enhancedExceptions: Boolean, + traceBufferSize: Int, + shutdownHookTimeout: Duration, + reportUnhandledFiberErrors: Boolean, + cpuStarvationCheckInterval: FiniteDuration, + cpuStarvationCheckInitialDelay: FiniteDuration, + cpuStarvationCheckThreshold: Double): IORuntimeConfig = + apply( + cancelationCheckThreshold, + autoYieldThreshold, + enhancedExceptions, + traceBufferSize, + shutdownHookTimeout, + reportUnhandledFiberErrors, + cpuStarvationCheckInterval, + cpuStarvationCheckInitialDelay: Duration, + cpuStarvationCheckThreshold + ) } diff --git a/laws/shared/src/test/scala/cats/effect/kernel/OutcomeSpec.scala b/laws/shared/src/test/scala/cats/effect/kernel/OutcomeSpec.scala index 08f27092bf..bb8feba90d 100644 --- a/laws/shared/src/test/scala/cats/effect/kernel/OutcomeSpec.scala +++ b/laws/shared/src/test/scala/cats/effect/kernel/OutcomeSpec.scala @@ -21,7 +21,6 @@ import cats.{Eq, Eval, Id, MonadError} import cats.effect.kernel.testkit.OutcomeGenerators import cats.laws.discipline.{ApplicativeErrorTests, MonadErrorTests} -import org.scalacheck.{Arbitrary, Cogen} import org.specs2.mutable.Specification import org.typelevel.discipline.specs2.mutable.Discipline @@ -35,9 +34,6 @@ class OutcomeSpec extends Specification with Discipline { implicit def monadErrorOutcomeIdInt: MonadError[OutcomeIdInt, Int] = Outcome.monadError[Id, Int] - implicit def arbitraryOutcomeId[A: Arbitrary: Cogen]: Arbitrary[OutcomeIdInt[A]] = - arbitraryOutcome[Id, Int, A] - implicit def eqOutcomeId[A]: Eq[OutcomeIdInt[A]] = Outcome.eq[Id, Int, A] implicit def eqId[A]: Eq[Id[A]] = Eq.instance(_ == _) diff --git a/project/build.properties b/project/build.properties index 40b3b8e7b6..52413ab79a 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.0 +sbt.version=1.9.3 diff --git a/project/plugins.sbt b/project/plugins.sbt index afe1e03800..142bf7c99b 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.0-M10") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.0-RC9") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.1") From 26065dd758044c96871774aaa1fa131df02b2538 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Thu, 3 Aug 2023 16:07:49 +0000 Subject: [PATCH 157/429] Update sbt-typelevel to 0.5.0-RC10 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index afe1e03800..b9571f2df8 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.0-M10") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.0-RC10") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.1") From c79636526e8658061d0123b022f657291191e63c Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Thu, 3 Aug 2023 16:09:43 +0000 Subject: [PATCH 158/429] Run prePR with sbt-typelevel Executed command: sbt tlPrePrBotHook --- .github/workflows/ci.yml | 165 +++++++-------------------------------- 1 file changed, 29 insertions(+), 136 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2c77d4ba8..c2a2a57e63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -116,22 +116,13 @@ jobs: with: fetch-depth: 0 - - name: Download Java (temurin@8) - id: download-java-temurin-8 - if: matrix.java == 'temurin@8' - uses: typelevel/download-java@v2 - with: - distribution: temurin - java-version: 8 - - name: Setup Java (temurin@8) id: setup-java-temurin-8 if: matrix.java == 'temurin@8' uses: actions/setup-java@v3 with: - distribution: jdkfile + distribution: temurin java-version: 8 - jdkFile: ${{ steps.download-java-temurin-8.outputs.jdkFile }} cache: sbt - name: sbt update @@ -139,22 +130,13 @@ jobs: shell: bash run: sbt '++ ${{ matrix.scala }}' reload +update - - name: Download Java (temurin@11) - id: download-java-temurin-11 - if: matrix.java == 'temurin@11' - uses: typelevel/download-java@v2 - with: - distribution: temurin - java-version: 11 - - name: Setup Java (temurin@11) id: setup-java-temurin-11 if: matrix.java == 'temurin@11' uses: actions/setup-java@v3 with: - distribution: jdkfile + distribution: temurin java-version: 11 - jdkFile: ${{ steps.download-java-temurin-11.outputs.jdkFile }} cache: sbt - name: sbt update @@ -162,22 +144,13 @@ jobs: shell: bash run: sbt '++ ${{ matrix.scala }}' reload +update - - name: Download Java (temurin@17) - id: download-java-temurin-17 - if: matrix.java == 'temurin@17' - uses: typelevel/download-java@v2 - with: - distribution: temurin - java-version: 17 - - name: Setup Java (temurin@17) id: setup-java-temurin-17 if: matrix.java == 'temurin@17' uses: actions/setup-java@v3 with: - distribution: jdkfile + distribution: temurin java-version: 17 - jdkFile: ${{ steps.download-java-temurin-17.outputs.jdkFile }} cache: sbt - name: sbt update @@ -185,22 +158,13 @@ jobs: shell: bash run: sbt '++ ${{ matrix.scala }}' reload +update - - name: Download Java (graalvm@11) - id: download-java-graalvm-11 - if: matrix.java == 'graalvm@11' - uses: typelevel/download-java@v2 - with: - distribution: graalvm - java-version: 11 - - name: Setup Java (graalvm@11) id: setup-java-graalvm-11 if: matrix.java == 'graalvm@11' - uses: actions/setup-java@v3 + uses: graalvm/setup-graalvm@v1 with: - distribution: jdkfile + distribution: graalvm java-version: 11 - jdkFile: ${{ steps.download-java-graalvm-11.outputs.jdkFile }} cache: sbt - name: sbt update @@ -288,12 +252,12 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) shell: bash - run: mkdir -p benchmarks/target testkit/native/target target stress-tests/target std/jvm/target example/js/target kernel-testkit/jvm/target testkit/js/target rootJS/target core/native/target site-docs/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target graalvm-example/target kernel-testkit/js/target core/jvm/target rootJVM/target rootNative/target example/native/target kernel/native/target example/jvm/target laws/jvm/target std/native/target testkit/jvm/target project/target + run: mkdir -p testkit/native/target std/jvm/target kernel-testkit/jvm/target testkit/js/target core/native/target site-docs/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target kernel-testkit/js/target core/jvm/target kernel/native/target laws/jvm/target std/native/target testkit/jvm/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) shell: bash - run: tar cf targets.tar benchmarks/target testkit/native/target target stress-tests/target std/jvm/target example/js/target kernel-testkit/jvm/target testkit/js/target rootJS/target core/native/target site-docs/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target graalvm-example/target kernel-testkit/js/target core/jvm/target rootJVM/target rootNative/target example/native/target kernel/native/target example/jvm/target laws/jvm/target std/native/target testkit/jvm/target project/target + run: tar cf targets.tar testkit/native/target std/jvm/target kernel-testkit/jvm/target testkit/js/target core/native/target site-docs/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target kernel-testkit/js/target core/jvm/target kernel/native/target laws/jvm/target std/native/target testkit/jvm/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) @@ -309,7 +273,6 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.11] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -322,93 +285,57 @@ jobs: with: fetch-depth: 0 - - name: Download Java (temurin@8) - id: download-java-temurin-8 - if: matrix.java == 'temurin@8' - uses: typelevel/download-java@v2 - with: - distribution: temurin - java-version: 8 - - name: Setup Java (temurin@8) id: setup-java-temurin-8 if: matrix.java == 'temurin@8' uses: actions/setup-java@v3 with: - distribution: jdkfile + distribution: temurin java-version: 8 - jdkFile: ${{ steps.download-java-temurin-8.outputs.jdkFile }} cache: sbt - name: sbt update if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' - run: sbt '++ ${{ matrix.scala }}' reload +update - - - name: Download Java (temurin@11) - id: download-java-temurin-11 - if: matrix.java == 'temurin@11' - uses: typelevel/download-java@v2 - with: - distribution: temurin - java-version: 11 + run: sbt reload +update - name: Setup Java (temurin@11) id: setup-java-temurin-11 if: matrix.java == 'temurin@11' uses: actions/setup-java@v3 with: - distribution: jdkfile + distribution: temurin java-version: 11 - jdkFile: ${{ steps.download-java-temurin-11.outputs.jdkFile }} cache: sbt - name: sbt update if: matrix.java == 'temurin@11' && steps.setup-java-temurin-11.outputs.cache-hit == 'false' - run: sbt '++ ${{ matrix.scala }}' reload +update - - - name: Download Java (temurin@17) - id: download-java-temurin-17 - if: matrix.java == 'temurin@17' - uses: typelevel/download-java@v2 - with: - distribution: temurin - java-version: 17 + run: sbt reload +update - name: Setup Java (temurin@17) id: setup-java-temurin-17 if: matrix.java == 'temurin@17' uses: actions/setup-java@v3 with: - distribution: jdkfile + distribution: temurin java-version: 17 - jdkFile: ${{ steps.download-java-temurin-17.outputs.jdkFile }} cache: sbt - name: sbt update if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' - run: sbt '++ ${{ matrix.scala }}' reload +update - - - name: Download Java (graalvm@11) - id: download-java-graalvm-11 - if: matrix.java == 'graalvm@11' - uses: typelevel/download-java@v2 - with: - distribution: graalvm - java-version: 11 + run: sbt reload +update - name: Setup Java (graalvm@11) id: setup-java-graalvm-11 if: matrix.java == 'graalvm@11' - uses: actions/setup-java@v3 + uses: graalvm/setup-graalvm@v1 with: - distribution: jdkfile + distribution: graalvm java-version: 11 - jdkFile: ${{ steps.download-java-graalvm-11.outputs.jdkFile }} cache: sbt - name: sbt update if: matrix.java == 'graalvm@11' && steps.setup-java-graalvm-11.outputs.cache-hit == 'false' - run: sbt '++ ${{ matrix.scala }}' reload +update + run: sbt reload +update - name: Download target directories (3.3.0, ciJVM) uses: actions/download-artifact@v3 @@ -545,7 +472,7 @@ jobs: SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} SONATYPE_CREDENTIAL_HOST: ${{ secrets.SONATYPE_CREDENTIAL_HOST }} - run: sbt '++ ${{ matrix.scala }}' tlCiRelease + run: sbt tlCiRelease dependency-submission: name: Submit Dependencies @@ -553,7 +480,6 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.11] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -566,93 +492,60 @@ jobs: with: fetch-depth: 0 - - name: Download Java (temurin@8) - id: download-java-temurin-8 - if: matrix.java == 'temurin@8' - uses: typelevel/download-java@v2 - with: - distribution: temurin - java-version: 8 - - name: Setup Java (temurin@8) id: setup-java-temurin-8 if: matrix.java == 'temurin@8' uses: actions/setup-java@v3 with: - distribution: jdkfile + distribution: temurin java-version: 8 - jdkFile: ${{ steps.download-java-temurin-8.outputs.jdkFile }} cache: sbt - name: sbt update if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' - run: sbt '++ ${{ matrix.scala }}' reload +update - - - name: Download Java (temurin@11) - id: download-java-temurin-11 - if: matrix.java == 'temurin@11' - uses: typelevel/download-java@v2 - with: - distribution: temurin - java-version: 11 + run: sbt reload +update - name: Setup Java (temurin@11) id: setup-java-temurin-11 if: matrix.java == 'temurin@11' uses: actions/setup-java@v3 with: - distribution: jdkfile + distribution: temurin java-version: 11 - jdkFile: ${{ steps.download-java-temurin-11.outputs.jdkFile }} cache: sbt - name: sbt update if: matrix.java == 'temurin@11' && steps.setup-java-temurin-11.outputs.cache-hit == 'false' - run: sbt '++ ${{ matrix.scala }}' reload +update - - - name: Download Java (temurin@17) - id: download-java-temurin-17 - if: matrix.java == 'temurin@17' - uses: typelevel/download-java@v2 - with: - distribution: temurin - java-version: 17 + run: sbt reload +update - name: Setup Java (temurin@17) id: setup-java-temurin-17 if: matrix.java == 'temurin@17' uses: actions/setup-java@v3 with: - distribution: jdkfile + distribution: temurin java-version: 17 - jdkFile: ${{ steps.download-java-temurin-17.outputs.jdkFile }} cache: sbt - name: sbt update if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' - run: sbt '++ ${{ matrix.scala }}' reload +update - - - name: Download Java (graalvm@11) - id: download-java-graalvm-11 - if: matrix.java == 'graalvm@11' - uses: typelevel/download-java@v2 - with: - distribution: graalvm - java-version: 11 + run: sbt reload +update - name: Setup Java (graalvm@11) id: setup-java-graalvm-11 if: matrix.java == 'graalvm@11' - uses: actions/setup-java@v3 + uses: graalvm/setup-graalvm@v1 with: - distribution: jdkfile + distribution: graalvm java-version: 11 - jdkFile: ${{ steps.download-java-graalvm-11.outputs.jdkFile }} cache: sbt - name: sbt update if: matrix.java == 'graalvm@11' && steps.setup-java-graalvm-11.outputs.cache-hit == 'false' - run: sbt '++ ${{ matrix.scala }}' reload +update + run: sbt reload +update - name: Submit Dependencies uses: scalacenter/sbt-dependency-submission@v2 + with: + modules-ignore: cats-effect-benchmarks_3 cats-effect-benchmarks_2.12 cats-effect-benchmarks_2.13 cats-effect_3 cats-effect_2.12 cats-effect_2.13 cats-effect-stress-tests_3 cats-effect-stress-tests_2.12 cats-effect-stress-tests_2.13 cats-effect-example_sjs1_3 cats-effect-example_sjs1_2.12 cats-effect-example_sjs1_2.13 rootjs_3 rootjs_2.12 rootjs_2.13 cats-effect-graalvm-example_3 cats-effect-graalvm-example_2.12 cats-effect-graalvm-example_2.13 cats-effect-tests_sjs1_3 cats-effect-tests_sjs1_2.12 cats-effect-tests_sjs1_2.13 rootjvm_3 rootjvm_2.12 rootjvm_2.13 rootnative_3 rootnative_2.12 rootnative_2.13 cats-effect-example_native0.4_3 cats-effect-example_native0.4_2.12 cats-effect-example_native0.4_2.13 cats-effect-example_3 cats-effect-example_2.12 cats-effect-example_2.13 cats-effect-tests_3 cats-effect-tests_2.12 cats-effect-tests_2.13 cats-effect-tests_native0.4_3 cats-effect-tests_native0.4_2.12 cats-effect-tests_native0.4_2.13 + configs-ignore: test scala-tool scala-doc-tool test-internal From 3ccef168a0a8cada1a21e9306bb387d1d68087ed Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 3 Aug 2023 16:26:46 +0000 Subject: [PATCH 159/429] Fix Scala 3 compile --- .../main/scala/cats/effect/IOPlatform.scala | 14 +++++------ .../unsafe/IORuntimeCompanionPlatform.scala | 1 + .../main/scala/cats/effect/IODeferred.scala | 4 +++- .../cats/effect/kernel/AsyncPlatform.scala | 24 +++++++++---------- .../effect/laws/OptionTPureConcSpec.scala | 3 +-- .../cats/effect/std/DispatcherPlatform.scala | 8 +++---- .../effect/std/ConsoleCrossPlatform.scala | 1 + .../scala/cats/effect/std/Semaphore.scala | 1 - .../test/scala/cats/effect/MemoizeSpec.scala | 16 +++++++++++++ .../test/scala/cats/effect/ResourceSpec.scala | 6 ++--- .../cats/effect/kernel/LensRefSpec.scala | 4 +++- .../effect/kernel/MiniSemaphoreSpec.scala | 2 -- .../scala/cats/effect/kernel/RefSpec.scala | 1 + .../cats/effect/std/AtomicCellSpec.scala | 2 -- .../cats/effect/std/DispatcherSpec.scala | 1 + 15 files changed, 53 insertions(+), 35 deletions(-) diff --git a/core/js/src/main/scala/cats/effect/IOPlatform.scala b/core/js/src/main/scala/cats/effect/IOPlatform.scala index 9b990008bf..2724367516 100644 --- a/core/js/src/main/scala/cats/effect/IOPlatform.scala +++ b/core/js/src/main/scala/cats/effect/IOPlatform.scala @@ -17,7 +17,7 @@ package cats.effect import scala.concurrent.Future -import scala.scalajs.js.{|, Function1, JavaScriptException, Promise, Thenable} +import scala.scalajs.js abstract private[effect] class IOPlatform[+A] { self: IO[A] => @@ -31,10 +31,10 @@ abstract private[effect] class IOPlatform[+A] { self: IO[A] => * @see * [[IO.fromPromise]] */ - def unsafeToPromise()(implicit runtime: unsafe.IORuntime): Promise[A] = - new Promise[A]((resolve: Function1[A | Thenable[A], _], reject: Function1[Any, _]) => + def unsafeToPromise()(implicit runtime: unsafe.IORuntime): js.Promise[A] = + new js.Promise[A]((resolve, reject) => self.unsafeRunAsync { - case Left(JavaScriptException(e)) => + case Left(js.JavaScriptException(e)) => reject(e) () @@ -76,10 +76,10 @@ abstract private[effect] class IOPlatform[+A] { self: IO[A] => * @see * [[IO.syncStep(limit:Int)*]] */ - def unsafeRunSyncToPromise()(implicit runtime: unsafe.IORuntime): Promise[A] = + def unsafeRunSyncToPromise()(implicit runtime: unsafe.IORuntime): js.Promise[A] = self.syncStep(runtime.config.autoYieldThreshold).attempt.unsafeRunSync() match { - case Left(t) => Promise.reject(t) + case Left(t) => js.Promise.reject(t) case Right(Left(ioa)) => ioa.unsafeToPromise() - case Right(Right(a)) => Promise.resolve[A](a) + case Right(Right(a)) => js.Promise.resolve[A](a) } } diff --git a/core/js/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala b/core/js/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala index 0fddca85cb..f103f9cb96 100644 --- a/core/js/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala +++ b/core/js/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala @@ -55,6 +55,7 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type () => resetGlobal(), IORuntimeConfig()) } + () } _global diff --git a/core/shared/src/main/scala/cats/effect/IODeferred.scala b/core/shared/src/main/scala/cats/effect/IODeferred.scala index 217af8360a..dd97abb27e 100644 --- a/core/shared/src/main/scala/cats/effect/IODeferred.scala +++ b/core/shared/src/main/scala/cats/effect/IODeferred.scala @@ -29,8 +29,10 @@ private final class IODeferred[A] extends Deferred[IO, A] { def clear(): Unit = { stack.clearCurrent(handle) val clearCount = clearCounter.incrementAndGet() - if ((clearCount & (clearCount - 1)) == 0) // power of 2 + if ((clearCount & (clearCount - 1)) == 0) { // power of 2 clearCounter.addAndGet(-callbacks.pack(clearCount)) + () + } () } diff --git a/kernel/js/src/main/scala/cats/effect/kernel/AsyncPlatform.scala b/kernel/js/src/main/scala/cats/effect/kernel/AsyncPlatform.scala index d3fee821e6..1046a6e0a5 100644 --- a/kernel/js/src/main/scala/cats/effect/kernel/AsyncPlatform.scala +++ b/kernel/js/src/main/scala/cats/effect/kernel/AsyncPlatform.scala @@ -16,42 +16,42 @@ package cats.effect.kernel -import scala.scalajs.js.{|, defined, Function1, JavaScriptException, Promise, Thenable} +import scala.scalajs.js private[kernel] trait AsyncPlatform[F[_]] { this: Async[F] => - def fromPromise[A](iop: F[Promise[A]]): F[A] = fromThenable(widen(iop)) + def fromPromise[A](iop: F[js.Promise[A]]): F[A] = fromThenable(widen(iop)) - def fromPromiseCancelable[A](iop: F[(Promise[A], F[Unit])]): F[A] = + def fromPromiseCancelable[A](iop: F[(js.Promise[A], F[Unit])]): F[A] = fromThenableCancelable(widen(iop)) - def fromThenable[A](iot: F[Thenable[A]]): F[A] = + def fromThenable[A](iot: F[js.Thenable[A]]): F[A] = flatMap(iot) { t => async_[A] { cb => - t.`then`[Unit](mkOnFulfilled(cb), defined(mkOnRejected(cb))) + t.`then`[Unit](mkOnFulfilled(cb), js.defined(mkOnRejected(cb))) () } } - def fromThenableCancelable[A](iot: F[(Thenable[A], F[Unit])]): F[A] = + def fromThenableCancelable[A](iot: F[(js.Thenable[A], F[Unit])]): F[A] = flatMap(iot) { case (t, fin) => async[A] { cb => - as(delay(t.`then`[Unit](mkOnFulfilled(cb), defined(mkOnRejected(cb)))), Some(fin)) + as(delay(t.`then`[Unit](mkOnFulfilled(cb), js.defined(mkOnRejected(cb)))), Some(fin)) } } @inline private[this] def mkOnFulfilled[A]( - cb: Either[Throwable, A] => Unit): Function1[A, Unit | Thenable[Unit]] = - (v: A) => cb(Right(v)): Unit | Thenable[Unit] + cb: Either[Throwable, A] => Unit): js.Function1[A, js.UndefOr[js.Thenable[Unit]]] = + (v: A) => cb(Right(v)): js.UndefOr[js.Thenable[Unit]] @inline private[this] def mkOnRejected[A]( - cb: Either[Throwable, A] => Unit): Function1[Any, Unit | Thenable[Unit]] = { (a: Any) => + cb: Either[Throwable, A] => Unit): js.Function1[Any, js.UndefOr[js.Thenable[Unit]]] = { (a: Any) => val e = a match { case th: Throwable => th - case _ => JavaScriptException(a) + case _ => js.JavaScriptException(a) } - cb(Left(e)): Unit | Thenable[Unit] + cb(Left(e)): js.UndefOr[js.Thenable[Unit]] } } diff --git a/laws/shared/src/test/scala/cats/effect/laws/OptionTPureConcSpec.scala b/laws/shared/src/test/scala/cats/effect/laws/OptionTPureConcSpec.scala index 269bdced8d..6b8e5f67c1 100644 --- a/laws/shared/src/test/scala/cats/effect/laws/OptionTPureConcSpec.scala +++ b/laws/shared/src/test/scala/cats/effect/laws/OptionTPureConcSpec.scala @@ -25,7 +25,6 @@ import cats.effect.kernel.testkit.{pure, OutcomeGenerators, PureConcGenerators, import cats.effect.kernel.testkit.TimeT._ import cats.effect.kernel.testkit.pure._ import cats.laws.discipline.arbitrary._ -import cats.syntax.all._ import org.scalacheck.Prop import org.specs2.mutable._ @@ -55,7 +54,7 @@ class OptionTPureConcSpec extends Specification with Discipline with BaseSpec { case Outcome.Succeeded(fa) => observed = true - OptionT(fa.value.map(_ must beNone).as(None)) + OptionT(fa.value.map(_ must beNone).map(_ => None)) case _ => Applicative[OptionT[PureConc[Int, *], *]].unit } diff --git a/std/js/src/main/scala/cats/effect/std/DispatcherPlatform.scala b/std/js/src/main/scala/cats/effect/std/DispatcherPlatform.scala index bf1cc7e113..4aba11336c 100644 --- a/std/js/src/main/scala/cats/effect/std/DispatcherPlatform.scala +++ b/std/js/src/main/scala/cats/effect/std/DispatcherPlatform.scala @@ -16,7 +16,7 @@ package cats.effect.std -import scala.scalajs.js.{|, Function1, JavaScriptException, Promise, Thenable} +import scala.scalajs.js private[std] trait DispatcherPlatform[F[_]] { this: Dispatcher[F] => @@ -24,10 +24,10 @@ private[std] trait DispatcherPlatform[F[_]] { this: Dispatcher[F] => * Submits an effect to be executed, returning a `Promise` that holds the result of its * evaluation. */ - def unsafeToPromise[A](fa: F[A]): Promise[A] = - new Promise[A]((resolve: Function1[A | Thenable[A], _], reject: Function1[Any, _]) => + def unsafeToPromise[A](fa: F[A]): js.Promise[A] = + new js.Promise[A]((resolve, reject) => unsafeRunAsync(fa) { - case Left(JavaScriptException(e)) => + case Left(js.JavaScriptException(e)) => reject(e) () diff --git a/std/shared/src/main/scala/cats/effect/std/ConsoleCrossPlatform.scala b/std/shared/src/main/scala/cats/effect/std/ConsoleCrossPlatform.scala index a49ce2ce2b..9277fbd4e3 100644 --- a/std/shared/src/main/scala/cats/effect/std/ConsoleCrossPlatform.scala +++ b/std/shared/src/main/scala/cats/effect/std/ConsoleCrossPlatform.scala @@ -256,6 +256,7 @@ private[std] abstract class ConsoleCompanionCrossPlatform { if (len > 0) { if (builder.charAt(len - 1) == '\r') { builder.deleteCharAt(len - 1) + () } } builder.toString() diff --git a/std/shared/src/main/scala/cats/effect/std/Semaphore.scala b/std/shared/src/main/scala/cats/effect/std/Semaphore.scala index 6fc4429e89..4a8ed2b590 100644 --- a/std/shared/src/main/scala/cats/effect/std/Semaphore.scala +++ b/std/shared/src/main/scala/cats/effect/std/Semaphore.scala @@ -18,7 +18,6 @@ package cats package effect package std -import cats.Applicative import cats.effect.kernel._ import cats.effect.kernel.syntax.all._ import cats.syntax.all._ diff --git a/tests/shared/src/test/scala/cats/effect/MemoizeSpec.scala b/tests/shared/src/test/scala/cats/effect/MemoizeSpec.scala index 78aab8c27a..15b386d2b7 100644 --- a/tests/shared/src/test/scala/cats/effect/MemoizeSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/MemoizeSpec.scala @@ -42,6 +42,8 @@ class MemoizeSpec extends BaseSpec with Discipline { "Concurrent.memoize does not evaluate the effect if the inner `F[A]` isn't bound" in ticked { implicit ticker => + import cats.syntax.all._ + val op = for { ref <- Ref.of[F, Int](0) action = ref.update(_ + 1) @@ -57,6 +59,8 @@ class MemoizeSpec extends BaseSpec with Discipline { "Concurrent.memoize evaluates effect once if inner `F[A]` is bound twice" in ticked { implicit ticker => + import cats.syntax.all._ + val op = for { ref <- Ref.of[F, Int](0) action = ref.modify { s => @@ -77,6 +81,8 @@ class MemoizeSpec extends BaseSpec with Discipline { "Concurrent.memoize effect evaluates effect once if the inner `F[A]` is bound twice (race)" in ticked { implicit ticker => + import cats.syntax.all._ + val op = for { ref <- Ref.of[F, Int](0) action = ref.modify { s => @@ -108,6 +114,8 @@ class MemoizeSpec extends BaseSpec with Discipline { "Memoized effects can be canceled when there are no other active subscribers (1)" in ticked { implicit ticker => + import cats.syntax.all._ + val op = for { completed <- Ref[F].of(false) action = liftK(IO.sleep(200.millis)) >> completed.set(true) @@ -127,6 +135,8 @@ class MemoizeSpec extends BaseSpec with Discipline { "Memoized effects can be canceled when there are no other active subscribers (2)" in ticked { implicit ticker => + import cats.syntax.all._ + val op = for { completed <- Ref[F].of(false) action = liftK(IO.sleep(300.millis)) >> completed.set(true) @@ -149,6 +159,8 @@ class MemoizeSpec extends BaseSpec with Discipline { "Memoized effects can be canceled when there are no other active subscribers (3)" in ticked { implicit ticker => + import cats.syntax.all._ + val op = for { completed <- Ref[F].of(false) action = liftK(IO.sleep(300.millis)) >> completed.set(true) @@ -171,6 +183,8 @@ class MemoizeSpec extends BaseSpec with Discipline { "Running a memoized effect after it was previously canceled reruns it" in ticked { implicit ticker => + import cats.syntax.all._ + val op = for { started <- Ref[F].of(0) completed <- Ref[F].of(0) @@ -195,6 +209,8 @@ class MemoizeSpec extends BaseSpec with Discipline { "Attempting to cancel a memoized effect with active subscribers is a no-op" in ticked { implicit ticker => + import cats.syntax.all._ + val op = for { startCounter <- Ref[F].of(0) condition <- Deferred[F, Unit] diff --git a/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala b/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala index d1c057086f..694cbc7198 100644 --- a/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala @@ -848,7 +848,7 @@ class ResourceSpec extends BaseSpec with ScalaCheck with Discipline { val outerInit = Resource.make(IO.unit)(_ => IO { outerClosed = true }) val async = Async[Resource[IO, *]].async[Unit] { cb => - (inner *> Resource.eval(IO(cb(Right(()))))).as(None) + (inner *> Resource.eval(IO(cb(Right(()))))).map(_ => None) } (outerInit *> async *> waitR).use_.unsafeToFuture() @@ -933,11 +933,11 @@ class ResourceSpec extends BaseSpec with ScalaCheck with Discipline { val winner = Resource .make(IO.unit)(_ => IO { winnerClosed = true }) .evalMap(_ => IO.sleep(100.millis)) - .as("winner") + .map(_ => "winner") val loser = Resource .make(IO.unit)(_ => IO { loserClosed = true }) .evalMap(_ => IO.sleep(200.millis)) - .as("loser") + .map(_ => "loser") val target = winner.race(loser).evalMap(e => IO { results = e }) *> waitR *> Resource.eval(IO { diff --git a/tests/shared/src/test/scala/cats/effect/kernel/LensRefSpec.scala b/tests/shared/src/test/scala/cats/effect/kernel/LensRefSpec.scala index f46e7ea395..193626b629 100644 --- a/tests/shared/src/test/scala/cats/effect/kernel/LensRefSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/kernel/LensRefSpec.scala @@ -18,7 +18,7 @@ package cats package effect package kernel -import cats.{Eq, Show} +import cats.Eq import cats.data.State import scala.concurrent.duration._ @@ -148,6 +148,7 @@ class LensRefSpec extends BaseSpec with DetectPlatform { outer => op must completeAs((false, Foo(5, -1))) } + else () "tryModify - successfully modifies underlying Ref" in ticked { implicit ticker => val op = for { @@ -180,6 +181,7 @@ class LensRefSpec extends BaseSpec with DetectPlatform { outer => op must completeAs((None, Foo(5, -1))) } + else () "tryModifyState - successfully modifies underlying Ref" in ticked { implicit ticker => val op = for { diff --git a/tests/shared/src/test/scala/cats/effect/kernel/MiniSemaphoreSpec.scala b/tests/shared/src/test/scala/cats/effect/kernel/MiniSemaphoreSpec.scala index 0816ab0e9d..e9c33469dd 100644 --- a/tests/shared/src/test/scala/cats/effect/kernel/MiniSemaphoreSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/kernel/MiniSemaphoreSpec.scala @@ -18,8 +18,6 @@ package cats package effect package kernel -import cats.syntax.all._ - import scala.concurrent.duration._ class MiniSemaphoreSpec extends BaseSpec { outer => diff --git a/tests/shared/src/test/scala/cats/effect/kernel/RefSpec.scala b/tests/shared/src/test/scala/cats/effect/kernel/RefSpec.scala index 31b340186a..4fc9731352 100644 --- a/tests/shared/src/test/scala/cats/effect/kernel/RefSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/kernel/RefSpec.scala @@ -121,6 +121,7 @@ class RefSpec extends BaseSpec with DetectPlatform { outer => op must completeAs(false) } + else () "tryModifyState - modification occurs successfully" in ticked { implicit ticker => val op = for { diff --git a/tests/shared/src/test/scala/cats/effect/std/AtomicCellSpec.scala b/tests/shared/src/test/scala/cats/effect/std/AtomicCellSpec.scala index ccca849288..16cdd02f9f 100644 --- a/tests/shared/src/test/scala/cats/effect/std/AtomicCellSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/std/AtomicCellSpec.scala @@ -18,8 +18,6 @@ package cats package effect package std -import cats.syntax.all._ - import org.specs2.specification.core.Fragments import scala.concurrent.duration._ diff --git a/tests/shared/src/test/scala/cats/effect/std/DispatcherSpec.scala b/tests/shared/src/test/scala/cats/effect/std/DispatcherSpec.scala index cf95a3ead9..b920ed8e5e 100644 --- a/tests/shared/src/test/scala/cats/effect/std/DispatcherSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/std/DispatcherSpec.scala @@ -349,6 +349,7 @@ class DispatcherSpec extends BaseSpec with DetectPlatform { _ <- IO(if (rogueResult == false) { // if the rogue task is not completed then we must have failed to submit it rogueSubmitResult must beLeft + () }) } yield ok } From abe2e2e312c18a657fb75564ac4f22ac0fc3224d Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 3 Aug 2023 16:40:45 +0000 Subject: [PATCH 160/429] Formatting --- .../scala/cats/effect/kernel/AsyncPlatform.scala | 13 +++++++------ .../src/test/scala/cats/effect/kernel/RefSpec.scala | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/kernel/js/src/main/scala/cats/effect/kernel/AsyncPlatform.scala b/kernel/js/src/main/scala/cats/effect/kernel/AsyncPlatform.scala index 1046a6e0a5..73619396b1 100644 --- a/kernel/js/src/main/scala/cats/effect/kernel/AsyncPlatform.scala +++ b/kernel/js/src/main/scala/cats/effect/kernel/AsyncPlatform.scala @@ -46,12 +46,13 @@ private[kernel] trait AsyncPlatform[F[_]] { this: Async[F] => (v: A) => cb(Right(v)): js.UndefOr[js.Thenable[Unit]] @inline private[this] def mkOnRejected[A]( - cb: Either[Throwable, A] => Unit): js.Function1[Any, js.UndefOr[js.Thenable[Unit]]] = { (a: Any) => - val e = a match { - case th: Throwable => th - case _ => js.JavaScriptException(a) - } + cb: Either[Throwable, A] => Unit): js.Function1[Any, js.UndefOr[js.Thenable[Unit]]] = { + (a: Any) => + val e = a match { + case th: Throwable => th + case _ => js.JavaScriptException(a) + } - cb(Left(e)): js.UndefOr[js.Thenable[Unit]] + cb(Left(e)): js.UndefOr[js.Thenable[Unit]] } } diff --git a/tests/shared/src/test/scala/cats/effect/kernel/RefSpec.scala b/tests/shared/src/test/scala/cats/effect/kernel/RefSpec.scala index 4fc9731352..65fefd197d 100644 --- a/tests/shared/src/test/scala/cats/effect/kernel/RefSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/kernel/RefSpec.scala @@ -121,7 +121,7 @@ class RefSpec extends BaseSpec with DetectPlatform { outer => op must completeAs(false) } - else () + else () "tryModifyState - modification occurs successfully" in ticked { implicit ticker => val op = for { From 6d0dc504de180069240e3dfaca2bb988bf463885 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 3 Aug 2023 17:23:09 +0000 Subject: [PATCH 161/429] More fixes --- core/jvm/src/main/scala/cats/effect/IOApp.scala | 2 ++ .../scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala | 1 + .../jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala | 1 + core/jvm/src/main/scala/cats/effect/unsafe/TimerSkipList.scala | 1 + .../scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala | 1 + .../scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala | 1 + core/shared/src/main/scala/cats/effect/IOFiber.scala | 2 +- std/shared/src/main/scala/cats/effect/std/MapRef.scala | 3 +-- 8 files changed, 9 insertions(+), 3 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/IOApp.scala b/core/jvm/src/main/scala/cats/effect/IOApp.scala index 065f714285..816ad51053 100644 --- a/core/jvm/src/main/scala/cats/effect/IOApp.scala +++ b/core/jvm/src/main/scala/cats/effect/IOApp.scala @@ -434,6 +434,8 @@ trait IOApp { if (isStackTracing) runtime.fiberMonitor.monitorSuspended(fiber) + else + () def handleShutdown(): Unit = { if (counter.compareAndSet(1, 0)) { diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala index 4548bc3fcd..002690230e 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala @@ -243,6 +243,7 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type }, IORuntimeConfig()) } + () } _global diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala index d8e41f6a4d..1cf97dc05e 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala @@ -91,6 +91,7 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS } key.attach(head) // if key was canceled this will null attachment + () } polled diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerSkipList.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerSkipList.scala index 5cd5cd884a..700fd3a6c1 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerSkipList.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerSkipList.scala @@ -429,6 +429,7 @@ private final class TimerSkipList() extends AtomicLong(MARKER + 1L) { sequenceNu val hx = new Index(z, x, null) val nh = new Index(h.node, h, hx) // new head head.compareAndSet(h, nh) + () } if (z.isDeleted()) { diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala index 90793b9495..fbf098eec7 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala @@ -128,6 +128,7 @@ private[effect] final class EventLoopExecutorScheduler[P]( */ if (system.needsPoll(poller) || timeout != -1) system.poll(poller, timeout, reportFailure) + else () continue = !executeQueue.isEmpty() || !sleepQueue.isEmpty() || system.needsPoll(poller) } diff --git a/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala index 7ec06fdf03..6b270106a3 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala @@ -70,6 +70,7 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type }, IORuntimeConfig()) } + () } _global diff --git a/core/shared/src/main/scala/cats/effect/IOFiber.scala b/core/shared/src/main/scala/cats/effect/IOFiber.scala index b8619f3817..039f73c3cb 100644 --- a/core/shared/src/main/scala/cats/effect/IOFiber.scala +++ b/core/shared/src/main/scala/cats/effect/IOFiber.scala @@ -1538,7 +1538,7 @@ private object IOFiber { private[IOFiber] val OutcomeCanceled = Outcome.Canceled() private[effect] val RightUnit = Right(()) - def onFatalFailure(t: Throwable): Null = { + def onFatalFailure(t: Throwable): Nothing = { val interrupted = Thread.interrupted() if (IORuntime.globalFatalFailureHandled.compareAndSet(false, true)) { diff --git a/std/shared/src/main/scala/cats/effect/std/MapRef.scala b/std/shared/src/main/scala/cats/effect/std/MapRef.scala index 6cc723b396..4115713fd3 100644 --- a/std/shared/src/main/scala/cats/effect/std/MapRef.scala +++ b/std/shared/src/main/scala/cats/effect/std/MapRef.scala @@ -17,7 +17,6 @@ package cats.effect.std import cats._ -import cats.conversions.all._ import cats.data._ import cats.effect.kernel._ import cats.syntax.all._ @@ -191,7 +190,7 @@ object MapRef extends MapRefCompanionPlatform { def tryModify[B](f: Option[V] => (Option[V], B)): F[Option[B]] = // we need the suspend because we do effects inside - delay { + delay[F[Option[B]]] { val init = chm.get(k) if (init == null) { f(None) match { From 4d8279b43415aace8733a79a560c81c178dc7187 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 3 Aug 2023 18:11:55 +0000 Subject: [PATCH 162/429] Fix --- tests/shared/src/test/scala/cats/effect/std/QueueSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/shared/src/test/scala/cats/effect/std/QueueSpec.scala b/tests/shared/src/test/scala/cats/effect/std/QueueSpec.scala index 8cd5e66a99..f2101e2fd4 100644 --- a/tests/shared/src/test/scala/cats/effect/std/QueueSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/std/QueueSpec.scala @@ -278,7 +278,7 @@ class BoundedQueueSpec extends BaseSpec with QueueTests[Queue] with DetectPlatfo q <- constructor(64) produce = 0.until(fiberCount).toList.parTraverse_(producer(q, _)) - consume = 0.until(fiberCount).toList.parTraverse(consumer(q, _)).map(_.flatten) + consume = 0.until(fiberCount).toList.parTraverse(consumer(q, _)).map(_.flatMap(identity)) results <- produce &> consume From 27c7bbae2d7561712ec3e0ebb4c0a4972ba63a5b Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 3 Aug 2023 18:15:02 +0000 Subject: [PATCH 163/429] Formatting --- tests/shared/src/test/scala/cats/effect/std/QueueSpec.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/shared/src/test/scala/cats/effect/std/QueueSpec.scala b/tests/shared/src/test/scala/cats/effect/std/QueueSpec.scala index f2101e2fd4..965cd6a35d 100644 --- a/tests/shared/src/test/scala/cats/effect/std/QueueSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/std/QueueSpec.scala @@ -278,7 +278,11 @@ class BoundedQueueSpec extends BaseSpec with QueueTests[Queue] with DetectPlatfo q <- constructor(64) produce = 0.until(fiberCount).toList.parTraverse_(producer(q, _)) - consume = 0.until(fiberCount).toList.parTraverse(consumer(q, _)).map(_.flatMap(identity)) + consume = 0 + .until(fiberCount) + .toList + .parTraverse(consumer(q, _)) + .map(_.flatMap(identity)) results <- produce &> consume From 418aeafcdffefc5ef551af5b2d145687ae8b0eea Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 3 Aug 2023 18:38:04 +0000 Subject: [PATCH 164/429] No fatal warnings in docs for Scala 3 --- build.sbt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index bb3619999e..2f1bf5a039 100644 --- a/build.sbt +++ b/build.sbt @@ -1027,4 +1027,8 @@ lazy val stressTests = project ) .enablePlugins(NoPublishPlugin, JCStressPlugin) -lazy val docs = project.in(file("site-docs")).dependsOn(core.jvm).enablePlugins(MdocPlugin) +lazy val docs = project + .in(file("site-docs")) + .dependsOn(core.jvm) + .enablePlugins(MdocPlugin) + .settings(tlFatalWarnings := { if (tlIsScala3.value) false else tlFatalWarnings.value }) From 110875d5001a08f085efe1c0f8744d1c74b05695 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 3 Aug 2023 19:05:51 +0000 Subject: [PATCH 165/429] More compiler bugs --- build.sbt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 2f1bf5a039..74fe6ed358 100644 --- a/build.sbt +++ b/build.sbt @@ -358,7 +358,8 @@ lazy val root = project name := "cats-effect", ScalaUnidoc / unidoc / unidocProjectFilter := { undocumentedRefs.foldLeft(inAnyProject)((acc, a) => acc -- inProjects(a)) - } + }, + scalacOptions -= "-Xsource:3" // bugged ) lazy val rootJVM = project From 2a96662ed1420086a8b210b1fcf33708fa635a5a Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Fri, 4 Aug 2023 15:22:28 +0000 Subject: [PATCH 166/429] Revert commit(s) c79636526, 26065dd75 --- .github/workflows/ci.yml | 165 ++++++++++++++++++++++++++++++++------- project/plugins.sbt | 2 +- 2 files changed, 137 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2a2a57e63..f2c77d4ba8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -116,13 +116,22 @@ jobs: with: fetch-depth: 0 + - name: Download Java (temurin@8) + id: download-java-temurin-8 + if: matrix.java == 'temurin@8' + uses: typelevel/download-java@v2 + with: + distribution: temurin + java-version: 8 + - name: Setup Java (temurin@8) id: setup-java-temurin-8 if: matrix.java == 'temurin@8' uses: actions/setup-java@v3 with: - distribution: temurin + distribution: jdkfile java-version: 8 + jdkFile: ${{ steps.download-java-temurin-8.outputs.jdkFile }} cache: sbt - name: sbt update @@ -130,13 +139,22 @@ jobs: shell: bash run: sbt '++ ${{ matrix.scala }}' reload +update + - name: Download Java (temurin@11) + id: download-java-temurin-11 + if: matrix.java == 'temurin@11' + uses: typelevel/download-java@v2 + with: + distribution: temurin + java-version: 11 + - name: Setup Java (temurin@11) id: setup-java-temurin-11 if: matrix.java == 'temurin@11' uses: actions/setup-java@v3 with: - distribution: temurin + distribution: jdkfile java-version: 11 + jdkFile: ${{ steps.download-java-temurin-11.outputs.jdkFile }} cache: sbt - name: sbt update @@ -144,13 +162,22 @@ jobs: shell: bash run: sbt '++ ${{ matrix.scala }}' reload +update + - name: Download Java (temurin@17) + id: download-java-temurin-17 + if: matrix.java == 'temurin@17' + uses: typelevel/download-java@v2 + with: + distribution: temurin + java-version: 17 + - name: Setup Java (temurin@17) id: setup-java-temurin-17 if: matrix.java == 'temurin@17' uses: actions/setup-java@v3 with: - distribution: temurin + distribution: jdkfile java-version: 17 + jdkFile: ${{ steps.download-java-temurin-17.outputs.jdkFile }} cache: sbt - name: sbt update @@ -158,13 +185,22 @@ jobs: shell: bash run: sbt '++ ${{ matrix.scala }}' reload +update + - name: Download Java (graalvm@11) + id: download-java-graalvm-11 + if: matrix.java == 'graalvm@11' + uses: typelevel/download-java@v2 + with: + distribution: graalvm + java-version: 11 + - name: Setup Java (graalvm@11) id: setup-java-graalvm-11 if: matrix.java == 'graalvm@11' - uses: graalvm/setup-graalvm@v1 + uses: actions/setup-java@v3 with: - distribution: graalvm + distribution: jdkfile java-version: 11 + jdkFile: ${{ steps.download-java-graalvm-11.outputs.jdkFile }} cache: sbt - name: sbt update @@ -252,12 +288,12 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) shell: bash - run: mkdir -p testkit/native/target std/jvm/target kernel-testkit/jvm/target testkit/js/target core/native/target site-docs/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target kernel-testkit/js/target core/jvm/target kernel/native/target laws/jvm/target std/native/target testkit/jvm/target project/target + run: mkdir -p benchmarks/target testkit/native/target target stress-tests/target std/jvm/target example/js/target kernel-testkit/jvm/target testkit/js/target rootJS/target core/native/target site-docs/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target graalvm-example/target kernel-testkit/js/target core/jvm/target rootJVM/target rootNative/target example/native/target kernel/native/target example/jvm/target laws/jvm/target std/native/target testkit/jvm/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) shell: bash - run: tar cf targets.tar testkit/native/target std/jvm/target kernel-testkit/jvm/target testkit/js/target core/native/target site-docs/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target kernel-testkit/js/target core/jvm/target kernel/native/target laws/jvm/target std/native/target testkit/jvm/target project/target + run: tar cf targets.tar benchmarks/target testkit/native/target target stress-tests/target std/jvm/target example/js/target kernel-testkit/jvm/target testkit/js/target rootJS/target core/native/target site-docs/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target graalvm-example/target kernel-testkit/js/target core/jvm/target rootJVM/target rootNative/target example/native/target kernel/native/target example/jvm/target laws/jvm/target std/native/target testkit/jvm/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) @@ -273,6 +309,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] + scala: [2.13.11] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -285,57 +322,93 @@ jobs: with: fetch-depth: 0 + - name: Download Java (temurin@8) + id: download-java-temurin-8 + if: matrix.java == 'temurin@8' + uses: typelevel/download-java@v2 + with: + distribution: temurin + java-version: 8 + - name: Setup Java (temurin@8) id: setup-java-temurin-8 if: matrix.java == 'temurin@8' uses: actions/setup-java@v3 with: - distribution: temurin + distribution: jdkfile java-version: 8 + jdkFile: ${{ steps.download-java-temurin-8.outputs.jdkFile }} cache: sbt - name: sbt update if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' - run: sbt reload +update + run: sbt '++ ${{ matrix.scala }}' reload +update + + - name: Download Java (temurin@11) + id: download-java-temurin-11 + if: matrix.java == 'temurin@11' + uses: typelevel/download-java@v2 + with: + distribution: temurin + java-version: 11 - name: Setup Java (temurin@11) id: setup-java-temurin-11 if: matrix.java == 'temurin@11' uses: actions/setup-java@v3 with: - distribution: temurin + distribution: jdkfile java-version: 11 + jdkFile: ${{ steps.download-java-temurin-11.outputs.jdkFile }} cache: sbt - name: sbt update if: matrix.java == 'temurin@11' && steps.setup-java-temurin-11.outputs.cache-hit == 'false' - run: sbt reload +update + run: sbt '++ ${{ matrix.scala }}' reload +update + + - name: Download Java (temurin@17) + id: download-java-temurin-17 + if: matrix.java == 'temurin@17' + uses: typelevel/download-java@v2 + with: + distribution: temurin + java-version: 17 - name: Setup Java (temurin@17) id: setup-java-temurin-17 if: matrix.java == 'temurin@17' uses: actions/setup-java@v3 with: - distribution: temurin + distribution: jdkfile java-version: 17 + jdkFile: ${{ steps.download-java-temurin-17.outputs.jdkFile }} cache: sbt - name: sbt update if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' - run: sbt reload +update + run: sbt '++ ${{ matrix.scala }}' reload +update + + - name: Download Java (graalvm@11) + id: download-java-graalvm-11 + if: matrix.java == 'graalvm@11' + uses: typelevel/download-java@v2 + with: + distribution: graalvm + java-version: 11 - name: Setup Java (graalvm@11) id: setup-java-graalvm-11 if: matrix.java == 'graalvm@11' - uses: graalvm/setup-graalvm@v1 + uses: actions/setup-java@v3 with: - distribution: graalvm + distribution: jdkfile java-version: 11 + jdkFile: ${{ steps.download-java-graalvm-11.outputs.jdkFile }} cache: sbt - name: sbt update if: matrix.java == 'graalvm@11' && steps.setup-java-graalvm-11.outputs.cache-hit == 'false' - run: sbt reload +update + run: sbt '++ ${{ matrix.scala }}' reload +update - name: Download target directories (3.3.0, ciJVM) uses: actions/download-artifact@v3 @@ -472,7 +545,7 @@ jobs: SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} SONATYPE_CREDENTIAL_HOST: ${{ secrets.SONATYPE_CREDENTIAL_HOST }} - run: sbt tlCiRelease + run: sbt '++ ${{ matrix.scala }}' tlCiRelease dependency-submission: name: Submit Dependencies @@ -480,6 +553,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] + scala: [2.13.11] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -492,60 +566,93 @@ jobs: with: fetch-depth: 0 + - name: Download Java (temurin@8) + id: download-java-temurin-8 + if: matrix.java == 'temurin@8' + uses: typelevel/download-java@v2 + with: + distribution: temurin + java-version: 8 + - name: Setup Java (temurin@8) id: setup-java-temurin-8 if: matrix.java == 'temurin@8' uses: actions/setup-java@v3 with: - distribution: temurin + distribution: jdkfile java-version: 8 + jdkFile: ${{ steps.download-java-temurin-8.outputs.jdkFile }} cache: sbt - name: sbt update if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' - run: sbt reload +update + run: sbt '++ ${{ matrix.scala }}' reload +update + + - name: Download Java (temurin@11) + id: download-java-temurin-11 + if: matrix.java == 'temurin@11' + uses: typelevel/download-java@v2 + with: + distribution: temurin + java-version: 11 - name: Setup Java (temurin@11) id: setup-java-temurin-11 if: matrix.java == 'temurin@11' uses: actions/setup-java@v3 with: - distribution: temurin + distribution: jdkfile java-version: 11 + jdkFile: ${{ steps.download-java-temurin-11.outputs.jdkFile }} cache: sbt - name: sbt update if: matrix.java == 'temurin@11' && steps.setup-java-temurin-11.outputs.cache-hit == 'false' - run: sbt reload +update + run: sbt '++ ${{ matrix.scala }}' reload +update + + - name: Download Java (temurin@17) + id: download-java-temurin-17 + if: matrix.java == 'temurin@17' + uses: typelevel/download-java@v2 + with: + distribution: temurin + java-version: 17 - name: Setup Java (temurin@17) id: setup-java-temurin-17 if: matrix.java == 'temurin@17' uses: actions/setup-java@v3 with: - distribution: temurin + distribution: jdkfile java-version: 17 + jdkFile: ${{ steps.download-java-temurin-17.outputs.jdkFile }} cache: sbt - name: sbt update if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' - run: sbt reload +update + run: sbt '++ ${{ matrix.scala }}' reload +update + + - name: Download Java (graalvm@11) + id: download-java-graalvm-11 + if: matrix.java == 'graalvm@11' + uses: typelevel/download-java@v2 + with: + distribution: graalvm + java-version: 11 - name: Setup Java (graalvm@11) id: setup-java-graalvm-11 if: matrix.java == 'graalvm@11' - uses: graalvm/setup-graalvm@v1 + uses: actions/setup-java@v3 with: - distribution: graalvm + distribution: jdkfile java-version: 11 + jdkFile: ${{ steps.download-java-graalvm-11.outputs.jdkFile }} cache: sbt - name: sbt update if: matrix.java == 'graalvm@11' && steps.setup-java-graalvm-11.outputs.cache-hit == 'false' - run: sbt reload +update + run: sbt '++ ${{ matrix.scala }}' reload +update - name: Submit Dependencies uses: scalacenter/sbt-dependency-submission@v2 - with: - modules-ignore: cats-effect-benchmarks_3 cats-effect-benchmarks_2.12 cats-effect-benchmarks_2.13 cats-effect_3 cats-effect_2.12 cats-effect_2.13 cats-effect-stress-tests_3 cats-effect-stress-tests_2.12 cats-effect-stress-tests_2.13 cats-effect-example_sjs1_3 cats-effect-example_sjs1_2.12 cats-effect-example_sjs1_2.13 rootjs_3 rootjs_2.12 rootjs_2.13 cats-effect-graalvm-example_3 cats-effect-graalvm-example_2.12 cats-effect-graalvm-example_2.13 cats-effect-tests_sjs1_3 cats-effect-tests_sjs1_2.12 cats-effect-tests_sjs1_2.13 rootjvm_3 rootjvm_2.12 rootjvm_2.13 rootnative_3 rootnative_2.12 rootnative_2.13 cats-effect-example_native0.4_3 cats-effect-example_native0.4_2.12 cats-effect-example_native0.4_2.13 cats-effect-example_3 cats-effect-example_2.12 cats-effect-example_2.13 cats-effect-tests_3 cats-effect-tests_2.12 cats-effect-tests_2.13 cats-effect-tests_native0.4_3 cats-effect-tests_native0.4_2.12 cats-effect-tests_native0.4_2.13 - configs-ignore: test scala-tool scala-doc-tool test-internal diff --git a/project/plugins.sbt b/project/plugins.sbt index b9571f2df8..afe1e03800 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.0-RC10") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.0-M10") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.1") From 07cc1ae831d51e0272b0a88586febbe0e6c698f8 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Fri, 4 Aug 2023 15:22:34 +0000 Subject: [PATCH 167/429] Update sbt-typelevel to 0.5.0-RC10 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 142bf7c99b..b9571f2df8 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.0-RC9") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.0-RC10") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.1") From 812106e8799a8369b7d643b158d9f693d8c1f90d Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 12:06:30 +0000 Subject: [PATCH 168/429] Update specs2-core, specs2-scalacheck to 4.20.1 in series/3.x --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 74fe6ed358..b4eb61c66a 100644 --- a/build.sbt +++ b/build.sbt @@ -306,7 +306,7 @@ ThisBuild / apiURL := Some(url("https://typelevel.org/cats-effect/api/3.x/")) ThisBuild / autoAPIMappings := true val CatsVersion = "2.9.0" -val Specs2Version = "4.20.0" +val Specs2Version = "4.20.1" val ScalaCheckVersion = "1.17.0" val DisciplineVersion = "1.4.0" val CoopVersion = "1.2.0" From 8c68b69094c35947da7b82aeb39734d7af378c07 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 8 Aug 2023 01:52:21 +0000 Subject: [PATCH 169/429] First attempt at unsychronized timer heap --- .cirrus.yml | 6 +- build.sbt | 19 +- .../scala/cats/effect/unsafe/TimerHeap.scala | 297 ++++++++++++++++++ .../unsafe/WorkStealingThreadPool.scala | 65 ++-- .../cats/effect/unsafe/WorkerThread.scala | 13 +- project/plugins.sbt | 1 - .../cats/effect/unsafe/SkipListTest1.scala | 81 ----- .../cats/effect/unsafe/SkipListTest2.scala | 94 ------ .../cats/effect/unsafe/SkipListTest3.scala | 92 ------ .../cats/effect/unsafe/SkipListTest4.scala | 81 ----- .../cats/effect/unsafe/SkipListTest5.scala | 73 ----- .../cats/effect/unsafe/SleepersSpec.scala | 68 ++-- .../effect/unsafe/TimerSkipListIOSpec.scala | 146 --------- 13 files changed, 369 insertions(+), 667 deletions(-) create mode 100644 core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala delete mode 100644 stress-tests/src/test/scala/cats/effect/unsafe/SkipListTest1.scala delete mode 100644 stress-tests/src/test/scala/cats/effect/unsafe/SkipListTest2.scala delete mode 100644 stress-tests/src/test/scala/cats/effect/unsafe/SkipListTest3.scala delete mode 100644 stress-tests/src/test/scala/cats/effect/unsafe/SkipListTest4.scala delete mode 100644 stress-tests/src/test/scala/cats/effect/unsafe/SkipListTest5.scala delete mode 100644 tests/jvm/src/test/scala/cats/effect/unsafe/TimerSkipListIOSpec.scala diff --git a/.cirrus.yml b/.cirrus.yml index 768443c3b6..c5accf9cf2 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -7,7 +7,7 @@ jvm_highcore_task: - name: JVM high-core-count 2.12 script: sbt '++ 2.12' testsJVM/test - name: JVM high-core-count 2.13 - script: sbt '++ 2.13' testsJVM/test stressTests/Jcstress/run + script: sbt '++ 2.13' testsJVM/test - name: JVM high-core-count 3 script: sbt '++ 3' testsJVM/test @@ -20,7 +20,7 @@ jvm_highcore_task: # - name: JVM ARM high-core-count 2.12 # script: sbt '++ 2.12' testsJVM/test # - name: JVM ARM high-core-count 2.13 -# script: sbt '++ 2.13' testsJVM/test stressTests/Jcstress/run +# script: sbt '++ 2.13' testsJVM/test # - name: JVM ARM high-core-count 3 # script: sbt '++ 3' testsJVM/test @@ -37,7 +37,7 @@ jvm_macos_highcore_task: - name: JVM Apple Silicon high-core-count 2.13 script: - brew install sbt - - sbt '++ 2.13' testsJVM/test stressTests/Jcstress/run + - sbt '++ 2.13' testsJVM/test - name: JVM Apple Silicon high-core-count 3 script: - brew install sbt diff --git a/build.sbt b/build.sbt index 82a522f7a7..0badeec4b2 100644 --- a/build.sbt +++ b/build.sbt @@ -347,7 +347,6 @@ val nativeProjects: Seq[ProjectReference] = val undocumentedRefs = jsProjects ++ nativeProjects ++ Seq[ProjectReference]( benchmarks, - stressTests, example.jvm, graalVMExample, tests.jvm, @@ -376,8 +375,7 @@ lazy val rootJVM = project std.jvm, example.jvm, graalVMExample, - benchmarks, - stressTests) + benchmarks) .enablePlugins(NoPublishPlugin) lazy val rootJS = project.aggregate(jsProjects: _*).enablePlugins(NoPublishPlugin) @@ -401,7 +399,6 @@ lazy val kernel = crossProject(JSPlatform, JVMPlatform, NativePlatform) ProblemFilters.exclude[Problem]("cats.effect.kernel.GenConcurrent#Memoize*") ) ) - .disablePlugins(JCStressPlugin) .jsSettings( libraryDependencies += "org.scala-js" %%% "scala-js-macrotask-executor" % MacrotaskExecutorVersion % Test ) @@ -437,7 +434,6 @@ lazy val kernelTestkit = crossProject(JSPlatform, JVMPlatform, NativePlatform) "cats.effect.kernel.testkit.TestContext#Task.copy") ) ) - .disablePlugins(JCStressPlugin) /** * The laws which constrain the abstractions. This is split from kernel to avoid jar file and @@ -453,7 +449,6 @@ lazy val laws = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.typelevel" %%% "cats-laws" % CatsVersion, "org.typelevel" %%% "discipline-specs2" % DisciplineVersion % Test) ) - .disablePlugins(JCStressPlugin) /** * Concrete, production-grade implementations of the abstractions. Or, more simply-put: IO. Also @@ -835,7 +830,6 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) } else Seq() } ) - .disablePlugins(JCStressPlugin) /** * Test support for the core project, providing various helpful instances like ScalaCheck @@ -851,7 +845,6 @@ lazy val testkit = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.specs2" %%% "specs2-core" % Specs2Version % Test ) ) - .disablePlugins(JCStressPlugin) /** * Unit tests for the core project, utilizing the support provided by testkit. @@ -980,7 +973,6 @@ lazy val std = crossProject(JSPlatform, JVMPlatform, NativePlatform) ProblemFilters.exclude[MissingClassProblem]("cats.effect.std.JavaSecureRandom$") ) ) - .disablePlugins(JCStressPlugin) /** * A trivial pair of trivial example apps primarily used to show that IOApp works as a practical @@ -1019,13 +1011,4 @@ lazy val benchmarks = project "-Dcats.effect.tracing.exceptions.enhanced=false")) .enablePlugins(NoPublishPlugin, JmhPlugin) -lazy val stressTests = project - .in(file("stress-tests")) - .dependsOn(core.jvm, std.jvm) - .settings( - name := "cats-effect-stress-tests", - Jcstress / version := "0.16" - ) - .enablePlugins(NoPublishPlugin, JCStressPlugin) - lazy val docs = project.in(file("site-docs")).dependsOn(core.jvm).enablePlugins(MdocPlugin) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala new file mode 100644 index 0000000000..43b8946b82 --- /dev/null +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -0,0 +1,297 @@ +/* + * Copyright 2020-2023 Typelevel + * + * 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. + */ + +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package cats.effect +package unsafe + +import scala.annotation.tailrec + +import java.util.concurrent.ThreadLocalRandom + +private final class TimerHeap { + + // The index 0 is not used; the root is at index 1. + // This is standard practice in binary heaps, to simplify arithmetics. + private[this] var heap: Array[TimerNode] = new Array(8) // TODO what initial value + private[this] var size: Int = 0 + + private[this] val RightUnit = IOFiber.RightUnit + + def peekFirstTriggerTime(): Long = { + val heap = this.heap // local copy + if (size > 0) { + val node = heap(1) + if (node ne null) + node.triggerTime + else java.lang.Long.MIN_VALUE + } else java.lang.Long.MIN_VALUE + } + + /** + * only called by owner thread + * + * returns `true` if we removed at least one timer TODO only return true if we actually + * triggered a timer, not just removed one + */ + def trigger(now: Long): Boolean = { + val heap = this.heap // local copy + + @tailrec + def loop(triggered: Boolean): Boolean = if (size > 0) { + val root = heap(1) + if (canRemove(root, now)) { + heap(1) = heap(size) + heap(size) = null + size -= 1 + fixDown(1) + + loop(true) + } else triggered + } else triggered + + loop(false) + } + + /** + * called by other threads + */ + def steal(now: Long): Boolean = { + def go(heap: Array[TimerNode], size: Int, m: Int): Boolean = + if (m <= size) { + val node = heap(m) + if ((node ne null) && triggered(node, now)) { + go(heap, size, 2 * m) + go(heap, size, 2 * m + 1) + true + } else false + } else false + + val heap = this.heap // local copy + val size = Math.min(this.size, heap.length - 1) + go(heap, size, 1) + } + + /** + * only called by owner thread + */ + def insert( + now: Long, + delay: Long, + callback: Right[Nothing, Unit] => Unit, + tlr: ThreadLocalRandom + ): Function0[Unit] with Runnable = if (size > 0) { + val heap = this.heap // local copy + val node = new TimerNode(now + delay, callback) + + if (canRemove(heap(1), now)) { // see if we can just replace the root + heap(1) = node + fixDown(1) + } else { // look for a canceled node we can replace + + @tailrec + def loop(m: Int, entropy: Int): Unit = if (m <= size) { + if (heap(m).isCanceled()) { // we found a spot! + heap(m) = node + fixUpOrDown(m) + } else loop(2 * m + (entropy % 2), entropy >>> 1) + } else { // insert at the end + val heap = growIfNeeded() // new heap array if it grew + size += 1 + heap(size) = node + fixUp(size) + } + + val entropy = tlr.nextInt() + loop(2 + (entropy % 2), entropy >>> 1) + } + + node + } else { + val node = new TimerNode(now + delay, callback) + this.heap(1) = node + size += 1 + node + } + + /** + * For testing + */ + private[unsafe] final def insertTlr( + now: Long, + delay: Long, + callback: Right[Nothing, Unit] => Unit + ): Runnable = { + insert(now, delay, callback, ThreadLocalRandom.current()) + } + + /** + * Determines if a node can be removed. Triggers it if relevant. + */ + private[this] def canRemove(node: TimerNode, now: Long): Boolean = + node.isCanceled() || triggered(node, now) + + private[this] def triggered(node: TimerNode, now: Long): Boolean = + if (cmp(node.triggerTime, now) <= 0) { // triggerTime <= now + node.trigger(RightUnit) + true + } else false + + private[this] def growIfNeeded(): Array[TimerNode] = { + val heap = this.heap // local copy + if (size >= heap.length - 1) { + val newHeap = new Array[TimerNode](heap.length * 2) + System.arraycopy(heap, 1, newHeap, 1, heap.length - 1) + this.heap = newHeap + newHeap + } else heap + } + + /** + * Fixes the heap property around the child at index `m`, either up the tree or down the tree, + * depending on which side is found to violate the heap property. + */ + private[this] def fixUpOrDown(m: Int): Unit = { + val heap = this.heap // local copy + if (m > 1 && cmp(heap(m >> 1), heap(m)) > 0) + fixUp(m) + else + fixDown(m) + } + + /** + * Fixes the heap property from the last child at index `size` up the tree, towards the root. + * Along the way we remove up to one canceled node. + */ + private[this] def fixUp(m: Int): Unit = { + val heap = this.heap // local copy + + /* At each step, even though `m` changes, the element moves with it, and + * hence heap(m) is always the same initial `heapAtM`. + */ + val heapAtM = heap(m) + + @tailrec + def loop(m: Int): Unit = { + if (m > 1) { + val parent = m >> 1 + val heapAtParent = heap(parent) + if (cmp(heapAtParent, heapAtM) > 0) { + heap(parent) = heapAtM + heap(m) = heapAtParent + loop(parent) + } + } + } + + loop(m) + } + + /** + * Fixes the heap property from the child at index `m` down the tree, towards the leaves. + */ + private[this] def fixDown(m: Int): Unit = { + val heap = this.heap // local copy + + /* At each step, even though `m` changes, the element moves with it, and + * hence heap(m) is always the same initial `heapAtM`. + */ + val heapAtM = heap(m) + + @tailrec + def loop(m: Int): Unit = { + var j = 2 * m // left child of `m` + if (j <= size) { + var heapAtJ = heap(j) + + // if the left child is greater than the right child, switch to the right child + if (j < size) { + val heapAtJPlus1 = heap(j + 1) + if (cmp(heapAtJ, heapAtJPlus1) > 0) { + j += 1 + heapAtJ = heapAtJPlus1 + } + } + + // if the node `m` is greater than the selected child, swap and recurse + if (cmp(heapAtM, heapAtJ) > 0) { + heap(m) = heapAtJ + heap(j) = heapAtM + loop(j) + } + } + } + + loop(m) + } + + /** + * Compares trigger times. + * + * The trigger times are `System.nanoTime` longs, so they have to be compared in a peculiar + * way (see javadoc there). This makes this order non-transitive, which is quite bad. However, + * `computeTriggerTime` makes sure that there is no overflow here, so we're okay. + */ + private[this] def cmp( + xTriggerTime: Long, + yTriggerTime: Long + ): Int = { + val d = xTriggerTime - yTriggerTime + java.lang.Long.signum(d) + } + + private[this] def cmp(x: TimerNode, y: TimerNode): Int = + cmp(x.triggerTime, y.triggerTime) + + override def toString() = heap.drop(1).take(size).mkString("TimerHeap(", ", ", ")") + +} + +private final class TimerNode( + val triggerTime: Long, + private[this] var callback: Right[Nothing, Unit] => Unit) + extends Function0[Unit] + with Runnable { + + def trigger(rightUnit: Right[Nothing, Unit]): Unit = { + val back = callback + callback = null + if (back ne null) back(rightUnit) + } + + /** + * racy cancelation + */ + def apply(): Unit = callback = null + + def run() = apply() + + def isCanceled(): Boolean = callback eq null + + override def toString() = s"TimerNode($triggerTime, $callback})" + +} diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index cd740b0b41..ec2eb046f7 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -41,7 +41,7 @@ import java.time.Instant import java.time.temporal.ChronoField import java.util.Comparator import java.util.concurrent.{ConcurrentSkipListSet, ThreadLocalRandom} -import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger} +import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger, AtomicReference} import java.util.concurrent.locks.LockSupport /** @@ -77,7 +77,7 @@ private[effect] final class WorkStealingThreadPool( */ private[this] val workerThreads: Array[WorkerThread] = new Array(threadCount) private[unsafe] val localQueues: Array[LocalQueue] = new Array(threadCount) - private[unsafe] val sleepers: Array[TimerSkipList] = new Array(threadCount) + private[unsafe] val sleepers: Array[TimerHeap] = new Array(threadCount) private[unsafe] val parkedSignals: Array[AtomicBoolean] = new Array(threadCount) private[unsafe] val fiberBags: Array[WeakBag[Runnable]] = new Array(threadCount) @@ -118,8 +118,8 @@ private[effect] final class WorkStealingThreadPool( while (i < threadCount) { val queue = new LocalQueue() localQueues(i) = queue - val sleepersList = new TimerSkipList() - sleepers(i) = sleepersList + val sleepersHeap = new TimerHeap() + sleepers(i) = sleepersHeap val parkedSignal = new AtomicBoolean(false) parkedSignals(i) = parkedSignal val index = i @@ -132,7 +132,7 @@ private[effect] final class WorkStealingThreadPool( parkedSignal, externalQueue, fiberBag, - sleepersList, + sleepersHeap, this) workerThreads(i) = thread i += 1 @@ -231,18 +231,7 @@ private[effect] final class WorkStealingThreadPool( // (note: it doesn't matter if we try to steal // from ourselves). val index = (from + i) % threadCount - val tsl = sleepers(index) - var invoked = false // whether we successfully invoked a timer - var cont = true - while (cont) { - val cb = tsl.pollFirstIfTriggered(now) - if (cb ne null) { - cb(RightUnit) - invoked = true - } else { - cont = false - } - } + val invoked = sleepers(index).steal(now) // whether we successfully invoked a timer if (invoked) { // we did some work, don't @@ -313,15 +302,15 @@ private[effect] final class WorkStealingThreadPool( * @param index * The index of the thread to notify (must be less than `threadCount`). */ - private[this] final def notifyForTimer(index: Int): Unit = { - val signal = parkedSignals(index) - if (signal.getAndSet(false)) { - state.getAndAdd(DeltaSearching) - workerThreadPublisher.get() - val worker = workerThreads(index) - LockSupport.unpark(worker) - } // else: was already unparked - } + // private[this] final def notifyForTimer(index: Int): Unit = { + // val signal = parkedSignals(index) + // if (signal.getAndSet(false)) { + // state.getAndAdd(DeltaSearching) + // workerThreadPublisher.get() + // val worker = workerThreads(index) + // LockSupport.unpark(worker) + // } // else: was already unparked + // } /** * Checks the number of active and searching worker threads and decides whether another thread @@ -616,8 +605,6 @@ private[effect] final class WorkStealingThreadPool( now.getEpochSecond() * 1000000 + now.getLong(ChronoField.MICRO_OF_SECOND) } - private[this] val RightUnit = IOFiber.RightUnit - /** * Tries to call the current worker's `sleep`, but falls back to `sleepExternal` if needed. */ @@ -645,20 +632,22 @@ private[effect] final class WorkStealingThreadPool( private[this] final def sleepExternal( delay: FiniteDuration, callback: Right[Nothing, Unit] => Unit): Function0[Unit] with Runnable = { - val random = ThreadLocalRandom.current() - val idx = random.nextInt(threadCount) - val tsl = sleepers(idx) - val cancel = tsl.insert( - now = System.nanoTime(), - delay = delay.toNanos, - callback = callback, - tlr = random - ) - notifyForTimer(idx) + val cancel = new AtomicReference[Function0[Unit]] with Function0[Unit] with Runnable { + def apply(): Unit = { + val back = get() + if (back ne null) back() + } + + def run() = apply() + } + + scheduleExternal(() => cancel.lazySet(sleepInternal(delay, callback))) + cancel } override def sleep(delay: FiniteDuration, task: Runnable): Runnable = { + // TODO task should be run at most once sleepInternal(delay, _ => task.run()) } diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala index 849e71a2d4..68a1f57e94 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala @@ -53,7 +53,7 @@ private final class WorkerThread( private[this] val external: ScalQueue[AnyRef], // A worker-thread-local weak bag for tracking suspended fibers. private[this] var fiberBag: WeakBag[Runnable], - private[this] var sleepers: TimerSkipList, + private[this] var sleepers: TimerHeap, // Reference to the `WorkStealingThreadPool` in which this thread operates. private[this] val pool: WorkStealingThreadPool) extends Thread @@ -256,7 +256,6 @@ private final class WorkerThread( val self = this random = ThreadLocalRandom.current() val rnd = random - val RightUnit = IOFiber.RightUnit /* * A counter (modulo `ExternalQueueTicks`) which represents the @@ -716,15 +715,7 @@ private final class WorkerThread( case _ => // Call all of our expired timers: - var cont = true - while (cont) { - val cb = sleepers.pollFirstIfTriggered(now) - if (cb ne null) { - cb(RightUnit) - } else { - cont = false - } - } + sleepers.trigger(now) // Check the queue bypass reference before dequeueing from the local // queue. diff --git a/project/plugins.sbt b/project/plugins.sbt index 943fe02da5..d999f79160 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -7,7 +7,6 @@ addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.1") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.13") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.1") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.4") -addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.7") addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.5.0") diff --git a/stress-tests/src/test/scala/cats/effect/unsafe/SkipListTest1.scala b/stress-tests/src/test/scala/cats/effect/unsafe/SkipListTest1.scala deleted file mode 100644 index 0857d5378a..0000000000 --- a/stress-tests/src/test/scala/cats/effect/unsafe/SkipListTest1.scala +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2020-2023 Typelevel - * - * 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.unsafe - -import org.openjdk.jcstress.annotations.{Outcome => JOutcome, Ref => _, _} -import org.openjdk.jcstress.annotations.Expect._ -import org.openjdk.jcstress.annotations.Outcome.Outcomes -import org.openjdk.jcstress.infra.results.JJJJ_Result - -@JCStressTest -@State -@Description("TimerSkipList insert/pollFirstIfTriggered race") -@Outcomes( - Array( - new JOutcome( - id = Array("1024, -9223372036854775679, 1, 0"), - expect = ACCEPTABLE_INTERESTING, - desc = "insert won"), - new JOutcome( - id = Array("1024, -9223372036854775679, 0, 1"), - expect = ACCEPTABLE_INTERESTING, - desc = "pollFirst won") - )) -class SkipListTest1 { - - private[this] val headCb = - newCallback() - - private[this] val m = { - val m = new TimerSkipList - // head is 1025L: - m.insertTlr(now = 1L, delay = 1024L, callback = headCb) - for (i <- 2 to 128) { - m.insertTlr(now = i.toLong, delay = 1024L, callback = newCallback()) - } - m - } - - private[this] val newCb = - newCallback() - - @Actor - def insert(r: JJJJ_Result): Unit = { - // head is 1025L now, we insert 1024L: - val cancel = m.insertTlr(now = 128L, delay = 896L, callback = newCb).asInstanceOf[m.Node] - r.r1 = cancel.triggerTime - r.r2 = cancel.sequenceNum - } - - @Actor - def pollFirst(r: JJJJ_Result): Unit = { - val cb = m.pollFirstIfTriggered(now = 2048L) - r.r3 = if (cb eq headCb) 0L else if (cb eq newCb) 1L else -1L - } - - @Arbiter - def arbiter(r: JJJJ_Result): Unit = { - val otherCb = m.pollFirstIfTriggered(now = 2048L) - r.r4 = if (otherCb eq headCb) 0L else if (otherCb eq newCb) 1L else -1L - } - - private[this] final def newCallback(): Right[Nothing, Unit] => Unit = { - new Function1[Right[Nothing, Unit], Unit] with Serializable { - final override def apply(r: Right[Nothing, Unit]): Unit = () - } - } -} diff --git a/stress-tests/src/test/scala/cats/effect/unsafe/SkipListTest2.scala b/stress-tests/src/test/scala/cats/effect/unsafe/SkipListTest2.scala deleted file mode 100644 index c8669cf5ed..0000000000 --- a/stress-tests/src/test/scala/cats/effect/unsafe/SkipListTest2.scala +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2020-2023 Typelevel - * - * 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.unsafe - -import org.openjdk.jcstress.annotations.{Outcome => JOutcome, Ref => _, _} -import org.openjdk.jcstress.annotations.Expect._ -import org.openjdk.jcstress.annotations.Outcome.Outcomes -import org.openjdk.jcstress.infra.results.JJJJJJ_Result - -@JCStressTest -@State -@Description("TimerSkipList insert/insert race") -@Outcomes( - Array( - new JOutcome( - id = Array("1100, -9223372036854775679, 1100, -9223372036854775678, 1, 2"), - expect = ACCEPTABLE_INTERESTING, - desc = "insert1 won"), - new JOutcome( - id = Array("1100, -9223372036854775678, 1100, -9223372036854775679, 2, 1"), - expect = ACCEPTABLE_INTERESTING, - desc = "insert2 won") - )) -class SkipListTest2 { - - private[this] val m = { - val DELAY = 1024L - val m = new TimerSkipList - for (i <- 1 to 128) { - m.insertTlr(now = i.toLong, delay = DELAY, callback = newCallback()) - } - m - } - - private[this] final val NOW = 128L - private[this] final val MAGIC = 972L - - private[this] val newCb1 = - newCallback() - - private[this] val newCb2 = - newCallback() - - @Actor - def insert1(r: JJJJJJ_Result): Unit = { - // the list contains times between 1025 and 1152, we insert at 1100: - val cancel = m.insertTlr(now = NOW, delay = MAGIC, callback = newCb1).asInstanceOf[m.Node] - r.r1 = cancel.triggerTime - r.r2 = cancel.sequenceNum - } - - @Actor - def insert2(r: JJJJJJ_Result): Unit = { - // the list contains times between 1025 and 1152, we insert at 1100: - val cancel = m.insertTlr(now = NOW, delay = MAGIC, callback = newCb2).asInstanceOf[m.Node] - r.r3 = cancel.triggerTime - r.r4 = cancel.sequenceNum - } - - @Arbiter - def arbiter(r: JJJJJJ_Result): Unit = { - // first remove all the items before the racy ones: - while ({ - val tt = m.peekFirstTriggerTime() - m.pollFirstIfTriggered(now = 2048L) - tt != (NOW + MAGIC) // there is an already existing callback with this triggerTime, we also remove that - }) {} - // then look at the 2 racy inserts: - val first = m.pollFirstIfTriggered(now = 2048L) - val second = m.pollFirstIfTriggered(now = 2048L) - r.r5 = if (first eq newCb1) 1L else if (first eq newCb2) 2L else -1L - r.r6 = if (second eq newCb1) 1L else if (second eq newCb2) 2L else -1L - } - - private[this] final def newCallback(): Right[Nothing, Unit] => Unit = { - new Function1[Right[Nothing, Unit], Unit] with Serializable { - final override def apply(r: Right[Nothing, Unit]): Unit = () - } - } -} diff --git a/stress-tests/src/test/scala/cats/effect/unsafe/SkipListTest3.scala b/stress-tests/src/test/scala/cats/effect/unsafe/SkipListTest3.scala deleted file mode 100644 index c246ccb921..0000000000 --- a/stress-tests/src/test/scala/cats/effect/unsafe/SkipListTest3.scala +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2020-2023 Typelevel - * - * 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.unsafe - -import org.openjdk.jcstress.annotations.{Outcome => JOutcome, Ref => _, _} -import org.openjdk.jcstress.annotations.Expect._ -import org.openjdk.jcstress.annotations.Outcome.Outcomes -import org.openjdk.jcstress.infra.results.JJJJ_Result - -@JCStressTest -@State -@Description("TimerSkipList insert/cancel race") -@Outcomes( - Array( - new JOutcome( - id = Array("1100, -9223372036854775678, 1, 1"), - expect = ACCEPTABLE_INTERESTING, - desc = "ok") - )) -class SkipListTest3 { - - private[this] val m = { - val DELAY = 1024L - val m = new TimerSkipList - for (i <- 1 to 128) { - m.insertTlr(now = i.toLong, delay = DELAY, callback = newCallback()) - } - m - } - - private[this] final val NOW = 128L - private[this] final val MAGIC = 972L - - private[this] val cancelledCb = - newCallback() - - private[this] val canceller: Runnable = - m.insertTlr(128L, MAGIC, cancelledCb) - - private[this] val newCb = - newCallback() - - @Actor - def insert(r: JJJJ_Result): Unit = { - // the list contains times between 1025 and 1152, we insert at 1100: - val cancel = - m.insertTlr(now = NOW, delay = MAGIC, callback = newCb).asInstanceOf[m.Node] - r.r1 = cancel.triggerTime - r.r2 = cancel.sequenceNum - } - - @Actor - def cancel(): Unit = { - canceller.run() - } - - @Arbiter - def arbiter(r: JJJJ_Result): Unit = { - // first remove all the items before the racy ones: - while ({ - val tt = m.peekFirstTriggerTime() - m.pollFirstIfTriggered(now = 2048L) - tt != (NOW + MAGIC) // there is an already existing callback with this triggerTime, we also remove that - }) {} - // then look at the inserted item: - val cb = m.pollFirstIfTriggered(now = 2048L) - r.r3 = if (cb eq newCb) 1L else 0L - // the cancelled one must be missing: - val other = m.pollFirstIfTriggered(now = 2048L) - r.r4 = if (other eq cancelledCb) 0L else if (other eq newCb) -1L else 1L - } - - private[this] final def newCallback(): Right[Nothing, Unit] => Unit = { - new Function1[Right[Nothing, Unit], Unit] with Serializable { - final override def apply(r: Right[Nothing, Unit]): Unit = () - } - } -} diff --git a/stress-tests/src/test/scala/cats/effect/unsafe/SkipListTest4.scala b/stress-tests/src/test/scala/cats/effect/unsafe/SkipListTest4.scala deleted file mode 100644 index ea51bb76ac..0000000000 --- a/stress-tests/src/test/scala/cats/effect/unsafe/SkipListTest4.scala +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2020-2023 Typelevel - * - * 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.unsafe - -import org.openjdk.jcstress.annotations.{Outcome => JOutcome, Ref => _, _} -import org.openjdk.jcstress.annotations.Expect._ -import org.openjdk.jcstress.annotations.Outcome.Outcomes -import org.openjdk.jcstress.infra.results.JJJ_Result - -@JCStressTest -@State -@Description("TimerSkipList pollFirstIfTriggered/pollFirstIfTriggered race") -@Outcomes( - Array( - new JOutcome( - id = Array("1, 0, 0"), - expect = ACCEPTABLE_INTERESTING, - desc = "pollFirst1 won"), - new JOutcome( - id = Array("0, 1, 0"), - expect = ACCEPTABLE_INTERESTING, - desc = "pollFirst2 won") - )) -class SkipListTest4 { - - private[this] val headCb = - newCallback() - - private[this] val secondCb = - newCallback() - - private[this] val m = { - val m = new TimerSkipList - // head is 1025L: - m.insertTlr(now = 1L, delay = 1024L, callback = headCb) - // second is 1026L: - m.insertTlr(now = 2L, delay = 1024L, callback = secondCb) - for (i <- 3 to 128) { - m.insertTlr(now = i.toLong, delay = 1024L, callback = newCallback()) - } - m - } - - @Actor - def pollFirst1(r: JJJ_Result): Unit = { - val cb = m.pollFirstIfTriggered(now = 2048L) - r.r1 = if (cb eq headCb) 1L else if (cb eq secondCb) 0L else -1L - } - - @Actor - def pollFirst2(r: JJJ_Result): Unit = { - val cb = m.pollFirstIfTriggered(now = 2048L) - r.r2 = if (cb eq headCb) 1L else if (cb eq secondCb) 0L else -1L - } - - @Arbiter - def arbiter(r: JJJ_Result): Unit = { - val otherCb = m.pollFirstIfTriggered(now = 2048L) - r.r3 = if (otherCb eq headCb) -1L else if (otherCb eq secondCb) -1L else 0L - } - - private[this] final def newCallback(): Right[Nothing, Unit] => Unit = { - new Function1[Right[Nothing, Unit], Unit] with Serializable { - final override def apply(r: Right[Nothing, Unit]): Unit = () - } - } -} diff --git a/stress-tests/src/test/scala/cats/effect/unsafe/SkipListTest5.scala b/stress-tests/src/test/scala/cats/effect/unsafe/SkipListTest5.scala deleted file mode 100644 index fb682d40b3..0000000000 --- a/stress-tests/src/test/scala/cats/effect/unsafe/SkipListTest5.scala +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2020-2023 Typelevel - * - * 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.unsafe - -import org.openjdk.jcstress.annotations.{Outcome => JOutcome, Ref => _, _} -import org.openjdk.jcstress.annotations.Expect._ -import org.openjdk.jcstress.annotations.Outcome.Outcomes -import org.openjdk.jcstress.infra.results.JJJ_Result - -@JCStressTest -@State -@Description("TimerSkipList pollFirstIfTriggered/pollFirstIfTriggered race (single element)") -@Outcomes( - Array( - new JOutcome( - id = Array("1, 0, 0"), - expect = ACCEPTABLE_INTERESTING, - desc = "pollFirst1 won"), - new JOutcome( - id = Array("0, 1, 0"), - expect = ACCEPTABLE_INTERESTING, - desc = "pollFirst2 won") - )) -class SkipListTest5 { - - private[this] val headCb = - newCallback() - - private[this] val m = { - val m = new TimerSkipList - // head is 1025L: - m.insertTlr(now = 1L, delay = 1024L, callback = headCb) - m - } - - @Actor - def pollFirst1(r: JJJ_Result): Unit = { - val cb = m.pollFirstIfTriggered(now = 2048L) - r.r1 = if (cb eq headCb) 1L else if (cb eq null) 0L else -1L - } - - @Actor - def pollFirst2(r: JJJ_Result): Unit = { - val cb = m.pollFirstIfTriggered(now = 2048L) - r.r2 = if (cb eq headCb) 1L else if (cb eq null) 0L else -1L - } - - @Arbiter - def arbiter(r: JJJ_Result): Unit = { - val cb = m.pollFirstIfTriggered(now = 2048L) - r.r3 = if (cb eq null) 0L else -1L - } - - private[this] final def newCallback(): Right[Nothing, Unit] => Unit = { - new Function1[Right[Nothing, Unit], Unit] with Serializable { - final override def apply(r: Right[Nothing, Unit]): Unit = () - } - } -} diff --git a/tests/jvm/src/test/scala/cats/effect/unsafe/SleepersSpec.scala b/tests/jvm/src/test/scala/cats/effect/unsafe/SleepersSpec.scala index 2438a88bb7..f5bb70e5fe 100644 --- a/tests/jvm/src/test/scala/cats/effect/unsafe/SleepersSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/unsafe/SleepersSpec.scala @@ -21,11 +21,13 @@ import org.specs2.mutable.Specification import scala.annotation.tailrec import scala.concurrent.duration._ +import java.util.concurrent.atomic.AtomicReference + class SleepersSpec extends Specification { "SleepCallback" should { "have a trigger time in the future" in { - val sleepers = new TimerSkipList + val sleepers = new TimerHeap val now = 100.millis.toNanos val delay = 500.millis.toNanos sleepers.insertTlr(now, delay, _ => ()) @@ -35,14 +37,16 @@ class SleepersSpec extends Specification { triggerTime mustEqual expected } - def dequeueAll(sleepers: TimerSkipList): List[(Long, Right[Nothing, Unit] => Unit)] = { + def dequeueAll(sleepers: TimerHeap, invoked: AtomicReference[Right[Nothing, Unit] => Unit]) + : List[(Long, Right[Nothing, Unit] => Unit)] = { @tailrec def loop(acc: List[(Long, Right[Nothing, Unit] => Unit)]) : List[(Long, Right[Nothing, Unit] => Unit)] = { val tt = sleepers.peekFirstTriggerTime() if (tt == Long.MinValue) acc.reverse else { - val cb = sleepers.pollFirstIfTriggered(now = tt) + sleepers.trigger(now = tt) + val cb = invoked.getAndSet(null) loop((tt, cb) :: acc) } } @@ -51,12 +55,16 @@ class SleepersSpec extends Specification { } // creates a new callback, making sure it's a separate object: - def newCb(): Right[Nothing, Unit] => Unit = { - new Function1[Right[Nothing, Unit], Unit] { def apply(x: Right[Nothing, Unit]) = () } + def newCb(invoked: AtomicReference[Right[Nothing, Unit] => Unit]) + : Right[Nothing, Unit] => Unit = { + new Function1[Right[Nothing, Unit], Unit] { self => + def apply(x: Right[Nothing, Unit]) = + invoked.set(self) + } } "be ordered according to the trigger time" in { - val sleepers = new TimerSkipList + val sleepers = new TimerHeap val now1 = 100.millis.toNanos val delay1 = 500.millis.toNanos @@ -66,45 +74,47 @@ class SleepersSpec extends Specification { val delay2 = 100.millis.toNanos val expected2 = 300.millis.toNanos // delay2 + now2 - val now3 = 300.millis.toNanos - val delay3 = 50.millis.toNanos - val expected3 = 350.millis.toNanos // delay3 + now3 + val now3 = 250.millis.toNanos + val delay3 = 150.millis.toNanos + val expected3 = 400.millis.toNanos // delay3 + now3 - val cb1 = newCb() - val cb2 = newCb() - val cb3 = newCb() + val invoked = new AtomicReference[Right[Nothing, Unit] => Unit] + val cb1 = newCb(invoked) + val cb2 = newCb(invoked) + val cb3 = newCb(invoked) sleepers.insertTlr(now1, delay1, cb1) sleepers.insertTlr(now2, delay2, cb2) sleepers.insertTlr(now3, delay3, cb3) - val ordering = dequeueAll(sleepers) + val ordering = dequeueAll(sleepers, invoked) val expectedOrdering = List(expected2 -> cb2, expected3 -> cb3, expected1 -> cb1) ordering mustEqual expectedOrdering } - "be ordered correctly even if Long overflows" in { - val sleepers = new TimerSkipList + // TODO + // "be ordered correctly even if Long overflows" in { + // val sleepers = new TimerSkipList - val now1 = Long.MaxValue - 20L - val delay1 = 10.nanos.toNanos - val expected1 = Long.MaxValue - 10L // no overflow yet + // val now1 = Long.MaxValue - 20L + // val delay1 = 10.nanos.toNanos + // val expected1 = Long.MaxValue - 10L // no overflow yet - val now2 = Long.MaxValue - 5L - val delay2 = 10.nanos.toNanos - val expected2 = Long.MinValue + 4L // overflow + // val now2 = Long.MaxValue - 5L + // val delay2 = 10.nanos.toNanos + // val expected2 = Long.MinValue + 4L // overflow - val cb1 = newCb() - val cb2 = newCb() + // val cb1 = newCb() + // val cb2 = newCb() - sleepers.insertTlr(now1, delay1, cb1) - sleepers.insertTlr(now2, delay2, cb2) + // sleepers.insertTlr(now1, delay1, cb1) + // sleepers.insertTlr(now2, delay2, cb2) - val ordering = dequeueAll(sleepers) - val expectedOrdering = List(expected1 -> cb1, expected2 -> cb2) + // val ordering = dequeueAll(sleepers) + // val expectedOrdering = List(expected1 -> cb1, expected2 -> cb2) - ordering mustEqual expectedOrdering - } + // ordering mustEqual expectedOrdering + // } } } diff --git a/tests/jvm/src/test/scala/cats/effect/unsafe/TimerSkipListIOSpec.scala b/tests/jvm/src/test/scala/cats/effect/unsafe/TimerSkipListIOSpec.scala deleted file mode 100644 index 76eb0432f4..0000000000 --- a/tests/jvm/src/test/scala/cats/effect/unsafe/TimerSkipListIOSpec.scala +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2020-2023 Typelevel - * - * 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 -package unsafe - -import cats.syntax.all._ - -import scala.concurrent.duration._ - -import java.util.concurrent.{ConcurrentSkipListSet, ThreadLocalRandom} -import java.util.concurrent.atomic.AtomicLong - -class TimerSkipListIOSpec extends BaseSpec { - - final val N = 50000 - final val DELAY = 10000L // ns - - private def drainUntilDone(m: TimerSkipList, done: Ref[IO, Boolean]): IO[Unit] = { - val pollSome: IO[Long] = IO { - while ({ - val cb = m.pollFirstIfTriggered(System.nanoTime()) - if (cb ne null) { - cb(Right(())) - true - } else false - }) {} - m.peekFirstTriggerTime() - } - def go(lastOne: Boolean): IO[Unit] = pollSome.flatMap { next => - if (next == Long.MinValue) IO.cede - else { - IO.defer { - val now = System.nanoTime() - val delay = next - now - if (delay > 0L) IO.sleep(delay.nanos) - else IO.unit - } - } - } *> { - if (lastOne) IO.unit - else done.get.ifM(go(lastOne = true), IO.cede *> go(lastOne = false)) - } - - go(lastOne = false) - } - - "TimerSkipList" should { - - "insert/pollFirstIfTriggered concurrently" in real { - IO.ref(false).flatMap { done => - IO { (new TimerSkipList, new AtomicLong) }.flatMap { - case (m, ctr) => - val insert = IO { - m.insert( - now = System.nanoTime(), - delay = DELAY, - callback = { _ => ctr.getAndIncrement; () }, - tlr = ThreadLocalRandom.current() - ) - } - val inserts = - (insert.parReplicateA_(N) *> IO.sleep(2 * DELAY.nanos)).guarantee(done.set(true)) - - val polls = drainUntilDone(m, done).parReplicateA_(2) - - IO.both(inserts, polls).flatMap { _ => - IO.sleep(0.5.second) *> IO { - m.pollFirstIfTriggered(System.nanoTime()) must beNull - ctr.get() mustEqual N.toLong - } - } - } - } - } - - "insert/cancel concurrently" in real { - IO.ref(false).flatMap { done => - IO { (new TimerSkipList, new ConcurrentSkipListSet[Int]) }.flatMap { - case (m, called) => - def insert(id: Int): IO[Runnable] = IO { - val now = System.nanoTime() - val canceller = m.insert( - now = now, - delay = DELAY, - callback = { _ => called.add(id); () }, - tlr = ThreadLocalRandom.current() - ) - canceller - } - - def cancel(c: Runnable): IO[Unit] = IO { - c.run() - } - - val firstBatch = (0 until N).toList - val secondBatch = (N until (2 * N)).toList - - for { - // add the first N callbacks: - cancellers <- firstBatch.traverse(insert) - // then race removing those, and adding another N: - _ <- IO.both( - cancellers.parTraverse(cancel), - secondBatch.parTraverse(insert) - ) - // since the fibers calling callbacks - // are not running yet, the cancelled - // ones must never be invoked - _ <- IO.both( - IO.sleep(2 * DELAY.nanos).guarantee(done.set(true)), - drainUntilDone(m, done).parReplicateA_(2) - ) - _ <- IO { - assert(m.pollFirstIfTriggered(System.nanoTime()) eq null) - // no cancelled callback should've been called, - // and all the other ones must've been called: - val calledIds = { - val b = Set.newBuilder[Int] - val it = called.iterator() - while (it.hasNext()) { - b += it.next() - } - b.result() - } - calledIds mustEqual secondBatch.toSet - } - } yield ok - } - } - } - } -} From dc979e4adc58f92554470b0f30ac85d074f24d67 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Thu, 10 Aug 2023 16:05:27 +0000 Subject: [PATCH 170/429] Update specs2-core, specs2-scalacheck to 4.20.2 in series/3.x --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index b4eb61c66a..b3aab74ac3 100644 --- a/build.sbt +++ b/build.sbt @@ -306,7 +306,7 @@ ThisBuild / apiURL := Some(url("https://typelevel.org/cats-effect/api/3.x/")) ThisBuild / autoAPIMappings := true val CatsVersion = "2.9.0" -val Specs2Version = "4.20.1" +val Specs2Version = "4.20.2" val ScalaCheckVersion = "1.17.0" val DisciplineVersion = "1.4.0" val CoopVersion = "1.2.0" From c4f0d7c00e6f872d13c05099f05a0cfb595f254f Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Sat, 12 Aug 2023 04:08:42 +0000 Subject: [PATCH 171/429] Update sbt-typelevel to 0.5.0-RC12 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index b9571f2df8..e9705fc6d1 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.0-RC10") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.0-RC12") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.1") From ca1a5b1ffa3b15d4dd55975fffccedcd14884351 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Sat, 12 Aug 2023 04:10:50 +0000 Subject: [PATCH 172/429] Run prePR with sbt-typelevel Executed command: sbt tlPrePrBotHook --- .github/workflows/ci.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33ac51d0f9..ecda39ffea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,7 +128,7 @@ jobs: - name: sbt update if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' shell: bash - run: sbt '++ ${{ matrix.scala }}' reload +update + run: sbt +update - name: Setup Java (temurin@11) id: setup-java-temurin-11 @@ -142,7 +142,7 @@ jobs: - name: sbt update if: matrix.java == 'temurin@11' && steps.setup-java-temurin-11.outputs.cache-hit == 'false' shell: bash - run: sbt '++ ${{ matrix.scala }}' reload +update + run: sbt +update - name: Setup Java (temurin@17) id: setup-java-temurin-17 @@ -156,7 +156,7 @@ jobs: - name: sbt update if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' shell: bash - run: sbt '++ ${{ matrix.scala }}' reload +update + run: sbt +update - name: Setup Java (graalvm@17) id: setup-java-graalvm-17 @@ -170,7 +170,7 @@ jobs: - name: sbt update if: matrix.java == 'graalvm@17' && steps.setup-java-graalvm-17.outputs.cache-hit == 'false' shell: bash - run: sbt '++ ${{ matrix.scala }}' reload +update + run: sbt +update - name: Setup NodeJS v18 LTS if: matrix.ci == 'ciJS' @@ -296,7 +296,7 @@ jobs: - name: sbt update if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' - run: sbt reload +update + run: sbt +update - name: Setup Java (temurin@11) id: setup-java-temurin-11 @@ -309,7 +309,7 @@ jobs: - name: sbt update if: matrix.java == 'temurin@11' && steps.setup-java-temurin-11.outputs.cache-hit == 'false' - run: sbt reload +update + run: sbt +update - name: Setup Java (temurin@17) id: setup-java-temurin-17 @@ -322,7 +322,7 @@ jobs: - name: sbt update if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' - run: sbt reload +update + run: sbt +update - name: Setup Java (graalvm@17) id: setup-java-graalvm-17 @@ -335,7 +335,7 @@ jobs: - name: sbt update if: matrix.java == 'graalvm@17' && steps.setup-java-graalvm-17.outputs.cache-hit == 'false' - run: sbt reload +update + run: sbt +update - name: Download target directories (3.3.0, ciJVM) uses: actions/download-artifact@v3 @@ -503,7 +503,7 @@ jobs: - name: sbt update if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' - run: sbt reload +update + run: sbt +update - name: Setup Java (temurin@11) id: setup-java-temurin-11 @@ -516,7 +516,7 @@ jobs: - name: sbt update if: matrix.java == 'temurin@11' && steps.setup-java-temurin-11.outputs.cache-hit == 'false' - run: sbt reload +update + run: sbt +update - name: Setup Java (temurin@17) id: setup-java-temurin-17 @@ -529,7 +529,7 @@ jobs: - name: sbt update if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' - run: sbt reload +update + run: sbt +update - name: Setup Java (graalvm@17) id: setup-java-graalvm-17 @@ -542,7 +542,7 @@ jobs: - name: sbt update if: matrix.java == 'graalvm@17' && steps.setup-java-graalvm-17.outputs.cache-hit == 'false' - run: sbt reload +update + run: sbt +update - name: Submit Dependencies uses: scalacenter/sbt-dependency-submission@v2 From c03539ae814dd6ea2719b27172bd509c4eb78cb9 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 08:06:24 +0000 Subject: [PATCH 173/429] Update cats-core, cats-free, ... to 2.10.0 in series/3.x --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index b4eb61c66a..bd9a6a2524 100644 --- a/build.sbt +++ b/build.sbt @@ -305,7 +305,7 @@ ThisBuild / apiURL := Some(url("https://typelevel.org/cats-effect/api/3.x/")) ThisBuild / autoAPIMappings := true -val CatsVersion = "2.9.0" +val CatsVersion = "2.10.0" val Specs2Version = "4.20.1" val ScalaCheckVersion = "1.17.0" val DisciplineVersion = "1.4.0" From c67bbe082e38ff3649b06c3a8db7374cb6286a5a Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 20:06:12 +0000 Subject: [PATCH 174/429] Revert commit(s) dc979e4ad --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index b3aab74ac3..b4eb61c66a 100644 --- a/build.sbt +++ b/build.sbt @@ -306,7 +306,7 @@ ThisBuild / apiURL := Some(url("https://typelevel.org/cats-effect/api/3.x/")) ThisBuild / autoAPIMappings := true val CatsVersion = "2.9.0" -val Specs2Version = "4.20.2" +val Specs2Version = "4.20.1" val ScalaCheckVersion = "1.17.0" val DisciplineVersion = "1.4.0" val CoopVersion = "1.2.0" From 19321b3a9d7da7d352be264ccfea69a565a038e0 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 20:06:19 +0000 Subject: [PATCH 175/429] Update specs2-core, specs2-scalacheck to 4.20.2 in series/3.x --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index bd9a6a2524..c8a3050c7e 100644 --- a/build.sbt +++ b/build.sbt @@ -306,7 +306,7 @@ ThisBuild / apiURL := Some(url("https://typelevel.org/cats-effect/api/3.x/")) ThisBuild / autoAPIMappings := true val CatsVersion = "2.10.0" -val Specs2Version = "4.20.1" +val Specs2Version = "4.20.2" val ScalaCheckVersion = "1.17.0" val DisciplineVersion = "1.4.0" val CoopVersion = "1.2.0" From 2164d1f57f4d1f1187a73faff268f53fb5b33a05 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Wed, 23 Aug 2023 00:21:01 +0000 Subject: [PATCH 176/429] Update sbt-typelevel to 0.5.0 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index e9705fc6d1..2b8a006a6d 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.0-RC12") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.0") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.1") From cd24f3cf3f487765df4076c45377a834239c0062 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Fri, 25 Aug 2023 00:18:46 +0000 Subject: [PATCH 177/429] Update sbt to 1.9.4 in series/3.x --- project/build.properties | 2 +- scalafix/project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project/build.properties b/project/build.properties index 52413ab79a..3040987151 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.3 +sbt.version=1.9.4 diff --git a/scalafix/project/build.properties b/scalafix/project/build.properties index 52413ab79a..3040987151 100644 --- a/scalafix/project/build.properties +++ b/scalafix/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.3 +sbt.version=1.9.4 From 8a37117cba1544a669f1036e7f0e5a74ca0b4c1c Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 27 Aug 2023 22:12:08 +0000 Subject: [PATCH 178/429] Join worker threads on pool shutdown --- build.sbt | 2 +- .../effect/unsafe/WorkStealingThreadPool.scala | 15 ++++++++++----- .../main/scala/catseffect/examplesplatform.scala | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/build.sbt b/build.sbt index c8a3050c7e..7632f8203d 100644 --- a/build.sbt +++ b/build.sbt @@ -879,7 +879,7 @@ lazy val tests: CrossProject = crossProject(JSPlatform, JVMPlatform, NativePlatf scalacOptions ~= { _.filterNot(_.startsWith("-P:scalajs:mapSourceURI")) } ) .jvmSettings( - Test / fork := true, + fork := true, Test / javaOptions += s"-Dsbt.classpath=${(Test / fullClasspath).value.map(_.data.getAbsolutePath).mkString(File.pathSeparator)}" ) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index 2921e7bf30..f6ea78edd8 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -689,6 +689,7 @@ private[effect] final class WorkStealingThreadPool[P]( def shutdown(): Unit = { // Clear the interrupt flag. val interruptCalling = Thread.interrupted() + val currentThread = Thread.currentThread() // Execute the shutdown logic only once. if (done.compareAndSet(false, true)) { @@ -701,27 +702,31 @@ private[effect] final class WorkStealingThreadPool[P]( // the face of unhandled exceptions or as part of the whole JVM exiting. var i = 0 while (i < threadCount) { - workerThreads(i).interrupt() + val workerThread = workerThreads(i) + if (workerThread ne currentThread) { + workerThread.interrupt() + workerThread.join() + // wait to stop before closing pollers + } system.closePoller(pollers(i)) i += 1 } system.close() - // Clear the interrupt flag. - Thread.interrupted() - var t: WorkerThread[P] = null while ({ t = cachedThreads.pollFirst() t ne null }) { t.interrupt() + // don't join, blocking threads may be uninterruptibly blocked. + // anyway, they do not have pollers to close. } // Drain the external queue. externalQueue.clear() - if (interruptCalling) Thread.currentThread().interrupt() + if (interruptCalling) currentThread.interrupt() } } diff --git a/tests/jvm/src/main/scala/catseffect/examplesplatform.scala b/tests/jvm/src/main/scala/catseffect/examplesplatform.scala index 2edbe9bcf9..8b9f818aa0 100644 --- a/tests/jvm/src/main/scala/catseffect/examplesplatform.scala +++ b/tests/jvm/src/main/scala/catseffect/examplesplatform.scala @@ -37,7 +37,7 @@ package examples { super.runtimeConfig.copy(shutdownHookTimeout = Duration.Zero) val run: IO[Unit] = - IO(System.exit(0)).uncancelable + IO.blocking(System.exit(0)).uncancelable } object FatalErrorUnsafeRun extends IOApp { From dc3fe9a95486db9afa864b2b48f5fbd4484d93cd Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 28 Aug 2023 20:06:45 +0000 Subject: [PATCH 179/429] Update scalafmt-core to 3.7.13 in series/3.x --- .scalafmt.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index a533d8d278..556b5b81c2 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = 3.7.10 +version = 3.7.13 runner.dialect = Scala213Source3 fileOverride { From d02bc4b0e9933912e6f9a989c2567663c55a2519 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 29 Aug 2023 07:48:49 +0000 Subject: [PATCH 180/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/15be638bdfec87bc3fd7b4111de0f572bbb29a5a' (2023-06-05) → 'github:typelevel/typelevel-nix/b1b16aaf47198209bf91d41a64007c5a39c02a13' (2023-08-28) • Updated input 'typelevel-nix/devshell': 'github:numtide/devshell/5143ea68647c4cf5227e4ad2100db6671fc4c369' (2023-05-09) → 'github:numtide/devshell/2aa26972b951bc05c3632d4e5ae683cb6771a7c6' (2023-08-23) • Updated input 'typelevel-nix/flake-utils': 'github:numtide/flake-utils/a1720a10a6cfe8234c0e93907ffe81be440f4cef' (2023-05-31) → 'github:numtide/flake-utils/f9e7cf818399d17d347f847525c5a5a8032e4e44' (2023-08-23) • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/2e56a850786211972d99d2bb39665a9b5a1801d6' (2023-06-04) → 'github:nixos/nixpkgs/cddebdb60de376c1bdb7a4e6ee3d98355453fe56' (2023-08-27) --- flake.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/flake.lock b/flake.lock index 1f68183097..6c1c6c3756 100644 --- a/flake.lock +++ b/flake.lock @@ -6,11 +6,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1683635384, - "narHash": "sha256-9goJTd05yOyD/McaMqZ4BUB8JW+mZMnZQJZ7VQ6C/Lw=", + "lastModified": 1692793255, + "narHash": "sha256-yVyj0AE280JkccDHuG1XO9oGxN6bW8ksr/xttXcXzK0=", "owner": "numtide", "repo": "devshell", - "rev": "5143ea68647c4cf5227e4ad2100db6671fc4c369", + "rev": "2aa26972b951bc05c3632d4e5ae683cb6771a7c6", "type": "github" }, "original": { @@ -24,11 +24,11 @@ "systems": "systems_2" }, "locked": { - "lastModified": 1685518550, - "narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=", + "lastModified": 1692799911, + "narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=", "owner": "numtide", "repo": "flake-utils", - "rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef", + "rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44", "type": "github" }, "original": { @@ -55,11 +55,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1685894048, - "narHash": "sha256-QKqv1QS+22k9oxncj1AnAxeqS5jGnQiUW3Jq3B+dI1w=", + "lastModified": 1693145325, + "narHash": "sha256-Gat9xskErH1zOcLjYMhSDBo0JTBZKfGS0xJlIRnj6Rc=", "owner": "nixos", "repo": "nixpkgs", - "rev": "2e56a850786211972d99d2bb39665a9b5a1801d6", + "rev": "cddebdb60de376c1bdb7a4e6ee3d98355453fe56", "type": "github" }, "original": { @@ -119,11 +119,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1685970436, - "narHash": "sha256-j+lPMw27GFKlU+r++N3I0Imu38dKhY4t/MW1r23+ZLw=", + "lastModified": 1693253287, + "narHash": "sha256-ii2k0cUNfJuOW35AkGAu85HzSZ1SaJOoTc1tJxqG7iQ=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "15be638bdfec87bc3fd7b4111de0f572bbb29a5a", + "rev": "b1b16aaf47198209bf91d41a64007c5a39c02a13", "type": "github" }, "original": { From fdfd891b8b6186c3b711f7dc26ef20368ba5a3b7 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 29 Aug 2023 09:12:42 -0700 Subject: [PATCH 181/429] Remove plugins we get transitively --- project/plugins.sbt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 2b8a006a6d..ee7dce680d 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -3,14 +3,10 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.0") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") -addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.1") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.14") -addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.1") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.5") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.7") -addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.5.0") addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.0") From 3881a92df540eb39f2bccc61f8a63e9ae179f294 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 31 Aug 2023 23:46:40 +0000 Subject: [PATCH 182/429] Refactor so timers can be canceled on same thread --- .../scala/cats/effect/unsafe/TimerHeap.scala | 236 +++++++++++------- .../cats/effect/unsafe/WorkerThread.scala | 25 +- .../cats/effect/unsafe/SleepersSpec.scala | 85 ++++--- ...SkipListSpec.scala => TimerHeapSpec.scala} | 51 ++-- 4 files changed, 241 insertions(+), 156 deletions(-) rename tests/jvm/src/test/scala/cats/effect/unsafe/{TimerSkipListSpec.scala => TimerHeapSpec.scala} (77%) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index 43b8946b82..cd81720011 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -31,8 +31,6 @@ package unsafe import scala.annotation.tailrec -import java.util.concurrent.ThreadLocalRandom - private final class TimerHeap { // The index 0 is not used; the root is at index 1. @@ -40,41 +38,45 @@ private final class TimerHeap { private[this] var heap: Array[TimerNode] = new Array(8) // TODO what initial value private[this] var size: Int = 0 - private[this] val RightUnit = IOFiber.RightUnit + private[this] val RightUnit = Right(()) - def peekFirstTriggerTime(): Long = { - val heap = this.heap // local copy - if (size > 0) { - val node = heap(1) - if (node ne null) - node.triggerTime - else java.lang.Long.MIN_VALUE - } else java.lang.Long.MIN_VALUE + def peekFirstTriggerTime(): Long = + if (size > 0) heap(1).triggerTime + else Long.MinValue + + /** + * for testing + */ + def peekFirstQuiescent(): Right[Nothing, Unit] => Unit = { + if (size > 0) heap(1).get() + else null } /** * only called by owner thread - * - * returns `true` if we removed at least one timer TODO only return true if we actually - * triggered a timer, not just removed one */ - def trigger(now: Long): Boolean = { + def pollFirstIfTriggered(now: Long): Right[Nothing, Unit] => Unit = { val heap = this.heap // local copy @tailrec - def loop(triggered: Boolean): Boolean = if (size > 0) { + def loop(): Right[Nothing, Unit] => Unit = if (size > 0) { val root = heap(1) - if (canRemove(root, now)) { - heap(1) = heap(size) + val rootCanceled = root.isCanceled() + val rootExpired = !rootCanceled && isExpired(root, now) + if (rootCanceled || rootExpired) { + if (size > 1) { + heap(1) = heap(size) + fixDown(1) + } heap(size) = null size -= 1 - fixDown(1) - loop(true) - } else triggered - } else triggered + val back = root.getAndClear() + if (rootExpired && (back ne null)) back else loop() + } else null + } else null - loop(false) + loop() } /** @@ -84,10 +86,15 @@ private final class TimerHeap { def go(heap: Array[TimerNode], size: Int, m: Int): Boolean = if (m <= size) { val node = heap(m) - if ((node ne null) && triggered(node, now)) { - go(heap, size, 2 * m) - go(heap, size, 2 * m + 1) - true + if ((node ne null) && isExpired(node, now)) { + val cb = node.getAndClear() + val invoked = cb ne null + if (invoked) cb(RightUnit) + + val leftInvoked = go(heap, size, 2 * m) + val rightInvoked = go(heap, size, 2 * m + 1) + + invoked || leftInvoked || rightInvoked } else false } else false @@ -103,63 +110,56 @@ private final class TimerHeap { now: Long, delay: Long, callback: Right[Nothing, Unit] => Unit, - tlr: ThreadLocalRandom + out: Array[Right[Nothing, Unit] => Unit] ): Function0[Unit] with Runnable = if (size > 0) { val heap = this.heap // local copy - val node = new TimerNode(now + delay, callback) - - if (canRemove(heap(1), now)) { // see if we can just replace the root + val triggerTime = computeTriggerTime(now, delay) + + val root = heap(1) + val rootCanceled = root.isCanceled() + val rootExpired = !rootCanceled && isExpired(root, now) + if (rootCanceled || rootExpired) { // see if we can just replace the root + if (rootExpired) out(0) = root.getAndClear() + val node = new TimerNode(triggerTime, callback, 1) heap(1) = node fixDown(1) - } else { // look for a canceled node we can replace - - @tailrec - def loop(m: Int, entropy: Int): Unit = if (m <= size) { - if (heap(m).isCanceled()) { // we found a spot! - heap(m) = node - fixUpOrDown(m) - } else loop(2 * m + (entropy % 2), entropy >>> 1) - } else { // insert at the end - val heap = growIfNeeded() // new heap array if it grew - size += 1 - heap(size) = node - fixUp(size) - } - - val entropy = tlr.nextInt() - loop(2 + (entropy % 2), entropy >>> 1) + node + } else { // insert at the end + val heap = growIfNeeded() // new heap array if it grew + size += 1 + val node = new TimerNode(triggerTime, callback, size) + heap(size) = node + fixUp(size) + node } - - node } else { - val node = new TimerNode(now + delay, callback) + val node = new TimerNode(now + delay, callback, 1) this.heap(1) = node size += 1 node } /** - * For testing + * only called by owner thread */ - private[unsafe] final def insertTlr( - now: Long, - delay: Long, - callback: Right[Nothing, Unit] => Unit - ): Runnable = { - insert(now, delay, callback, ThreadLocalRandom.current()) + private def removeAt(i: Int): Unit = { + val heap = this.heap // local copy + heap(i).getAndClear() + if (i == size) { + heap(i) = null + size -= 1 + } else { + val last = heap(size) + heap(size) = null + heap(i) = last + last.index = i + size -= 1 + fixUpOrDown(i) + } } - /** - * Determines if a node can be removed. Triggers it if relevant. - */ - private[this] def canRemove(node: TimerNode, now: Long): Boolean = - node.isCanceled() || triggered(node, now) - - private[this] def triggered(node: TimerNode, now: Long): Boolean = - if (cmp(node.triggerTime, now) <= 0) { // triggerTime <= now - node.trigger(RightUnit) - true - } else false + private[this] def isExpired(node: TimerNode, now: Long): Boolean = + cmp(node.triggerTime, now) <= 0 // triggerTime <= now private[this] def growIfNeeded(): Array[TimerNode] = { val heap = this.heap // local copy @@ -185,7 +185,6 @@ private final class TimerHeap { /** * Fixes the heap property from the last child at index `size` up the tree, towards the root. - * Along the way we remove up to one canceled node. */ private[this] def fixUp(m: Int): Unit = { val heap = this.heap // local copy @@ -203,9 +202,10 @@ private final class TimerHeap { if (cmp(heapAtParent, heapAtM) > 0) { heap(parent) = heapAtM heap(m) = heapAtParent + heapAtParent.index = m loop(parent) - } - } + } else heapAtM.index = m + } else heapAtM.index = m } loop(m) @@ -240,10 +240,11 @@ private final class TimerHeap { // if the node `m` is greater than the selected child, swap and recurse if (cmp(heapAtM, heapAtJ) > 0) { heap(m) = heapAtJ + heapAtJ.index = m heap(j) = heapAtM loop(j) - } - } + } else heapAtM.index = m + } else heapAtM.index = m } loop(m) @@ -267,31 +268,80 @@ private final class TimerHeap { private[this] def cmp(x: TimerNode, y: TimerNode): Int = cmp(x.triggerTime, y.triggerTime) - override def toString() = heap.drop(1).take(size).mkString("TimerHeap(", ", ", ")") - -} - -private final class TimerNode( - val triggerTime: Long, - private[this] var callback: Right[Nothing, Unit] => Unit) - extends Function0[Unit] - with Runnable { - - def trigger(rightUnit: Right[Nothing, Unit]): Unit = { - val back = callback - callback = null - if (back ne null) back(rightUnit) + /** + * Computes the trigger time in an overflow-safe manner. The trigger time is essentially `now + * + delay`. However, we must constrain all trigger times in the skip list to be within + * `Long.MaxValue` of each other (otherwise there will be overflow when comparing in `cpr`). + * Thus, if `delay` is so big, we'll reduce it to the greatest allowable (in `overflowFree`). + * + * From the public domain JSR-166 `ScheduledThreadPoolExecutor` (`triggerTime` method). + */ + private[this] def computeTriggerTime(now: Long, delay: Long): Long = { + val safeDelay = if (delay < (Long.MaxValue >> 1)) delay else overflowFree(now, delay) + now + safeDelay } /** - * racy cancelation + * See `computeTriggerTime`. The overflow can happen if a callback was already triggered + * (based on `now`), but was not removed yet; and `delay` is sufficiently big. + * + * From the public domain JSR-166 `ScheduledThreadPoolExecutor` (`overflowFree` method). */ - def apply(): Unit = callback = null + private[this] def overflowFree(now: Long, delay: Long): Long = { + val root = heap(1) + if (root ne null) { + val rootDelay = root.triggerTime - now + if ((rootDelay < 0) && (delay - rootDelay < 0)) { + // head was already triggered, and `delay` is big enough, + // so we must clamp `delay`: + Long.MaxValue + rootDelay + } else { + delay + } + } else { + delay // empty + } + } + + override def toString() = if (size > 0) "TimerHeap(...)" else "TimerHeap()" + + private final class TimerNode( + val triggerTime: Long, + private[this] var callback: Right[Nothing, Unit] => Unit, + var index: Int + ) extends Function0[Unit] + with Runnable { + + def getAndClear(): Right[Nothing, Unit] => Unit = { + val back = callback + callback = null + back + } + + def get(): Right[Nothing, Unit] => Unit = callback - def run() = apply() + /** + * Cancel this timer. + */ + def apply(): Unit = { + // we can always clear the callback, without explicitly publishing + callback = null + + // if we're on the thread that owns this heap, we can remove ourselves immediately + val thread = Thread.currentThread() + if (thread.isInstanceOf[WorkerThread]) { + val worker = thread.asInstanceOf[WorkerThread] + val heap = TimerHeap.this + if (worker.ownsTimers(heap)) heap.removeAt(index) + } + } - def isCanceled(): Boolean = callback eq null + def run() = apply() - override def toString() = s"TimerNode($triggerTime, $callback})" + def isCanceled(): Boolean = callback eq null + + override def toString() = s"TimerNode($triggerTime, $callback})" + + } } diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala index 68a1f57e94..2ff0a2d837 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala @@ -106,6 +106,8 @@ private final class WorkerThread( private val indexTransfer: LinkedTransferQueue[Integer] = new LinkedTransferQueue() private[this] val runtimeBlockingExpiration: Duration = pool.runtimeBlockingExpiration + private[this] val RightUnit = Right(()) + val nameIndex: Int = pool.blockedWorkerThreadNamingIndex.getAndIncrement() // Constructor code. @@ -158,14 +160,20 @@ private final class WorkerThread( // take the opportunity to update the current time, just in case other timers can benefit val _now = System.nanoTime() now = _now + val out = new Array[Right[Nothing, Unit] => Unit](1) // note that blockers aren't owned by the pool, meaning we only end up here if !blocking - sleepers.insert( + val cancel = sleepers.insert( now = _now, delay = delay.toNanos, callback = callback, - tlr = random + out = out ) + + val cb = out(0) + if (cb ne null) cb(RightUnit) + + cancel } /** @@ -249,6 +257,9 @@ private final class WorkerThread( foreign.toMap } + private[unsafe] def ownsTimers(timers: TimerHeap): Boolean = + sleepers eq timers + /** * The run loop of the [[WorkerThread]]. */ @@ -715,7 +726,15 @@ private final class WorkerThread( case _ => // Call all of our expired timers: - sleepers.trigger(now) + var cont = true + while (cont) { + val cb = sleepers.pollFirstIfTriggered(now) + if (cb ne null) { + cb(RightUnit) + } else { + cont = false + } + } // Check the queue bypass reference before dequeueing from the local // queue. diff --git a/tests/jvm/src/test/scala/cats/effect/unsafe/SleepersSpec.scala b/tests/jvm/src/test/scala/cats/effect/unsafe/SleepersSpec.scala index f5bb70e5fe..f5690525fc 100644 --- a/tests/jvm/src/test/scala/cats/effect/unsafe/SleepersSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/unsafe/SleepersSpec.scala @@ -21,8 +21,6 @@ import org.specs2.mutable.Specification import scala.annotation.tailrec import scala.concurrent.duration._ -import java.util.concurrent.atomic.AtomicReference - class SleepersSpec extends Specification { "SleepCallback" should { @@ -30,23 +28,28 @@ class SleepersSpec extends Specification { val sleepers = new TimerHeap val now = 100.millis.toNanos val delay = 500.millis.toNanos - sleepers.insertTlr(now, delay, _ => ()) + sleepers.insert(now, delay, _ => (), new Array(1)) val triggerTime = sleepers.peekFirstTriggerTime() val expected = 600.millis.toNanos // delay + now triggerTime mustEqual expected } - def dequeueAll(sleepers: TimerHeap, invoked: AtomicReference[Right[Nothing, Unit] => Unit]) - : List[(Long, Right[Nothing, Unit] => Unit)] = { + def collectOuts(outs: (Long, Array[Right[Nothing, Unit] => Unit])*) + : List[(Long, Right[Nothing, Unit] => Unit)] = + outs.toList.flatMap { + case (now, out) => + Option(out(0)).map(now -> _).toList + } + + def dequeueAll(sleepers: TimerHeap): List[(Long, Right[Nothing, Unit] => Unit)] = { @tailrec def loop(acc: List[(Long, Right[Nothing, Unit] => Unit)]) : List[(Long, Right[Nothing, Unit] => Unit)] = { val tt = sleepers.peekFirstTriggerTime() if (tt == Long.MinValue) acc.reverse else { - sleepers.trigger(now = tt) - val cb = invoked.getAndSet(null) + val cb = sleepers.pollFirstIfTriggered(now = tt) loop((tt, cb) :: acc) } } @@ -55,12 +58,8 @@ class SleepersSpec extends Specification { } // creates a new callback, making sure it's a separate object: - def newCb(invoked: AtomicReference[Right[Nothing, Unit] => Unit]) - : Right[Nothing, Unit] => Unit = { - new Function1[Right[Nothing, Unit], Unit] { self => - def apply(x: Right[Nothing, Unit]) = - invoked.set(self) - } + def newCb(): Right[Nothing, Unit] => Unit = { + new Function1[Right[Nothing, Unit], Unit] { def apply(x: Right[Nothing, Unit]) = () } } "be ordered according to the trigger time" in { @@ -74,47 +73,51 @@ class SleepersSpec extends Specification { val delay2 = 100.millis.toNanos val expected2 = 300.millis.toNanos // delay2 + now2 - val now3 = 250.millis.toNanos - val delay3 = 150.millis.toNanos - val expected3 = 400.millis.toNanos // delay3 + now3 + val now3 = 300.millis.toNanos + val delay3 = 50.millis.toNanos + val expected3 = 350.millis.toNanos // delay3 + now3 - val invoked = new AtomicReference[Right[Nothing, Unit] => Unit] - val cb1 = newCb(invoked) - val cb2 = newCb(invoked) - val cb3 = newCb(invoked) + val cb1 = newCb() + val cb2 = newCb() + val cb3 = newCb() - sleepers.insertTlr(now1, delay1, cb1) - sleepers.insertTlr(now2, delay2, cb2) - sleepers.insertTlr(now3, delay3, cb3) + val out1 = new Array[Right[Nothing, Unit] => Unit](1) + val out2 = new Array[Right[Nothing, Unit] => Unit](1) + val out3 = new Array[Right[Nothing, Unit] => Unit](1) + sleepers.insert(now1, delay1, cb1, out1) + sleepers.insert(now2, delay2, cb2, out2) + sleepers.insert(now3, delay3, cb3, out3) - val ordering = dequeueAll(sleepers, invoked) + val ordering = + collectOuts(now1 -> out1, now2 -> out2, now3 -> out3) ::: dequeueAll(sleepers) val expectedOrdering = List(expected2 -> cb2, expected3 -> cb3, expected1 -> cb1) ordering mustEqual expectedOrdering } - // TODO - // "be ordered correctly even if Long overflows" in { - // val sleepers = new TimerSkipList + "be ordered correctly even if Long overflows" in { + val sleepers = new TimerHeap - // val now1 = Long.MaxValue - 20L - // val delay1 = 10.nanos.toNanos - // val expected1 = Long.MaxValue - 10L // no overflow yet + val now1 = Long.MaxValue - 20L + val delay1 = 10.nanos.toNanos + // val expected1 = Long.MaxValue - 10L // no overflow yet - // val now2 = Long.MaxValue - 5L - // val delay2 = 10.nanos.toNanos - // val expected2 = Long.MinValue + 4L // overflow + val now2 = Long.MaxValue - 5L + val delay2 = 10.nanos.toNanos + val expected2 = Long.MinValue + 4L // overflow - // val cb1 = newCb() - // val cb2 = newCb() + val cb1 = newCb() + val cb2 = newCb() - // sleepers.insertTlr(now1, delay1, cb1) - // sleepers.insertTlr(now2, delay2, cb2) + val out1 = new Array[Right[Nothing, Unit] => Unit](1) + val out2 = new Array[Right[Nothing, Unit] => Unit](1) + sleepers.insert(now1, delay1, cb1, out1) + sleepers.insert(now2, delay2, cb2, out2) - // val ordering = dequeueAll(sleepers) - // val expectedOrdering = List(expected1 -> cb1, expected2 -> cb2) + val ordering = collectOuts(now1 -> out1, now2 -> out2) ::: dequeueAll(sleepers) + val expectedOrdering = List(now2 -> cb1, expected2 -> cb2) - // ordering mustEqual expectedOrdering - // } + ordering mustEqual expectedOrdering + } } } diff --git a/tests/jvm/src/test/scala/cats/effect/unsafe/TimerSkipListSpec.scala b/tests/jvm/src/test/scala/cats/effect/unsafe/TimerHeapSpec.scala similarity index 77% rename from tests/jvm/src/test/scala/cats/effect/unsafe/TimerSkipListSpec.scala rename to tests/jvm/src/test/scala/cats/effect/unsafe/TimerHeapSpec.scala index 369eb6da61..0e6160613e 100644 --- a/tests/jvm/src/test/scala/cats/effect/unsafe/TimerSkipListSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/unsafe/TimerHeapSpec.scala @@ -18,7 +18,7 @@ package cats.effect.unsafe import org.specs2.mutable.Specification -class TimerSkipListSpec extends Specification { +class TimerHeapSpec extends Specification { /** * Creates a new callback, making sure it's a separate object @@ -34,25 +34,31 @@ class TimerSkipListSpec extends Specification { private val cb4 = newCb() private val cb5 = newCb() - "TimerSkipList" should { + "TimerHeap" should { "correctly insert / pollFirstIfTriggered" in { - val m = new TimerSkipList + val m = new TimerHeap + val out = new Array[Right[Nothing, Unit] => Unit](1) m.pollFirstIfTriggered(Long.MinValue) must beNull m.pollFirstIfTriggered(Long.MaxValue) must beNull - m.toString mustEqual "TimerSkipList()" + m.toString mustEqual "TimerHeap()" - m.insertTlr(0L, 0L, cb0) - m.toString mustEqual "TimerSkipList(...)" + m.insert(0L, 0L, cb0, out) + out(0) must beNull + m.toString mustEqual "TimerHeap(...)" m.pollFirstIfTriggered(Long.MinValue) must beNull m.pollFirstIfTriggered(Long.MaxValue) mustEqual cb0 m.pollFirstIfTriggered(Long.MaxValue) must beNull m.pollFirstIfTriggered(Long.MinValue) must beNull - m.insertTlr(0L, 10L, cb0) - m.insertTlr(0L, 30L, cb1) - m.insertTlr(0L, 0L, cb2) - m.insertTlr(0L, 20L, cb3) + m.insert(0L, 10L, cb0, out) + out(0) must beNull + m.insert(0L, 30L, cb1, out) + out(0) must beNull + m.insert(0L, 0L, cb2, out) + out(0) must beNull + m.insert(0L, 20L, cb3, out) + out(0) must beNull m.pollFirstIfTriggered(-1L) must beNull m.pollFirstIfTriggered(0L) mustEqual cb2 m.pollFirstIfTriggered(0L) must beNull @@ -66,13 +72,20 @@ class TimerSkipListSpec extends Specification { } "correctly insert / remove (cancel)" in { - val m = new TimerSkipList - val r0 = m.insertTlr(0L, 0L, cb0) - val r1 = m.insertTlr(0L, 1L, cb1) - val r5 = m.insertTlr(0L, 5L, cb5) - val r4 = m.insertTlr(0L, 4L, cb4) - val r2 = m.insertTlr(0L, 2L, cb2) - val r3 = m.insertTlr(0L, 3L, cb3) + val m = new TimerHeap + val out = new Array[Right[Nothing, Unit] => Unit](1) + val r0 = m.insert(0L, 0L, cb0, out) + out(0) must beNull + val r1 = m.insert(0L, 1L, cb1, out) + out(0) must beNull + val r5 = m.insert(0L, 5L, cb5, out) + out(0) must beNull + val r4 = m.insert(0L, 4L, cb4, out) + out(0) must beNull + val r2 = m.insert(0L, 2L, cb2, out) + out(0) must beNull + val r3 = m.insert(0L, 3L, cb3, out) + out(0) must beNull m.peekFirstQuiescent() mustEqual cb0 m.peekFirstTriggerTime() mustEqual 0L @@ -102,14 +115,14 @@ class TimerSkipListSpec extends Specification { } "behave correctly when nanoTime wraps around" in { - val m = new TimerSkipList + val m = new TimerHeap val startFrom = Long.MaxValue - 100L var nanoTime = startFrom val removersBuilder = Vector.newBuilder[Runnable] val callbacksBuilder = Vector.newBuilder[Right[Nothing, Unit] => Unit] for (_ <- 0 until 200) { val cb = newCb() - val r = m.insertTlr(nanoTime, 10L, cb) + val r = m.insert(nanoTime, 10L, cb, new Array(1)) removersBuilder += r callbacksBuilder += cb nanoTime += 1L From 8bb7da7bc2d01bfff08bfb5a28c853ded8fcfc58 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 1 Sep 2023 01:17:13 +0000 Subject: [PATCH 183/429] Cancel at most once on owning thread --- core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index cd81720011..e6fa30a5ec 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -323,7 +323,7 @@ private final class TimerHeap { /** * Cancel this timer. */ - def apply(): Unit = { + def apply(): Unit = if (callback ne null) { // if we're not already canceled // we can always clear the callback, without explicitly publishing callback = null From 8397877e49b1bbb7a2a17e5e31d6bab2d27e724e Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 1 Sep 2023 13:55:07 +0000 Subject: [PATCH 184/429] Remove `TimerSkipList` --- build.sbt | 4 +- .../effect/unsafe/TimerSkipListNodeBase.java | 56 -- .../cats/effect/unsafe/TimerSkipList.scala | 778 ------------------ 3 files changed, 3 insertions(+), 835 deletions(-) delete mode 100644 core/jvm/src/main/java/cats/effect/unsafe/TimerSkipListNodeBase.java delete mode 100644 core/jvm/src/main/scala/cats/effect/unsafe/TimerSkipList.scala diff --git a/build.sbt b/build.sbt index 15aaebd556..699d7ead30 100644 --- a/build.sbt +++ b/build.sbt @@ -646,7 +646,9 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) "cats.effect.unsafe.WorkerThread.sleep"), // #3787, internal utility that was no longer needed ProblemFilters.exclude[MissingClassProblem]("cats.effect.Thunk"), - ProblemFilters.exclude[MissingClassProblem]("cats.effect.Thunk$") + ProblemFilters.exclude[MissingClassProblem]("cats.effect.Thunk$"), + // #3781, replaced TimerSkipList with TimerHeap + ProblemFilters.exclude[MissingClassProblem]("cats.effect.unsafe.TimerSkipList*") ) ++ { if (tlIsScala3.value) { // Scala 3 specific exclusions diff --git a/core/jvm/src/main/java/cats/effect/unsafe/TimerSkipListNodeBase.java b/core/jvm/src/main/java/cats/effect/unsafe/TimerSkipListNodeBase.java deleted file mode 100644 index 0491dff09a..0000000000 --- a/core/jvm/src/main/java/cats/effect/unsafe/TimerSkipListNodeBase.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2020-2023 Typelevel - * - * 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.unsafe; - -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; - -/** - * Base class for `TimerSkipList#Node`, because we can't use `AtomicReferenceFieldUpdater` from - * Scala. - */ -@SuppressWarnings("serial") // do not serialize this! -abstract class TimerSkipListNodeBase> - extends AtomicReference { - - private volatile C callback; - - @SuppressWarnings("rawtypes") - private static final AtomicReferenceFieldUpdater CALLBACK = - AtomicReferenceFieldUpdater.newUpdater(TimerSkipListNodeBase.class, Object.class, "callback"); - - protected TimerSkipListNodeBase(C cb, N next) { - super(next); - this.callback = cb; - } - - public final N getNext() { - return this.get(); // could be `getAcquire` - } - - public final boolean casNext(N ov, N nv) { - return this.compareAndSet(ov, nv); - } - - public final C getCb() { - return this.callback; // could be `getAcquire` - } - - public final boolean casCb(C ov, C nv) { - return CALLBACK.compareAndSet(this, ov, nv); - } -} diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerSkipList.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerSkipList.scala deleted file mode 100644 index e02a2422b9..0000000000 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerSkipList.scala +++ /dev/null @@ -1,778 +0,0 @@ -/* - * Copyright 2020-2023 Typelevel - * - * 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.unsafe - -import scala.annotation.tailrec - -import java.lang.Long.{MAX_VALUE, MIN_VALUE => MARKER} -import java.util.concurrent.ThreadLocalRandom -import java.util.concurrent.atomic.{AtomicLong, AtomicReference} - -/** - * Concurrent skip list holding timer callbacks and their associated trigger times. The 3 main - * operations are `pollFirstIfTriggered`, `insert`, and the "remove" returned by `insert` (for - * cancelling timers). - */ -private final class TimerSkipList() extends AtomicLong(MARKER + 1L) { sequenceNumber => - - /* - * This implementation is based on the public - * domain JSR-166 `ConcurrentSkipListMap`. - * Contains simplifications, because we just - * need a few main operations. Also, - * `pollFirstIfTriggered` contains an extra - * condition (compared to `pollFirstEntry` - * in the JSR-166 implementation), because - * we don't want to remove if the trigger time - * is still in the future. - * - * Our values are the callbacks, and used - * similarly. Our keys are essentially the - * trigger times, but see the comment in - * `insert`. Due to longs not having nulls, - * we use a special value for designating - * "marker" nodes (see `Node#isMarker`). - */ - - private[this] type Callback = - Right[Nothing, Unit] => Unit - - /** - * Base nodes (which form the base list) store the payload. - * - * `next` is the next node in the base list (with a key > than this). - * - * A `Node` is a special "marker" node (for deletion) if `sequenceNum == MARKER`. A `Node` is - * logically deleted if `cb eq null`. - * - * We're also (ab)using the `Node` class as the "canceller" for an inserted timer callback - * (see `run` method). - */ - private[unsafe] final class Node private[TimerSkipList] ( - val triggerTime: Long, - val sequenceNum: Long, - cb: Callback, - next: Node - ) extends TimerSkipListNodeBase[Callback, Node](cb, next) - with Function0[Unit] - with Runnable { - - /** - * Cancels the timer - */ - final def apply(): Unit = { - // TODO: We could null the callback here directly, - // TODO: and the do the lookup after (for unlinking). - TimerSkipList.this.doRemove(triggerTime, sequenceNum) - () - } - - final def run() = apply() - - private[TimerSkipList] final def isMarker: Boolean = { - // note: a marker node also has `triggerTime == MARKER`, - // but that's also a valid trigger time, so we need - // `sequenceNum` here - sequenceNum == MARKER - } - - private[TimerSkipList] final def isDeleted(): Boolean = { - getCb() eq null - } - - final override def toString: String = - "" - } - - /** - * Index nodes - */ - private[this] final class Index( - val node: Node, - val down: Index, - r: Index - ) extends AtomicReference[Index](r) { right => - - require(node ne null) - - final def getRight(): Index = { - right.get() // could be `getAcquire` - } - - final def setRight(nv: Index): Unit = { - right.set(nv) // could be `setPlain` - } - - final def casRight(ov: Index, nv: Index): Boolean = { - right.compareAndSet(ov, nv) - } - - final override def toString: String = - "Index(...)" - } - - /** - * The top left index node (or null if empty) - */ - private[this] val head = - new AtomicReference[Index] - - /** - * For testing - */ - private[unsafe] final def insertTlr( - now: Long, - delay: Long, - callback: Right[Nothing, Unit] => Unit - ): Runnable = { - insert(now, delay, callback, ThreadLocalRandom.current()) - } - - /** - * Inserts a new `callback` which will be triggered not earlier than `now + delay`. Returns a - * "canceller", which (if executed) removes (cancels) the inserted callback. (Of course, by - * that time the callback might've been already invoked.) - * - * @param now - * the current time as returned by `System.nanoTime` - * @param delay - * nanoseconds delay, must be nonnegative - * @param callback - * the callback to insert into the skip list - * @param tlr - * the `ThreadLocalRandom` of the current (calling) thread - */ - final def insert( - now: Long, - delay: Long, - callback: Right[Nothing, Unit] => Unit, - tlr: ThreadLocalRandom - ): Function0[Unit] with Runnable = { - require(delay >= 0L) - // we have to check for overflow: - val triggerTime = computeTriggerTime(now = now, delay = delay) - // Because our skip list can't handle multiple - // values (callbacks) for the same key, the - // key is not only the `triggerTime`, but - // conceptually a `(triggerTime, seqNo)` tuple. - // We generate unique (for this skip list) - // sequence numbers with an atomic counter. - val seqNo = { - val sn = sequenceNumber.getAndIncrement() - // In case of overflow (very unlikely), - // we make sure we don't use MARKER for - // a valid node (which would be very bad); - // otherwise the overflow can only cause - // problems with the ordering of callbacks - // with the exact same triggerTime... - // which is unspecified anyway (due to - // stealing). - if (sn != MARKER) sn - else sequenceNumber.getAndIncrement() - } - - doPut(triggerTime, seqNo, callback, tlr) - } - - /** - * Removes and returns the first (earliest) timer callback, if its trigger time is not later - * than `now`. Can return `null` if there is no such callback. - * - * It is the caller's responsibility to check for `null`, and actually invoke the callback (if - * desired). - * - * @param now - * the current time as returned by `System.nanoTime` - */ - final def pollFirstIfTriggered(now: Long): Right[Nothing, Unit] => Unit = { - doRemoveFirstNodeIfTriggered(now) - } - - /** - * Looks at the first callback in the list, and returns its trigger time. - * - * @return - * the `triggerTime` of the first callback, or `Long.MinValue` if the list is empty. - */ - final def peekFirstTriggerTime(): Long = { - val head = peekFirstNode() - if (head ne null) { - val tt = head.triggerTime - if (tt != MARKER) { - tt - } else { - // in the VERY unlikely case when - // the trigger time is exactly our - // sentinel, we just cheat a little - // (this could cause threads to wake - // up 1 ns too early): - MAX_VALUE - } - } else { - MARKER - } - } - - final override def toString: String = { - peekFirstNode() match { - case null => - "TimerSkipList()" - case _ => - "TimerSkipList(...)" - } - } - - /** - * For testing - */ - private[unsafe] final def peekFirstQuiescent(): Callback = { - val n = peekFirstNode() - if (n ne null) { - n.getCb() - } else { - null - } - } - - /** - * Compares keys, first by trigger time, then by sequence number; this method determines the - * "total order" that is used by the skip list. - * - * The trigger times are `System.nanoTime` longs, so they have to be compared in a peculiar - * way (see javadoc there). This makes this order non-transitive, which is quite bad. However, - * `computeTriggerTime` makes sure that there is no overflow here, so we're okay. - * - * Analogous to `cpr` in the JSR-166 `ConcurrentSkipListMap`. - */ - private[this] final def cpr( - xTriggerTime: Long, - xSeqNo: Long, - yTriggerTime: Long, - ySeqNo: Long): Int = { - // first compare trigger times: - val d = xTriggerTime - yTriggerTime - if (d < 0) -1 - else if (d > 0) 1 - else { - // if times are equal, compare seq numbers: - if (xSeqNo < ySeqNo) -1 - else if (xSeqNo == ySeqNo) 0 - else 1 - } - } - - /** - * Computes the trigger time in an overflow-safe manner. The trigger time is essentially `now - * + delay`. However, we must constrain all trigger times in the skip list to be within - * `Long.MaxValue` of each other (otherwise there will be overflow when comparing in `cpr`). - * Thus, if `delay` is so big, we'll reduce it to the greatest allowable (in `overflowFree`). - * - * From the public domain JSR-166 `ScheduledThreadPoolExecutor` (`triggerTime` method). - */ - private[this] final def computeTriggerTime(now: Long, delay: Long): Long = { - val safeDelay = if (delay < (MAX_VALUE >> 1)) delay else overflowFree(now, delay) - now + safeDelay - } - - /** - * See `computeTriggerTime`. The overflow can happen if a callback was already triggered - * (based on `now`), but was not removed yet; and `delay` is sufficiently big. - * - * From the public domain JSR-166 `ScheduledThreadPoolExecutor` (`overflowFree` method). - */ - private[this] final def overflowFree(now: Long, delay: Long): Long = { - val head = peekFirstNode() - // Note, that there is a race condition here: - // the node we're looking at (`head`) can be - // concurrently removed/cancelled. But the - // consequence of that here is only that we - // will be MORE careful with `delay` than - // necessary. - if (head ne null) { - val headDelay = head.triggerTime - now - if ((headDelay < 0) && (delay - headDelay < 0)) { - // head was already triggered, and `delay` is big enough, - // so we must clamp `delay`: - MAX_VALUE + headDelay - } else { - delay - } - } else { - delay // empty - } - } - - /** - * Analogous to `doPut` in the JSR-166 `ConcurrentSkipListMap`. - */ - @tailrec - private[this] final def doPut( - triggerTime: Long, - seqNo: Long, - cb: Callback, - tlr: ThreadLocalRandom): Node = { - val h = head.get() // could be `getAcquire` - var levels = 0 // number of levels descended - var b: Node = if (h eq null) { - // head not initialized yet, do it now; - // first node of the base list is a sentinel - // (without payload): - val base = new Node(MARKER, MARKER, null: Callback, null) - val h = new Index(base, null, null) - if (head.compareAndSet(null, h)) base else null - } else { - // we have a head; find a node in the base list - // "close to" (but before) the inserion point: - var q: Index = h // current position, start from the head - var foundBase: Node = null // we're looking for this - while (foundBase eq null) { - // first try to go right: - q = walkRight(q, triggerTime, seqNo) - // then try to go down: - val d = q.down - if (d ne null) { - levels += 1 - q = d // went down 1 level, will continue going right - } else { - // reached the base list, break outer loop: - foundBase = q.node - } - } - foundBase - } - if (b ne null) { - // `b` is a node in the base list, "close to", - // but before the insertion point - var z: Node = null // will be the new node when inserted - var n: Node = null // next node - var go = true - while (go) { - var c = 0 // `cpr` result - n = b.getNext() - if (n eq null) { - // end of the list, insert right here - c = -1 - } else if (n.isMarker) { - // someone is deleting `b` right now, will - // restart insertion (as `z` is still null) - go = false - } else if (n.isDeleted()) { - unlinkNode(b, n) - c = 1 // will retry going right - } else { - c = cpr(triggerTime, seqNo, n.triggerTime, n.sequenceNum) - if (c > 0) { - // continue right - b = n - } // else: we assume c < 0, due to seqNr being unique - } - - if (c < 0) { - // found insertion point - val p = new Node(triggerTime, seqNo, cb, n) - if (b.casNext(n, p)) { - z = p - go = false - } // else: lost a race, retry - } - } - - if (z ne null) { - // we successfully inserted a new node; - // maybe add extra indices: - var rnd = tlr.nextLong() - if ((rnd & 0x3L) == 0L) { // add at least one index with 1/4 probability - // first create a "tower" of index - // nodes (all with `.right == null`): - var skips = levels - var x: Index = null // most recently created (topmost) index node in the tower - var go = true - while (go) { - // the height of the tower is at most 62 - // we create at most 62 indices in the tower - // (62 = 64 - 2; the 2 low bits are 0); - // also, the height is at most the number - // of levels we descended when inserting - x = new Index(z, x, null) - if (rnd >= 0L) { - // reached the first 0 bit in `rnd` - go = false - } else { - skips -= 1 - if (skips < 0) { - // reached the existing levels - go = false - } else { - // each additional index level has 1/2 probability - rnd <<= 1 - } - } - } - - // then actually add these index nodes to the skiplist: - if (addIndices(h, skips, x) && (skips < 0) && (head - .get() eq h)) { // could be `getAcquire` - // if we successfully added a full height - // "tower", try to also add a new level - // (with only 1 index node + the head) - val hx = new Index(z, x, null) - val nh = new Index(h.node, h, hx) // new head - head.compareAndSet(h, nh) - } - - if (z.isDeleted()) { - // was deleted while we added indices, - // need to clean up: - findPredecessor(triggerTime, seqNo) - () - } - } // else: we're done, and won't add indices - - z - } else { // restart - doPut(triggerTime, seqNo, cb, tlr) - } - } else { // restart - doPut(triggerTime, seqNo, cb, tlr) - } - } - - /** - * Starting from the `q` index node, walks right while possible by comparing keys - * (`triggerTime` and `seqNo`). Returns the last index node (at this level) which is still a - * predecessor of the node with the specified key (`triggerTime` and `seqNo`). This returned - * index node can be `q` itself. (This method assumes that the specified `q` is a predecessor - * of the node with the key.) - * - * This method has no direct equivalent in the JSR-166 `ConcurrentSkipListMap`; the same logic - * is embedded in various methods as a `while` loop. - */ - @tailrec - private[this] final def walkRight(q: Index, triggerTime: Long, seqNo: Long): Index = { - val r = q.getRight() - if (r ne null) { - val p = r.node - if (p.isMarker || p.isDeleted()) { - // marker or deleted node, unlink it: - q.casRight(r, r.getRight()) - // and retry: - walkRight(q, triggerTime, seqNo) - } else if (cpr(triggerTime, seqNo, p.triggerTime, p.sequenceNum) > 0) { - // we can still go right: - walkRight(r, triggerTime, seqNo) - } else { - // can't go right any more: - q - } - } else { - // can't go right any more: - q - } - } - - /** - * Finds the node with the specified key; deletes it logically by CASing the callback to null; - * unlinks it (first inserting a marker); removes associated index nodes; and possibly reduces - * index level. - * - * Analogous to `doRemove` in the JSR-166 `ConcurrentSkipListMap`. - */ - private[this] final def doRemove(triggerTime: Long, seqNo: Long): Boolean = { - var b = findPredecessor(triggerTime, seqNo) - while (b ne null) { // outer - var inner = true - while (inner) { - val n = b.getNext() - if (n eq null) { - return false - } else if (n.isMarker) { - inner = false - b = findPredecessor(triggerTime, seqNo) - } else { - val ncb = n.getCb() - if (ncb eq null) { - unlinkNode(b, n) - // and retry `b.getNext()` - } else { - val c = cpr(triggerTime, seqNo, n.triggerTime, n.sequenceNum) - if (c > 0) { - b = n - } else if (c < 0) { - return false - } else if (n.casCb(ncb, null)) { - // successfully logically deleted - unlinkNode(b, n) - findPredecessor(triggerTime, seqNo) // cleanup - tryReduceLevel() - return true - } - } - } - } - } - - false - } - - /** - * Returns the first node of the base list. Skips logically deleted nodes, so the returned - * node was non-deleted when calling this method (but beware of concurrent deleters). - */ - private[this] final def peekFirstNode(): Node = { - var b = baseHead() - if (b ne null) { - var n: Node = null - while ({ - n = b.getNext() - (n ne null) && (n.isDeleted()) - }) { - b = n - } - - n - } else { - null - } - } - - /** - * Analogous to `doRemoveFirstEntry` in the JSR-166 `ConcurrentSkipListMap`. - */ - private[this] final def doRemoveFirstNodeIfTriggered(now: Long): Callback = { - val b = baseHead() - if (b ne null) { - - @tailrec - def go(): Callback = { - val n = b.getNext() - if (n ne null) { - val tt = n.triggerTime - if (now - tt >= 0) { // triggered - val cb = n.getCb() - if (cb eq null) { - // alread (logically) deleted node - unlinkNode(b, n) - go() - } else if (n.casCb(cb, null)) { - unlinkNode(b, n) - tryReduceLevel() - findPredecessor(tt, n.sequenceNum) // clean index - cb - } else { - // lost race, retry - go() - } - } else { // not triggered yet - null - } - } else { - null - } - } - - go() - } else { - null - } - } - - /** - * The head of the base list (or `null` if uninitialized). - * - * Analogous to `baseHead` in the JSR-166 `ConcurrentSkipListMap`. - */ - private[this] final def baseHead(): Node = { - val h = head.get() // could be `getAcquire` - if (h ne null) h.node else null - } - - /** - * Adds indices after an insertion was performed (e.g. `doPut`). Descends iteratively to the - * highest index to insert, and from then recursively calls itself to insert lower level - * indices. Returns `false` on staleness, which disables higher level insertions (from the - * recursive calls). - * - * Analogous to `addIndices` in the JSR-166 `ConcurrentSkipListMap`. - * - * @param _q - * starting index node for the current level - * @param _skips - * levels to skip down before inserting - * @param x - * the top of a "tower" of new indices (with `.right == null`) - * @return - * `true` iff we successfully inserted the new indices - */ - private[this] final def addIndices(_q: Index, _skips: Int, x: Index): Boolean = { - if (x ne null) { - var q = _q - var skips = _skips - val z = x.node - if ((z ne null) && !z.isMarker && (q ne null)) { - var retrying = false - while (true) { // find splice point - val r = q.getRight() - var c: Int = 0 // comparison result - if (r ne null) { - val p = r.node - if (p.isMarker || p.isDeleted()) { - // clean deleted node: - q.casRight(r, r.getRight()) - c = 0 - } else { - c = cpr(z.triggerTime, z.sequenceNum, p.triggerTime, p.sequenceNum) - } - if (c > 0) { - q = r - } else if (c == 0) { - // stale - return false - } - } else { - c = -1 - } - - if (c < 0) { - val d = q.down - if ((d ne null) && (skips > 0)) { - skips -= 1 - q = d - } else if ((d ne null) && !retrying && !addIndices(d, 0, x.down)) { - return false - } else { - x.setRight(r) - if (q.casRight(r, x)) { - return true - } else { - retrying = true // re-find splice point - } - } - } - } - } - } - - false - } - - /** - * Returns a base node whith key < the parameters. Also unlinks indices to deleted nodes while - * searching. - * - * Analogous to `findPredecessor` in the JSR-166 `ConcurrentSkipListMap`. - */ - private[this] final def findPredecessor(triggerTime: Long, seqNo: Long): Node = { - var q: Index = head.get() // current index node (could be `getAcquire`) - if ((q eq null) || (seqNo == MARKER)) { - null - } else { - while (true) { - // go right: - q = walkRight(q, triggerTime, seqNo) - // go down: - val d = q.down - if (d ne null) { - q = d - } else { - // can't go down, we're done: - return q.node - } - } - - null // unreachable - } - } - - /** - * Tries to unlink the (logically) already deleted node `n` from its predecessor `b`. Before - * unlinking, this method inserts a "marker" node after `n`, to make sure there are no lost - * concurrent inserts. (An insert would do a CAS on `n.next`; linking a marker node after `n` - * makes sure the concurrent CAS on `n.next` will fail.) - * - * When this method returns, `n` is already unlinked from `b` (either by this method, or a - * concurrent thread). - * - * `b` or `n` may be `null`, in which case this method is a no-op. - * - * Analogous to `unlinkNode` in the JSR-166 `ConcurrentSkipListMap`. - */ - private[this] final def unlinkNode(b: Node, n: Node): Unit = { - if ((b ne null) && (n ne null)) { - // makes sure `n` is marked, - // returns node after the marker - def mark(): Node = { - val f = n.getNext() - if ((f ne null) && f.isMarker) { - f.getNext() // `n` is already marked - } else if (n.casNext(f, new Node(MARKER, MARKER, null: Callback, f))) { - f // we've successfully marked `n` - } else { - mark() // lost race, retry - } - } - - val p = mark() - b.casNext(n, p) - // if this CAS failed, someone else already unlinked the marked `n` - () - } - } - - /** - * Tries to reduce the number of levels by removing the topmost level. - * - * Multiple conditions must be fulfilled to actually remove the level: not only the topmost - * (1st) level must be (likely) empty, but the 2nd and 3rd too. This is to (1) reduce the - * chance of mistakes (see below), and (2) reduce the chance of frequent adding/removing of - * levels (hysteresis). - * - * We can make mistakes here: we can (with a small probability) remove a level which is - * concurrently becoming non-empty. This can degrade performance, but does not impact - * correctness (e.g., we won't lose keys/values). To even further reduce the possibility of - * mistakes, if we detect one, we try to quickly undo the deletion we did. - * - * The reason for (rarely) allowing the removal of a level which shouldn't be removed, is that - * this is still better than allowing levels to just grow (which would also degrade - * performance). - * - * Analogous to `tryReduceLevel` in the JSR-166 `ConcurrentSkipListMap`. - */ - private[this] final def tryReduceLevel(): Unit = { - val lv1 = head.get() // could be `getAcquire` - if ((lv1 ne null) && (lv1.getRight() eq null)) { // 1st level seems empty - val lv2 = lv1.down - if ((lv2 ne null) && (lv2.getRight() eq null)) { // 2nd level seems empty - val lv3 = lv2.down - if ((lv3 ne null) && (lv3.getRight() eq null)) { // 3rd level seems empty - // the topmost 3 levels seem empty, - // so try to decrease levels by 1: - if (head.compareAndSet(lv1, lv2)) { - // successfully reduced level, - // but re-check if it's still empty: - if (lv1.getRight() ne null) { - // oops, we deleted a level - // with concurrent insert(s), - // try to fix our mistake: - head.compareAndSet(lv2, lv1) - () - } - } - } - } - } - } -} From 4352294ff4d3b51ec44c39329ba47e9c269f78a5 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 1 Sep 2023 16:26:09 +0000 Subject: [PATCH 185/429] Better handling of external sleeps --- .../unsafe/WorkStealingThreadPool.scala | 59 ++++++++++++++----- .../cats/effect/unsafe/WorkerThread.scala | 41 +++++++++++-- 2 files changed, 81 insertions(+), 19 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index ec2eb046f7..bc6e10ce4e 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -41,9 +41,11 @@ import java.time.Instant import java.time.temporal.ChronoField import java.util.Comparator import java.util.concurrent.{ConcurrentSkipListSet, ThreadLocalRandom} -import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger, AtomicReference} +import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger} import java.util.concurrent.locks.LockSupport +import WorkStealingThreadPool._ + /** * Work-stealing thread pool which manages a pool of [[WorkerThread]] s for the specific purpose * of executing [[java.lang.Runnable]] instancess with work-stealing scheduling semantics. @@ -627,29 +629,29 @@ private[effect] final class WorkStealingThreadPool( } /** - * Chooses a random `TimerSkipList` from this pool, and inserts the `callback`. + * Reschedule onto a worker thread and then submit the sleep. */ private[this] final def sleepExternal( delay: FiniteDuration, callback: Right[Nothing, Unit] => Unit): Function0[Unit] with Runnable = { - val cancel = new AtomicReference[Function0[Unit]] with Function0[Unit] with Runnable { - def apply(): Unit = { - val back = get() - if (back ne null) back() - } + val scheduledAt = monotonicNanos() + val cancel = new ExternalSleepCancel - def run() = apply() + scheduleExternal { () => + val worker = Thread.currentThread().asInstanceOf[WorkerThread] + cancel.setCallback(worker.sleepLate(scheduledAt, delay, callback)) } - scheduleExternal(() => cancel.lazySet(sleepInternal(delay, callback))) - cancel } - override def sleep(delay: FiniteDuration, task: Runnable): Runnable = { - // TODO task should be run at most once - sleepInternal(delay, _ => task.run()) - } + override def sleep(delay: FiniteDuration, task: Runnable): Runnable = + sleepInternal( + delay, + new AtomicBoolean with (Right[Nothing, Unit] => Unit) { // run at most once + def apply(ru: Right[Nothing, Unit]) = if (compareAndSet(false, true)) task.run() + } + ) /** * Shut down the thread pool and clean up the pool state. Calling this method after the pool @@ -765,3 +767,32 @@ private[effect] final class WorkStealingThreadPool( private[unsafe] def getSuspendedFiberCount(): Long = workerThreads.map(_.getSuspendedFiberCount().toLong).sum } + +private object WorkStealingThreadPool { + + /** + * A wrapper for a cancelation callback that is created asynchronously. Best-effort: does not + * explicitly publish. + */ + private final class ExternalSleepCancel extends Function0[Unit] with Runnable { + private[this] var callback: Function0[Unit] = null + + def setCallback(cb: Function0[Unit]) = { + val back = callback + if (back eq CanceledSleepSentinel) + cb() // we were already canceled, invoke right away + else + callback = cb + } + + def apply() = { + val back = callback + callback = CanceledSleepSentinel + if (back ne null) back() + } + + def run() = apply() + } + + private val CanceledSleepSentinel: Function0[Unit] = () => () +} diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala index 2ff0a2d837..7eb5ed12ab 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala @@ -107,6 +107,10 @@ private final class WorkerThread( private[this] val runtimeBlockingExpiration: Duration = pool.runtimeBlockingExpiration private[this] val RightUnit = Right(()) + private[this] val noop = new Function0[Unit] with Runnable { + def apply() = () + def run() = () + } val nameIndex: Int = pool.blockedWorkerThreadNamingIndex.getAndIncrement() @@ -154,18 +158,45 @@ private final class WorkerThread( } } - def sleep( - delay: FiniteDuration, - callback: Right[Nothing, Unit] => Unit): Function0[Unit] with Runnable = { + private[this] def nanoTime(): Long = { // take the opportunity to update the current time, just in case other timers can benefit val _now = System.nanoTime() now = _now + _now + } + + def sleep( + delay: FiniteDuration, + callback: Right[Nothing, Unit] => Unit): Function0[Unit] with Runnable = + sleepImpl(nanoTime(), delay.toNanos, callback) + + /** + * A sleep that is being scheduled "late" + */ + def sleepLate( + scheduledAt: Long, + delay: FiniteDuration, + callback: Right[Nothing, Unit] => Unit): Function0[Unit] with Runnable = { + val _now = nanoTime() + val newDelay = delay.toNanos - (_now - scheduledAt) + if (newDelay > 0) { + sleepImpl(_now, newDelay, callback) + } else { + callback(RightUnit) + noop + } + } + + private[this] def sleepImpl( + now: Long, + delay: Long, + callback: Right[Nothing, Unit] => Unit): Function0[Unit] with Runnable = { val out = new Array[Right[Nothing, Unit] => Unit](1) // note that blockers aren't owned by the pool, meaning we only end up here if !blocking val cancel = sleepers.insert( - now = _now, - delay = delay.toNanos, + now = now, + delay = delay, callback = callback, out = out ) From 243d04a882808099980f128bc29090d03367ec9c Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Fri, 1 Sep 2023 20:07:08 +0000 Subject: [PATCH 186/429] Update scalafmt-core to 3.7.14 in series/3.x --- .scalafmt.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index 556b5b81c2..4f413283ab 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = 3.7.13 +version = 3.7.14 runner.dialect = Scala213Source3 fileOverride { From 8416159478bb27a3fe5f83661e09ae061a390cad Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 1 Sep 2023 15:42:21 -0700 Subject: [PATCH 187/429] Cherry-pick Cirrus CI updates to series/3.x --- .cirrus.yml | 66 +++++++++++++++-------------------------------------- 1 file changed, 19 insertions(+), 47 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 768443c3b6..6e3b151060 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -4,72 +4,44 @@ jvm_highcore_task: cpu: 4 memory: 8G matrix: - - name: JVM high-core-count 2.12 - script: sbt '++ 2.12' testsJVM/test - name: JVM high-core-count 2.13 - script: sbt '++ 2.13' testsJVM/test stressTests/Jcstress/run + script: sbt '++ 2.13' testsJVM/test - name: JVM high-core-count 3 script: sbt '++ 3' testsJVM/test -# jvm_arm_highcore_task: -# arm_container: -# image: sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.0_3.3.0 -# cpu: 4 -# memory: 8G -# matrix: -# - name: JVM ARM high-core-count 2.12 -# script: sbt '++ 2.12' testsJVM/test -# - name: JVM ARM high-core-count 2.13 -# script: sbt '++ 2.13' testsJVM/test stressTests/Jcstress/run -# - name: JVM ARM high-core-count 3 -# script: sbt '++ 3' testsJVM/test +jvm_arm_highcore_task: + arm_container: + image: sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.5_8_1.9.0_3.3.0 + cpu: 4 + memory: 8G + matrix: + - name: JVM ARM high-core-count 2.13 + script: sbt '++ 2.13' testsJVM/test + - name: JVM ARM high-core-count 3 + script: sbt '++ 3' testsJVM/test jvm_macos_highcore_task: macos_instance: image: ghcr.io/cirruslabs/macos-ventura-base:latest - cpu: 4 - memory: 8G matrix: - - name: JVM Apple Silicon high-core-count 2.12 - script: - - brew install sbt - - sbt '++ 2.12' testsJVM/test - - name: JVM Apple Silicon high-core-count 2.13 - script: - - brew install sbt - - sbt '++ 2.13' testsJVM/test stressTests/Jcstress/run - name: JVM Apple Silicon high-core-count 3 script: - brew install sbt - sbt '++ 3' testsJVM/test -# native_arm_task: -# arm_container: -# dockerfile: .cirrus/Dockerfile -# cpu: 2 -# memory: 8G -# matrix: -# - name: Native ARM 2.12 -# script: sbt '++ 2.12' testsNative/test -# - name: Native ARM 2.13 -# script: sbt '++ 2.13' testsNative/test -# - name: Native ARM 3 -# script: sbt '++ 3' testsNative/test +native_arm_task: + arm_container: + dockerfile: .cirrus/Dockerfile + cpu: 2 + memory: 8G + matrix: + - name: Native ARM 3 + script: sbt '++ 3' testsNative/test native_macos_task: macos_instance: image: ghcr.io/cirruslabs/macos-ventura-base:latest - cpu: 2 - memory: 8G matrix: - - name: Native Apple Silicon 2.12 - script: - - brew install sbt - - sbt '++ 2.12' testsNative/test - - name: Native Apple Silicon 2.13 - script: - - brew install sbt - - sbt '++ 2.13' testsNative/test - name: Native Apple Silicon 3 script: - brew install sbt From 4e73d375e755890584f68bae49e141b407762c34 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 2 Sep 2023 16:29:06 +0000 Subject: [PATCH 188/429] Condense `SIGUSR1` --- core/native/src/main/scala/cats/effect/Signal.scala | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/Signal.scala b/core/native/src/main/scala/cats/effect/Signal.scala index 2ebd683b2d..b380dea64e 100644 --- a/core/native/src/main/scala/cats/effect/Signal.scala +++ b/core/native/src/main/scala/cats/effect/Signal.scala @@ -93,15 +93,13 @@ private object Signal { private[this] final val SIGINT = 2 private[this] final val SIGTERM = 15 - private[this] final val SIGUSR1Linux = 10 - private[this] final val SIGUSR1Mac = 30 + private[this] final val SIGUSR1 = if (isLinux) 10 else if (isMac) 30 else 0 private[this] final val SIGINFO = 29 if (isLinux || isMac) { installHandler(SIGINT, onInterrupt(_)) installHandler(SIGTERM, onTerm(_)) - if (isLinux) installHandler(SIGUSR1Linux, onDump(_)) - if (isMac) installHandler(SIGUSR1Mac, onDump(_)) + installHandler(SIGUSR1, onDump(_)) if (isMac) installHandler(SIGINFO, onDump(_)) } From 4a92a6a289d590dbdbb4fcedafbea21d6e2e8ed9 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 3 Sep 2023 13:57:57 +0000 Subject: [PATCH 189/429] Fix Scala 3 warnings --- ioapp-tests/src/test/scala/IOAppSpec.scala | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/ioapp-tests/src/test/scala/IOAppSpec.scala b/ioapp-tests/src/test/scala/IOAppSpec.scala index cbd4a0e5da..2caf1983fd 100644 --- a/ioapp-tests/src/test/scala/IOAppSpec.scala +++ b/ioapp-tests/src/test/scala/IOAppSpec.scala @@ -224,11 +224,13 @@ class IOAppSpec extends Specification { h.awaitStatus() mustEqual 0 } - if (platform != Native) + if (platform != Native){ "abort awaiting shutdown hooks" in { val h = platform("ShutdownHookImmediateTimeout", List.empty) h.awaitStatus() mustEqual 0 } + () + } } if (!isWindows) { @@ -270,7 +272,7 @@ class IOAppSpec extends Specification { } readTest() must contain("canceled") } - } + } else () "exit on fatal error without IOApp" in { val h = platform("FatalErrorRaw", List.empty) @@ -302,6 +304,7 @@ class IOAppSpec extends Specification { val stderr = h.stderr() stderr must contain("cats.effect.IOFiber") } + () } if (platform == JVM) { @@ -329,6 +332,7 @@ class IOAppSpec extends Specification { err must contain( "[WARNING] A Cats Effect worker thread was detected to be in a blocked state") } + () } if (platform == Node) { @@ -336,6 +340,7 @@ class IOAppSpec extends Specification { val h = platform("UndefinedProcessExit", List.empty) h.awaitStatus() mustEqual 0 } + () } "make specs2 happy" in ok From 31c46ebb2a541da071b0908e4f157d77ff286506 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 3 Sep 2023 14:02:08 +0000 Subject: [PATCH 190/429] Formatting --- ioapp-tests/src/test/scala/IOAppSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ioapp-tests/src/test/scala/IOAppSpec.scala b/ioapp-tests/src/test/scala/IOAppSpec.scala index 2caf1983fd..6f640336b9 100644 --- a/ioapp-tests/src/test/scala/IOAppSpec.scala +++ b/ioapp-tests/src/test/scala/IOAppSpec.scala @@ -224,7 +224,7 @@ class IOAppSpec extends Specification { h.awaitStatus() mustEqual 0 } - if (platform != Native){ + if (platform != Native) { "abort awaiting shutdown hooks" in { val h = platform("ShutdownHookImmediateTimeout", List.empty) h.awaitStatus() mustEqual 0 From 5e383de5d5504ecf18a84940a8f0793d83ee85dc Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 12:07:07 +0000 Subject: [PATCH 191/429] Update nscplugin, sbt-scala-native, ... to 0.4.15 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index ee7dce680d..24cfd6ba29 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -3,7 +3,7 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.0") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.14") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.15") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.5") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.7") From 03c981d2b1cb9bd06c81fab4fa6ddb5f9d24c075 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 12:07:11 +0000 Subject: [PATCH 192/429] Update sbt-jmh to 0.4.6 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index ee7dce680d..95035e7a31 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -4,7 +4,7 @@ addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.0") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.14") -addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.5") +addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.6") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.7") addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0") From 852da3f890f93c1dd8374a2eee232bbff567730d Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 5 Sep 2023 14:07:34 +0000 Subject: [PATCH 193/429] Use `atUnsafe` --- .../src/main/scala/cats/effect/unsafe/KqueueSystem.scala | 3 ++- .../cats/effect/std/SecureRandomCompanionPlatform.scala | 2 +- .../test/scala/cats/effect/FileDescriptorPollerSpec.scala | 8 +++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index 9e79e101ab..ae748a8673 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -136,7 +136,8 @@ object KqueueSystem extends PollingSystem { final class Poller private[KqueueSystem] (kqfd: Int) { private[this] val changelistArray = new Array[Byte](sizeof[kevent64_s].toInt * MaxEvents) - @inline private[this] def changelist = changelistArray.at(0).asInstanceOf[Ptr[kevent64_s]] + @inline private[this] def changelist = + changelistArray.atUnsafe(0).asInstanceOf[Ptr[kevent64_s]] private[this] var changeCount = 0 private[this] val callbacks = new LongMap[Either[Throwable, Unit] => Unit]() diff --git a/std/native/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala b/std/native/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala index 679b1c4b26..6589e53a4d 100644 --- a/std/native/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala +++ b/std/native/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala @@ -34,7 +34,7 @@ private[std] trait SecureRandomCompanionPlatform { var i = 0 while (i < len) { val n = Math.min(256, len - i) - if (sysrandom.getentropy(bytes.at(i), n.toULong) < 0) + if (sysrandom.getentropy(bytes.atUnsafe(i), n.toULong) < 0) throw new RuntimeException(fromCString(strerror(errno))) i += n } diff --git a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala index e2027a4619..b06450cdcc 100644 --- a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala +++ b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala @@ -40,13 +40,15 @@ class FileDescriptorPollerSpec extends BaseSpec { ) { def read(buf: Array[Byte], offset: Int, length: Int): IO[Unit] = readHandle - .pollReadRec(()) { _ => IO(guard(unistd.read(readFd, buf.at(offset), length.toULong))) } + .pollReadRec(()) { _ => + IO(guard(unistd.read(readFd, buf.atUnsafe(offset), length.toULong))) + } .void def write(buf: Array[Byte], offset: Int, length: Int): IO[Unit] = writeHandle .pollWriteRec(()) { _ => - IO(guard(unistd.write(writeFd, buf.at(offset), length.toULong))) + IO(guard(unistd.write(writeFd, buf.atUnsafe(offset), length.toULong))) } .void @@ -121,7 +123,7 @@ class FileDescriptorPollerSpec extends BaseSpec { .surround { IO { // trigger all the pipes at once pipes.foreach { pipe => - unistd.write(pipe.writeFd, Array[Byte](42).at(0), 1.toULong) + unistd.write(pipe.writeFd, Array[Byte](42).atUnsafe(0), 1.toULong) } }.background.surround(latch.await.as(true)) } From 0a6a8e0b85bc753509428123e7abf89f4131bd26 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 5 Sep 2023 14:18:24 +0000 Subject: [PATCH 194/429] Enable fiber dumps on Native --- .../main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala | 3 ++- ioapp-tests/src/test/scala/IOAppSpec.scala | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala b/core/native/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala index f38fb411b7..f545b92e91 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala @@ -18,10 +18,11 @@ package cats.effect package unsafe import scala.concurrent.ExecutionContext +import scala.scalanative.meta.LinktimeInfo private[effect] abstract class FiberMonitorPlatform { def apply(compute: ExecutionContext): FiberMonitor = { - if (false) { // LinktimeInfo.debugMode && LinktimeInfo.isWeakReferenceSupported + if (LinktimeInfo.debugMode && LinktimeInfo.isWeakReferenceSupported) { if (compute.isInstanceOf[EventLoopExecutorScheduler[_]]) { val loop = compute.asInstanceOf[EventLoopExecutorScheduler[_]] new FiberMonitorImpl(loop) diff --git a/ioapp-tests/src/test/scala/IOAppSpec.scala b/ioapp-tests/src/test/scala/IOAppSpec.scala index 6f640336b9..808e2fe122 100644 --- a/ioapp-tests/src/test/scala/IOAppSpec.scala +++ b/ioapp-tests/src/test/scala/IOAppSpec.scala @@ -286,7 +286,7 @@ class IOAppSpec extends Specification { h.awaitStatus() mustEqual 1 } - if (!isJava8 && !isWindows && platform != Native) { + if (!isJava8 && !isWindows) { // JDK 8 does not have free signals for live fiber snapshots // cannot observe signals sent to process termination on Windows "live fiber snapshot" in { From 3b033358f733067d32687c9ed837b8819f6ed2d4 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 17 Jun 2023 00:00:37 +0000 Subject: [PATCH 195/429] Less iterations for mutex test --- tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala b/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala index 8748af8b8e..75c7148d35 100644 --- a/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala @@ -124,7 +124,10 @@ final class MutexSpec extends BaseSpec with DetectPlatform { } "not deadlock when highly contended" in real { - mutex.flatMap(_.lock.use_.parReplicateA_(10)).replicateA_(10000).as(true) + mutex + .flatMap(_.lock.use_.parReplicateA_(10)) + .replicateA_(if (isJVM) 10000 else 100) + .as(true) } "handle cancelled acquire" in real { From 72386423c70a1db8a4f5d2b5c9d0a3a7d2b8010f Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 7 Sep 2023 15:02:08 +0000 Subject: [PATCH 196/429] Try a bit more replicates --- tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala b/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala index 75c7148d35..7563e468a0 100644 --- a/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala @@ -126,7 +126,7 @@ final class MutexSpec extends BaseSpec with DetectPlatform { "not deadlock when highly contended" in real { mutex .flatMap(_.lock.use_.parReplicateA_(10)) - .replicateA_(if (isJVM) 10000 else 100) + .replicateA_(if (isJVM) 10000 else 1000) .as(true) } From 4a053e195a275df4f6b070f956facc4e9db5a0d6 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 7 Sep 2023 15:56:00 +0000 Subject: [PATCH 197/429] Revert "Try a bit more replicates" This reverts commit 72386423c70a1db8a4f5d2b5c9d0a3a7d2b8010f. --- tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala b/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala index 7563e468a0..75c7148d35 100644 --- a/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala @@ -126,7 +126,7 @@ final class MutexSpec extends BaseSpec with DetectPlatform { "not deadlock when highly contended" in real { mutex .flatMap(_.lock.use_.parReplicateA_(10)) - .replicateA_(if (isJVM) 10000 else 1000) + .replicateA_(if (isJVM) 10000 else 100) .as(true) } From 03ad7866f990fba9807a767605e5bf62a916757f Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 7 Sep 2023 17:19:01 +0000 Subject: [PATCH 198/429] Try restricting heap size --- build.sbt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index bc5af57474..96ee23b2e7 100644 --- a/build.sbt +++ b/build.sbt @@ -893,7 +893,8 @@ lazy val tests: CrossProject = crossProject(JSPlatform, JVMPlatform, NativePlatf Test / fork := true ) .nativeSettings( - Compile / mainClass := Some("catseffect.examples.NativeRunner") + Compile / mainClass := Some("catseffect.examples.NativeRunner"), + Test / envVars += "GC_MAXIMUM_MAX_HEAP_SIZE" -> "2G" ) def configureIOAppTests(p: Project): Project = From 55ef1273c41d8bd2ad3f0182273f92ef9b7ef745 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Thu, 7 Sep 2023 20:10:26 +0000 Subject: [PATCH 199/429] Update scala3-library, ... to 3.3.1 in series/3.x --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index bc5af57474..b4cb2fc504 100644 --- a/build.sbt +++ b/build.sbt @@ -114,7 +114,7 @@ val MacOS = "macos-latest" val Scala212 = "2.12.18" val Scala213 = "2.13.11" -val Scala3 = "3.3.0" +val Scala3 = "3.3.1" ThisBuild / crossScalaVersions := Seq(Scala3, Scala212, Scala213) ThisBuild / githubWorkflowScalaVersions := crossScalaVersions.value From 6ecbafbc8dc5d993fa15a89a23d93ce9a2adb88b Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Thu, 7 Sep 2023 20:11:03 +0000 Subject: [PATCH 200/429] Regenerate GitHub Actions workflow Executed command: sbt githubWorkflowGenerate --- .github/workflows/ci.yml | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d47174a81e..b41c7cba22 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,11 +28,11 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - scala: [3.3.0, 2.12.18, 2.13.11] + scala: [3.3.1, 2.12.18, 2.13.11] java: [temurin@8, temurin@11, temurin@17, graalvm@17] ci: [ciJVM, ciNative, ciJS, ciFirefox, ciChrome] exclude: - - scala: 3.3.0 + - scala: 3.3.1 java: temurin@11 - scala: 2.12.18 java: temurin@11 @@ -41,10 +41,10 @@ jobs: - scala: 2.12.18 java: graalvm@17 - os: windows-latest - scala: 3.3.0 + scala: 3.3.1 ci: ciJVM - os: macos-latest - scala: 3.3.0 + scala: 3.3.1 ci: ciJVM - os: windows-latest scala: 2.12.18 @@ -53,9 +53,9 @@ jobs: scala: 2.12.18 ci: ciJVM - ci: ciFirefox - scala: 3.3.0 + scala: 3.3.1 - ci: ciChrome - scala: 3.3.0 + scala: 3.3.1 - ci: ciFirefox scala: 2.12.18 - ci: ciChrome @@ -201,24 +201,24 @@ jobs: run: sbt githubWorkflowCheck - name: Check that scalafix has been run on JVM - if: matrix.ci == 'ciJVM' && matrix.scala != '3.3.0' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' + if: matrix.ci == 'ciJVM' && matrix.scala != '3.3.1' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' shell: bash run: sbt '++ ${{ matrix.scala }}' 'rootJVM/scalafixAll --check' - name: Check that scalafix has been run on JS - if: matrix.ci == 'ciJS' && matrix.scala != '3.3.0' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' + if: matrix.ci == 'ciJS' && matrix.scala != '3.3.1' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' shell: bash run: sbt '++ ${{ matrix.scala }}' 'rootJS/scalafixAll --check' - name: Check that scalafix has been run on Native - if: matrix.ci == 'ciNative' && matrix.scala != '3.3.0' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' + if: matrix.ci == 'ciNative' && matrix.scala != '3.3.1' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' shell: bash run: sbt '++ ${{ matrix.scala }}' 'rootNative/scalafixAll --check' - shell: bash run: sbt '++ ${{ matrix.scala }}' '${{ matrix.ci }}' - - if: (matrix.scala == '2.13.11' || matrix.scala == '3.3.0') && matrix.ci == 'ciJVM' + - if: (matrix.scala == '2.13.11' || matrix.scala == '3.3.1') && matrix.ci == 'ciJVM' shell: bash run: sbt '++ ${{ matrix.scala }}' docs/mdoc @@ -337,32 +337,32 @@ jobs: if: matrix.java == 'graalvm@17' && steps.setup-java-graalvm-17.outputs.cache-hit == 'false' run: sbt +update - - name: Download target directories (3.3.0, ciJVM) + - name: Download target directories (3.3.1, ciJVM) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.0-ciJVM + name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.1-ciJVM - - name: Inflate target directories (3.3.0, ciJVM) + - name: Inflate target directories (3.3.1, ciJVM) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (3.3.0, ciNative) + - name: Download target directories (3.3.1, ciNative) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.0-ciNative + name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.1-ciNative - - name: Inflate target directories (3.3.0, ciNative) + - name: Inflate target directories (3.3.1, ciNative) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (3.3.0, ciJS) + - name: Download target directories (3.3.1, ciJS) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.0-ciJS + name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.1-ciJS - - name: Inflate target directories (3.3.0, ciJS) + - name: Inflate target directories (3.3.1, ciJS) run: | tar xf targets.tar rm targets.tar From f9518940c295fffaa4a92960672e969dcb6cc05f Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 7 Sep 2023 13:57:56 -0700 Subject: [PATCH 201/429] Even less heap? --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 96ee23b2e7..bed8ea52e8 100644 --- a/build.sbt +++ b/build.sbt @@ -894,7 +894,7 @@ lazy val tests: CrossProject = crossProject(JSPlatform, JVMPlatform, NativePlatf ) .nativeSettings( Compile / mainClass := Some("catseffect.examples.NativeRunner"), - Test / envVars += "GC_MAXIMUM_MAX_HEAP_SIZE" -> "2G" + Test / envVars += "GC_MAXIMUM_MAX_HEAP_SIZE" -> "1G" ) def configureIOAppTests(p: Project): Project = From bcc460dd8d1263ffcaab8d5d8f064d83f51705cb Mon Sep 17 00:00:00 2001 From: Diogo Canut Date: Sat, 9 Sep 2023 17:13:34 -0300 Subject: [PATCH 202/429] Add warnOnNonMainThreadDetected configuration to IOApp --- core/jvm/src/main/scala/cats/effect/IOApp.scala | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/core/jvm/src/main/scala/cats/effect/IOApp.scala b/core/jvm/src/main/scala/cats/effect/IOApp.scala index 816ad51053..32199bb9c4 100644 --- a/core/jvm/src/main/scala/cats/effect/IOApp.scala +++ b/core/jvm/src/main/scala/cats/effect/IOApp.scala @@ -317,6 +317,17 @@ 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` + */ + def warnOnNonMainThreadDetected: Boolean = + Option(System.getProperty("cats.effect.warnOnNonMainThreadDetected")) + .map(_.equalsIgnoreCase("true")) + .getOrElse(true) + + /** * 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 From c564d0c80686cc46a659bae915f2dc3dda9d8a2a Mon Sep 17 00:00:00 2001 From: Diogo Canut Date: Sat, 9 Sep 2023 17:33:51 -0300 Subject: [PATCH 203/429] running prePR --- core/jvm/src/main/scala/cats/effect/IOApp.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/core/jvm/src/main/scala/cats/effect/IOApp.scala b/core/jvm/src/main/scala/cats/effect/IOApp.scala index 32199bb9c4..bc55646f6f 100644 --- a/core/jvm/src/main/scala/cats/effect/IOApp.scala +++ b/core/jvm/src/main/scala/cats/effect/IOApp.scala @@ -327,7 +327,6 @@ trait IOApp { .map(_.equalsIgnoreCase("true")) .getOrElse(true) - /** * 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 From 24ce2ca3d3069ceb13f563c1173c39ccc94ae2a7 Mon Sep 17 00:00:00 2001 From: Diogo Canut Date: Sat, 9 Sep 2023 17:42:05 -0300 Subject: [PATCH 204/429] adding onNonMainThreadDetected --- .../jvm/src/main/scala/cats/effect/IOApp.scala | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/jvm/src/main/scala/cats/effect/IOApp.scala b/core/jvm/src/main/scala/cats/effect/IOApp.scala index bc55646f6f..884495aaed 100644 --- a/core/jvm/src/main/scala/cats/effect/IOApp.scala +++ b/core/jvm/src/main/scala/cats/effect/IOApp.scala @@ -327,6 +327,23 @@ trait IOApp { .map(_.equalsIgnoreCase("true")) .getOrElse(true) + private def onNonMainThreadDetected(): Unit = { + if (warnOnNonMainThreadDetected) + 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 `run` in an interactive sbt session with `fork := false`. + |Set `Compile / run / fork := true` in this project to resolve this. + | + |To silence this warning set the system property: + |`-Dcats.effect.warnOnNonMainThreadDetected=false`. + |""".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 @@ -346,6 +363,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 From a0c7ecf10ef467d1586ee561db58f7e8cca69624 Mon Sep 17 00:00:00 2001 From: Diogo Canut Date: Sat, 9 Sep 2023 17:50:13 -0300 Subject: [PATCH 205/429] adding warnOnNonMainThreadDetected to io-runtime-config --- docs/core/io-runtime-config.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/core/io-runtime-config.md b/docs/core/io-runtime-config.md index 0a48199b48..f107efc750 100644 --- a/docs/core/io-runtime-config.md +++ b/docs/core/io-runtime-config.md @@ -28,6 +28,7 @@ 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.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). | From 7ae37612a5eef696df5ec3ea10bce75dc5fd870d Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 9 Sep 2023 21:03:25 +0000 Subject: [PATCH 206/429] `TimerNode` -> `Node` --- .../scala/cats/effect/unsafe/TimerHeap.scala | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index e6fa30a5ec..16aab6a989 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -35,7 +35,7 @@ private final class TimerHeap { // The index 0 is not used; the root is at index 1. // This is standard practice in binary heaps, to simplify arithmetics. - private[this] var heap: Array[TimerNode] = new Array(8) // TODO what initial value + private[this] var heap: Array[Node] = new Array(8) // TODO what initial value private[this] var size: Int = 0 private[this] val RightUnit = Right(()) @@ -83,7 +83,7 @@ private final class TimerHeap { * called by other threads */ def steal(now: Long): Boolean = { - def go(heap: Array[TimerNode], size: Int, m: Int): Boolean = + def go(heap: Array[Node], size: Int, m: Int): Boolean = if (m <= size) { val node = heap(m) if ((node ne null) && isExpired(node, now)) { @@ -120,20 +120,20 @@ private final class TimerHeap { val rootExpired = !rootCanceled && isExpired(root, now) if (rootCanceled || rootExpired) { // see if we can just replace the root if (rootExpired) out(0) = root.getAndClear() - val node = new TimerNode(triggerTime, callback, 1) + val node = new Node(triggerTime, callback, 1) heap(1) = node fixDown(1) node } else { // insert at the end val heap = growIfNeeded() // new heap array if it grew size += 1 - val node = new TimerNode(triggerTime, callback, size) + val node = new Node(triggerTime, callback, size) heap(size) = node fixUp(size) node } } else { - val node = new TimerNode(now + delay, callback, 1) + val node = new Node(now + delay, callback, 1) this.heap(1) = node size += 1 node @@ -158,13 +158,13 @@ private final class TimerHeap { } } - private[this] def isExpired(node: TimerNode, now: Long): Boolean = + private[this] def isExpired(node: Node, now: Long): Boolean = cmp(node.triggerTime, now) <= 0 // triggerTime <= now - private[this] def growIfNeeded(): Array[TimerNode] = { + private[this] def growIfNeeded(): Array[Node] = { val heap = this.heap // local copy if (size >= heap.length - 1) { - val newHeap = new Array[TimerNode](heap.length * 2) + val newHeap = new Array[Node](heap.length * 2) System.arraycopy(heap, 1, newHeap, 1, heap.length - 1) this.heap = newHeap newHeap @@ -265,7 +265,7 @@ private final class TimerHeap { java.lang.Long.signum(d) } - private[this] def cmp(x: TimerNode, y: TimerNode): Int = + private[this] def cmp(x: Node, y: Node): Int = cmp(x.triggerTime, y.triggerTime) /** @@ -305,7 +305,7 @@ private final class TimerHeap { override def toString() = if (size > 0) "TimerHeap(...)" else "TimerHeap()" - private final class TimerNode( + private final class Node( val triggerTime: Long, private[this] var callback: Right[Nothing, Unit] => Unit, var index: Int @@ -340,7 +340,7 @@ private final class TimerHeap { def isCanceled(): Boolean = callback eq null - override def toString() = s"TimerNode($triggerTime, $callback})" + override def toString() = s"Node($triggerTime, $callback})" } From ac32fb25ecd50bcde1b585af37f48a3bfc47e960 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 9 Sep 2023 21:37:01 +0000 Subject: [PATCH 207/429] Implement timer heap packing --- .../scala/cats/effect/unsafe/TimerHeap.scala | 29 +++++++++++++++++-- .../cats/effect/unsafe/WorkerThread.scala | 3 ++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index 16aab6a989..16f5ed566c 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -31,7 +31,9 @@ package unsafe import scala.annotation.tailrec -private final class TimerHeap { +import java.util.concurrent.atomic.AtomicBoolean + +private final class TimerHeap extends AtomicBoolean { needsPack => // The index 0 is not used; the root is at index 1. // This is standard practice in binary heaps, to simplify arithmetics. @@ -139,6 +141,23 @@ private final class TimerHeap { node } + /** + * only called by owner thread + */ + def packIfNeeded(): Unit = + if (needsPack.getAndSet(false)) { // we now see all external cancelations + val heap = this.heap // local copy + var i = 1 + while (i <= size) { + if (heap(i).isCanceled()) { + removeAt(i) + // don't increment i, the new i may be canceled too + } else { + i += 1 + } + } + } + /** * only called by owner thread */ @@ -332,8 +351,14 @@ private final class TimerHeap { if (thread.isInstanceOf[WorkerThread]) { val worker = thread.asInstanceOf[WorkerThread] val heap = TimerHeap.this - if (worker.ownsTimers(heap)) heap.removeAt(index) + if (worker.ownsTimers(heap)) { + heap.removeAt(index) + return () + } } + + // otherwise this heap will need packing + needsPack.set(true) } def run() = apply() diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala index 7eb5ed12ab..963429d005 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala @@ -551,6 +551,9 @@ private final class WorkerThread( } } + // Clean up any externally canceled timers + sleepers.packIfNeeded() + // Obtain a fiber or batch of fibers from the external queue. val element = external.poll(rnd) if (element.isInstanceOf[Array[Runnable]]) { From 76b9e8310c9b16ec06849f3ee7e27fedd31bbede Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 9 Sep 2023 23:02:48 +0000 Subject: [PATCH 208/429] Add test for externally canceled timers --- .../scala/cats/effect/IOPlatformSpecification.scala | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala index 244f96ad0c..182f2283f8 100644 --- a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala +++ b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala @@ -421,6 +421,16 @@ trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => spin.as(ok) } + "lots of externally-canceled timers" in real { + Resource + .make(IO(Executors.newSingleThreadExecutor()))(exec => IO(exec.shutdownNow()).void) + .map(ExecutionContext.fromExecutor(_)) + .use { ec => + IO.sleep(1.day).start.flatMap(_.cancel.evalOn(ec)).parReplicateA_(100000) + } + .as(ok) + } + "not lose cedeing threads from the bypass when blocker transitioning" in { // writing this test in terms of IO seems to not reproduce the issue 0.until(5) foreach { _ => From 1a079458611f4428fe5d195dd850e7868d49757d Mon Sep 17 00:00:00 2001 From: xuwei-k <6b656e6a69@gmail.com> Date: Sun, 10 Sep 2023 11:02:41 +0900 Subject: [PATCH 209/429] add explicit type --- .../src/main/scala/cats/effect/IOFiber.scala | 3 ++- .../main/scala/cats/effect/kernel/Async.scala | 10 +++++----- .../main/scala/cats/effect/kernel/Clock.scala | 18 ++++++++++-------- .../scala/cats/effect/kernel/GenTemporal.scala | 10 +++++----- .../scala/cats/effect/kernel/Resource.scala | 8 ++++---- .../main/scala/cats/effect/kernel/Sync.scala | 14 +++++++------- .../scala/cats/effect/laws/AsyncTests.scala | 5 +++-- .../scala/cats/effect/laws/ClockTests.scala | 4 ++-- .../scala/cats/effect/laws/GenSpawnTests.scala | 5 +++-- .../cats/effect/laws/GenTemporalTests.scala | 5 +++-- .../cats/effect/laws/MonadCancelTests.scala | 3 ++- .../scala/cats/effect/laws/SyncTests.scala | 3 ++- .../scala/cats/effect/laws/UniqueTests.scala | 4 ++-- .../cats/effect/IOPlatformSpecification.scala | 2 +- .../cats/effect/std/DeferredJVMSpec.scala | 4 ++-- .../src/test/scala/cats/effect/IOSpec.scala | 6 ++++-- 16 files changed, 57 insertions(+), 47 deletions(-) diff --git a/core/shared/src/main/scala/cats/effect/IOFiber.scala b/core/shared/src/main/scala/cats/effect/IOFiber.scala index 039f73c3cb..f574b0894d 100644 --- a/core/shared/src/main/scala/cats/effect/IOFiber.scala +++ b/core/shared/src/main/scala/cats/effect/IOFiber.scala @@ -527,7 +527,8 @@ private final class IOFiber[A]( masks += 1 val id = masks val poll = new Poll[IO] { - def apply[B](ioa: IO[B]) = IO.Uncancelable.UnmaskRunLoop(ioa, id, IOFiber.this) + def apply[B](ioa: IO[B]): IO[B] = + IO.Uncancelable.UnmaskRunLoop(ioa, id, IOFiber.this) } val next = diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/Async.scala b/kernel/shared/src/main/scala/cats/effect/kernel/Async.scala index 25a1599928..32be6cddeb 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/Async.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/Async.scala @@ -406,7 +406,7 @@ object Async { implicit protected def F: Async[F] override protected final def delegate = super.delegate - override protected final def C = F + override protected final def C: Clock[F] = F override def unique: OptionT[F, Unique.Token] = delay(new Unique.Token()) @@ -477,7 +477,7 @@ object Async { implicit protected def F: Async[F] override protected final def delegate = super.delegate - override protected final def C = F + override protected final def C: Clock[F] = F override def unique: EitherT[F, E, Unique.Token] = delay(new Unique.Token()) @@ -549,7 +549,7 @@ object Async { implicit protected def F: Async[F] override protected final def delegate = super.delegate - override protected final def C = F + override protected final def C: Clock[F] = F override def unique: IorT[F, L, Unique.Token] = delay(new Unique.Token()) @@ -620,7 +620,7 @@ object Async { implicit protected def F: Async[F] override protected final def delegate = super.delegate - override protected final def C = F + override protected final def C: Clock[F] = F override def unique: WriterT[F, L, Unique.Token] = delay(new Unique.Token()) @@ -688,7 +688,7 @@ object Async { implicit protected def F: Async[F] override protected final def delegate = super.delegate - override protected final def C = F + override protected final def C: Clock[F] = F override def unique: Kleisli[F, R, Unique.Token] = delay(new Unique.Token()) diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/Clock.scala b/kernel/shared/src/main/scala/cats/effect/kernel/Clock.scala index a77ad08ba3..2fdef6645b 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/Clock.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/Clock.scala @@ -64,7 +64,7 @@ object Clock { implicit F0: Monad[F], C0: Clock[F]): Clock[OptionT[F, *]] = new OptionTClock[F] { - def applicative = OptionT.catsDataMonadForOptionT(F) + def applicative: Applicative[OptionT[F, *]] = OptionT.catsDataMonadForOptionT(F) implicit override def F: Monad[F] = F0 implicit override def C: Clock[F] = C0 } @@ -73,7 +73,7 @@ object Clock { implicit F0: Monad[F], C0: Clock[F]): Clock[EitherT[F, E, *]] = new EitherTClock[F, E] { - def applicative = EitherT.catsDataMonadErrorForEitherT(F) + def applicative: Applicative[EitherT[F, E, *]] = EitherT.catsDataMonadErrorForEitherT(F) implicit override def F: Monad[F] = F0 implicit override def C: Clock[F] = C0 } @@ -82,7 +82,8 @@ object Clock { implicit F0: Monad[F], C0: Clock[F]): Clock[StateT[F, S, *]] = new StateTClock[F, S] { - def applicative = IndexedStateT.catsDataMonadForIndexedStateT(F) + def applicative: Applicative[IndexedStateT[F, S, S, *]] = + IndexedStateT.catsDataMonadForIndexedStateT(F) implicit override def F: Monad[F] = F0 implicit override def C: Clock[F] = C0 } @@ -92,7 +93,7 @@ object Clock { C0: Clock[F], L0: Monoid[L]): Clock[WriterT[F, L, *]] = new WriterTClock[F, L] { - def applicative = WriterT.catsDataMonadForWriterT(F, L) + def applicative: Applicative[WriterT[F, L, *]] = WriterT.catsDataMonadForWriterT(F, L) implicit override def F: Monad[F] = F0 implicit override def C: Clock[F] = C0 @@ -105,7 +106,7 @@ object Clock { C0: Clock[F], L0: Semigroup[L]): Clock[IorT[F, L, *]] = new IorTClock[F, L] { - def applicative = IorT.catsDataMonadErrorForIorT(F, L) + def applicative: Applicative[IorT[F, L, *]] = IorT.catsDataMonadErrorForIorT(F, L) implicit override def F: Monad[F] = F0 implicit override def C: Clock[F] = C0 @@ -116,7 +117,7 @@ object Clock { implicit F0: Monad[F], C0: Clock[F]): Clock[Kleisli[F, R, *]] = new KleisliClock[F, R] { - def applicative = Kleisli.catsDataMonadForKleisli(F) + def applicative: Applicative[Kleisli[F, R, *]] = Kleisli.catsDataMonadForKleisli(F) implicit override def F: Monad[F] = F0 implicit override def C: Clock[F] = C0 } @@ -126,7 +127,7 @@ object Clock { C0: Clock[F], D0: Defer[F]): Clock[ContT[F, R, *]] = new ContTClock[F, R] { - def applicative = ContT.catsDataContTMonad(D) + def applicative: Applicative[ContT[F, R, *]] = ContT.catsDataContTMonad(D) implicit override def F: Monad[F] = F0 implicit override def C: Clock[F] = C0 implicit override def D: Defer[F] = D0 @@ -137,7 +138,8 @@ object Clock { C0: Clock[F], L0: Monoid[L]): Clock[ReaderWriterStateT[F, R, L, S, *]] = new ReaderWriterStateTClock[F, R, L, S] { - def applicative = IndexedReaderWriterStateT.catsDataMonadForRWST(F, L) + def applicative: Applicative[ReaderWriterStateT[F, R, L, S, *]] = + IndexedReaderWriterStateT.catsDataMonadForRWST(F, L) implicit override def F: Monad[F] = F0 implicit override def C: Clock[F] = C0 diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala b/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala index 305cf8ece2..27cdacbf68 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/GenTemporal.scala @@ -310,7 +310,7 @@ object GenTemporal { with Clock.OptionTClock[F] { implicit protected def F: GenTemporal[F, E] - protected def C = F + protected def C: Clock[F] = F override protected def delegate: MonadError[OptionT[F, *], E] = OptionT.catsDataMonadErrorForOptionT[F, E] @@ -325,7 +325,7 @@ object GenTemporal { with Clock.EitherTClock[F, E0] { implicit protected def F: GenTemporal[F, E] - protected def C = F + protected def C: Clock[F] = F override protected def delegate: MonadError[EitherT[F, E0, *], E] = EitherT.catsDataMonadErrorFForEitherT[F, E, E0] @@ -339,7 +339,7 @@ object GenTemporal { with Clock.IorTClock[F, L] { implicit protected def F: GenTemporal[F, E] - protected def C = F + protected def C: Clock[F] = F override protected def delegate: MonadError[IorT[F, L, *], E] = IorT.catsDataMonadErrorFForIorT[F, L, E] @@ -353,7 +353,7 @@ object GenTemporal { with Clock.WriterTClock[F, L] { implicit protected def F: GenTemporal[F, E] - protected def C = F + protected def C: Clock[F] = F implicit protected def L: Monoid[L] @@ -369,7 +369,7 @@ object GenTemporal { with Clock.KleisliClock[F, R] { implicit protected def F: GenTemporal[F, E] - protected def C = F + protected def C: Clock[F] = F override protected def delegate: MonadError[Kleisli[F, R, *], E] = Kleisli.catsDataMonadErrorForKleisli[F, R, E] diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala b/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala index 36dfeb07d4..b93f73cce5 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala @@ -1223,7 +1223,7 @@ private[effect] trait ResourceHOInstances0 extends ResourceHOInstances1 { implicit def catsEffectAsyncForResource[F[_]](implicit F0: Async[F]): Async[Resource[F, *]] = new ResourceAsync[F] { def F = F0 - override def applicative = this + override def applicative: ResourceAsync[F] = this } implicit def catsEffectSemigroupKForResource[F[_], A]( @@ -1242,7 +1242,7 @@ private[effect] trait ResourceHOInstances1 extends ResourceHOInstances2 { implicit F0: Temporal[F]): Temporal[Resource[F, *]] = new ResourceTemporal[F] { def F = F0 - override def applicative = this + override def applicative: Applicative[Resource[F, *]] = this } implicit def catsEffectSyncForResource[F[_]](implicit F0: Sync[F]): Sync[Resource[F, *]] = @@ -1257,7 +1257,7 @@ private[effect] trait ResourceHOInstances2 extends ResourceHOInstances3 { implicit F0: Concurrent[F]): Concurrent[Resource[F, *]] = new ResourceConcurrent[F] { def F = F0 - override def applicative = this + override def applicative: ResourceConcurrent[F] = this } implicit def catsEffectClockForResource[F[_]]( @@ -1430,7 +1430,7 @@ abstract private[effect] class ResourceAsync[F[_]] with Async[Resource[F, *]] { self => implicit protected def F: Async[F] - override def applicative = this + override def applicative: ResourceAsync[F] = this override def unique: Resource[F, Unique.Token] = Resource.unique diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/Sync.scala b/kernel/shared/src/main/scala/cats/effect/kernel/Sync.scala index 68ad11ef65..dc63120be7 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/Sync.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/Sync.scala @@ -214,7 +214,7 @@ object Sync { with Clock.OptionTClock[F] { implicit protected def F: Sync[F] - protected def C = F + protected def C: Clock[F] = F def suspend[A](hint: Type)(thunk: => A): OptionT[F, A] = OptionT.liftF(F.suspend(hint)(thunk)) @@ -225,7 +225,7 @@ object Sync { with MonadCancel.EitherTMonadCancel[F, E, Throwable] with Clock.EitherTClock[F, E] { implicit protected def F: Sync[F] - protected def C = F + protected def C: Clock[F] = F def suspend[A](hint: Type)(thunk: => A): EitherT[F, E, A] = EitherT.liftF(F.suspend(hint)(thunk)) @@ -236,7 +236,7 @@ object Sync { with MonadCancel.StateTMonadCancel[F, S, Throwable] with Clock.StateTClock[F, S] { implicit protected def F: Sync[F] - protected def C = F + protected def C: Clock[F] = F def suspend[A](hint: Type)(thunk: => A): StateT[F, S, A] = StateT.liftF(F.suspend(hint)(thunk)) @@ -247,7 +247,7 @@ object Sync { with MonadCancel.WriterTMonadCancel[F, S, Throwable] with Clock.WriterTClock[F, S] { implicit protected def F: Sync[F] - protected def C = F + protected def C: Clock[F] = F def suspend[A](hint: Type)(thunk: => A): WriterT[F, S, A] = WriterT.liftF(F.suspend(hint)(thunk)) @@ -258,7 +258,7 @@ object Sync { with MonadCancel.IorTMonadCancel[F, L, Throwable] with Clock.IorTClock[F, L] { implicit protected def F: Sync[F] - protected def C = F + protected def C: Clock[F] = F def suspend[A](hint: Type)(thunk: => A): IorT[F, L, A] = IorT.liftF(F.suspend(hint)(thunk)) @@ -269,7 +269,7 @@ object Sync { with MonadCancel.KleisliMonadCancel[F, R, Throwable] with Clock.KleisliClock[F, R] { implicit protected def F: Sync[F] - protected def C = F + protected def C: Clock[F] = F def suspend[A](hint: Type)(thunk: => A): Kleisli[F, R, A] = Kleisli.liftF(F.suspend(hint)(thunk)) @@ -280,7 +280,7 @@ object Sync { with MonadCancel.ReaderWriterStateTMonadCancel[F, R, L, S, Throwable] with Clock.ReaderWriterStateTClock[F, R, L, S] { implicit protected def F: Sync[F] - protected def C = F + protected def C: Clock[F] = F def suspend[A](hint: Type)(thunk: => A): ReaderWriterStateT[F, R, L, S, A] = ReaderWriterStateT.liftF(F.suspend(hint)(thunk)) diff --git a/laws/shared/src/main/scala/cats/effect/laws/AsyncTests.scala b/laws/shared/src/main/scala/cats/effect/laws/AsyncTests.scala index 45bbedd57f..7b1bbd7de1 100644 --- a/laws/shared/src/main/scala/cats/effect/laws/AsyncTests.scala +++ b/laws/shared/src/main/scala/cats/effect/laws/AsyncTests.scala @@ -24,6 +24,7 @@ import cats.laws.discipline.SemigroupalTests.Isomorphisms import org.scalacheck._ import org.scalacheck.Prop.forAll import org.scalacheck.util.Pretty +import org.typelevel.discipline.Laws import scala.concurrent.ExecutionContext import scala.concurrent.duration.FiniteDuration @@ -84,7 +85,7 @@ trait AsyncTests[F[_]] extends GenTemporalTests[F, Throwable] with SyncTests[F] new RuleSet { val name = "async" - val bases = Nil + val bases: Seq[(String, Laws#RuleSet)] = Nil val parents = Seq( temporal[A, B, C]( tolerance, @@ -194,7 +195,7 @@ trait AsyncTests[F[_]] extends GenTemporalTests[F, Throwable] with SyncTests[F] new RuleSet { val name = "async" - val bases = Nil + val bases: Seq[(String, Laws#RuleSet)] = Nil val parents = Seq( temporal[A, B, C](tolerance)( implicitly[Arbitrary[A]], diff --git a/laws/shared/src/main/scala/cats/effect/laws/ClockTests.scala b/laws/shared/src/main/scala/cats/effect/laws/ClockTests.scala index c58871f53e..dc5178c8ba 100644 --- a/laws/shared/src/main/scala/cats/effect/laws/ClockTests.scala +++ b/laws/shared/src/main/scala/cats/effect/laws/ClockTests.scala @@ -29,8 +29,8 @@ trait ClockTests[F[_]] extends Laws { def clock(implicit exec: F[Boolean] => Prop): RuleSet = { new RuleSet { val name = "clock" - val bases = Nil - val parents = Seq() + val bases: Seq[(String, Laws#RuleSet)] = Nil + val parents: Seq[RuleSet] = Seq() val props = Seq("monotonicity" -> exec(laws.monotonicity)) } diff --git a/laws/shared/src/main/scala/cats/effect/laws/GenSpawnTests.scala b/laws/shared/src/main/scala/cats/effect/laws/GenSpawnTests.scala index 44aac420e6..8ab1a64495 100644 --- a/laws/shared/src/main/scala/cats/effect/laws/GenSpawnTests.scala +++ b/laws/shared/src/main/scala/cats/effect/laws/GenSpawnTests.scala @@ -24,6 +24,7 @@ import cats.laws.discipline.SemigroupalTests.Isomorphisms import org.scalacheck._ import org.scalacheck.Prop.forAll import org.scalacheck.util.Pretty +import org.typelevel.discipline.Laws trait GenSpawnTests[F[_], E] extends MonadCancelTests[F, E] with UniqueTests[F] { @@ -68,7 +69,7 @@ trait GenSpawnTests[F[_], E] extends MonadCancelTests[F, E] with UniqueTests[F] // these are the OLD LAWS retained only for bincompat new RuleSet { val name = "concurrent" - val bases = Nil + val bases: Seq[(String, Laws#RuleSet)] = Nil val parents = Seq( monadCancel[A, B, C]( implicitly[Arbitrary[A]], @@ -159,7 +160,7 @@ trait GenSpawnTests[F[_], E] extends MonadCancelTests[F, E] with UniqueTests[F] new RuleSet { val name = "concurrent" - val bases = Nil + val bases: Seq[(String, Laws#RuleSet)] = Nil val parents = Seq( monadCancel[A, B, C]( implicitly[Arbitrary[A]], diff --git a/laws/shared/src/main/scala/cats/effect/laws/GenTemporalTests.scala b/laws/shared/src/main/scala/cats/effect/laws/GenTemporalTests.scala index 83127ae52e..c63579943c 100644 --- a/laws/shared/src/main/scala/cats/effect/laws/GenTemporalTests.scala +++ b/laws/shared/src/main/scala/cats/effect/laws/GenTemporalTests.scala @@ -24,6 +24,7 @@ import cats.laws.discipline.SemigroupalTests.Isomorphisms import org.scalacheck._ import org.scalacheck.Prop.forAll import org.scalacheck.util.Pretty +import org.typelevel.discipline.Laws import scala.concurrent.duration.FiniteDuration @@ -84,7 +85,7 @@ trait GenTemporalTests[F[_], E] extends GenSpawnTests[F, E] with ClockTests[F] { new RuleSet { val name = "temporal" - val bases = Nil + val bases: Seq[(String, Laws#RuleSet)] = Nil val parents = Seq( spawn[A, B, C]( implicitly[Arbitrary[A]], @@ -177,7 +178,7 @@ trait GenTemporalTests[F[_], E] extends GenSpawnTests[F, E] with ClockTests[F] { new RuleSet { val name = "temporal" - val bases = Nil + val bases: Seq[(String, Laws#RuleSet)] = Nil val parents = Seq( spawn[A, B, C]( implicitly[Arbitrary[A]], diff --git a/laws/shared/src/main/scala/cats/effect/laws/MonadCancelTests.scala b/laws/shared/src/main/scala/cats/effect/laws/MonadCancelTests.scala index 870815de69..f91ebfa3d5 100644 --- a/laws/shared/src/main/scala/cats/effect/laws/MonadCancelTests.scala +++ b/laws/shared/src/main/scala/cats/effect/laws/MonadCancelTests.scala @@ -25,6 +25,7 @@ import cats.laws.discipline.SemigroupalTests.Isomorphisms import org.scalacheck._ import org.scalacheck.Prop.forAll import org.scalacheck.util.Pretty +import org.typelevel.discipline.Laws trait MonadCancelTests[F[_], E] extends MonadErrorTests[F, E] { @@ -111,7 +112,7 @@ trait MonadCancelTests[F[_], E] extends MonadErrorTests[F, E] { new RuleSet { val name = "monadCancel" - val bases = Nil + val bases: Seq[(String, Laws#RuleSet)] = Nil val parents = Seq(monadError[A, B, C]) val props = { diff --git a/laws/shared/src/main/scala/cats/effect/laws/SyncTests.scala b/laws/shared/src/main/scala/cats/effect/laws/SyncTests.scala index 78af874d09..269a40a995 100644 --- a/laws/shared/src/main/scala/cats/effect/laws/SyncTests.scala +++ b/laws/shared/src/main/scala/cats/effect/laws/SyncTests.scala @@ -23,6 +23,7 @@ import cats.laws.discipline.SemigroupalTests.Isomorphisms import org.scalacheck._ import org.scalacheck.Prop.forAll +import org.typelevel.discipline.Laws trait SyncTests[F[_]] extends MonadCancelTests[F, Throwable] @@ -58,7 +59,7 @@ trait SyncTests[F[_]] new RuleSet { val name = "sync" - val bases = Nil + val bases: Seq[(String, Laws#RuleSet)] = Nil val parents = Seq( monadCancel[A, B, C]( implicitly[Arbitrary[A]], diff --git a/laws/shared/src/main/scala/cats/effect/laws/UniqueTests.scala b/laws/shared/src/main/scala/cats/effect/laws/UniqueTests.scala index 32f111a1fa..f44b470ef6 100644 --- a/laws/shared/src/main/scala/cats/effect/laws/UniqueTests.scala +++ b/laws/shared/src/main/scala/cats/effect/laws/UniqueTests.scala @@ -29,8 +29,8 @@ trait UniqueTests[F[_]] extends Laws { def unique(implicit exec: F[Boolean] => Prop): RuleSet = { new RuleSet { val name = "unique" - val bases = Nil - val parents = Seq() + val bases: Seq[(String, Laws#RuleSet)] = Nil + val parents: Seq[RuleSet] = Seq() val props = Seq("uniqueness" -> exec(laws.uniqueness)) } diff --git a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala index 5d095460b6..9e69674c6f 100644 --- a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala +++ b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala @@ -501,7 +501,7 @@ trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => } } - def makeApi(register: (Poller => Unit) => Unit) = + def makeApi(register: (Poller => Unit) => Unit): DummySystem.Api = new DummyPoller { def poll = IO.async_[Unit] { cb => register { poller => diff --git a/tests/jvm/src/test/scala/cats/effect/std/DeferredJVMSpec.scala b/tests/jvm/src/test/scala/cats/effect/std/DeferredJVMSpec.scala index 9aa628f984..6d045b6822 100644 --- a/tests/jvm/src/test/scala/cats/effect/std/DeferredJVMSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/std/DeferredJVMSpec.scala @@ -47,7 +47,7 @@ abstract class BaseDeferredJVMTests(parallelism: Int) implicit val runtime: IORuntime = IORuntime.global - def before = + def before: Any = service = Executors.newFixedThreadPool( parallelism, new ThreadFactory { @@ -61,7 +61,7 @@ abstract class BaseDeferredJVMTests(parallelism: Int) } ) - def after = { + def after: Any = { service.shutdown() assert(service.awaitTermination(60, TimeUnit.SECONDS), "has active threads") } diff --git a/tests/shared/src/test/scala/cats/effect/IOSpec.scala b/tests/shared/src/test/scala/cats/effect/IOSpec.scala index d8152ccc81..34134247a0 100644 --- a/tests/shared/src/test/scala/cats/effect/IOSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/IOSpec.scala @@ -24,6 +24,7 @@ import cats.kernel.laws.discipline.MonoidTests import cats.laws.discipline.{AlignTests, SemigroupKTests} import cats.laws.discipline.arbitrary._ import cats.syntax.all._ +import cats.~> import org.scalacheck.Prop import org.typelevel.discipline.specs2.mutable.Discipline @@ -1325,8 +1326,9 @@ class IOSpec extends BaseSpec with Discipline with IOPlatformSpecification { "catch exceptions in cont" in ticked { implicit ticker => IO.cont[Unit, Unit](new Cont[IO, Unit, Unit] { - override def apply[F[_]](implicit F: MonadCancel[F, Throwable]) = { (_, _, _) => - throw new Exception + override def apply[F[_]](implicit F: MonadCancel[F, Throwable]) + : (Either[Throwable, Unit] => Unit, F[Unit], cats.effect.IO ~> F) => F[Unit] = { + (_, _, _) => throw new Exception } }).voidError must completeAs(()) } From 560deb0c726f781626794d130c27d232c569242a Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 10 Sep 2023 19:23:24 +0000 Subject: [PATCH 210/429] Remove unneeded overrides --- .../shared/src/main/scala/cats/effect/kernel/Resource.scala | 5 ----- 1 file changed, 5 deletions(-) diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala b/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala index b93f73cce5..6169d4ee3a 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala @@ -1223,7 +1223,6 @@ private[effect] trait ResourceHOInstances0 extends ResourceHOInstances1 { implicit def catsEffectAsyncForResource[F[_]](implicit F0: Async[F]): Async[Resource[F, *]] = new ResourceAsync[F] { def F = F0 - override def applicative: ResourceAsync[F] = this } implicit def catsEffectSemigroupKForResource[F[_], A]( @@ -1242,7 +1241,6 @@ private[effect] trait ResourceHOInstances1 extends ResourceHOInstances2 { implicit F0: Temporal[F]): Temporal[Resource[F, *]] = new ResourceTemporal[F] { def F = F0 - override def applicative: Applicative[Resource[F, *]] = this } implicit def catsEffectSyncForResource[F[_]](implicit F0: Sync[F]): Sync[Resource[F, *]] = @@ -1257,7 +1255,6 @@ private[effect] trait ResourceHOInstances2 extends ResourceHOInstances3 { implicit F0: Concurrent[F]): Concurrent[Resource[F, *]] = new ResourceConcurrent[F] { def F = F0 - override def applicative: ResourceConcurrent[F] = this } implicit def catsEffectClockForResource[F[_]]( @@ -1430,8 +1427,6 @@ abstract private[effect] class ResourceAsync[F[_]] with Async[Resource[F, *]] { self => implicit protected def F: Async[F] - override def applicative: ResourceAsync[F] = this - override def unique: Resource[F, Unique.Token] = Resource.unique From 2d19a69285f28e01ee8876d870ff82e9147a22f3 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 10 Sep 2023 22:20:28 +0000 Subject: [PATCH 211/429] Regenerate workflow --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49fcc0ae04..13e153de6a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -287,12 +287,12 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) shell: bash - run: mkdir -p benchmarks/target testkit/native/target target stress-tests/target std/jvm/target example/js/target kernel-testkit/jvm/target testkit/js/target rootJS/target core/native/target site-docs/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target graalvm-example/target kernel-testkit/js/target core/jvm/target rootJVM/target rootNative/target example/native/target kernel/native/target example/jvm/target laws/jvm/target std/native/target testkit/jvm/target project/target + run: mkdir -p benchmarks/target testkit/native/target target std/jvm/target example/js/target kernel-testkit/jvm/target testkit/js/target rootJS/target core/native/target site-docs/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target graalvm-example/target kernel-testkit/js/target core/jvm/target rootJVM/target rootNative/target example/native/target kernel/native/target example/jvm/target laws/jvm/target std/native/target testkit/jvm/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) shell: bash - run: tar cf targets.tar benchmarks/target testkit/native/target target stress-tests/target std/jvm/target example/js/target kernel-testkit/jvm/target testkit/js/target rootJS/target core/native/target site-docs/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target graalvm-example/target kernel-testkit/js/target core/jvm/target rootJVM/target rootNative/target example/native/target kernel/native/target example/jvm/target laws/jvm/target std/native/target testkit/jvm/target project/target + run: tar cf targets.tar benchmarks/target testkit/native/target target std/jvm/target example/js/target kernel-testkit/jvm/target testkit/js/target rootJS/target core/native/target site-docs/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target graalvm-example/target kernel-testkit/js/target core/jvm/target rootJVM/target rootNative/target example/native/target kernel/native/target example/jvm/target laws/jvm/target std/native/target testkit/jvm/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) From 078740ae6cb154a1f6221c1b99285d509a80bf79 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 03:08:26 +0000 Subject: [PATCH 212/429] Update sbt-scalafix to 0.11.1 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 95035e7a31..56c624895c 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -9,5 +9,5 @@ addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.7") addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.0") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.1") addSbtPlugin("org.scalameta" % "sbt-native-image" % "0.3.4") From b013c7e81be4306300357c2e2c6bfa3167d9bfea Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 11 Sep 2023 03:09:32 +0000 Subject: [PATCH 213/429] Fix some tests --- .../scala/cats/effect/unsafe/TimerHeap.scala | 15 ++++++-- .../cats/effect/unsafe/TimerHeapSpec.scala | 34 +++++++++++-------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index 16f5ed566c..a3c13efccc 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -43,8 +43,19 @@ private final class TimerHeap extends AtomicBoolean { needsPack => private[this] val RightUnit = Right(()) def peekFirstTriggerTime(): Long = - if (size > 0) heap(1).triggerTime - else Long.MinValue + if (size > 0) { + val tt = heap(1).triggerTime + if (tt != Long.MinValue) { + tt + } else { + // in the VERY unlikely case when + // the trigger time is exactly our + // sentinel, we just cheat a little + // (this could cause threads to wake + // up 1 ns too early): + Long.MaxValue + } + } else Long.MinValue /** * for testing diff --git a/tests/jvm/src/test/scala/cats/effect/unsafe/TimerHeapSpec.scala b/tests/jvm/src/test/scala/cats/effect/unsafe/TimerHeapSpec.scala index 0e6160613e..682db2e9e9 100644 --- a/tests/jvm/src/test/scala/cats/effect/unsafe/TimerHeapSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/unsafe/TimerHeapSpec.scala @@ -46,7 +46,7 @@ class TimerHeapSpec extends Specification { m.insert(0L, 0L, cb0, out) out(0) must beNull m.toString mustEqual "TimerHeap(...)" - m.pollFirstIfTriggered(Long.MinValue) must beNull + m.pollFirstIfTriggered(Long.MinValue + 1) must beNull m.pollFirstIfTriggered(Long.MaxValue) mustEqual cb0 m.pollFirstIfTriggered(Long.MaxValue) must beNull m.pollFirstIfTriggered(Long.MinValue) must beNull @@ -58,9 +58,8 @@ class TimerHeapSpec extends Specification { m.insert(0L, 0L, cb2, out) out(0) must beNull m.insert(0L, 20L, cb3, out) - out(0) must beNull + out(0) mustEqual cb2 m.pollFirstIfTriggered(-1L) must beNull - m.pollFirstIfTriggered(0L) mustEqual cb2 m.pollFirstIfTriggered(0L) must beNull m.pollFirstIfTriggered(10L) mustEqual cb0 m.pollFirstIfTriggered(10L) must beNull @@ -74,43 +73,47 @@ class TimerHeapSpec extends Specification { "correctly insert / remove (cancel)" in { val m = new TimerHeap val out = new Array[Right[Nothing, Unit] => Unit](1) - val r0 = m.insert(0L, 0L, cb0, out) + val r0 = m.insert(0L, 1L, cb0, out) out(0) must beNull - val r1 = m.insert(0L, 1L, cb1, out) + val r1 = m.insert(0L, 2L, cb1, out) out(0) must beNull - val r5 = m.insert(0L, 5L, cb5, out) + val r5 = m.insert(0L, 6L, cb5, out) out(0) must beNull - val r4 = m.insert(0L, 4L, cb4, out) + val r4 = m.insert(0L, 5L, cb4, out) out(0) must beNull - val r2 = m.insert(0L, 2L, cb2, out) + val r2 = m.insert(0L, 3L, cb2, out) out(0) must beNull - val r3 = m.insert(0L, 3L, cb3, out) + val r3 = m.insert(0L, 4L, cb3, out) out(0) must beNull m.peekFirstQuiescent() mustEqual cb0 - m.peekFirstTriggerTime() mustEqual 0L + m.peekFirstTriggerTime() mustEqual 1L r0.run() + m.packIfNeeded() m.peekFirstQuiescent() mustEqual cb1 - m.peekFirstTriggerTime() mustEqual 1L + m.peekFirstTriggerTime() mustEqual 2L m.pollFirstIfTriggered(Long.MaxValue) mustEqual cb1 m.peekFirstQuiescent() mustEqual cb2 - m.peekFirstTriggerTime() mustEqual 2L + m.peekFirstTriggerTime() mustEqual 3L r1.run() // NOP r3.run() + m.packIfNeeded() m.peekFirstQuiescent() mustEqual cb2 - m.peekFirstTriggerTime() mustEqual 2L + m.peekFirstTriggerTime() mustEqual 3L m.pollFirstIfTriggered(Long.MaxValue) mustEqual cb2 m.peekFirstQuiescent() mustEqual cb4 - m.peekFirstTriggerTime() mustEqual 4L + m.peekFirstTriggerTime() mustEqual 5L m.pollFirstIfTriggered(Long.MaxValue) mustEqual cb4 m.peekFirstQuiescent() mustEqual cb5 - m.peekFirstTriggerTime() mustEqual 5L + m.peekFirstTriggerTime() mustEqual 6L r2.run() r5.run() + m.packIfNeeded() m.peekFirstQuiescent() must beNull m.peekFirstTriggerTime() mustEqual Long.MinValue m.pollFirstIfTriggered(Long.MaxValue) must beNull r4.run() // NOP + m.packIfNeeded() m.pollFirstIfTriggered(Long.MaxValue) must beNull } @@ -122,6 +125,7 @@ class TimerHeapSpec extends Specification { val callbacksBuilder = Vector.newBuilder[Right[Nothing, Unit] => Unit] for (_ <- 0 until 200) { val cb = newCb() + val out = new Array[Right[Nothing, Unit] => Unit](1) val r = m.insert(nanoTime, 10L, cb, new Array(1)) removersBuilder += r callbacksBuilder += cb From 8aca9e3b5beac1521794b98b0f2f04015077d64a Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 11 Sep 2023 03:43:43 +0000 Subject: [PATCH 214/429] Workaround Scala 3 crash --- .../src/main/scala/cats/effect/unsafe/TimerHeap.scala | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index a3c13efccc..1c3907033f 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -362,14 +362,10 @@ private final class TimerHeap extends AtomicBoolean { needsPack => if (thread.isInstanceOf[WorkerThread]) { val worker = thread.asInstanceOf[WorkerThread] val heap = TimerHeap.this - if (worker.ownsTimers(heap)) { + if (worker.ownsTimers(heap)) heap.removeAt(index) - return () - } - } - - // otherwise this heap will need packing - needsPack.set(true) + else needsPack.set(true) // otherwise this heap will need packing + } else needsPack.set(true) } def run() = apply() From 1d462fd8dfdf97c9e8f6c085586f562a7e859e34 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 11 Sep 2023 03:44:11 +0000 Subject: [PATCH 215/429] Formatting --- core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index 1c3907033f..490088a8b4 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -364,7 +364,8 @@ private final class TimerHeap extends AtomicBoolean { needsPack => val heap = TimerHeap.this if (worker.ownsTimers(heap)) heap.removeAt(index) - else needsPack.set(true) // otherwise this heap will need packing + else // otherwise this heap will need packing + needsPack.set(true) } else needsPack.set(true) } From dc1b8dce1002da949d8b81b911acd6beaa2e4691 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 12:15:23 +0000 Subject: [PATCH 216/429] Update scala-library to 2.13.12 in series/3.x --- .github/workflows/ci.yml | 8 ++++---- build.sbt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b41c7cba22..67d39fbc01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - scala: [3.3.1, 2.12.18, 2.13.11] + scala: [3.3.1, 2.12.18, 2.13.12] java: [temurin@8, temurin@11, temurin@17, graalvm@17] ci: [ciJVM, ciNative, ciJS, ciFirefox, ciChrome] exclude: @@ -218,7 +218,7 @@ jobs: - shell: bash run: sbt '++ ${{ matrix.scala }}' '${{ matrix.ci }}' - - if: (matrix.scala == '2.13.11' || matrix.scala == '3.3.1') && matrix.ci == 'ciJVM' + - if: (matrix.scala == '2.13.12' || matrix.scala == '3.3.1') && matrix.ci == 'ciJVM' shell: bash run: sbt '++ ${{ matrix.scala }}' docs/mdoc @@ -233,7 +233,7 @@ jobs: run: example/test-js.sh ${{ matrix.scala }} - name: Test GraalVM Native Image - if: matrix.scala == '2.13.11' && matrix.java == 'graalvm@17' && matrix.os == 'ubuntu-latest' + if: matrix.scala == '2.13.12' && matrix.java == 'graalvm@17' && matrix.os == 'ubuntu-latest' shell: bash run: sbt '++ ${{ matrix.scala }}' graalVMExample/nativeImage graalVMExample/nativeImageRun @@ -243,7 +243,7 @@ jobs: run: example/test-native.sh ${{ matrix.scala }} - name: Scalafix tests - if: matrix.scala == '2.13.11' && matrix.ci == 'ciJVM' && matrix.os == 'ubuntu-latest' + if: matrix.scala == '2.13.12' && matrix.ci == 'ciJVM' && matrix.os == 'ubuntu-latest' shell: bash run: | cd scalafix diff --git a/build.sbt b/build.sbt index b4cb2fc504..5e7c66c989 100644 --- a/build.sbt +++ b/build.sbt @@ -113,7 +113,7 @@ val Windows = "windows-latest" val MacOS = "macos-latest" val Scala212 = "2.12.18" -val Scala213 = "2.13.11" +val Scala213 = "2.13.12" val Scala3 = "3.3.1" ThisBuild / crossScalaVersions := Seq(Scala3, Scala212, Scala213) From d102a1c6e08c436bf0e17325512ad411f192a707 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 12:16:08 +0000 Subject: [PATCH 217/429] Regenerate GitHub Actions workflow Executed command: sbt githubWorkflowGenerate --- .github/workflows/ci.yml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67d39fbc01..44625f72e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -397,52 +397,52 @@ jobs: tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.11, ciJVM) + - name: Download target directories (2.13.12, ciJVM) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.11-ciJVM + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.12-ciJVM - - name: Inflate target directories (2.13.11, ciJVM) + - name: Inflate target directories (2.13.12, ciJVM) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.11, ciNative) + - name: Download target directories (2.13.12, ciNative) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.11-ciNative + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.12-ciNative - - name: Inflate target directories (2.13.11, ciNative) + - name: Inflate target directories (2.13.12, ciNative) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.11, ciJS) + - name: Download target directories (2.13.12, ciJS) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.11-ciJS + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.12-ciJS - - name: Inflate target directories (2.13.11, ciJS) + - name: Inflate target directories (2.13.12, ciJS) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.11, ciFirefox) + - name: Download target directories (2.13.12, ciFirefox) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.11-ciFirefox + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.12-ciFirefox - - name: Inflate target directories (2.13.11, ciFirefox) + - name: Inflate target directories (2.13.12, ciFirefox) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.11, ciChrome) + - name: Download target directories (2.13.12, ciChrome) uses: actions/download-artifact@v3 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.11-ciChrome + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.12-ciChrome - - name: Inflate target directories (2.13.11, ciChrome) + - name: Inflate target directories (2.13.12, ciChrome) run: | tar xf targets.tar rm targets.tar From 80946dc3aab81089dbef84abcfa0e0fa0084d35f Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 21:23:45 +0000 Subject: [PATCH 218/429] Update sbt-typelevel to 0.5.2 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 56c624895c..c79090e283 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.0") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.2") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.14") From 07c2d34ee25186e0cd9da510d3846df6939a4625 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 21:25:21 +0000 Subject: [PATCH 219/429] Run prePR with sbt-typelevel Executed command: sbt tlPrePrBotHook --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b41c7cba22..8d782dc7b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -112,7 +112,7 @@ jobs: run: git config --global core.autocrlf false - name: Checkout current branch (full) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -281,7 +281,7 @@ jobs: run: git config --global core.autocrlf false - name: Checkout current branch (full) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -488,7 +488,7 @@ jobs: run: git config --global core.autocrlf false - name: Checkout current branch (full) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 From ad65e749c424c35284a9cc3f647653beb0533f13 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 12 Sep 2023 07:48:42 +0000 Subject: [PATCH 220/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/b1b16aaf47198209bf91d41a64007c5a39c02a13' (2023-08-28) → 'github:typelevel/typelevel-nix/91bcfa0f1d3788371c07a9aa8df8ad5800eaebd8' (2023-09-05) • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/cddebdb60de376c1bdb7a4e6ee3d98355453fe56' (2023-08-27) → 'github:nixos/nixpkgs/d816b5ab44187a2dd84806630ce77a733724f95f' (2023-09-03) --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 6c1c6c3756..21bd031b0c 100644 --- a/flake.lock +++ b/flake.lock @@ -55,11 +55,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1693145325, - "narHash": "sha256-Gat9xskErH1zOcLjYMhSDBo0JTBZKfGS0xJlIRnj6Rc=", + "lastModified": 1693714546, + "narHash": "sha256-3EMJZeGSZT6pD1eNwI/6Yc0R4rxklNvJ2SDFcsCnjpM=", "owner": "nixos", "repo": "nixpkgs", - "rev": "cddebdb60de376c1bdb7a4e6ee3d98355453fe56", + "rev": "d816b5ab44187a2dd84806630ce77a733724f95f", "type": "github" }, "original": { @@ -119,11 +119,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1693253287, - "narHash": "sha256-ii2k0cUNfJuOW35AkGAu85HzSZ1SaJOoTc1tJxqG7iQ=", + "lastModified": 1693881189, + "narHash": "sha256-thSMcpHW8Lw4q5oP6h9d4jcUZi5ev56gijhWbqaUMMU=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "b1b16aaf47198209bf91d41a64007c5a39c02a13", + "rev": "91bcfa0f1d3788371c07a9aa8df8ad5800eaebd8", "type": "github" }, "original": { From 9bc926013b619a1f5176f8b81dd50372588cfb83 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Fri, 15 Sep 2023 16:09:39 +0000 Subject: [PATCH 221/429] Update sbt to 1.9.6 in series/3.x --- project/build.properties | 2 +- scalafix/project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project/build.properties b/project/build.properties index 3040987151..27430827bc 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.4 +sbt.version=1.9.6 diff --git a/scalafix/project/build.properties b/scalafix/project/build.properties index 3040987151..27430827bc 100644 --- a/scalafix/project/build.properties +++ b/scalafix/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.4 +sbt.version=1.9.6 From 16f9ff2fa5a1b8f57dc63fde7e8d063c55bdb2b5 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Fri, 15 Sep 2023 20:16:39 +0000 Subject: [PATCH 222/429] Update sbt-typelevel to 0.5.3 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index c79090e283..46c20db612 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.2") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.3") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.14") From 1a4a24930c5496f6c61bb77b5c6db8e18ff869aa Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 17 Sep 2023 03:31:04 +0000 Subject: [PATCH 223/429] Only null callback if it is non-null Co-authored-by: Daniel Urban --- core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index 490088a8b4..5ac434894c 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -344,7 +344,8 @@ private final class TimerHeap extends AtomicBoolean { needsPack => def getAndClear(): Right[Nothing, Unit] => Unit = { val back = callback - callback = null + if (back ne null) // only clear if we read something + callback = null back } From 7176530b9bf8cb6fd0964049c3340b517a89aa9a Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 17 Sep 2023 03:39:08 +0000 Subject: [PATCH 224/429] `isCanceled` -> `isDeleted` --- .../scala/cats/effect/unsafe/TimerHeap.scala | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index 5ac434894c..d8bc2f5ba2 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -74,9 +74,9 @@ private final class TimerHeap extends AtomicBoolean { needsPack => @tailrec def loop(): Right[Nothing, Unit] => Unit = if (size > 0) { val root = heap(1) - val rootCanceled = root.isCanceled() - val rootExpired = !rootCanceled && isExpired(root, now) - if (rootCanceled || rootExpired) { + val rootDeleted = root.isDeleted() + val rootExpired = !rootDeleted && isExpired(root, now) + if (rootDeleted || rootExpired) { if (size > 1) { heap(1) = heap(size) fixDown(1) @@ -129,9 +129,9 @@ private final class TimerHeap extends AtomicBoolean { needsPack => val triggerTime = computeTriggerTime(now, delay) val root = heap(1) - val rootCanceled = root.isCanceled() - val rootExpired = !rootCanceled && isExpired(root, now) - if (rootCanceled || rootExpired) { // see if we can just replace the root + val rootDeleted = root.isDeleted() + val rootExpired = !rootDeleted && isExpired(root, now) + if (rootDeleted || rootExpired) { // see if we can just replace the root if (rootExpired) out(0) = root.getAndClear() val node = new Node(triggerTime, callback, 1) heap(1) = node @@ -160,9 +160,9 @@ private final class TimerHeap extends AtomicBoolean { needsPack => val heap = this.heap // local copy var i = 1 while (i <= size) { - if (heap(i).isCanceled()) { + if (heap(i).isDeleted()) { removeAt(i) - // don't increment i, the new i may be canceled too + // don't increment i, the new i may be deleted too } else { i += 1 } @@ -354,7 +354,7 @@ private final class TimerHeap extends AtomicBoolean { needsPack => /** * Cancel this timer. */ - def apply(): Unit = if (callback ne null) { // if we're not already canceled + def apply(): Unit = if (callback ne null) { // if we're not already deleted // we can always clear the callback, without explicitly publishing callback = null @@ -372,7 +372,7 @@ private final class TimerHeap extends AtomicBoolean { needsPack => def run() = apply() - def isCanceled(): Boolean = callback eq null + def isDeleted(): Boolean = callback eq null override def toString() = s"Node($triggerTime, $callback})" From 37cb91f7a24e43d8e5d23720483de8edf58d5849 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 17 Sep 2023 15:47:01 +0000 Subject: [PATCH 225/429] Fix nanoTime-wrapping test --- .../cats/effect/unsafe/TimerHeapSpec.scala | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/tests/jvm/src/test/scala/cats/effect/unsafe/TimerHeapSpec.scala b/tests/jvm/src/test/scala/cats/effect/unsafe/TimerHeapSpec.scala index 682db2e9e9..efd694e4e5 100644 --- a/tests/jvm/src/test/scala/cats/effect/unsafe/TimerHeapSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/unsafe/TimerHeapSpec.scala @@ -121,27 +121,33 @@ class TimerHeapSpec extends Specification { val m = new TimerHeap val startFrom = Long.MaxValue - 100L var nanoTime = startFrom - val removersBuilder = Vector.newBuilder[Runnable] + val removers = new Array[Runnable](200) val callbacksBuilder = Vector.newBuilder[Right[Nothing, Unit] => Unit] - for (_ <- 0 until 200) { + val triggeredBuilder = Vector.newBuilder[Right[Nothing, Unit] => Unit] + for (i <- 0 until 200) { + if (i >= 10 && i % 2 == 0) removers(i - 10).run() val cb = newCb() val out = new Array[Right[Nothing, Unit] => Unit](1) - val r = m.insert(nanoTime, 10L, cb, new Array(1)) - removersBuilder += r + val r = m.insert(nanoTime, 10L, cb, out) + triggeredBuilder ++= Option(out(0)) + removers(i) = r callbacksBuilder += cb nanoTime += 1L } - val removers = removersBuilder.result() - for (idx <- 0 until removers.size by 2) { + for (idx <- 190 until removers.size by 2) { removers(idx).run() } nanoTime += 100L val callbacks = callbacksBuilder.result() - for (i <- 0 until 200 by 2) { + while ({ val cb = m.pollFirstIfTriggered(nanoTime) - val expected = callbacks(i + 1) - cb mustEqual expected - } + triggeredBuilder ++= Option(cb) + cb ne null + }) {} + val triggered = triggeredBuilder.result() + + val nonCanceled = callbacks.grouped(2).map(_.last).toVector + triggered should beEqualTo(nonCanceled) ok } From 84d330fdd69a3d3b0643de901cd09d7c76dee74b Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 17 Sep 2023 08:53:00 -0700 Subject: [PATCH 226/429] Drop macOS jobs from Cirrus --- .cirrus.yml | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 3b16d27fd1..19ce4fe0a2 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -20,15 +20,6 @@ jvm_arm_highcore_task: - name: JVM ARM high-core-count 3 script: sbt '++ 3' testsJVM/test ioAppTestsJVM/test -jvm_macos_highcore_task: - macos_instance: - image: ghcr.io/cirruslabs/macos-ventura-base:latest - matrix: - - name: JVM Apple Silicon high-core-count 3 - script: - - brew install sbt - - sbt '++ 3' testsJVM/test ioAppTestsJVM/test - native_arm_task: arm_container: dockerfile: .cirrus/Dockerfile @@ -37,12 +28,3 @@ native_arm_task: matrix: - name: Native ARM 3 script: sbt '++ 3' testsNative/test ioAppTestsNative/test - -native_macos_task: - macos_instance: - image: ghcr.io/cirruslabs/macos-ventura-base:latest - matrix: - - name: Native Apple Silicon 3 - script: - - brew install sbt - - sbt '++ 3' testsNative/test ioAppTestsNative/test From 34b922f88f0c37b9f30d80f9441b045f1ed30413 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 17 Sep 2023 18:40:30 +0000 Subject: [PATCH 227/429] Disable fiber dumps again :( --- build.sbt | 3 +-- .../main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala | 3 +-- ioapp-tests/src/test/scala/IOAppSpec.scala | 2 +- tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala | 5 +---- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/build.sbt b/build.sbt index bed8ea52e8..bc5af57474 100644 --- a/build.sbt +++ b/build.sbt @@ -893,8 +893,7 @@ lazy val tests: CrossProject = crossProject(JSPlatform, JVMPlatform, NativePlatf Test / fork := true ) .nativeSettings( - Compile / mainClass := Some("catseffect.examples.NativeRunner"), - Test / envVars += "GC_MAXIMUM_MAX_HEAP_SIZE" -> "1G" + Compile / mainClass := Some("catseffect.examples.NativeRunner") ) def configureIOAppTests(p: Project): Project = diff --git a/core/native/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala b/core/native/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala index f545b92e91..f38fb411b7 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala @@ -18,11 +18,10 @@ package cats.effect package unsafe import scala.concurrent.ExecutionContext -import scala.scalanative.meta.LinktimeInfo private[effect] abstract class FiberMonitorPlatform { def apply(compute: ExecutionContext): FiberMonitor = { - if (LinktimeInfo.debugMode && LinktimeInfo.isWeakReferenceSupported) { + if (false) { // LinktimeInfo.debugMode && LinktimeInfo.isWeakReferenceSupported if (compute.isInstanceOf[EventLoopExecutorScheduler[_]]) { val loop = compute.asInstanceOf[EventLoopExecutorScheduler[_]] new FiberMonitorImpl(loop) diff --git a/ioapp-tests/src/test/scala/IOAppSpec.scala b/ioapp-tests/src/test/scala/IOAppSpec.scala index 808e2fe122..6f640336b9 100644 --- a/ioapp-tests/src/test/scala/IOAppSpec.scala +++ b/ioapp-tests/src/test/scala/IOAppSpec.scala @@ -286,7 +286,7 @@ class IOAppSpec extends Specification { h.awaitStatus() mustEqual 1 } - if (!isJava8 && !isWindows) { + if (!isJava8 && !isWindows && platform != Native) { // JDK 8 does not have free signals for live fiber snapshots // cannot observe signals sent to process termination on Windows "live fiber snapshot" in { diff --git a/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala b/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala index 75c7148d35..8748af8b8e 100644 --- a/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/std/MutexSpec.scala @@ -124,10 +124,7 @@ final class MutexSpec extends BaseSpec with DetectPlatform { } "not deadlock when highly contended" in real { - mutex - .flatMap(_.lock.use_.parReplicateA_(10)) - .replicateA_(if (isJVM) 10000 else 100) - .as(true) + mutex.flatMap(_.lock.use_.parReplicateA_(10)).replicateA_(10000).as(true) } "handle cancelled acquire" in real { From 1e1b82a7375224b0fab90e5868258c5a412722a6 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 17 Sep 2023 20:44:17 +0000 Subject: [PATCH 228/429] Delete dead code --- .../effect/unsafe/WorkStealingThreadPool.scala | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index bc6e10ce4e..07afd83c07 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -296,24 +296,6 @@ private[effect] final class WorkStealingThreadPool( false } - /** - * A specialized version of `notifyParked`, for when we know which thread to wake up, and know - * that it should wake up due to a new timer (i.e., it must always wake up, even if only to go - * back to sleep, because its current sleeping time might be incorrect). - * - * @param index - * The index of the thread to notify (must be less than `threadCount`). - */ - // private[this] final def notifyForTimer(index: Int): Unit = { - // val signal = parkedSignals(index) - // if (signal.getAndSet(false)) { - // state.getAndAdd(DeltaSearching) - // workerThreadPublisher.get() - // val worker = workerThreads(index) - // LockSupport.unpark(worker) - // } // else: was already unparked - // } - /** * Checks the number of active and searching worker threads and decides whether another thread * should be notified of new work. From 8d08a32c69cb3db1944a900ca2176b72727029b3 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 17 Sep 2023 21:52:46 +0000 Subject: [PATCH 229/429] Use atomic `getAndSet` in `ExternalSleepCancel` --- .../effect/unsafe/WorkStealingThreadPool.scala | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index 07afd83c07..dec3dcb8f1 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -41,7 +41,7 @@ import java.time.Instant import java.time.temporal.ChronoField import java.util.Comparator import java.util.concurrent.{ConcurrentSkipListSet, ThreadLocalRandom} -import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger} +import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger, AtomicReference} import java.util.concurrent.locks.LockSupport import WorkStealingThreadPool._ @@ -756,20 +756,18 @@ private object WorkStealingThreadPool { * A wrapper for a cancelation callback that is created asynchronously. Best-effort: does not * explicitly publish. */ - private final class ExternalSleepCancel extends Function0[Unit] with Runnable { - private[this] var callback: Function0[Unit] = null - + private final class ExternalSleepCancel + extends AtomicReference[Function0[Unit]] + with Function0[Unit] + with Runnable { callback => def setCallback(cb: Function0[Unit]) = { - val back = callback + val back = callback.getAndSet(cb) if (back eq CanceledSleepSentinel) cb() // we were already canceled, invoke right away - else - callback = cb } def apply() = { - val back = callback - callback = CanceledSleepSentinel + val back = callback.getAndSet(CanceledSleepSentinel) if (back ne null) back() } From e0c644cb9d1d3e6e4f34814b6a87131bbf5ed9c4 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 17 Sep 2023 22:04:06 +0000 Subject: [PATCH 230/429] Track cancelation in generic sleep --- .../unsafe/WorkStealingThreadPool.scala | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index dec3dcb8f1..f008a8fa56 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -627,13 +627,18 @@ private[effect] final class WorkStealingThreadPool( cancel } - override def sleep(delay: FiniteDuration, task: Runnable): Runnable = - sleepInternal( - delay, - new AtomicBoolean with (Right[Nothing, Unit] => Unit) { // run at most once - def apply(ru: Right[Nothing, Unit]) = if (compareAndSet(false, true)) task.run() - } - ) + override def sleep(delay: FiniteDuration, task: Runnable): Runnable = { + val cb = new AtomicBoolean with (Right[Nothing, Unit] => Unit) { // run at most once + def apply(ru: Right[Nothing, Unit]) = if (compareAndSet(false, true)) task.run() + } + + val cancel = sleepInternal(delay, cb) + + () => { + cb.set(true) + cancel.run() + } + } /** * Shut down the thread pool and clean up the pool state. Calling this method after the pool From fb465db36715616348873dff80dd20116e3987ef Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 17 Sep 2023 22:14:22 +0000 Subject: [PATCH 231/429] Make cancelation robust to unpublished callback --- .../src/main/scala/cats/effect/unsafe/TimerHeap.scala | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index d8bc2f5ba2..b1a5d52710 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -354,7 +354,7 @@ private final class TimerHeap extends AtomicBoolean { needsPack => /** * Cancel this timer. */ - def apply(): Unit = if (callback ne null) { // if we're not already deleted + def apply(): Unit = { // we can always clear the callback, without explicitly publishing callback = null @@ -363,9 +363,12 @@ private final class TimerHeap extends AtomicBoolean { needsPack => if (thread.isInstanceOf[WorkerThread]) { val worker = thread.asInstanceOf[WorkerThread] val heap = TimerHeap.this - if (worker.ownsTimers(heap)) - heap.removeAt(index) - else // otherwise this heap will need packing + if (worker.ownsTimers(heap)) { + if (index >= 0) { // remove ourselves at most once + heap.removeAt(index) + index = -1 // prevent further removals + } + } else // otherwise this heap will need packing needsPack.set(true) } else needsPack.set(true) } From 856b151c1e8c8b08f7e663754bedac9a4ebe17ca Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 22 Sep 2023 03:09:14 +0000 Subject: [PATCH 232/429] Fix NPE when canceling removed timer --- .../main/scala/cats/effect/unsafe/TimerHeap.scala | 1 + tests/shared/src/test/scala/cats/effect/IOSpec.scala | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index b1a5d52710..3964cf9fc2 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -77,6 +77,7 @@ private final class TimerHeap extends AtomicBoolean { needsPack => val rootDeleted = root.isDeleted() val rootExpired = !rootDeleted && isExpired(root, now) if (rootDeleted || rootExpired) { + root.index = -1 if (size > 1) { heap(1) = heap(size) fixDown(1) diff --git a/tests/shared/src/test/scala/cats/effect/IOSpec.scala b/tests/shared/src/test/scala/cats/effect/IOSpec.scala index d8152ccc81..0e4117cb80 100644 --- a/tests/shared/src/test/scala/cats/effect/IOSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/IOSpec.scala @@ -28,7 +28,7 @@ import cats.syntax.all._ import org.scalacheck.Prop import org.typelevel.discipline.specs2.mutable.Discipline -import scala.concurrent.{CancellationException, ExecutionContext, TimeoutException} +import scala.concurrent.{CancellationException, ExecutionContext, Promise, TimeoutException} import scala.concurrent.duration._ import Prop.forAll @@ -1822,6 +1822,16 @@ class IOSpec extends BaseSpec with Discipline with IOPlatformSpecification { } } } + + "no-op when canceling an expired timer" in realWithRuntime { rt => + IO(Promise[Unit]()) + .flatMap { p => + IO(rt.scheduler.sleep(1.nanosecond, () => p.success(()))).flatMap { cancel => + IO.fromFuture(IO(p.future)) *> IO(cancel.run()) + } + } + .as(ok) + } } "syncStep" should { From 9d00ba0c864a04af1c90ee031335c6657ef96cdc Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 22 Sep 2023 03:20:47 +0000 Subject: [PATCH 233/429] Invalidate node index when removing --- .../src/main/scala/cats/effect/unsafe/TimerHeap.scala | 11 ++++++----- tests/shared/src/test/scala/cats/effect/IOSpec.scala | 6 ++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index 3964cf9fc2..62c5106568 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -175,7 +175,9 @@ private final class TimerHeap extends AtomicBoolean { needsPack => */ private def removeAt(i: Int): Unit = { val heap = this.heap // local copy - heap(i).getAndClear() + val back = heap(i) + back.getAndClear() + back.index = -1 if (i == size) { heap(i) = null size -= 1 @@ -365,10 +367,9 @@ private final class TimerHeap extends AtomicBoolean { needsPack => val worker = thread.asInstanceOf[WorkerThread] val heap = TimerHeap.this if (worker.ownsTimers(heap)) { - if (index >= 0) { // remove ourselves at most once - heap.removeAt(index) - index = -1 // prevent further removals - } + // remove only if we are still in the heap + if (index >= 0) heap.removeAt(index) + else () } else // otherwise this heap will need packing needsPack.set(true) } else needsPack.set(true) diff --git a/tests/shared/src/test/scala/cats/effect/IOSpec.scala b/tests/shared/src/test/scala/cats/effect/IOSpec.scala index 0e4117cb80..82f340f11f 100644 --- a/tests/shared/src/test/scala/cats/effect/IOSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/IOSpec.scala @@ -1832,6 +1832,12 @@ class IOSpec extends BaseSpec with Discipline with IOPlatformSpecification { } .as(ok) } + + "no-op when canceling a timer twice" in realWithRuntime { rt => + IO(rt.scheduler.sleep(1.day, () => ())) + .flatMap(cancel => IO(cancel.run()) *> IO(cancel.run())) + .as(ok) + } } "syncStep" should { From bc8fcf96e9c588c0c4f190d05b6fec444a22ff60 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 22 Sep 2023 03:31:39 +0000 Subject: [PATCH 234/429] Fix another NPE when canceling removed timer --- .../main/scala/cats/effect/unsafe/TimerHeap.scala | 2 +- .../shared/src/test/scala/cats/effect/IOSpec.scala | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index 62c5106568..6e476d5621 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -133,6 +133,7 @@ private final class TimerHeap extends AtomicBoolean { needsPack => val rootDeleted = root.isDeleted() val rootExpired = !rootDeleted && isExpired(root, now) if (rootDeleted || rootExpired) { // see if we can just replace the root + root.index = -1 if (rootExpired) out(0) = root.getAndClear() val node = new Node(triggerTime, callback, 1) heap(1) = node @@ -369,7 +370,6 @@ private final class TimerHeap extends AtomicBoolean { needsPack => if (worker.ownsTimers(heap)) { // remove only if we are still in the heap if (index >= 0) heap.removeAt(index) - else () } else // otherwise this heap will need packing needsPack.set(true) } else needsPack.set(true) diff --git a/tests/shared/src/test/scala/cats/effect/IOSpec.scala b/tests/shared/src/test/scala/cats/effect/IOSpec.scala index 82f340f11f..927b4187c3 100644 --- a/tests/shared/src/test/scala/cats/effect/IOSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/IOSpec.scala @@ -1823,7 +1823,8 @@ class IOSpec extends BaseSpec with Discipline with IOPlatformSpecification { } } - "no-op when canceling an expired timer" in realWithRuntime { rt => + "no-op when canceling an expired timer 1" in realWithRuntime { rt => + // this one excercises a timer removed via `TimerHeap#pollFirstIfTriggered` IO(Promise[Unit]()) .flatMap { p => IO(rt.scheduler.sleep(1.nanosecond, () => p.success(()))).flatMap { cancel => @@ -1833,6 +1834,17 @@ class IOSpec extends BaseSpec with Discipline with IOPlatformSpecification { .as(ok) } + "no-op when canceling an expired timer 2" in realWithRuntime { rt => + // this one excercises a timer removed via `TimerHeap#insert` + IO(Promise[Unit]()) + .flatMap { p => + IO(rt.scheduler.sleep(1.nanosecond, () => p.success(()))).flatMap { cancel => + IO.sleep(1.nanosecond) *> IO.fromFuture(IO(p.future)) *> IO(cancel.run()) + } + } + .as(ok) + } + "no-op when canceling a timer twice" in realWithRuntime { rt => IO(rt.scheduler.sleep(1.day, () => ())) .flatMap(cancel => IO(cancel.run()) *> IO(cancel.run())) From 4a03e2f5cd92a7e561aec020ba5b7de2efe38895 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 23 Sep 2023 20:39:16 +0000 Subject: [PATCH 235/429] Enable forking for `SleepDrift` --- build.sbt | 1 + 1 file changed, 1 insertion(+) diff --git a/build.sbt b/build.sbt index 699d7ead30..a4f0aed050 100644 --- a/build.sbt +++ b/build.sbt @@ -1011,6 +1011,7 @@ lazy val benchmarks = project .dependsOn(core.jvm, std.jvm) .settings( name := "cats-effect-benchmarks", + fork := true, javaOptions ++= Seq( "-Dcats.effect.tracing.mode=none", "-Dcats.effect.tracing.exceptions.enhanced=false")) From f436fe691418164f61eb4d3dcd3b9eea761ac2cf Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Mon, 25 Sep 2023 14:53:26 -0500 Subject: [PATCH 236/429] Fix discarded value issue --- .../js/src/test/scala/cats/effect/unsafe/JSArrayQueueSpec.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/js/src/test/scala/cats/effect/unsafe/JSArrayQueueSpec.scala b/tests/js/src/test/scala/cats/effect/unsafe/JSArrayQueueSpec.scala index df0b7edd52..432e82bdf8 100644 --- a/tests/js/src/test/scala/cats/effect/unsafe/JSArrayQueueSpec.scala +++ b/tests/js/src/test/scala/cats/effect/unsafe/JSArrayQueueSpec.scala @@ -66,6 +66,8 @@ class JSArrayQueueSpec extends BaseSpec with ScalaCheck { val expected = shadow.dequeue() got must beEqualTo(expected) checkContents() + } else { + ok } } From 6feeecf7d6eab812c2e1639d7dc6498bb3e3ca76 Mon Sep 17 00:00:00 2001 From: Diogo Canut Date: Mon, 25 Sep 2023 23:45:31 -0300 Subject: [PATCH 237/429] fix: merge --- docs/core/io-runtime-config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/io-runtime-config.md b/docs/core/io-runtime-config.md index f107efc750..39828ad429 100644 --- a/docs/core/io-runtime-config.md +++ b/docs/core/io-runtime-config.md @@ -29,7 +29,7 @@ This can be done for example with the [EnvironmentPlugin for Webpack](https://we | `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.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.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. | From 861e9d46432e3dbbba407317303cb5bbca4bbfd1 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 26 Sep 2023 10:08:21 +0000 Subject: [PATCH 238/429] Add `shutdownTimeout` to WSTP --- .../benchmarks/WorkStealingBenchmark.scala | 1 + .../unsafe/IORuntimeCompanionPlatform.scala | 6 ++- .../unsafe/WorkStealingThreadPool.scala | 47 +++++++++++++++---- .../cats/effect/IOPlatformSpecification.scala | 1 + 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/benchmarks/src/main/scala/cats/effect/benchmarks/WorkStealingBenchmark.scala b/benchmarks/src/main/scala/cats/effect/benchmarks/WorkStealingBenchmark.scala index 7a3e5265f1..f1f2f3fa4c 100644 --- a/benchmarks/src/main/scala/cats/effect/benchmarks/WorkStealingBenchmark.scala +++ b/benchmarks/src/main/scala/cats/effect/benchmarks/WorkStealingBenchmark.scala @@ -171,6 +171,7 @@ class WorkStealingBenchmark { "io-blocker", 60.seconds, false, + 1.second, SleepSystem, _.printStackTrace()) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala index 002690230e..12c81bc5dc 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala @@ -65,6 +65,7 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type runtimeBlockingExpiration, reportFailure, false, + 1.second, SleepSystem ) (pool, shutdown) @@ -77,6 +78,7 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type runtimeBlockingExpiration: Duration = 60.seconds, reportFailure: Throwable => Unit = _.printStackTrace(), blockedThreadDetectionEnabled: Boolean = false, + shutdownTimeout: Duration = 1.second, pollingSystem: PollingSystem = SelectorSystem()) : (WorkStealingThreadPool[_], pollingSystem.Api, () => Unit) = { val threadPool = @@ -86,8 +88,10 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type blockerThreadPrefix, runtimeBlockingExpiration, blockedThreadDetectionEnabled && (threads > 1), + shutdownTimeout, pollingSystem, - reportFailure) + reportFailure + ) val unregisterMBeans = if (isStackTracing) { diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index 562e2ab81d..41a86ca0f1 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -64,6 +64,7 @@ private[effect] final class WorkStealingThreadPool[P]( private[unsafe] val blockerThreadPrefix: String, // prefix for the name of worker threads currently in a blocking region private[unsafe] val runtimeBlockingExpiration: Duration, private[unsafe] val blockedThreadDetectionEnabled: Boolean, + shutdownTimeout: Duration, system: PollingSystem.WithPoller[P], reportFailure0: Throwable => Unit ) extends ExecutionContextExecutor @@ -695,26 +696,55 @@ private[effect] final class WorkStealingThreadPool[P]( // Execute the shutdown logic only once. if (done.compareAndSet(false, true)) { - // Send an interrupt signal to each of the worker threads. - workerThreadPublisher.get() - // Note: while loops and mutable variables are used throughout this method // to avoid allocations of objects, since this method is expected to be // executed mostly in situations where the thread pool is shutting down in // the face of unhandled exceptions or as part of the whole JVM exiting. + + workerThreadPublisher.get() + + // Send an interrupt signal to each of the worker threads. var i = 0 while (i < threadCount) { val workerThread = workerThreads(i) if (workerThread ne currentThread) { workerThread.interrupt() - workerThread.join() - // wait to stop before closing pollers } - system.closePoller(pollers(i)) i += 1 } - system.close() + i = 0 + var joinTimeout = shutdownTimeout match { + case Duration.Inf => Long.MaxValue + case d => d.toNanos + } + while (i < threadCount && joinTimeout > 0) { + val workerThread = workerThreads(i) + if (workerThread ne currentThread) { + val now = System.nanoTime() + workerThread.join(joinTimeout / 1000000, (joinTimeout % 1000000).toInt) + val elapsed = System.nanoTime() - now + joinTimeout -= elapsed + } + i += 1 + } + + i = 0 + var allClosed = true + while (i < threadCount) { + val workerThread = workerThreads(i) + // only close the poller if it is safe to do so, leak otherwise ... + if ((workerThread eq currentThread) || !workerThread.isAlive()) { + system.closePoller(pollers(i)) + } else { + allClosed = false + } + i += 1 + } + + if (allClosed) { + system.close() + } var t: WorkerThread[P] = null while ({ @@ -722,8 +752,7 @@ private[effect] final class WorkStealingThreadPool[P]( t ne null }) { t.interrupt() - // don't join, blocking threads may be uninterruptibly blocked. - // anyway, they do not have pollers to close. + // don't bother joining, cached threads are not doing anything interesting } // Drain the external queue. diff --git a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala index 9e69674c6f..f934c3269f 100644 --- a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala +++ b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala @@ -437,6 +437,7 @@ trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => runtimeBlockingExpiration = 3.seconds, reportFailure0 = _.printStackTrace(), blockedThreadDetectionEnabled = false, + shutdownTimeout = 1.second, system = SleepSystem ) From f9bd7cb490edb4ffb44594aa664a684c36343481 Mon Sep 17 00:00:00 2001 From: Diogo Canut Date: Tue, 26 Sep 2023 08:12:14 -0300 Subject: [PATCH 239/429] remove stray space --- core/jvm/src/main/scala/cats/effect/IOApp.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/core/jvm/src/main/scala/cats/effect/IOApp.scala b/core/jvm/src/main/scala/cats/effect/IOApp.scala index 9a95bf9775..f9b8198701 100644 --- a/core/jvm/src/main/scala/cats/effect/IOApp.scala +++ b/core/jvm/src/main/scala/cats/effect/IOApp.scala @@ -337,7 +337,6 @@ trait IOApp { * isn't the main process thread. This condition can happen when we are running inside of an * `sbt run` with `fork := false` */ - def warnOnNonMainThreadDetected: Boolean = Option(System.getProperty("cats.effect.warnOnNonMainThreadDetected")) .map(_.equalsIgnoreCase("true")) From 965f7d3f943d69e04c4fd040a6b3aebe1be3e086 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 26 Sep 2023 09:39:59 -0700 Subject: [PATCH 240/429] Fix bad merge in 07cafdbf3d32f747dbc58d46daea1644b4eeef10 --- build.sbt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index b66a1690d7..159fa1a10c 100644 --- a/build.sbt +++ b/build.sbt @@ -898,8 +898,7 @@ lazy val tests: CrossProject = crossProject(JSPlatform, JVMPlatform, NativePlatf scalacOptions ~= { _.filterNot(_.startsWith("-P:scalajs:mapSourceURI")) } ) .jvmSettings( - fork := true, - Test / javaOptions += s"-Dsbt.classpath=${(Test / fullClasspath).value.map(_.data.getAbsolutePath).mkString(File.pathSeparator)}" + fork := true ) .nativeSettings( Compile / mainClass := Some("catseffect.examples.NativeRunner") From 65ee6018b7c0b5582f06df23daac1a2cc8af43df Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 26 Sep 2023 17:29:35 +0000 Subject: [PATCH 241/429] Update sbt-scalajs, scalajs-compiler, ... to 1.14.0 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 5cfe507747..bdf34500e1 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,7 +2,7 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.3") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.15") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.6") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") From 74c92ae94f0446b8cedcd4038d49083696756709 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 27 Sep 2023 00:37:08 +0000 Subject: [PATCH 242/429] Use `Arrays.copyOf` --- core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index 6e476d5621..f3cd0182dc 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -31,6 +31,7 @@ package unsafe import scala.annotation.tailrec +import java.util.Arrays import java.util.concurrent.atomic.AtomicBoolean private final class TimerHeap extends AtomicBoolean { needsPack => @@ -198,8 +199,7 @@ private final class TimerHeap extends AtomicBoolean { needsPack => private[this] def growIfNeeded(): Array[Node] = { val heap = this.heap // local copy if (size >= heap.length - 1) { - val newHeap = new Array[Node](heap.length * 2) - System.arraycopy(heap, 1, newHeap, 1, heap.length - 1) + val newHeap = Arrays.copyOf(heap, heap.length * 2, classOf[Array[Node]]) this.heap = newHeap newHeap } else heap From 9f74fff4f255553356b946bd7db61f17d4a6faf2 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 27 Sep 2023 00:43:33 +0000 Subject: [PATCH 243/429] Remove branch from `overflowFree` --- .../scala/cats/effect/unsafe/TimerHeap.scala | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index f3cd0182dc..504bac2f1b 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -320,20 +320,18 @@ private final class TimerHeap extends AtomicBoolean { needsPack => * (based on `now`), but was not removed yet; and `delay` is sufficiently big. * * From the public domain JSR-166 `ScheduledThreadPoolExecutor` (`overflowFree` method). + * + * Pre-condition that the heap is non-empty. */ private[this] def overflowFree(now: Long, delay: Long): Long = { val root = heap(1) - if (root ne null) { - val rootDelay = root.triggerTime - now - if ((rootDelay < 0) && (delay - rootDelay < 0)) { - // head was already triggered, and `delay` is big enough, - // so we must clamp `delay`: - Long.MaxValue + rootDelay - } else { - delay - } + val rootDelay = root.triggerTime - now + if ((rootDelay < 0) && (delay - rootDelay < 0)) { + // head was already triggered, and `delay` is big enough, + // so we must clamp `delay`: + Long.MaxValue + rootDelay } else { - delay // empty + delay } } From c001d4002acd35c0be0040b8e7a0c5ef3a02748b Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 27 Sep 2023 01:10:37 +0000 Subject: [PATCH 244/429] Add NOTICE --- NOTICE.txt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 NOTICE.txt diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 0000000000..eebb95040d --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,8 @@ +cats-effect +Copyright 2020-2023 Typelevel +Licensed under Apache License 2.0 (see LICENSE) + +This software contains portions of code derived from scala-js +https://github.com/scala-js/scala-js +Copyright EPFL +Licensed under Apache License 2.0 (see LICENSE) From 04e54eaf96337ce5c0884b64f3d12607519cd8cd Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 27 Sep 2023 02:03:14 +0000 Subject: [PATCH 245/429] Add overview scaladoc for `TimerHeap` --- .../scala/cats/effect/unsafe/TimerHeap.scala | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index 504bac2f1b..2b7e00d4fa 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -34,6 +34,22 @@ import scala.annotation.tailrec import java.util.Arrays import java.util.concurrent.atomic.AtomicBoolean +/** + * A specialized heap that serves as a priority queue for timers i.e. callbacks with trigger + * times. + * + * In general, this heap is not threadsafe and modifications (insertion/removal) may only be + * performed on its owner WorkerThread. The exception is that the callback value of nodes may be + * `null`ed by other threads and published via data race. + * + * Other threads may traverse the heap with the `steal` method during which they may `null` some + * callbacks. This is entirely subject to data races. + * + * The only explicit synchronization is the `needsPack` atomic, which is used to track and + * publish "removals" from other threads. Because other threads cannot safely remove a node, + * they only `null` the callback and toggle a boolean to indicate that the owner thread should + * iterate the heap to properly remove these nodes. + */ private final class TimerHeap extends AtomicBoolean { needsPack => // The index 0 is not used; the root is at index 1. @@ -43,6 +59,9 @@ private final class TimerHeap extends AtomicBoolean { needsPack => private[this] val RightUnit = Right(()) + /** + * only called by owner thread + */ def peekFirstTriggerTime(): Long = if (size > 0) { val tt = heap(1).triggerTime From 80aeaaec77a7548ca1fcc19c34ea727e225c8f28 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 27 Sep 2023 02:25:43 +0000 Subject: [PATCH 246/429] Track externally deleted timers with counter --- .../scala/cats/effect/unsafe/TimerHeap.scala | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index 2b7e00d4fa..f0790ccbc5 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -32,7 +32,7 @@ package unsafe import scala.annotation.tailrec import java.util.Arrays -import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger /** * A specialized heap that serves as a priority queue for timers i.e. callbacks with trigger @@ -45,12 +45,12 @@ import java.util.concurrent.atomic.AtomicBoolean * Other threads may traverse the heap with the `steal` method during which they may `null` some * callbacks. This is entirely subject to data races. * - * The only explicit synchronization is the `needsPack` atomic, which is used to track and + * The only explicit synchronization is the `deletedCounter` atomic, which is used to track and * publish "removals" from other threads. Because other threads cannot safely remove a node, - * they only `null` the callback and toggle a boolean to indicate that the owner thread should - * iterate the heap to properly remove these nodes. + * they only `null` the callback and increment the counter to indicate that the owner thread + * should iterate the heap to properly remove these nodes. */ -private final class TimerHeap extends AtomicBoolean { needsPack => +private final class TimerHeap extends AtomicInteger { deletedCounter => // The index 0 is not used; the root is at index 1. // This is standard practice in binary heaps, to simplify arithmetics. @@ -177,19 +177,23 @@ private final class TimerHeap extends AtomicBoolean { needsPack => /** * only called by owner thread */ - def packIfNeeded(): Unit = - if (needsPack.getAndSet(false)) { // we now see all external cancelations - val heap = this.heap // local copy - var i = 1 - while (i <= size) { - if (heap(i).isDeleted()) { - removeAt(i) - // don't increment i, the new i may be deleted too - } else { - i += 1 - } + def packIfNeeded(): Unit = { + val deleted = deletedCounter.getAndSet(0) // we now see all external deletions + val heap = this.heap // local copy + + // we track how many deleted we found so we can try to exit the loop early + var i = 1 + var d = 0 + while (d < deleted && i <= size) { + if (heap(i).isDeleted()) { + removeAt(i) + d += 1 + // don't increment i, the new i may be deleted too + } else { + i += 1 } } + } /** * only called by owner thread @@ -387,9 +391,14 @@ private final class TimerHeap extends AtomicBoolean { needsPack => if (worker.ownsTimers(heap)) { // remove only if we are still in the heap if (index >= 0) heap.removeAt(index) - } else // otherwise this heap will need packing - needsPack.set(true) - } else needsPack.set(true) + } else { // otherwise this heap will need packing + deletedCounter.getAndIncrement() + () + } + } else { + deletedCounter.getAndIncrement() + () + } } def run() = apply() From 19449a0492dfc417676e751f3f790d0c6cc39cee Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 27 Sep 2023 02:31:39 +0000 Subject: [PATCH 247/429] Revert "Track externally deleted timers with counter" This reverts commit 80aeaaec77a7548ca1fcc19c34ea727e225c8f28. --- .../scala/cats/effect/unsafe/TimerHeap.scala | 47 ++++++++----------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index f0790ccbc5..2b7e00d4fa 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -32,7 +32,7 @@ package unsafe import scala.annotation.tailrec import java.util.Arrays -import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicBoolean /** * A specialized heap that serves as a priority queue for timers i.e. callbacks with trigger @@ -45,12 +45,12 @@ import java.util.concurrent.atomic.AtomicInteger * Other threads may traverse the heap with the `steal` method during which they may `null` some * callbacks. This is entirely subject to data races. * - * The only explicit synchronization is the `deletedCounter` atomic, which is used to track and + * The only explicit synchronization is the `needsPack` atomic, which is used to track and * publish "removals" from other threads. Because other threads cannot safely remove a node, - * they only `null` the callback and increment the counter to indicate that the owner thread - * should iterate the heap to properly remove these nodes. + * they only `null` the callback and toggle a boolean to indicate that the owner thread should + * iterate the heap to properly remove these nodes. */ -private final class TimerHeap extends AtomicInteger { deletedCounter => +private final class TimerHeap extends AtomicBoolean { needsPack => // The index 0 is not used; the root is at index 1. // This is standard practice in binary heaps, to simplify arithmetics. @@ -177,23 +177,19 @@ private final class TimerHeap extends AtomicInteger { deletedCounter => /** * only called by owner thread */ - def packIfNeeded(): Unit = { - val deleted = deletedCounter.getAndSet(0) // we now see all external deletions - val heap = this.heap // local copy - - // we track how many deleted we found so we can try to exit the loop early - var i = 1 - var d = 0 - while (d < deleted && i <= size) { - if (heap(i).isDeleted()) { - removeAt(i) - d += 1 - // don't increment i, the new i may be deleted too - } else { - i += 1 + def packIfNeeded(): Unit = + if (needsPack.getAndSet(false)) { // we now see all external cancelations + val heap = this.heap // local copy + var i = 1 + while (i <= size) { + if (heap(i).isDeleted()) { + removeAt(i) + // don't increment i, the new i may be deleted too + } else { + i += 1 + } } } - } /** * only called by owner thread @@ -391,14 +387,9 @@ private final class TimerHeap extends AtomicInteger { deletedCounter => if (worker.ownsTimers(heap)) { // remove only if we are still in the heap if (index >= 0) heap.removeAt(index) - } else { // otherwise this heap will need packing - deletedCounter.getAndIncrement() - () - } - } else { - deletedCounter.getAndIncrement() - () - } + } else // otherwise this heap will need packing + needsPack.set(true) + } else needsPack.set(true) } def run() = apply() From 6d0b285cecc5a0d0e3cf0d84fbfdc3bb97789536 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 30 Sep 2023 20:59:59 -0700 Subject: [PATCH 248/429] Fix out-dated comment --- .../main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index f008a8fa56..f1bf2b8f49 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -758,8 +758,7 @@ private[effect] final class WorkStealingThreadPool( private object WorkStealingThreadPool { /** - * A wrapper for a cancelation callback that is created asynchronously. Best-effort: does not - * explicitly publish. + * A wrapper for a cancelation callback that is created asynchronously. */ private final class ExternalSleepCancel extends AtomicReference[Function0[Unit]] From 3059d7173358bee0513392b8658088e02db9d73a Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 1 Oct 2023 06:04:05 +0000 Subject: [PATCH 249/429] Fix outdated comments --- core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala | 2 +- .../src/test/scala/cats/effect/IOPlatformSpecification.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index 2b7e00d4fa..84260f6368 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -323,7 +323,7 @@ private final class TimerHeap extends AtomicBoolean { needsPack => /** * Computes the trigger time in an overflow-safe manner. The trigger time is essentially `now - * + delay`. However, we must constrain all trigger times in the skip list to be within + * + delay`. However, we must constrain all trigger times in the heap to be within * `Long.MaxValue` of each other (otherwise there will be overflow when comparing in `cpr`). * Thus, if `delay` is so big, we'll reduce it to the greatest allowable (in `overflowFree`). * diff --git a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala index 182f2283f8..8b6550bdaf 100644 --- a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala +++ b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala @@ -387,7 +387,7 @@ trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => // we race a lot of "sleeps", it must not hang // (this includes inserting and cancelling - // a lot of callbacks into the skip list, + // a lot of callbacks into the heap, // thus hopefully stressing the data structure): List .fill(500) { From 87547e8fa35052779fc4c4a15a3d800b0fe8bf30 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 1 Oct 2023 07:01:09 +0000 Subject: [PATCH 250/429] Track externally canceled timers with counter --- .../scala/cats/effect/unsafe/TimerHeap.scala | 60 +++++++++++++------ 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index 84260f6368..d4b2dadc04 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -32,7 +32,7 @@ package unsafe import scala.annotation.tailrec import java.util.Arrays -import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger /** * A specialized heap that serves as a priority queue for timers i.e. callbacks with trigger @@ -45,12 +45,12 @@ import java.util.concurrent.atomic.AtomicBoolean * Other threads may traverse the heap with the `steal` method during which they may `null` some * callbacks. This is entirely subject to data races. * - * The only explicit synchronization is the `needsPack` atomic, which is used to track and + * The only explicit synchronization is the `canceledCounter` atomic, which is used to track and * publish "removals" from other threads. Because other threads cannot safely remove a node, - * they only `null` the callback and toggle a boolean to indicate that the owner thread should - * iterate the heap to properly remove these nodes. + * they only `null` the callback and increment the counter to indicate that the owner thread + * should iterate the heap to properly remove these nodes. */ -private final class TimerHeap extends AtomicBoolean { needsPack => +private final class TimerHeap extends AtomicInteger { canceledCounter => // The index 0 is not used; the root is at index 1. // This is standard practice in binary heaps, to simplify arithmetics. @@ -177,19 +177,26 @@ private final class TimerHeap extends AtomicBoolean { needsPack => /** * only called by owner thread */ - def packIfNeeded(): Unit = - if (needsPack.getAndSet(false)) { // we now see all external cancelations - val heap = this.heap // local copy - var i = 1 - while (i <= size) { - if (heap(i).isDeleted()) { - removeAt(i) - // don't increment i, the new i may be deleted too - } else { - i += 1 - } + def packIfNeeded(): Unit = { + val canceled = canceledCounter.getAndSet(0) // we now see all external cancelations + val heap = this.heap // local copy + + // we track how many canceled nodes we found so we can try to exit the loop early + var i = 1 + var c = 0 + while (c < canceled && i <= size) { + // we are careful to consider only *canceled* nodes, which increment the canceledCounter + // a node may be deleted b/c it was stolen, but this does not increment the canceledCounter + // to avoid leaks we must attempt to find a canceled node for every increment + if (heap(i).isCanceled()) { + removeAt(i) + c += 1 + // don't increment i, the new i may be canceled too + } else { + i += 1 } } + } /** * only called by owner thread @@ -363,6 +370,8 @@ private final class TimerHeap extends AtomicBoolean { needsPack => ) extends Function0[Unit] with Runnable { + private[this] var canceled: Boolean = false + def getAndClear(): Right[Nothing, Unit] => Unit = { val back = callback if (back ne null) // only clear if we read something @@ -379,6 +388,9 @@ private final class TimerHeap extends AtomicBoolean { needsPack => // we can always clear the callback, without explicitly publishing callback = null + // if this node is not removed immediately, this will be published by canceledCounter + canceled = true + // if we're on the thread that owns this heap, we can remove ourselves immediately val thread = Thread.currentThread() if (thread.isInstanceOf[WorkerThread]) { @@ -387,15 +399,25 @@ private final class TimerHeap extends AtomicBoolean { needsPack => if (worker.ownsTimers(heap)) { // remove only if we are still in the heap if (index >= 0) heap.removeAt(index) - } else // otherwise this heap will need packing - needsPack.set(true) - } else needsPack.set(true) + } else { // otherwise this heap will need packing + // it is okay to increment more than once if invoked multiple times + // but it will undermine the packIfNeeded short-circuit optimization + // b/c it will keep looking for more canceled nodes + canceledCounter.getAndIncrement() + () + } + } else { + canceledCounter.getAndIncrement() + () + } } def run() = apply() def isDeleted(): Boolean = callback eq null + def isCanceled(): Boolean = canceled + override def toString() = s"Node($triggerTime, $callback})" } From 7492faa81b2acddfb4c2c61e3eb5ee098818d40e Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 1 Oct 2023 07:03:52 +0000 Subject: [PATCH 251/429] Track cancel state atomically for generic sleep --- .../scala/cats/effect/unsafe/WorkStealingThreadPool.scala | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index f1bf2b8f49..6d64ac320c 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -634,10 +634,7 @@ private[effect] final class WorkStealingThreadPool( val cancel = sleepInternal(delay, cb) - () => { - cb.set(true) - cancel.run() - } + () => if (cb.compareAndSet(false, true)) cancel.run() else () } /** From 22ee156ca9fe81a3e3114f0e97e87da52f6b3a30 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 1 Oct 2023 07:21:29 +0000 Subject: [PATCH 252/429] Improve code comments --- .../src/main/scala/cats/effect/unsafe/TimerHeap.scala | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index d4b2dadc04..ffc470660b 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -46,11 +46,13 @@ import java.util.concurrent.atomic.AtomicInteger * callbacks. This is entirely subject to data races. * * The only explicit synchronization is the `canceledCounter` atomic, which is used to track and - * publish "removals" from other threads. Because other threads cannot safely remove a node, - * they only `null` the callback and increment the counter to indicate that the owner thread - * should iterate the heap to properly remove these nodes. + * publish cancelations from other threads. Because other threads cannot safely remove a node, + * they `null` the callback, toggle the `canceled` flag, and increment the counter to indicate + * that the owner thread should iterate the heap to properly remove these nodes. */ -private final class TimerHeap extends AtomicInteger { canceledCounter => +private final class TimerHeap extends AtomicInteger { + // at most this many nodes are externally canceled and waiting to be removed from the heap + canceledCounter => // The index 0 is not used; the root is at index 1. // This is standard practice in binary heaps, to simplify arithmetics. From eaa6211d35ba1dd31c371b48d0b4159b18de8947 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Sat, 7 Oct 2023 00:21:59 +0000 Subject: [PATCH 253/429] Update sbt-typelevel to 0.5.4 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index bdf34500e1..43e912aea1 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.3") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.4") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.15") From acbdb48ec2413fe67c0624e02c303a9ea2cbc430 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Sat, 7 Oct 2023 00:23:45 +0000 Subject: [PATCH 254/429] Run prePR with sbt-typelevel Executed command: sbt tlPrePrBotHook --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f9585b7e7..b0570dbaf3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -452,7 +452,7 @@ jobs: env: PGP_SECRET: ${{ secrets.PGP_SECRET }} PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} - run: echo $PGP_SECRET | base64 -di | gpg --import + run: echo $PGP_SECRET | base64 -d -i - | gpg --import - name: Import signing key and strip passphrase if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE != '' @@ -460,7 +460,7 @@ jobs: PGP_SECRET: ${{ secrets.PGP_SECRET }} PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} run: | - echo "$PGP_SECRET" | base64 -di > /tmp/signing-key.gpg + echo "$PGP_SECRET" | base64 -d -i - > /tmp/signing-key.gpg echo "$PGP_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --import /tmp/signing-key.gpg (echo "$PGP_PASSPHRASE"; echo; echo) | gpg --command-fd 0 --pinentry-mode loopback --change-passphrase $(gpg --list-secret-keys --with-colons 2> /dev/null | grep '^sec:' | cut --delimiter ':' --fields 5 | tail -n 1) From 5603295cf140ecc4e52a03406ab0c62287722ff4 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 7 Oct 2023 06:07:19 +0000 Subject: [PATCH 255/429] `register`->`access` --- .../scala/cats/effect/unsafe/SelectorSystem.scala | 8 ++++---- .../scala/cats/effect/unsafe/SleepSystem.scala | 2 +- .../scala/cats/effect/unsafe/EpollSystem.scala | 8 ++++---- .../scala/cats/effect/unsafe/KqueueSystem.scala | 14 +++++++------- .../effect/unsafe/PollingExecutorScheduler.scala | 2 +- .../scala/cats/effect/unsafe/SleepSystem.scala | 2 +- .../cats/effect/IOPlatformSpecification.scala | 4 ++-- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala index 1cf97dc05e..a958076cea 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala @@ -30,8 +30,8 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS def close(): Unit = () - def makeApi(register: (Poller => Unit) => Unit): Selector = - new SelectorImpl(register, provider) + def makeApi(access: (Poller => Unit) => Unit): Selector = + new SelectorImpl(access, provider) def makePoller(): Poller = new Poller(provider.openSelector()) @@ -107,13 +107,13 @@ final class SelectorSystem private (provider: SelectorProvider) extends PollingS } final class SelectorImpl private[SelectorSystem] ( - register: (Poller => Unit) => Unit, + access: (Poller => Unit) => Unit, val provider: SelectorProvider ) extends Selector { def select(ch: SelectableChannel, ops: Int): IO[Int] = IO.async { selectCb => IO.async_[CallbackNode] { cb => - register { data => + access { data => try { val selector = data.selector val key = ch.keyFor(selector) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala index d39a446c7c..541335e62d 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -26,7 +26,7 @@ object SleepSystem extends PollingSystem { def close(): Unit = () - def makeApi(register: (Poller => Unit) => Unit): Api = this + def makeApi(access: (Poller => Unit) => Unit): Api = this def makePoller(): Poller = this diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index 37539dfd83..28c7f4afb3 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -47,8 +47,8 @@ object EpollSystem extends PollingSystem { def close(): Unit = () - def makeApi(register: (Poller => Unit) => Unit): Api = - new FileDescriptorPollerImpl(register) + def makeApi(access: (Poller => Unit) => Unit): Api = + new FileDescriptorPollerImpl(access) def makePoller(): Poller = { val fd = epoll_create1(0) @@ -67,7 +67,7 @@ object EpollSystem extends PollingSystem { def interrupt(targetThread: Thread, targetPoller: Poller): Unit = () private final class FileDescriptorPollerImpl private[EpollSystem] ( - register: (Poller => Unit) => Unit) + access: (Poller => Unit) => Unit) extends FileDescriptorPoller { def registerFileDescriptor( @@ -78,7 +78,7 @@ object EpollSystem extends PollingSystem { Resource { (Mutex[IO], Mutex[IO]).flatMapN { (readMutex, writeMutex) => IO.async_[(PollHandle, IO[Unit])] { cb => - register { epoll => + access { epoll => val handle = new PollHandle(readMutex, writeMutex) epoll.register(fd, reads, writes, handle, cb) } diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index ae748a8673..8f5febc439 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -46,8 +46,8 @@ object KqueueSystem extends PollingSystem { def close(): Unit = () - def makeApi(register: (Poller => Unit) => Unit): FileDescriptorPoller = - new FileDescriptorPollerImpl(register) + def makeApi(access: (Poller => Unit) => Unit): FileDescriptorPoller = + new FileDescriptorPollerImpl(access) def makePoller(): Poller = { val fd = kqueue() @@ -67,7 +67,7 @@ object KqueueSystem extends PollingSystem { def interrupt(targetThread: Thread, targetPoller: Poller): Unit = () private final class FileDescriptorPollerImpl private[KqueueSystem] ( - register: (Poller => Unit) => Unit + access: (Poller => Unit) => Unit ) extends FileDescriptorPoller { def registerFileDescriptor( fd: Int, @@ -76,7 +76,7 @@ object KqueueSystem extends PollingSystem { ): Resource[IO, FileDescriptorPollHandle] = Resource.eval { (Mutex[IO], Mutex[IO]).mapN { - new PollHandle(register, fd, _, _) + new PollHandle(access, fd, _, _) } } } @@ -86,7 +86,7 @@ object KqueueSystem extends PollingSystem { (filter.toLong << 32) | ident.toLong private final class PollHandle( - register: (Poller => Unit) => Unit, + access: (Poller => Unit) => Unit, fd: Int, readMutex: Mutex[IO], writeMutex: Mutex[IO] @@ -101,7 +101,7 @@ object KqueueSystem extends PollingSystem { else IO.async[Unit] { kqcb => IO.async_[Option[IO[Unit]]] { cb => - register { kqueue => + access { kqueue => kqueue.evSet(fd, EVFILT_READ, EV_ADD.toUShort, kqcb) cb(Right(Some(IO(kqueue.removeCallback(fd, EVFILT_READ))))) } @@ -121,7 +121,7 @@ object KqueueSystem extends PollingSystem { else IO.async[Unit] { kqcb => IO.async_[Option[IO[Unit]]] { cb => - register { kqueue => + access { kqueue => kqueue.evSet(fd, EVFILT_WRITE, EV_ADD.toUShort, kqcb) cb(Right(Some(IO(kqueue.removeCallback(fd, EVFILT_WRITE))))) } diff --git a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala index 6ca79ad3bd..a430a236f4 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala @@ -32,7 +32,7 @@ abstract class PollingExecutorScheduler(pollEvery: Int) type Poller = outer.type private[this] var needsPoll = true def close(): Unit = () - def makeApi(register: (Poller => Unit) => Unit): Api = outer + def makeApi(access: (Poller => Unit) => Unit): Api = outer def makePoller(): Poller = outer def closePoller(poller: Poller): Unit = () def poll(poller: Poller, nanos: Long, reportFailure: Throwable => Unit): Boolean = { diff --git a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala index 0848e41adb..f6afe5ca26 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -24,7 +24,7 @@ object SleepSystem extends PollingSystem { def close(): Unit = () - def makeApi(register: (Poller => Unit) => Unit): Api = this + def makeApi(access: (Poller => Unit) => Unit): Api = this def makePoller(): Poller = this diff --git a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala index f934c3269f..be828c588b 100644 --- a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala +++ b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala @@ -502,10 +502,10 @@ trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => } } - def makeApi(register: (Poller => Unit) => Unit): DummySystem.Api = + def makeApi(access: (Poller => Unit) => Unit): DummySystem.Api = new DummyPoller { def poll = IO.async_[Unit] { cb => - register { poller => + access { poller => poller.getAndUpdate(cb :: _) () } From 67a19810bd941575109e6f988799b9d6789c63c8 Mon Sep 17 00:00:00 2001 From: berndlosert Date: Sat, 7 Oct 2023 19:07:04 +0200 Subject: [PATCH 256/429] Fix typo in typeclasses.md every possibly -> every possible --- docs/typeclasses.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/typeclasses.md b/docs/typeclasses.md index 45ce671d74..672f4621ef 100644 --- a/docs/typeclasses.md +++ b/docs/typeclasses.md @@ -23,4 +23,4 @@ Beyond the above, certain capabilities are *assumed* by Cats Effect but defined - Composing multiple effectful computations together sequentially, such that each is dependent on the previous - Raising and handling errors -Taken together, all of these capabilities define what it means to be an effect. Just as you can rely on the properties of integers when you perform basic mental arithmetic (e.g. you can assume that `1 + 2 + 3` is the same as `1 + 5`), so too can you rely on these powerful and general properties of *effects* to hold when you write complex programs. This allows you to understand and refactor your code based on rules and abstractions, rather than having to think about every possibly implementation detail and use-case. Additionally, it makes it possible for you and others to write very generic code which composes together making an absolute minimum of assumptions. This is the foundation of the Cats Effect ecosystem. +Taken together, all of these capabilities define what it means to be an effect. Just as you can rely on the properties of integers when you perform basic mental arithmetic (e.g. you can assume that `1 + 2 + 3` is the same as `1 + 5`), so too can you rely on these powerful and general properties of *effects* to hold when you write complex programs. This allows you to understand and refactor your code based on rules and abstractions, rather than having to think about every possible implementation detail and use-case. Additionally, it makes it possible for you and others to write very generic code which composes together making an absolute minimum of assumptions. This is the foundation of the Cats Effect ecosystem. From e256a8ba9c6da3793ccb0b817ae4c9703780d0bb Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 9 Oct 2023 23:32:27 +0000 Subject: [PATCH 257/429] Block in-place if running on a virtual thread --- .github/workflows/ci.yml | 59 ++++++++++++++++++- build.sbt | 8 ++- .../scala/cats/effect/IOFiberConstants.scala | 2 + .../java/cats/effect/IOFiberConstants.java | 26 ++++++++ .../src/main/scala/cats/effect/IOFiber.scala | 14 +++++ .../scala/cats/effect/DetectPlatform.scala | 3 + .../cats/effect/IOPlatformSpecification.scala | 26 +++++--- 7 files changed, 128 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0570dbaf3..8840b50bd9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,15 +29,24 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] scala: [3.3.1, 2.12.18, 2.13.12] - java: [temurin@8, temurin@11, temurin@17, graalvm@17] + java: + - temurin@8 + - temurin@11 + - temurin@17 + - temurin@21 + - graalvm@17 ci: [ciJVM, ciNative, ciJS, ciFirefox, ciChrome] exclude: - scala: 3.3.1 java: temurin@11 + - scala: 3.3.1 + java: temurin@21 - scala: 2.12.18 java: temurin@11 - scala: 2.12.18 java: temurin@17 + - scala: 2.12.18 + java: temurin@21 - scala: 2.12.18 java: graalvm@17 - os: windows-latest @@ -64,6 +73,8 @@ jobs: java: temurin@11 - ci: ciJS java: temurin@17 + - ci: ciJS + java: temurin@21 - ci: ciJS java: graalvm@17 - os: windows-latest @@ -74,6 +85,8 @@ jobs: java: temurin@11 - ci: ciFirefox java: temurin@17 + - ci: ciFirefox + java: temurin@21 - ci: ciFirefox java: graalvm@17 - os: windows-latest @@ -84,6 +97,8 @@ jobs: java: temurin@11 - ci: ciChrome java: temurin@17 + - ci: ciChrome + java: temurin@21 - ci: ciChrome java: graalvm@17 - os: windows-latest @@ -94,6 +109,8 @@ jobs: java: temurin@11 - ci: ciNative java: temurin@17 + - ci: ciNative + java: temurin@21 - ci: ciNative java: graalvm@17 - os: windows-latest @@ -158,6 +175,20 @@ jobs: shell: bash run: sbt +update + - name: Setup Java (temurin@21) + id: setup-java-temurin-21 + if: matrix.java == 'temurin@21' + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 21 + cache: sbt + + - name: sbt update + if: matrix.java == 'temurin@21' && steps.setup-java-temurin-21.outputs.cache-hit == 'false' + shell: bash + run: sbt +update + - name: Setup Java (graalvm@17) id: setup-java-graalvm-17 if: matrix.java == 'graalvm@17' @@ -324,6 +355,19 @@ jobs: if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' run: sbt +update + - name: Setup Java (temurin@21) + id: setup-java-temurin-21 + if: matrix.java == 'temurin@21' + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 21 + cache: sbt + + - name: sbt update + if: matrix.java == 'temurin@21' && steps.setup-java-temurin-21.outputs.cache-hit == 'false' + run: sbt +update + - name: Setup Java (graalvm@17) id: setup-java-graalvm-17 if: matrix.java == 'graalvm@17' @@ -531,6 +575,19 @@ jobs: if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' run: sbt +update + - name: Setup Java (temurin@21) + id: setup-java-temurin-21 + if: matrix.java == 'temurin@21' + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 21 + cache: sbt + + - name: sbt update + if: matrix.java == 'temurin@21' && steps.setup-java-temurin-21.outputs.cache-hit == 'false' + run: sbt +update + - name: Setup Java (graalvm@17) id: setup-java-graalvm-17 if: matrix.java == 'graalvm@17' diff --git a/build.sbt b/build.sbt index 159fa1a10c..ac8744e563 100644 --- a/build.sbt +++ b/build.sbt @@ -135,11 +135,17 @@ ThisBuild / githubWorkflowPublishPreamble += val OldGuardJava = JavaSpec.temurin("8") val LTSJava = JavaSpec.temurin("11") val LatestJava = JavaSpec.temurin("17") +val LoomJava = JavaSpec.temurin("21") val ScalaJSJava = OldGuardJava val ScalaNativeJava = OldGuardJava val GraalVM = JavaSpec.graalvm("17") -ThisBuild / githubWorkflowJavaVersions := Seq(OldGuardJava, LTSJava, LatestJava, GraalVM) +ThisBuild / githubWorkflowJavaVersions := Seq( + OldGuardJava, + LTSJava, + LatestJava, + LoomJava, + GraalVM) ThisBuild / githubWorkflowOSes := Seq(PrimaryOS, Windows, MacOS) ThisBuild / githubWorkflowBuildPreamble ++= Seq( diff --git a/core/js-native/src/main/scala/cats/effect/IOFiberConstants.scala b/core/js-native/src/main/scala/cats/effect/IOFiberConstants.scala index 782fefdfb2..390ea9d895 100644 --- a/core/js-native/src/main/scala/cats/effect/IOFiberConstants.scala +++ b/core/js-native/src/main/scala/cats/effect/IOFiberConstants.scala @@ -43,4 +43,6 @@ private object IOFiberConstants { final val CedeR = 6 final val AutoCedeR = 7 final val DoneR = 8 + + @inline def isVirtualThread(t: Thread): Boolean = false } diff --git a/core/jvm/src/main/java/cats/effect/IOFiberConstants.java b/core/jvm/src/main/java/cats/effect/IOFiberConstants.java index 5951b44745..735f1b5823 100644 --- a/core/jvm/src/main/java/cats/effect/IOFiberConstants.java +++ b/core/jvm/src/main/java/cats/effect/IOFiberConstants.java @@ -16,6 +16,10 @@ package cats.effect; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; + // defined in Java since Scala doesn't let us define static fields final class IOFiberConstants { @@ -43,4 +47,26 @@ final class IOFiberConstants { static final byte CedeR = 6; static final byte AutoCedeR = 7; static final byte DoneR = 8; + + static boolean isVirtualThread(final Thread thread) { + try { + return (boolean) THREAD_IS_VIRTUAL_HANDLE.invokeExact(thread); + } catch (Throwable t) { + return false; + } + } + + private static final MethodHandle THREAD_IS_VIRTUAL_HANDLE; + + static { + final MethodHandles.Lookup lookup = MethodHandles.publicLookup(); + final MethodType mt = MethodType.methodType(boolean.class); + MethodHandle mh; + try { + mh = lookup.findVirtual(Thread.class, "isVirtual", mt); + } catch (Throwable t) { + mh = MethodHandles.dropArguments(MethodHandles.constant(boolean.class, false), 0, Thread.class); + } + THREAD_IS_VIRTUAL_HANDLE = mh; + } } diff --git a/core/shared/src/main/scala/cats/effect/IOFiber.scala b/core/shared/src/main/scala/cats/effect/IOFiber.scala index c082411443..8522dc4d4f 100644 --- a/core/shared/src/main/scala/cats/effect/IOFiber.scala +++ b/core/shared/src/main/scala/cats/effect/IOFiber.scala @@ -996,6 +996,20 @@ private final class IOFiber[A]( } else { blockingFallback(cur) } + } else if (isVirtualThread(Thread.currentThread())) { + var error: Throwable = null + val r = + try { + cur.thunk() + } catch { + case t if NonFatal(t) => + error = t + case t: Throwable => + onFatalFailure(t) + } + + val next = if (error eq null) succeeded(r, 0) else failed(error, 0) + runLoop(next, nextCancelation, nextAutoCede) } else { blockingFallback(cur) } diff --git a/tests/jvm/src/test/scala/cats/effect/DetectPlatform.scala b/tests/jvm/src/test/scala/cats/effect/DetectPlatform.scala index 99ee2660ed..0cd7da54fd 100644 --- a/tests/jvm/src/test/scala/cats/effect/DetectPlatform.scala +++ b/tests/jvm/src/test/scala/cats/effect/DetectPlatform.scala @@ -21,4 +21,7 @@ trait DetectPlatform { def isJS: Boolean = false def isJVM: Boolean = true def isNative: Boolean = false + + def javaMajorVersion: Int = + System.getProperty("java.version").stripPrefix("1.").takeWhile(_.isDigit).toInt } diff --git a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala index f934c3269f..40f3c490ef 100644 --- a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala +++ b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala @@ -32,16 +32,10 @@ import org.specs2.ScalaCheck import scala.concurrent.ExecutionContext import scala.concurrent.duration._ -import java.util.concurrent.{ - CancellationException, - CompletableFuture, - CountDownLatch, - Executors, - ThreadLocalRandom -} +import java.util.concurrent.{CancellationException, CompletableFuture, CountDownLatch, ExecutorService, Executors, ThreadLocalRandom} import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger, AtomicLong, AtomicReference} -trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => +trait IOPlatformSpecification extends DetectPlatform { self: BaseSpec with ScalaCheck => def platformSpecs = { "platform" should { @@ -531,6 +525,22 @@ trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => runtime.shutdown() } } + + if (javaMajorVersion >= 21) + "block in-place on virtual threads" in real { + val loomExec = classOf[Executors] + .getDeclaredMethod("newVirtualThreadPerTaskExecutor") + .invoke(null) + .asInstanceOf[ExecutorService] + + val loomEc = ExecutionContext.fromExecutor(loomExec) + + IO.blocking { + Thread.currentThread().asInstanceOf[{ def isVirtual(): Boolean }].isVirtual() + }.evalOn(loomEc) + } + else + "block in-place on virtual threads" in skipped("virtual threads not supported") } } } From aeda41c9c4c81a4f2429d240a3433a4eed35c77c Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 10 Oct 2023 03:48:27 +0000 Subject: [PATCH 258/429] Try using Oracle 21 instead --- .github/workflows/ci.yml | 44 ++++++++++++++++++++-------------------- build.sbt | 2 +- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8840b50bd9..9bd8d97165 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,20 +33,20 @@ jobs: - temurin@8 - temurin@11 - temurin@17 - - temurin@21 + - oracle@21 - graalvm@17 ci: [ciJVM, ciNative, ciJS, ciFirefox, ciChrome] exclude: - scala: 3.3.1 java: temurin@11 - scala: 3.3.1 - java: temurin@21 + java: oracle@21 - scala: 2.12.18 java: temurin@11 - scala: 2.12.18 java: temurin@17 - scala: 2.12.18 - java: temurin@21 + java: oracle@21 - scala: 2.12.18 java: graalvm@17 - os: windows-latest @@ -74,7 +74,7 @@ jobs: - ci: ciJS java: temurin@17 - ci: ciJS - java: temurin@21 + java: oracle@21 - ci: ciJS java: graalvm@17 - os: windows-latest @@ -86,7 +86,7 @@ jobs: - ci: ciFirefox java: temurin@17 - ci: ciFirefox - java: temurin@21 + java: oracle@21 - ci: ciFirefox java: graalvm@17 - os: windows-latest @@ -98,7 +98,7 @@ jobs: - ci: ciChrome java: temurin@17 - ci: ciChrome - java: temurin@21 + java: oracle@21 - ci: ciChrome java: graalvm@17 - os: windows-latest @@ -110,7 +110,7 @@ jobs: - ci: ciNative java: temurin@17 - ci: ciNative - java: temurin@21 + java: oracle@21 - ci: ciNative java: graalvm@17 - os: windows-latest @@ -175,17 +175,17 @@ jobs: shell: bash run: sbt +update - - name: Setup Java (temurin@21) - id: setup-java-temurin-21 - if: matrix.java == 'temurin@21' + - name: Setup Java (oracle@21) + id: setup-java-oracle-21 + if: matrix.java == 'oracle@21' uses: actions/setup-java@v3 with: - distribution: temurin + distribution: oracle java-version: 21 cache: sbt - name: sbt update - if: matrix.java == 'temurin@21' && steps.setup-java-temurin-21.outputs.cache-hit == 'false' + if: matrix.java == 'oracle@21' && steps.setup-java-oracle-21.outputs.cache-hit == 'false' shell: bash run: sbt +update @@ -355,17 +355,17 @@ jobs: if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' run: sbt +update - - name: Setup Java (temurin@21) - id: setup-java-temurin-21 - if: matrix.java == 'temurin@21' + - name: Setup Java (oracle@21) + id: setup-java-oracle-21 + if: matrix.java == 'oracle@21' uses: actions/setup-java@v3 with: - distribution: temurin + distribution: oracle java-version: 21 cache: sbt - name: sbt update - if: matrix.java == 'temurin@21' && steps.setup-java-temurin-21.outputs.cache-hit == 'false' + if: matrix.java == 'oracle@21' && steps.setup-java-oracle-21.outputs.cache-hit == 'false' run: sbt +update - name: Setup Java (graalvm@17) @@ -575,17 +575,17 @@ jobs: if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' run: sbt +update - - name: Setup Java (temurin@21) - id: setup-java-temurin-21 - if: matrix.java == 'temurin@21' + - name: Setup Java (oracle@21) + id: setup-java-oracle-21 + if: matrix.java == 'oracle@21' uses: actions/setup-java@v3 with: - distribution: temurin + distribution: oracle java-version: 21 cache: sbt - name: sbt update - if: matrix.java == 'temurin@21' && steps.setup-java-temurin-21.outputs.cache-hit == 'false' + if: matrix.java == 'oracle@21' && steps.setup-java-oracle-21.outputs.cache-hit == 'false' run: sbt +update - name: Setup Java (graalvm@17) diff --git a/build.sbt b/build.sbt index ac8744e563..fb52e3328e 100644 --- a/build.sbt +++ b/build.sbt @@ -135,7 +135,7 @@ ThisBuild / githubWorkflowPublishPreamble += val OldGuardJava = JavaSpec.temurin("8") val LTSJava = JavaSpec.temurin("11") val LatestJava = JavaSpec.temurin("17") -val LoomJava = JavaSpec.temurin("21") +val LoomJava = JavaSpec.oracle("21") val ScalaJSJava = OldGuardJava val ScalaNativeJava = OldGuardJava val GraalVM = JavaSpec.graalvm("17") From 85723095c76727801f2b76b6276741c2f5fb9c64 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 10 Oct 2023 03:57:40 +0000 Subject: [PATCH 259/429] Formatting --- .../test/scala/cats/effect/IOPlatformSpecification.scala | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala index 40f3c490ef..324dbe07eb 100644 --- a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala +++ b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala @@ -32,7 +32,14 @@ import org.specs2.ScalaCheck import scala.concurrent.ExecutionContext import scala.concurrent.duration._ -import java.util.concurrent.{CancellationException, CompletableFuture, CountDownLatch, ExecutorService, Executors, ThreadLocalRandom} +import java.util.concurrent.{ + CancellationException, + CompletableFuture, + CountDownLatch, + ExecutorService, + Executors, + ThreadLocalRandom +} import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger, AtomicLong, AtomicReference} trait IOPlatformSpecification extends DetectPlatform { self: BaseSpec with ScalaCheck => From b4061113972a18ee663a04d71ca6f3e1c23065d2 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 10 Oct 2023 04:03:59 +0000 Subject: [PATCH 260/429] More formatting --- core/jvm/src/main/java/cats/effect/IOFiberConstants.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/jvm/src/main/java/cats/effect/IOFiberConstants.java b/core/jvm/src/main/java/cats/effect/IOFiberConstants.java index 735f1b5823..0adf899b2f 100644 --- a/core/jvm/src/main/java/cats/effect/IOFiberConstants.java +++ b/core/jvm/src/main/java/cats/effect/IOFiberConstants.java @@ -65,7 +65,9 @@ static boolean isVirtualThread(final Thread thread) { try { mh = lookup.findVirtual(Thread.class, "isVirtual", mt); } catch (Throwable t) { - mh = MethodHandles.dropArguments(MethodHandles.constant(boolean.class, false), 0, Thread.class); + mh = + MethodHandles.dropArguments( + MethodHandles.constant(boolean.class, false), 0, Thread.class); } THREAD_IS_VIRTUAL_HANDLE = mh; } From 35de2aa2b38565f7cead11c64a29eaaf257b969a Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 10 Oct 2023 05:38:10 +0000 Subject: [PATCH 261/429] Use reflection API in test --- .../src/test/scala/cats/effect/IOPlatformSpecification.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala index 324dbe07eb..a17d5564cc 100644 --- a/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala +++ b/tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala @@ -543,7 +543,10 @@ trait IOPlatformSpecification extends DetectPlatform { self: BaseSpec with Scala val loomEc = ExecutionContext.fromExecutor(loomExec) IO.blocking { - Thread.currentThread().asInstanceOf[{ def isVirtual(): Boolean }].isVirtual() + classOf[Thread] + .getDeclaredMethod("isVirtual") + .invoke(Thread.currentThread()) + .asInstanceOf[Boolean] }.evalOn(loomEc) } else From 3090ba08ec62cd6762c50c08e4ecbef2197d8052 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 10 Oct 2023 06:06:16 +0000 Subject: [PATCH 262/429] Tweak javac linting --- build.sbt | 1 + 1 file changed, 1 insertion(+) diff --git a/build.sbt b/build.sbt index fb52e3328e..d3a7c65563 100644 --- a/build.sbt +++ b/build.sbt @@ -120,6 +120,7 @@ ThisBuild / crossScalaVersions := Seq(Scala3, Scala212, Scala213) ThisBuild / githubWorkflowScalaVersions := crossScalaVersions.value ThisBuild / tlVersionIntroduced := Map("3" -> "3.1.1") ThisBuild / tlJdkRelease := Some(8) +ThisBuild / javacOptions += "-Xlint:-options" ThisBuild / githubWorkflowTargetBranches := Seq("series/3.*") ThisBuild / tlCiReleaseTags := true From 8f9c1edc4269638d37821ccd172b09569c708ada Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 10 Oct 2023 06:22:38 +0000 Subject: [PATCH 263/429] nowarn --- .../src/main/scala/cats/effect/IOFiberConstants.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/js-native/src/main/scala/cats/effect/IOFiberConstants.scala b/core/js-native/src/main/scala/cats/effect/IOFiberConstants.scala index 390ea9d895..3d90575b42 100644 --- a/core/js-native/src/main/scala/cats/effect/IOFiberConstants.scala +++ b/core/js-native/src/main/scala/cats/effect/IOFiberConstants.scala @@ -16,6 +16,8 @@ package cats.effect +import scala.annotation.nowarn + // defined in Java for the JVM, Scala for ScalaJS (where object field access is faster) private object IOFiberConstants { @@ -44,5 +46,6 @@ private object IOFiberConstants { final val AutoCedeR = 7 final val DoneR = 8 + @nowarn @inline def isVirtualThread(t: Thread): Boolean = false } From cdf70248abd09d6669f8cffa120d101d3e106d45 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 10 Oct 2023 06:28:21 +0000 Subject: [PATCH 264/429] workaround unused warning --- .../src/main/scala/cats/effect/IOFiberConstants.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/js-native/src/main/scala/cats/effect/IOFiberConstants.scala b/core/js-native/src/main/scala/cats/effect/IOFiberConstants.scala index 3d90575b42..6d9dcd1c06 100644 --- a/core/js-native/src/main/scala/cats/effect/IOFiberConstants.scala +++ b/core/js-native/src/main/scala/cats/effect/IOFiberConstants.scala @@ -16,8 +16,6 @@ package cats.effect -import scala.annotation.nowarn - // defined in Java for the JVM, Scala for ScalaJS (where object field access is faster) private object IOFiberConstants { @@ -46,6 +44,8 @@ private object IOFiberConstants { final val AutoCedeR = 7 final val DoneR = 8 - @nowarn - @inline def isVirtualThread(t: Thread): Boolean = false + @inline def isVirtualThread(t: Thread): Boolean = { + val _ = t + false + } } From ba4eb08d7d9146315479048afaf3df44019e1400 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 10 Oct 2023 07:13:21 +0000 Subject: [PATCH 265/429] Add JDK 21 to CI matrix --- .github/workflows/ci.yml | 59 ++++++++++++++++++- build.sbt | 9 ++- scalafix/project/plugins.sbt | 2 +- .../src/test/scala/fix/CatsEffectTests.scala | 4 +- .../src/test/scala/fix/CatsEffectTests.scala | 4 +- 5 files changed, 71 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0570dbaf3..9bd8d97165 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,15 +29,24 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] scala: [3.3.1, 2.12.18, 2.13.12] - java: [temurin@8, temurin@11, temurin@17, graalvm@17] + java: + - temurin@8 + - temurin@11 + - temurin@17 + - oracle@21 + - graalvm@17 ci: [ciJVM, ciNative, ciJS, ciFirefox, ciChrome] exclude: - scala: 3.3.1 java: temurin@11 + - scala: 3.3.1 + java: oracle@21 - scala: 2.12.18 java: temurin@11 - scala: 2.12.18 java: temurin@17 + - scala: 2.12.18 + java: oracle@21 - scala: 2.12.18 java: graalvm@17 - os: windows-latest @@ -64,6 +73,8 @@ jobs: java: temurin@11 - ci: ciJS java: temurin@17 + - ci: ciJS + java: oracle@21 - ci: ciJS java: graalvm@17 - os: windows-latest @@ -74,6 +85,8 @@ jobs: java: temurin@11 - ci: ciFirefox java: temurin@17 + - ci: ciFirefox + java: oracle@21 - ci: ciFirefox java: graalvm@17 - os: windows-latest @@ -84,6 +97,8 @@ jobs: java: temurin@11 - ci: ciChrome java: temurin@17 + - ci: ciChrome + java: oracle@21 - ci: ciChrome java: graalvm@17 - os: windows-latest @@ -94,6 +109,8 @@ jobs: java: temurin@11 - ci: ciNative java: temurin@17 + - ci: ciNative + java: oracle@21 - ci: ciNative java: graalvm@17 - os: windows-latest @@ -158,6 +175,20 @@ jobs: shell: bash run: sbt +update + - name: Setup Java (oracle@21) + id: setup-java-oracle-21 + if: matrix.java == 'oracle@21' + uses: actions/setup-java@v3 + with: + distribution: oracle + java-version: 21 + cache: sbt + + - name: sbt update + if: matrix.java == 'oracle@21' && steps.setup-java-oracle-21.outputs.cache-hit == 'false' + shell: bash + run: sbt +update + - name: Setup Java (graalvm@17) id: setup-java-graalvm-17 if: matrix.java == 'graalvm@17' @@ -324,6 +355,19 @@ jobs: if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' run: sbt +update + - name: Setup Java (oracle@21) + id: setup-java-oracle-21 + if: matrix.java == 'oracle@21' + uses: actions/setup-java@v3 + with: + distribution: oracle + java-version: 21 + cache: sbt + + - name: sbt update + if: matrix.java == 'oracle@21' && steps.setup-java-oracle-21.outputs.cache-hit == 'false' + run: sbt +update + - name: Setup Java (graalvm@17) id: setup-java-graalvm-17 if: matrix.java == 'graalvm@17' @@ -531,6 +575,19 @@ jobs: if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' run: sbt +update + - name: Setup Java (oracle@21) + id: setup-java-oracle-21 + if: matrix.java == 'oracle@21' + uses: actions/setup-java@v3 + with: + distribution: oracle + java-version: 21 + cache: sbt + + - name: sbt update + if: matrix.java == 'oracle@21' && steps.setup-java-oracle-21.outputs.cache-hit == 'false' + run: sbt +update + - name: Setup Java (graalvm@17) id: setup-java-graalvm-17 if: matrix.java == 'graalvm@17' diff --git a/build.sbt b/build.sbt index 159fa1a10c..02dce3dac7 100644 --- a/build.sbt +++ b/build.sbt @@ -120,6 +120,7 @@ ThisBuild / crossScalaVersions := Seq(Scala3, Scala212, Scala213) ThisBuild / githubWorkflowScalaVersions := crossScalaVersions.value ThisBuild / tlVersionIntroduced := Map("3" -> "3.1.1") ThisBuild / tlJdkRelease := Some(8) +ThisBuild / javacOptions += "-Xlint:-options" // --release 8 is deprecated on 21 ThisBuild / githubWorkflowTargetBranches := Seq("series/3.*") ThisBuild / tlCiReleaseTags := true @@ -135,11 +136,17 @@ ThisBuild / githubWorkflowPublishPreamble += val OldGuardJava = JavaSpec.temurin("8") val LTSJava = JavaSpec.temurin("11") val LatestJava = JavaSpec.temurin("17") +val LoomJava = JavaSpec.oracle("21") val ScalaJSJava = OldGuardJava val ScalaNativeJava = OldGuardJava val GraalVM = JavaSpec.graalvm("17") -ThisBuild / githubWorkflowJavaVersions := Seq(OldGuardJava, LTSJava, LatestJava, GraalVM) +ThisBuild / githubWorkflowJavaVersions := Seq( + OldGuardJava, + LTSJava, + LatestJava, + LoomJava, + GraalVM) ThisBuild / githubWorkflowOSes := Seq(PrimaryOS, Windows, MacOS) ThisBuild / githubWorkflowBuildPreamble ++= Seq( diff --git a/scalafix/project/plugins.sbt b/scalafix/project/plugins.sbt index 56a53c90c3..00d82eb2cc 100644 --- a/scalafix/project/plugins.sbt +++ b/scalafix/project/plugins.sbt @@ -1 +1 @@ -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.29") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.1") diff --git a/scalafix/v3_0_0/tests/src/test/scala/fix/CatsEffectTests.scala b/scalafix/v3_0_0/tests/src/test/scala/fix/CatsEffectTests.scala index 8447707b34..4d1a178364 100644 --- a/scalafix/v3_0_0/tests/src/test/scala/fix/CatsEffectTests.scala +++ b/scalafix/v3_0_0/tests/src/test/scala/fix/CatsEffectTests.scala @@ -1,8 +1,8 @@ package fix -import org.scalatest.FunSuiteLike +import org.scalatest.funsuite.AnyFunSuiteLike import scalafix.testkit.AbstractSemanticRuleSuite -class CatsEffectTests extends AbstractSemanticRuleSuite with FunSuiteLike { +class CatsEffectTests extends AbstractSemanticRuleSuite with AnyFunSuiteLike { runAllTests() } diff --git a/scalafix/v3_3_0/tests/src/test/scala/fix/CatsEffectTests.scala b/scalafix/v3_3_0/tests/src/test/scala/fix/CatsEffectTests.scala index 8447707b34..4d1a178364 100644 --- a/scalafix/v3_3_0/tests/src/test/scala/fix/CatsEffectTests.scala +++ b/scalafix/v3_3_0/tests/src/test/scala/fix/CatsEffectTests.scala @@ -1,8 +1,8 @@ package fix -import org.scalatest.FunSuiteLike +import org.scalatest.funsuite.AnyFunSuiteLike import scalafix.testkit.AbstractSemanticRuleSuite -class CatsEffectTests extends AbstractSemanticRuleSuite with FunSuiteLike { +class CatsEffectTests extends AbstractSemanticRuleSuite with AnyFunSuiteLike { runAllTests() } From bc0dab12f3879e108fbe0eb57074c04c3d00d474 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 10 Oct 2023 09:02:00 +0000 Subject: [PATCH 266/429] Another attempt to nowarn --- .../src/main/scala/cats/effect/IOFiberConstants.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/js-native/src/main/scala/cats/effect/IOFiberConstants.scala b/core/js-native/src/main/scala/cats/effect/IOFiberConstants.scala index 6d9dcd1c06..b389cddae8 100644 --- a/core/js-native/src/main/scala/cats/effect/IOFiberConstants.scala +++ b/core/js-native/src/main/scala/cats/effect/IOFiberConstants.scala @@ -16,6 +16,8 @@ package cats.effect +import org.typelevel.scalaccompat.annotation._ + // defined in Java for the JVM, Scala for ScalaJS (where object field access is faster) private object IOFiberConstants { @@ -44,8 +46,6 @@ private object IOFiberConstants { final val AutoCedeR = 7 final val DoneR = 8 - @inline def isVirtualThread(t: Thread): Boolean = { - val _ = t - false - } + @nowarn212 + @inline def isVirtualThread(t: Thread): Boolean = false } From 3f59414c83b75d0885045f8fa8feae5f5754b7b3 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 10 Oct 2023 12:06:19 +0000 Subject: [PATCH 267/429] Update sbt-mdoc to 2.3.8 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 43e912aea1..d70d9f4dab 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,7 +6,7 @@ addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.15") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.6") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.7") +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.8") addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.1") From 7b0fdb611d4be1804434a7f9eca6ee4919412871 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 12 Oct 2023 17:57:31 +0000 Subject: [PATCH 268/429] Swap Oracle JDK 21 for Temurin JDK 21 --- .github/workflows/ci.yml | 44 ++++++++++++++++++++-------------------- build.sbt | 2 +- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9bd8d97165..8840b50bd9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,20 +33,20 @@ jobs: - temurin@8 - temurin@11 - temurin@17 - - oracle@21 + - temurin@21 - graalvm@17 ci: [ciJVM, ciNative, ciJS, ciFirefox, ciChrome] exclude: - scala: 3.3.1 java: temurin@11 - scala: 3.3.1 - java: oracle@21 + java: temurin@21 - scala: 2.12.18 java: temurin@11 - scala: 2.12.18 java: temurin@17 - scala: 2.12.18 - java: oracle@21 + java: temurin@21 - scala: 2.12.18 java: graalvm@17 - os: windows-latest @@ -74,7 +74,7 @@ jobs: - ci: ciJS java: temurin@17 - ci: ciJS - java: oracle@21 + java: temurin@21 - ci: ciJS java: graalvm@17 - os: windows-latest @@ -86,7 +86,7 @@ jobs: - ci: ciFirefox java: temurin@17 - ci: ciFirefox - java: oracle@21 + java: temurin@21 - ci: ciFirefox java: graalvm@17 - os: windows-latest @@ -98,7 +98,7 @@ jobs: - ci: ciChrome java: temurin@17 - ci: ciChrome - java: oracle@21 + java: temurin@21 - ci: ciChrome java: graalvm@17 - os: windows-latest @@ -110,7 +110,7 @@ jobs: - ci: ciNative java: temurin@17 - ci: ciNative - java: oracle@21 + java: temurin@21 - ci: ciNative java: graalvm@17 - os: windows-latest @@ -175,17 +175,17 @@ jobs: shell: bash run: sbt +update - - name: Setup Java (oracle@21) - id: setup-java-oracle-21 - if: matrix.java == 'oracle@21' + - name: Setup Java (temurin@21) + id: setup-java-temurin-21 + if: matrix.java == 'temurin@21' uses: actions/setup-java@v3 with: - distribution: oracle + distribution: temurin java-version: 21 cache: sbt - name: sbt update - if: matrix.java == 'oracle@21' && steps.setup-java-oracle-21.outputs.cache-hit == 'false' + if: matrix.java == 'temurin@21' && steps.setup-java-temurin-21.outputs.cache-hit == 'false' shell: bash run: sbt +update @@ -355,17 +355,17 @@ jobs: if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' run: sbt +update - - name: Setup Java (oracle@21) - id: setup-java-oracle-21 - if: matrix.java == 'oracle@21' + - name: Setup Java (temurin@21) + id: setup-java-temurin-21 + if: matrix.java == 'temurin@21' uses: actions/setup-java@v3 with: - distribution: oracle + distribution: temurin java-version: 21 cache: sbt - name: sbt update - if: matrix.java == 'oracle@21' && steps.setup-java-oracle-21.outputs.cache-hit == 'false' + if: matrix.java == 'temurin@21' && steps.setup-java-temurin-21.outputs.cache-hit == 'false' run: sbt +update - name: Setup Java (graalvm@17) @@ -575,17 +575,17 @@ jobs: if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false' run: sbt +update - - name: Setup Java (oracle@21) - id: setup-java-oracle-21 - if: matrix.java == 'oracle@21' + - name: Setup Java (temurin@21) + id: setup-java-temurin-21 + if: matrix.java == 'temurin@21' uses: actions/setup-java@v3 with: - distribution: oracle + distribution: temurin java-version: 21 cache: sbt - name: sbt update - if: matrix.java == 'oracle@21' && steps.setup-java-oracle-21.outputs.cache-hit == 'false' + if: matrix.java == 'temurin@21' && steps.setup-java-temurin-21.outputs.cache-hit == 'false' run: sbt +update - name: Setup Java (graalvm@17) diff --git a/build.sbt b/build.sbt index 02dce3dac7..5426a613f8 100644 --- a/build.sbt +++ b/build.sbt @@ -136,7 +136,7 @@ ThisBuild / githubWorkflowPublishPreamble += val OldGuardJava = JavaSpec.temurin("8") val LTSJava = JavaSpec.temurin("11") val LatestJava = JavaSpec.temurin("17") -val LoomJava = JavaSpec.oracle("21") +val LoomJava = JavaSpec.temurin("21") val ScalaJSJava = OldGuardJava val ScalaNativeJava = OldGuardJava val GraalVM = JavaSpec.graalvm("17") From 5baa114bc602f23eed3a094048534cbf34a9543b Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Sat, 14 Oct 2023 00:14:10 +0000 Subject: [PATCH 269/429] Update nscplugin, sbt-scala-native, ... to 0.4.16 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 43e912aea1..3a7969b3c6 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -3,7 +3,7 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.4") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.15") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.16") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.6") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.7") From 120af74fd05c79f5cd9d3dee3808e92899bf78fd Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 08:11:41 +0000 Subject: [PATCH 270/429] Update sbt-typelevel to 0.6.0 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index e28e32d394..1e45fc9ff5 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.4") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.6.0") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.16") From 05920cce0907570ccf4252a05d88b33eaf91117f Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 12:07:36 +0000 Subject: [PATCH 271/429] Update sbt-mdoc to 2.4.0 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index e28e32d394..2894d0ab16 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,7 +6,7 @@ addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.16") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.6") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.8") +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.4.0") addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.1") From 40496010a97bc2a3d06c765a78af1f855a86e0d3 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 00:18:46 +0000 Subject: [PATCH 272/429] Update sbt to 1.9.7 in series/3.x --- project/build.properties | 2 +- scalafix/project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project/build.properties b/project/build.properties index 27430827bc..e8a1e246e8 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.6 +sbt.version=1.9.7 diff --git a/scalafix/project/build.properties b/scalafix/project/build.properties index 27430827bc..e8a1e246e8 100644 --- a/scalafix/project/build.properties +++ b/scalafix/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.6 +sbt.version=1.9.7 From 65d9f119cc95fae9931041b48517c457b7ef8628 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 12:08:59 +0000 Subject: [PATCH 273/429] Update scalafmt-core to 3.7.15 in series/3.x --- .scalafmt.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index 4f413283ab..dde2f7133d 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = 3.7.14 +version = 3.7.15 runner.dialect = Scala213Source3 fileOverride { From 4727501419a8ba00bce36687a429081b158ea683 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Sun, 5 Nov 2023 16:04:40 +0000 Subject: [PATCH 274/429] Update specs2-core, specs2-scalacheck to 4.20.3 in series/3.x --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 5426a613f8..1a0ace69ef 100644 --- a/build.sbt +++ b/build.sbt @@ -309,7 +309,7 @@ ThisBuild / apiURL := Some(url("https://typelevel.org/cats-effect/api/3.x/")) ThisBuild / autoAPIMappings := true val CatsVersion = "2.10.0" -val Specs2Version = "4.20.2" +val Specs2Version = "4.20.3" val ScalaCheckVersion = "1.17.0" val DisciplineVersion = "1.4.0" val CoopVersion = "1.2.0" From 2e9c799a1189524584284d19d5cccb38aac0cbeb Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Fri, 10 Nov 2023 20:04:19 +0000 Subject: [PATCH 275/429] Update sbt-mdoc to 2.5.1 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 2af8e502fd..41d6f47287 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,7 +6,7 @@ addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.16") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.6") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.4.0") +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.1") addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.1") From 82a605eacf7d2c80447fb4109119f0d8df309a26 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 08:11:15 +0000 Subject: [PATCH 276/429] Update sbt-typelevel to 0.6.2 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 2af8e502fd..ddd301a381 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.6.0") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.6.2") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.16") From 23337d6854bfeba416c03996ba706211bce94065 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Thu, 16 Nov 2023 20:06:26 +0000 Subject: [PATCH 277/429] Update scalafmt-core to 3.7.17 in series/3.x --- .scalafmt.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index dde2f7133d..90853fc664 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = 3.7.15 +version = 3.7.17 runner.dialect = Scala213Source3 fileOverride { From 0f4d42b0c46f684233d3580fe7998560400ccde7 Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Thu, 16 Nov 2023 22:47:13 +0100 Subject: [PATCH 278/429] Remove an old comment We do have `replicateA_` for cats now. --- core/shared/src/main/scala/cats/effect/IO.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/core/shared/src/main/scala/cats/effect/IO.scala b/core/shared/src/main/scala/cats/effect/IO.scala index 4e08af3a77..e235df8d4d 100644 --- a/core/shared/src/main/scala/cats/effect/IO.scala +++ b/core/shared/src/main/scala/cats/effect/IO.scala @@ -669,7 +669,6 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] { else flatMap(a => replicateA(n - 1).map(a :: _)) - // TODO PR to cats def replicateA_(n: Int): IO[Unit] = if (n <= 0) IO.unit From 6631be1dc9f22057d244502f59c31fe98db2e146 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 21 Nov 2023 07:48:39 +0000 Subject: [PATCH 279/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/91bcfa0f1d3788371c07a9aa8df8ad5800eaebd8' (2023-09-05) → 'github:typelevel/typelevel-nix/66232545cc967ff9d4d5e1805d8dbbc621a3c2e0' (2023-11-20) • Updated input 'typelevel-nix/devshell': 'github:numtide/devshell/2aa26972b951bc05c3632d4e5ae683cb6771a7c6' (2023-08-23) → 'github:numtide/devshell/1aed986e3c81a4f6698e85a7452cbfcc4b31a36e' (2023-10-27) • Updated input 'typelevel-nix/flake-utils': 'github:numtide/flake-utils/f9e7cf818399d17d347f847525c5a5a8032e4e44' (2023-08-23) → 'github:numtide/flake-utils/ff7b65b44d01cf9ba6a71320833626af21126384' (2023-09-12) • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/d816b5ab44187a2dd84806630ce77a733724f95f' (2023-09-03) → 'github:nixos/nixpkgs/0bf3f5cf6a98b5d077cdcdb00a6d4b3d92bc78b5' (2023-11-19) --- flake.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/flake.lock b/flake.lock index 21bd031b0c..5808e7b1f1 100644 --- a/flake.lock +++ b/flake.lock @@ -6,11 +6,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1692793255, - "narHash": "sha256-yVyj0AE280JkccDHuG1XO9oGxN6bW8ksr/xttXcXzK0=", + "lastModified": 1698410321, + "narHash": "sha256-MphuSlgpmKwtJncGMohryHiK55J1n6WzVQ/OAfmfoMc=", "owner": "numtide", "repo": "devshell", - "rev": "2aa26972b951bc05c3632d4e5ae683cb6771a7c6", + "rev": "1aed986e3c81a4f6698e85a7452cbfcc4b31a36e", "type": "github" }, "original": { @@ -24,11 +24,11 @@ "systems": "systems_2" }, "locked": { - "lastModified": 1692799911, - "narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=", + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", "owner": "numtide", "repo": "flake-utils", - "rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", "type": "github" }, "original": { @@ -55,11 +55,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1693714546, - "narHash": "sha256-3EMJZeGSZT6pD1eNwI/6Yc0R4rxklNvJ2SDFcsCnjpM=", + "lastModified": 1700416016, + "narHash": "sha256-Qp8Of0BUYGjqodmE912h+/uGknB7J11ypcQMKnEDUrg=", "owner": "nixos", "repo": "nixpkgs", - "rev": "d816b5ab44187a2dd84806630ce77a733724f95f", + "rev": "0bf3f5cf6a98b5d077cdcdb00a6d4b3d92bc78b5", "type": "github" }, "original": { @@ -119,11 +119,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1693881189, - "narHash": "sha256-thSMcpHW8Lw4q5oP6h9d4jcUZi5ev56gijhWbqaUMMU=", + "lastModified": 1700513164, + "narHash": "sha256-/9j5Cyo73LUUPilA5ml8tx1Q5isHHLwPUckRMtqWsxE=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "91bcfa0f1d3788371c07a9aa8df8ad5800eaebd8", + "rev": "66232545cc967ff9d4d5e1805d8dbbc621a3c2e0", "type": "github" }, "original": { From aa41a474fdeb98676338a54ff3cb40ff1da6dbda Mon Sep 17 00:00:00 2001 From: Luis Rodero-Merino Date: Wed, 26 Jul 2023 20:43:04 +0200 Subject: [PATCH 280/429] Changed tutorial so consumers are not overwhelmed by producers --- docs/tutorial.md | 302 ++++++++++++++++++++++++++++------------------- 1 file changed, 179 insertions(+), 123 deletions(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index ffdf9c3e87..ad7f2b7deb 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -44,7 +44,7 @@ name := "cats-effect-tutorial" version := "3.5.2" -scalaVersion := "2.13.6" +scalaVersion := "2.13.11" libraryDependencies += "org.typelevel" %% "cats-effect" % "3.5.2" withSources() withJavadoc() @@ -56,11 +56,11 @@ scalacOptions ++= Seq( ) ``` -Also make sure that you use a recent version of `sbt`, at least `1.4.2`. You can -set the `sbt` version in `project/build.properties` file: +Also make sure that you use a recent version of `sbt`, _e.g._ `1.9.7`. You can set +the `sbt` version in `project/build.properties` file: ```scala -sbt.version=1.4.2 +sbt.version=1.9.7 ``` Almost all code snippets in this tutorial can be pasted and compiled right in @@ -162,7 +162,7 @@ import cats.effect.{IO, Resource} import java.io.{File, FileInputStream} def inputStream(f: File): Resource[IO, FileInputStream] = - Resource.fromAutoCloseable(IO(new FileInputStream(f))) + Resource.fromAutoCloseable(IO.blocking(new FileInputStream(f))) ``` That code is way simpler, but with that code we would not have control over what @@ -181,11 +181,11 @@ import java.io._ def inputOutputStreams(in: File, out: File): Resource[IO, (InputStream, OutputStream)] = ??? // transfer will do the real work -def transfer(origin: InputStream, destination: OutputStream): IO[Long] = ??? +def transfer(origin: InputStream, destination: OutputStream, buffer: Array[Byte], acc: Long): IO[Long] = ??? def copy(origin: File, destination: File): IO[Long] = inputOutputStreams(origin, destination).use { case (in, out) => - transfer(in, out) + transfer(in, out, new Array[Byte](1024 * 10), 0) } ``` @@ -220,17 +220,17 @@ import java.io._ // function inputOutputStreams not needed // transfer will do the real work -def transfer(origin: InputStream, destination: OutputStream): IO[Long] = ??? +def transfer(origin: InputStream, destination: OutputStream, buffer: Array[Byte], acc: Long): IO[Long] = ??? def copy(origin: File, destination: File): IO[Long] = { - val inIO: IO[InputStream] = IO(new FileInputStream(origin)) - val outIO:IO[OutputStream] = IO(new FileOutputStream(destination)) + val inIO: IO[InputStream] = IO.blocking(new FileInputStream(origin)) + val outIO:IO[OutputStream] = IO.blocking(new FileOutputStream(destination)) (inIO, outIO) // Stage 1: Getting resources .tupled // From (IO[InputStream], IO[OutputStream]) to IO[(InputStream, OutputStream)] .bracket{ case (in, out) => - transfer(in, out) // Stage 2: Using resources (for copying data, in this case) + transfer(in, out, new Array[Byte](1024 * 10), 0L) // Stage 2: Using resources (for copying data, in this case) } { case (in, out) => // Stage 3: Freeing resources (IO(in.close()), IO(out.close())) @@ -261,28 +261,22 @@ Finally we have our streams ready to go! We have to focus now on coding `transfer`. That function will have to define a loop that at each iteration reads data from the input stream into a buffer, and then writes the buffer contents into the output stream. At the same time, the loop will keep a counter -of the bytes transferred. To reuse the same buffer we should define it outside -the main loop, and leave the actual transmission of data to another function -`transmit` that uses that loop. Something like: +of the bytes transferred. ```scala mdoc:compile-only import cats.effect.IO -import cats.syntax.all._ import java.io._ -def transmit(origin: InputStream, destination: OutputStream, buffer: Array[Byte], acc: Long): IO[Long] = +def transfer(origin: InputStream, destination: OutputStream, buffer: Array[Byte], acc: Long): IO[Long] = for { - amount <- IO.blocking(origin.read(buffer, 0, buffer.size)) - count <- if(amount > -1) IO.blocking(destination.write(buffer, 0, amount)) >> transmit(origin, destination, buffer, acc + amount) - else IO.pure(acc) // End of read stream reached (by java.io.InputStream contract), nothing to write - } yield count // Returns the actual amount of bytes transmitted - -def transfer(origin: InputStream, destination: OutputStream): IO[Long] = - transmit(origin, destination, new Array[Byte](1024 * 10), 0L) + amount <- IO.blocking(origin.read(buffer, 0, buffer.length)) + count <- if (amount > -1) IO.blocking(destination.write(buffer, 0, amount)) >> transfer(origin, destination, buffer, acc + amount) + else IO.pure(acc) // End of read stream reached (by java.io.InputStream contract), nothing to write + } yield count // Returns the actual amount of bytes transferred ``` -Take a look at `transmit`, observe that both input and output actions are -created by invoking `IO.blocking` which return the actions encapsulated in a +Take a look at `transfer`, observe that both input and output actions are +created by invoking `IO.blocking` which return the actions encapsulated in (suspended in) `IO`. We can also just embed the actions by calling `IO(action)`, but when dealing with input/output actions it is advised that you instead use `IO.blocking(action)`. This way we help cats-effect to better plan how to assign @@ -295,7 +289,7 @@ the call to `read()` does not return a negative value that would signal that the end of the stream has been reached. `>>` is a Cats operator to sequence two operations where the output of the first is not needed by the second (_i.e._ it is equivalent to `first.flatMap(_ => second)`). In the code above that means -that after each write operation we recursively call `transmit` again, but as +that after each write operation we recursively call `transfer` again, but as `IO` is stack safe we are not concerned about stack overflow issues. At each iteration we increase the counter `acc` with the amount of bytes read at that iteration. @@ -309,7 +303,7 @@ cancelation in the next section. ### Dealing with cancelation Cancelation is a powerful but non-trivial cats-effect feature. In cats-effect, -some `IO` instances can be canceled ( _e.g._ by other `IO` instances running +some `IO` instances can be canceled (_e.g._ by other `IO` instances running concurrently) meaning that their evaluation will be aborted. If the programmer is careful, an alternative `IO` task will be run under cancelation, for example to deal with potential cleaning up activities. @@ -318,7 +312,7 @@ Thankfully, `Resource` makes dealing with cancelation an easy task. If the `IO` inside a `Resource.use` is canceled, the release section of that resource is run. In our example this means the input and output streams will be properly closed. Also, cats-effect does not cancel code inside `IO.blocking` instances. -In the case of our `transmit` function this means the execution would be +In the case of our `transfer` function this means the execution would be interrupted only between two calls to `IO.blocking`. If we want the execution of an IO instance to be interrupted when canceled, without waiting for it to finish, we must instantiate it using `IO.interruptible`. @@ -351,8 +345,7 @@ object Main extends IOApp { override def run(args: List[String]): IO[ExitCode] = for { - _ <- if(args.length < 2) IO.raiseError(new IllegalArgumentException("Need origin and destination files")) - else IO.unit + _ <- IO.raiseWhen(args.length < 2)(new IllegalArgumentException("Need origin and destination files")) orig = new File(args(0)) dest = new File(args(1)) count <- copy(orig, dest) @@ -363,8 +356,9 @@ object Main extends IOApp { Heed how `run` verifies the `args` list passed. If there are fewer than two arguments, an error is raised. As `IO` implements `MonadError` we can at any -moment call to `IO.raiseError` to interrupt a sequence of `IO` operations. The log -message is printed by means of handy `IO.println` method. +moment call to `IO.raiseWhen` or `IO.raiseError` to interrupt a sequence of `IO` +operations. The log message is printed by means of the handy `IO.println` +method. #### Copy program code You can check the [final version of our copy program @@ -373,7 +367,7 @@ here](https://github.com/lrodero/cats-effect-tutorial/blob/series/3.x/src/main/s The program can be run from `sbt` just by issuing this call: ```scala -> runMain catseffecttutorial.CopyFile origin.txt destination.txt +> runMain catseffecttutorial.copyfile.CopyFile origin.txt destination.txt ``` It can be argued that using `IO{java.nio.file.Files.copy(...)}` would get an @@ -407,23 +401,22 @@ import cats.effect.Sync import cats.syntax.all._ import java.io._ -def transmit[F[_]: Sync](origin: InputStream, destination: OutputStream, buffer: Array[Byte], acc: Long): F[Long] = +def transfer[F[_]: Sync](origin: InputStream, destination: OutputStream, buffer: Array[Byte], acc: Long): F[Long] = for { amount <- Sync[F].blocking(origin.read(buffer, 0, buffer.length)) - count <- if(amount > -1) Sync[F].blocking(destination.write(buffer, 0, amount)) >> transmit(origin, destination, buffer, acc + amount) + count <- if(amount > -1) Sync[F].blocking(destination.write(buffer, 0, amount)) >> transfer(origin, destination, buffer, acc + amount) else Sync[F].pure(acc) // End of read stream reached (by java.io.InputStream contract), nothing to write - } yield count // Returns the actual amount of bytes transmitted + } yield count // Returns the actual amount of bytes transferred ``` We leave as an exercise to code the polymorphic versions of `inputStream`, -`outputStream`, `inputOutputStreams`, `transfer` and `copy` functions. +`outputStream`, `inputOutputStreams` and `copy` functions. ```scala mdoc:compile-only import cats.effect._ import java.io._ -def transmit[F[_]: Sync](origin: InputStream, destination: OutputStream, buffer: Array[Byte], acc: Long): F[Long] = ??? -def transfer[F[_]: Sync](origin: InputStream, destination: OutputStream): F[Long] = ??? +def transfer[F[_]: Sync](origin: InputStream, destination: OutputStream, buffer: Array[Byte], acc: Long): F[Long] = ??? def inputStream[F[_]: Sync](f: File): Resource[F, FileInputStream] = ??? def outputStream[F[_]: Sync](f: File): Resource[F, FileOutputStream] = ??? def inputOutputStreams[F[_]: Sync](in: File, out: File): Resource[F, (InputStream, OutputStream)] = ??? @@ -452,18 +445,16 @@ your IO-kungfu: 1. Modify the `IOApp` so it shows an error and abort the execution if the origin and destination files are the same, the origin file cannot be open for - reading or the destination file cannot be opened for writing. Also, if the + reading, or the destination file cannot be opened for writing. Also, if the destination file already exists, the program should ask for confirmation before overwriting that file. -2. Modify `transmit` so the buffer size is not hardcoded but passed as - parameter. -3. Test safe cancelation, checking that the streams are indeed being properly +2. Test safe cancelation, checking that the streams are indeed being properly closed. You can do that just by interrupting the program execution pressing `Ctrl-c`. To make sure you have the time to interrupt the program, introduce - a delay of a few seconds in the `transmit` function (see `IO.sleep`). And to + a delay of a few seconds in the `transfer` function (see `IO.sleep`). And to ensure that the release functionality in the `Resource`s is run you can add some log message there (see `IO.println`). -4. Create a new program able to copy folders. If the origin folder has +3. Create a new program able to copy folders. If the origin folder has subfolders, then their contents must be recursively copied too. Of course the copying must be safely cancelable at any moment. @@ -509,23 +500,26 @@ info as a hint to optimize `IO` scheduling. Another difference with threads is that fibers are very cheap entities. We can spawn millions of them at ease without impacting the performance. -A worthy note is that you do not have to explicitly shut down fibers. If you spawn -a fiber and it finishes actively running its `IO` it will get cleaned up by the -garbage collector unless there is some other active memory reference to it. So basically -you can treat a fiber as any other regular object, except that when the fiber is _running_ -(present tense), the cats-effect runtime itself keeps the fiber alive. - -This has some interesting implications as well. Like if you create an `IO.async` node and -register the callback with something, and you're in a Fiber which has no strong object -references anywhere else (i.e. you did some sort of fire-and-forget thing), then the callback -itself is the only strong reference to the fiber. Meaning if the registration fails or the -system you registered with throws it away, the fiber will just gracefully disappear. - -Cats-effect implements some concurrency primitives to coordinate concurrent -fibers: [Deferred](std/deferred.md), [Ref](std/ref.md), `Semaphore`... - -Way more detailed info about concurrency in cats-effect can be found in [this -other tutorial 'Concurrency in Scala with +A worthy note is that you do not have to explicitly shut down fibers. If you +spawn a fiber and it finishes actively running its `IO` it will get cleaned up +by the garbage collector unless there is some other active memory reference to +it. So basically you can treat a fiber as any other regular object, except that +when the fiber is _running_ (present tense), the cats-effect runtime itself +keeps the fiber alive. + +This has some interesting implications as well. Like if you create an `IO.async` +node and register the callback with something, and you're in a fiber which has +no strong object references anywhere else (i.e. you did some sort of +fire-and-forget thing), then the callback itself is the only strong reference to +the fiber. Meaning if the registration fails or the system you registered with +throws it away, the fiber will just gracefully disappear. + +And a final hint: as with threads, often you will need to coordinate the work of +concurrent fibers. Writing concurrent code is a difficult exercise, but +cats-effect implements some concurrency primitives such as +[Deferred](std/deferred.md), [Ref](std/ref.md), [Semaphore](std/semaphore.md)... +that will help you in that task. Way more detailed info about concurrency in +cats-effect can be found in [this other tutorial 'Concurrency in Scala with Cats-Effect'](https://github.com/slouc/concurrency-in-scala-with-ce). Ok, now we have briefly discussed fibers we can start working on our @@ -539,33 +533,34 @@ integers (`1`, `2`, `3`...), consumer will just read that sequence. Our shared queue will be an instance of an immutable `Queue[Int]`. Accesses to the queue can (and will!) be concurrent, thus we need some way to -protect the queue so only one fiber at a time is handling it. The best way to -ensure an ordered access to some shared data is [Ref](std/ref.md). A -`Ref` instance wraps some given data and implements methods to manipulate that -data in a safe manner. When some fiber is runnning one of those methods, any -other call to any method of the `Ref` instance will be blocked. +protect the queue so only one fiber at a time is handling it. A good way to +ensure an ordered access to some shared data is [Ref](std/ref.md). A `Ref` +instance wraps some given data and implements methods to manipulate that data in +a safe manner. The `Ref` wrapping our queue will be `Ref[F, Queue[Int]]` (for some `F[_]`). Now, our `producer` method will be: - ```scala mdoc:compile-only import cats.effect._ import cats.effect.std.Console import cats.syntax.all._ -import collection.immutable.Queue +import scala.collection.immutable.Queue def producer[F[_]: Sync: Console](queueR: Ref[F, Queue[Int]], counter: Int): F[Unit] = for { - _ <- if(counter % 10000 == 0) Console[F].println(s"Produced $counter items") else Sync[F].unit + _ <- Sync[F].whenA(counter % 10000 == 0)(Console[F].println(s"Produced $counter items")) _ <- queueR.getAndUpdate(_.enqueue(counter + 1)) _ <- producer(queueR, counter + 1) } yield () ``` -First line just prints some log message every `10000` items, so we know if it is -'alive'. It uses type class `Console[_]`, which brings the capacity to print -and read strings (`IO.println` just uses `Console[IO].println` underneath). +First line just prints some log message every `10000` items, so we know if our +producer is still 'alive'. We can as well do `if(cond) then Console... else +Sync[F].unit` but this approach is more idiomatic. To print logs the code uses +type class `Console[_]`, which brings the capacity to print and read strings +(the `IO.println` call we used before just invokes `Console[IO].println` under +the hood). Then our code calls `queueR.getAndUpdate` to add data into the queue. Note that `.getAndUpdate` provides the current queue, then we use `.enqueue` to @@ -580,14 +575,14 @@ queue but it must be aware that the queue can be empty: import cats.effect._ import cats.effect.std.Console import cats.syntax.all._ -import collection.immutable.Queue +import scala.collection.immutable.Queue def consumer[F[_]: Sync: Console](queueR: Ref[F, Queue[Int]]): F[Unit] = for { iO <- queueR.modify{ queue => queue.dequeueOption.fold((queue, Option.empty[Int])){case (i,queue) => (queue, Option(i))} } - _ <- if(iO.exists(_ % 10000 == 0)) Console[F].println(s"Consumed ${iO.get} items") else Sync[F].unit + _ <- Sync[F].whenA(iO.exists(_ % 10000 == 0))(Console[F].println(s"Consumed ${iO.get} items")) _ <- consumer(queueR) } yield () ``` @@ -605,7 +600,7 @@ We can now create a program that instantiates our `queueR` and runs both import cats.effect._ import cats.effect.std.Console import cats.syntax.all._ -import collection.immutable.Queue +import scala.collection.immutable.Queue object InefficientProducerConsumer extends IOApp { @@ -625,14 +620,11 @@ object InefficientProducerConsumer extends IOApp { } ``` -The full implementation of this naive producer consumer is available -[here](https://github.com/lrodero/cats-effect-tutorial/blob/series/3.x/src/main/scala/catseffecttutorial/producerconsumer/InefficientProducerConsumer.scala). - Our `run` function instantiates the shared queue wrapped in a `Ref` and boots the producer and consumer in parallel. To do to it uses `parMapN`, that creates and runs the fibers that will run the `IO`s passed as parameter. Then it takes the output of each fiber and applies a given function to them. In our case -both producer and consumer shall run forever until user presses CTRL-C which +both producer and consumer shall run forever until the user presses CTRL-C which will trigger a cancelation. Alternatively we could have used `start` method to explicitly create new @@ -641,7 +633,7 @@ wait for them to finish, something like: ```scala mdoc:compile-only import cats.effect._ -import collection.immutable.Queue +import scala.collection.immutable.Queue object InefficientProducerConsumer extends IOApp { @@ -668,7 +660,7 @@ happened. Cats Effect provides additional `joinWith` or `joinWithNever` methods to make sure at least that the error is raised with the usual `MonadError` semantics -(e.g., short-circuiting). Now that we are raising the error, we also need to +(_i.e._, short-circuiting). Now that we are raising the error, we also need to cancel the other running fibers. We can easily get ourselves trapped in a tangled mess of fibers to keep an eye on. On top of that the error raised by a fiber is not promoted until the call to `joinWith` or `.joinWithNever` is @@ -685,29 +677,87 @@ have some specific and unusual requirements you should prefer to use higher level commands such as `parMapN` or `parSequence` to work with fibers_. Ok, we stick to our implementation based on `.parMapN`. Are we done? Does it -Work? Well, it works... but it is far from ideal. If we run it we will find that -the producer runs faster than the consumer so the queue is constantly growing. -And, even if that was not the case, we must realize that the consumer will be -continually running regardless if there are elements in the queue, which is far -from ideal. We will try to improve it in the next section by using -[Deferred](std/deferred.md). Also we will use several consumers and -producers to balance production and consumption rate. +Work? Well, it works... but it is far from ideal. + +#### Issue 1: the producer outpaces the consumer +Now, if you run the program you will notice that almost no consumer logs are +shown, if any. This is a signal that the producer is running way faster than +the consumer. And why is that? Well, this is because how `Ref.modify` works. It +gets the current value, then it computes the update, and finally it tries to set +the new value if the current one has not been changed (by some other fiber), +otherwise it starts from the beginning. Unfortunately the producer is way faster +running its `queueR.getAndUpdate` call than the consumer is running its +`queueR.modify` call. So the consumer gets 'stuck' trying once and again to +update the `queueR` content. + +Can we alleviate this? Sure! There are a few options you can implement: +1. Making the producer artifically slower by introducing a call to `Async[F].sleep` + (_e.g._ for 1 microsecond). Truth is, in real world scenarios a producer will + not be as fast as in our example so this tweak is not that 'strange'. Note that + to be able to use `sleep` now `F` requires an implicit `Async[F]` instance. The + new producer will look like this: + ```scala mdoc:compile-only + import cats.effect._ + import cats.effect.std.Console + import cats.syntax.all._ + import scala.collection.immutable.Queue + import scala.concurrent.duration.DurationInt + + def producer[F[_]: Async: Console](queueR: Ref[F, Queue[Int]], counter: Int): F[Unit] = + for { + _ <- Async[F].whenA(counter % 10000 == 0)(Console[F].println(s"Produced $counter items")) + _ <- Async[F].sleep(1.microsecond) // To prevent overwhelming consumers + _ <- queueR.getAndUpdate(_.enqueue(counter + 1)) + _ <- producer(queueR, counter + 1) + } yield () + ``` +2. Replace `Ref` with `AtomicCell` to keep the `Queue` instance. `AtomicCell`, + as `Ref`, is a concurrent data structure to keep a reference to some data. + But unlike `Ref` it ensures that only one fiber can operate on that reference + at any given time. Thus the consumer won't have to try once and again to modify + its content. Otoh `AtomicCell` is slower than `Ref`. This is because `Ref` is + nonblocking while `AtomicCell` will block calling fibers to ensure only one + operates on its content. +3. Make the queue bound by size so producers are forced to wait for consumers to + extract data when the queue is full. We will do this later on in Section + [Producer consumer with bounded +queue](#producer-consumer-with-bounded-queue). + +By the way, you may be tempted to speed up the `queueR.modify` call in the +consumer by using a mutable `Queue` instance. Do not! `Ref` _must_ be used only +with immutable data. + +#### Issue 2: consumer runs even if there are no elements in the queue +The consumer will be continually running regardless if there are elements in the +queue, which is far from ideal. If we have several consumers competing for the +data the problem gets even worse. We will address this problem in the next +section by using [Deferred](std/deferred.md). + +The full implementation of the naive producer consumer we have just created in +this section is available +[here](https://github.com/lrodero/cats-effect-tutorial/blob/series/3.x/src/main/scala/catseffecttutorial/producerconsumer/InefficientProducerConsumer.scala). ### A more solid implementation of the producer/consumer problem In our producer/consumer code we already protect access to the queue (our shared resource) using a `Ref`. Now, instead of using `Option` to represent elements retrieved from a possibly empty queue, we should instead block the caller fiber -somehow if queue is empty until some element can be returned. This will be done -by creating and keeping instances of `Deferred`. A `Deferred[F, A]` instance can -hold one single element of some type `A`. `Deferred` instances are created -empty, and can be filled only once. If some fiber tries to read the element from -an empty `Deferred` then it will be semantically blocked until some other fiber -fills (completes) it. - -Thus, alongside the queue of produced but not yet consumed elements, we have to -keep track of the `Deferred` instances created when the queue was empty that are -waiting for elements to be available. These instances will be kept in a new -queue `takers`. We will keep both queues in a new type `State`: +somehow if queue is empty until some element can be returned. This way we +prevent having consumer fibers running even when there is no element to +consume. This will be done by creating and keeping instances of `Deferred`. A +`Deferred[F, A]` instance can hold one single element of some type `A`. +`Deferred` instances are created empty, and can be filled only once. If some +fiber tries to read the element from an empty `Deferred` then it will wait +until some other fiber fills (completes) it. But recall that this waiting does +not involve blocking any physical thread, that's the beauty of fibers! + +Also, we will step up our code so we can handle several producers and consumers +in parallel. + +Ok, so, alongside the queue of produced but not yet consumed elements, we have +to keep track of the `Deferred` instances (created because consumers found an +emnpty queue) that are waiting for elements to be available. These instances +will be kept in a new queue `takers`. We will keep both queues in a new type +`State`: ```scala mdoc:compile-only import cats.effect.Deferred @@ -749,7 +799,7 @@ def consumer[F[_]: Async: Console](id: Int, stateR: Ref[F, State[F, Int]]): F[Un for { i <- take - _ <- if(i % 10000 == 0) Console[F].println(s"Consumer $id has reached $i items") else Async[F].unit + _ <- Async[F].whenA(i % 10000 == 0)(Console[F].println(s"Consumer $id has reached $i items")) _ <- consumer(id, stateR) } yield () } @@ -761,21 +811,22 @@ we will have now several producers and consumers running in parallel). The Note how it will block on `taker.get` when the queue is empty. The producer, for its part, will: -1. If there are waiting `takers`, it will take the first in the queue and offer - it the newly produced element (`taker.complete`). +1. If there are waiting `takers`, it will take the first one in the takers queue + and offer it the newly produced element (`taker.complete`). 2. If no `takers` are present, it will just enqueue the produced element. Thus the producer will look like: ```scala mdoc:compile-only -import cats.effect.{Deferred, Ref, Sync} +import cats.effect.{Async, Deferred, Ref} import cats.effect.std.Console import cats.syntax.all._ import scala.collection.immutable.Queue +import scala.concurrent.duration.DurationInt case class State[F[_], A](queue: Queue[A], takers: Queue[Deferred[F,A]]) -def producer[F[_]: Sync: Console](id: Int, counterR: Ref[F, Int], stateR: Ref[F, State[F,Int]]): F[Unit] = { +def producer[F[_]: Async: Console](id: Int, counterR: Ref[F, Int], stateR: Ref[F, State[F,Int]]): F[Unit] = { def offer(i: Int): F[Unit] = stateR.modify { @@ -783,18 +834,22 @@ def producer[F[_]: Sync: Console](id: Int, counterR: Ref[F, Int], stateR: Ref[F, val (taker, rest) = takers.dequeue State(queue, rest) -> taker.complete(i).void case State(queue, takers) => - State(queue.enqueue(i), takers) -> Sync[F].unit + State(queue.enqueue(i), takers) -> Async[F].unit }.flatten for { i <- counterR.getAndUpdate(_ + 1) _ <- offer(i) - _ <- if(i % 10000 == 0) Console[F].println(s"Producer $id has reached $i items") else Sync[F].unit + _ <- Async[F].whenA(i % 100000 == 0)(Console[F].println(s"Producer $id has reached $i items")) + _ <- Async[F].sleep(1.microsecond) // To prevent overwhelming consumers _ <- producer(id, counterR, stateR) } yield () } ``` +As in the previous section we introduce an artificial delay in order not to +overwhelm consumers. + Finally we modify our main program so it instantiates the counter and state `Ref`s. Also it will create several consumers and producers, 10 of each, and will start all of them in parallel: @@ -842,18 +897,12 @@ are started in their own fiber by the call to `parSequence`, which will wait for all of them to finish and then return the value passed as parameter. As in the previous example this program shall run forever until the user presses CTRL-C. -Having several consumers and producers improves the balance between consumers -and producers... but still, on the long run, queue tends to grow in size. To -fix this we will ensure the size of the queue is bounded, so whenever that max -size is reached producers will block as consumers do when the queue is empty. - - ### Producer consumer with bounded queue -Having a bounded queue implies that producers, when queue is full, will wait (be -'semantically blocked') until there is some empty bucket available to be filled. -So an implementation needs to keep track of these waiting producers. To do so we -will add a new queue `offerers` that will be added to the `State` alongside -`takers`. For each waiting producer the `offerers` queue will keep a +Having a bounded queue implies that producers, when the queue is full, will wait +(be 'semantically blocked') until there is some empty bucket available to be +filled. So an implementation needs to keep track of these waiting producers. To +do so we will add a new queue `offerers` that will be added to the `State` +alongside `takers`. For each waiting producer the `offerers` queue will keep a `Deferred[F, Unit]` that will be used to block the producer until the element it offers can be added to `queue` or directly passed to some consumer (`taker`). Alongside the `Deferred` instance we need to keep as well the actual element @@ -917,7 +966,7 @@ def consumer[F[_]: Async: Console](id: Int, stateR: Ref[F, State[F, Int]]): F[Un for { i <- take - _ <- if(i % 10000 == 0) Console[F].println(s"Consumer $id has reached $i items") else Async[F].unit + _ <- Async[F].whenA(i % 100000 == 0)(Console[F].println(s"Consumer $id has reached $i items")) _ <- consumer(id, stateR) } yield () } @@ -960,14 +1009,21 @@ def producer[F[_]: Async: Console](id: Int, counterR: Ref[F, Int], stateR: Ref[F for { i <- counterR.getAndUpdate(_ + 1) _ <- offer(i) - _ <- if(i % 10000 == 0) Console[F].println(s"Producer $id has reached $i items") else Async[F].unit + _ <- Async[F].whenA(i % 100000 == 0)(Console[F].println(s"Producer $id has reached $i items")) _ <- producer(id, counterR, stateR) } yield () } ``` +We have removed the `Async[F].sleep` call that we used to slow down producers +because we do not need it any more. Even if producers could run faster than +consumers they have to wait when the queue is full for consumers to extract data +from the queue. So at the end of the day they will run at the same pace. + As you see, producer and consumer are coded around the idea of keeping and -modifying state, just as with unbounded queues. +modifying state, just as with unbounded queues. Also we do not need to introduce +an artificial delay in producers, as soon as the queue gets full they will be +'blocked' thus giving a chance to consumers to read data. As the final step we must adapt the main program to use these new consumers and producers. Let's say we limit the queue size to `100`, then we have: @@ -1100,7 +1156,7 @@ def producer[F[_]: Async: Console](id: Int, counterR: Ref[F, Int], stateR: Ref[F for { i <- counterR.getAndUpdate(_ + 1) _ <- offer(i) - _ <- if(i % 10000 == 0) Console[F].println(s"Producer $id has reached $i items") else Async[F].unit + _ <- Async[F].whenA(i % 100000 == 0)(Console[F].println(s"Producer $id has reached $i items")) _ <- producer(id, counterR, stateR) } yield () } @@ -1146,7 +1202,7 @@ def consumer[F[_]: Async: Console](id: Int, stateR: Ref[F, State[F, Int]]): F[Un for { i <- take - _ <- if(i % 10000 == 0) Console[F].println(s"Consumer $id has reached $i items") else Async[F].unit + _ <- Async[F].whenA(i % 100000 == 0)(Console[F].println(s"Consumer $id has reached $i items")) _ <- consumer(id, stateR) } yield () } From 6370c3f1267ead57b39e650aab0dfd489f9da086 Mon Sep 17 00:00:00 2001 From: Luis Rodero-Merino Date: Sat, 16 Sep 2023 16:55:21 +0200 Subject: [PATCH 281/429] Semantically blocked -> fiber blocked, otoh -> on the other hand, as -> like --- docs/tutorial.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index ad7f2b7deb..3e7e563066 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -488,7 +488,7 @@ the other hand when the execution of some fiber is blocked _e.g._ because it must wait for a semaphore to be released, the thread running the fiber is recycled by cats-effect so it is available for other fibers. When the fiber execution can be resumed cats-effect will look for some free thread to continue -the execution. The term "_semantically blocked_" is used sometimes to denote +the execution. The term "_fiber blocking_" is used sometimes to denote that blocking the fiber does not involve halting any thread. Cats-effect also recycles threads of finished and canceled fibers. But keep in mind that, in contrast, if the fiber is truly blocked by some external action like waiting for @@ -566,7 +566,7 @@ Then our code calls `queueR.getAndUpdate` to add data into the queue. Note that `.getAndUpdate` provides the current queue, then we use `.enqueue` to insert the next value `counter+1`. This call returns a new queue with the value added that is stored by the ref instance. If some other fiber is accessing to -`queueR` then the fiber is (semantically) blocked. +`queueR` then the fiber (but no thread) is blocked. The `consumer` method is a bit different. It will try to read data from the queue but it must be aware that the queue can be empty: @@ -712,12 +712,12 @@ Can we alleviate this? Sure! There are a few options you can implement: } yield () ``` 2. Replace `Ref` with `AtomicCell` to keep the `Queue` instance. `AtomicCell`, - as `Ref`, is a concurrent data structure to keep a reference to some data. + like `Ref`, is a concurrent data structure to keep a reference to some data. But unlike `Ref` it ensures that only one fiber can operate on that reference at any given time. Thus the consumer won't have to try once and again to modify - its content. Otoh `AtomicCell` is slower than `Ref`. This is because `Ref` is - nonblocking while `AtomicCell` will block calling fibers to ensure only one - operates on its content. + its content. On the other hand `AtomicCell` is slower than `Ref`. This is + because `Ref` is nonblocking while `AtomicCell` will block calling fibers to + ensure only one operates on its content. 3. Make the queue bound by size so producers are forced to wait for consumers to extract data when the queue is full. We will do this later on in Section [Producer consumer with bounded @@ -899,7 +899,7 @@ previous example this program shall run forever until the user presses CTRL-C. ### Producer consumer with bounded queue Having a bounded queue implies that producers, when the queue is full, will wait -(be 'semantically blocked') until there is some empty bucket available to be +(be 'filter blocked') until there is some empty bucket available to be filled. So an implementation needs to keep track of these waiting producers. To do so we will add a new queue `offerers` that will be added to the `State` alongside `takers`. For each waiting producer the `offerers` queue will keep a From 61f28c32682ab5f2bc5aa0adba6f8edf8d365d54 Mon Sep 17 00:00:00 2001 From: Luis Rodero-Merino Date: Sat, 30 Sep 2023 16:30:10 +0200 Subject: [PATCH 282/429] filter -> fiber --- docs/tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index 3e7e563066..37bb2a1e9b 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -899,7 +899,7 @@ previous example this program shall run forever until the user presses CTRL-C. ### Producer consumer with bounded queue Having a bounded queue implies that producers, when the queue is full, will wait -(be 'filter blocked') until there is some empty bucket available to be +(be 'fiber blocked') until there is some empty bucket available to be filled. So an implementation needs to keep track of these waiting producers. To do so we will add a new queue `offerers` that will be added to the `State` alongside `takers`. For each waiting producer the `offerers` queue will keep a From dff1bf014d0b5ba295f15dad7d099533c7799dce Mon Sep 17 00:00:00 2001 From: Akhtiam Sakaev Date: Thu, 7 Dec 2023 13:04:51 +0300 Subject: [PATCH 283/429] Fix typo --- docs/tutorial.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index 37bb2a1e9b..7327cb6804 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -916,8 +916,8 @@ case class State[F[_], A](queue: Queue[A], capacity: Int, takers: Queue[Deferred ``` Of course both consumer and producer have to be modified to handle this new -queue `offerers`. A consumer can find four escenarios, depending on if `queue` -and `offerers` are each one empty or not. For each escenario a consumer shall: +queue `offerers`. A consumer can find four scenarios, depending on if `queue` +and `offerers` are each one empty or not. For each scenario a consumer shall: 1. If `queue` is not empty: 1. If `offerers` is empty then it will extract and return `queue`'s head. 2. If `offerers` is not empty (there is some producer waiting) then things From 9b99e52ab84d5b69ef83e9317a0747a70637a4b0 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 07:48:51 +0000 Subject: [PATCH 284/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/66232545cc967ff9d4d5e1805d8dbbc621a3c2e0' (2023-11-20) → 'github:typelevel/typelevel-nix/1cf38c7377dbf9e2cb0d20bbbe9e4d994a228343' (2023-12-06) • Updated input 'typelevel-nix/devshell': 'github:numtide/devshell/1aed986e3c81a4f6698e85a7452cbfcc4b31a36e' (2023-10-27) → 'github:numtide/devshell/7ad1c417c87e98e56dcef7ecd0e0a2f2e5669d51' (2023-11-24) • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/0bf3f5cf6a98b5d077cdcdb00a6d4b3d92bc78b5' (2023-11-19) → 'github:nixos/nixpkgs/0c6d8c783336a59f4c59d4a6daed6ab269c4b361' (2023-12-03) --- flake.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/flake.lock b/flake.lock index 5808e7b1f1..af3f06123b 100644 --- a/flake.lock +++ b/flake.lock @@ -6,11 +6,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1698410321, - "narHash": "sha256-MphuSlgpmKwtJncGMohryHiK55J1n6WzVQ/OAfmfoMc=", + "lastModified": 1700815693, + "narHash": "sha256-JtKZEQUzosrCwDsLgm+g6aqbP1aseUl1334OShEAS3s=", "owner": "numtide", "repo": "devshell", - "rev": "1aed986e3c81a4f6698e85a7452cbfcc4b31a36e", + "rev": "7ad1c417c87e98e56dcef7ecd0e0a2f2e5669d51", "type": "github" }, "original": { @@ -55,11 +55,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1700416016, - "narHash": "sha256-Qp8Of0BUYGjqodmE912h+/uGknB7J11ypcQMKnEDUrg=", + "lastModified": 1701626906, + "narHash": "sha256-ugr1QyzzwNk505ICE4VMQzonHQ9QS5W33xF2FXzFQ00=", "owner": "nixos", "repo": "nixpkgs", - "rev": "0bf3f5cf6a98b5d077cdcdb00a6d4b3d92bc78b5", + "rev": "0c6d8c783336a59f4c59d4a6daed6ab269c4b361", "type": "github" }, "original": { @@ -119,11 +119,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1700513164, - "narHash": "sha256-/9j5Cyo73LUUPilA5ml8tx1Q5isHHLwPUckRMtqWsxE=", + "lastModified": 1701880040, + "narHash": "sha256-7yKDDltsZHdF8xKYHKUGl6C3Sfcsb7NDOTAOAxkjgTo=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "66232545cc967ff9d4d5e1805d8dbbc621a3c2e0", + "rev": "1cf38c7377dbf9e2cb0d20bbbe9e4d994a228343", "type": "github" }, "original": { From 608d645fa2d8ab3d7700d31ff09f1687ddfda3af Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 12:14:59 +0000 Subject: [PATCH 285/429] Update sbt to 1.9.8 in series/3.x --- project/build.properties | 2 +- scalafix/project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project/build.properties b/project/build.properties index e8a1e246e8..abbbce5da4 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.7 +sbt.version=1.9.8 diff --git a/scalafix/project/build.properties b/scalafix/project/build.properties index e8a1e246e8..abbbce5da4 100644 --- a/scalafix/project/build.properties +++ b/scalafix/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.7 +sbt.version=1.9.8 From 723f0b43060e1a503255c6dceb9f1a1e178a2697 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 12:15:31 +0000 Subject: [PATCH 286/429] Update sbt-typelevel to 0.6.4 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 55b6105518..02317e9e3c 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.6.2") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.6.4") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.16") From 6064282a27ba60121d98c0742008b15abcfa49ee Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 12:16:37 +0000 Subject: [PATCH 287/429] Run prePR with sbt-typelevel Executed command: sbt tlPrePrBotHook --- .github/workflows/ci.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8840b50bd9..4b6eadf4db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,7 +136,7 @@ jobs: - name: Setup Java (temurin@8) id: setup-java-temurin-8 if: matrix.java == 'temurin@8' - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 8 @@ -150,7 +150,7 @@ jobs: - name: Setup Java (temurin@11) id: setup-java-temurin-11 if: matrix.java == 'temurin@11' - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 11 @@ -164,7 +164,7 @@ jobs: - name: Setup Java (temurin@17) id: setup-java-temurin-17 if: matrix.java == 'temurin@17' - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 @@ -178,7 +178,7 @@ jobs: - name: Setup Java (temurin@21) id: setup-java-temurin-21 if: matrix.java == 'temurin@21' - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 21 @@ -319,7 +319,7 @@ jobs: - name: Setup Java (temurin@8) id: setup-java-temurin-8 if: matrix.java == 'temurin@8' - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 8 @@ -332,7 +332,7 @@ jobs: - name: Setup Java (temurin@11) id: setup-java-temurin-11 if: matrix.java == 'temurin@11' - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 11 @@ -345,7 +345,7 @@ jobs: - name: Setup Java (temurin@17) id: setup-java-temurin-17 if: matrix.java == 'temurin@17' - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 @@ -358,7 +358,7 @@ jobs: - name: Setup Java (temurin@21) id: setup-java-temurin-21 if: matrix.java == 'temurin@21' - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 21 @@ -539,7 +539,7 @@ jobs: - name: Setup Java (temurin@8) id: setup-java-temurin-8 if: matrix.java == 'temurin@8' - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 8 @@ -552,7 +552,7 @@ jobs: - name: Setup Java (temurin@11) id: setup-java-temurin-11 if: matrix.java == 'temurin@11' - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 11 @@ -565,7 +565,7 @@ jobs: - name: Setup Java (temurin@17) id: setup-java-temurin-17 if: matrix.java == 'temurin@17' - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 @@ -578,7 +578,7 @@ jobs: - name: Setup Java (temurin@21) id: setup-java-temurin-21 if: matrix.java == 'temurin@21' - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 21 From 7eca63a65216fabe21a6928b8b27d79bb462dff8 Mon Sep 17 00:00:00 2001 From: Kamil Kloch Date: Thu, 14 Dec 2023 17:16:20 +0100 Subject: [PATCH 288/429] Add `GenConcurrent#parSequenceN_` and `GenConcurrent#parTraverseN_`. --- .../src/main/scala/cats/effect/IO.scala | 13 +++++++++ .../cats/effect/kernel/GenConcurrent.scala | 21 +++++++++++++- .../kernel/syntax/GenConcurrentSyntax.scala | 10 ++++++- .../scala/cats/effect/kernel/SyntaxSpec.scala | 10 +++++++ .../test/scala/cats/effect/IOPropSpec.scala | 27 ++++++++++++++++++ .../src/test/scala/cats/effect/IOSpec.scala | 28 +++++++++++++++++++ 6 files changed, 107 insertions(+), 2 deletions(-) diff --git a/core/shared/src/main/scala/cats/effect/IO.scala b/core/shared/src/main/scala/cats/effect/IO.scala index e235df8d4d..a54724a9e0 100644 --- a/core/shared/src/main/scala/cats/effect/IO.scala +++ b/core/shared/src/main/scala/cats/effect/IO.scala @@ -22,6 +22,7 @@ import cats.{ Applicative, CommutativeApplicative, Eval, + Foldable, Functor, Id, Monad, @@ -1420,6 +1421,18 @@ object IO extends IOCompanionPlatform with IOLowPriorityImplicits { def parTraverseN[T[_]: Traverse, A, B](n: Int)(ta: T[A])(f: A => IO[B]): IO[T[B]] = _asyncForIO.parTraverseN(n)(ta)(f) + /** + * Like `Parallel.parTraverse_`, but limits the degree of parallelism. + */ + def parTraverseN_[T[_]: Foldable, A, B](n: Int)(ta: T[A])(f: A => IO[B]): IO[Unit] = + _asyncForIO.parTraverseN_(n)(ta)(f) + + /** + * Like `Parallel.parSequence_`, but limits the degree of parallelism. + */ + def parSequenceN_[T[_]: Foldable, A](n: Int)(tma: T[IO[A]]): IO[Unit] = + _asyncForIO.parSequenceN_(n)(tma) + /** * Like `Parallel.parSequence`, but limits the degree of parallelism. */ diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/GenConcurrent.scala b/kernel/shared/src/main/scala/cats/effect/kernel/GenConcurrent.scala index e1cce47391..6da5142b74 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/GenConcurrent.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/GenConcurrent.scala @@ -16,7 +16,7 @@ package cats.effect.kernel -import cats.{Monoid, Semigroup, Traverse} +import cats.{Foldable, Monoid, Semigroup, Traverse} import cats.data.{EitherT, IorT, Kleisli, OptionT, WriterT} import cats.effect.kernel.instances.spawn._ import cats.effect.kernel.syntax.all._ @@ -123,6 +123,12 @@ trait GenConcurrent[F[_], E] extends GenSpawn[F, E] { def parSequenceN[T[_]: Traverse, A](n: Int)(tma: T[F[A]]): F[T[A]] = parTraverseN(n)(tma)(identity) + /** + * Like `Parallel.parSequence_`, but limits the degree of parallelism. + */ + def parSequenceN_[T[_]: Foldable, A](n: Int)(tma: T[F[A]]): F[Unit] = + parTraverseN_(n)(tma)(identity) + /** * Like `Parallel.parTraverse`, but limits the degree of parallelism. Note that the semantics * of this operation aim to maximise fairness: when a spot to execute becomes available, every @@ -136,6 +142,19 @@ trait GenConcurrent[F[_], E] extends GenSpawn[F, E] { MiniSemaphore[F](n).flatMap { sem => ta.parTraverse { a => sem.withPermit(f(a)) } } } + /** + * Like `Parallel.parTraverse_`, but limits the degree of parallelism. Note that the semantics + * of this operation aim to maximise fairness: when a spot to execute becomes available, every + * task has a chance to claim it, and not only the next `n` tasks in `ta` + */ + def parTraverseN_[T[_]: Foldable, A, B](n: Int)(ta: T[A])(f: A => F[B]): F[Unit] = { + require(n >= 1, s"Concurrency limit should be at least 1, was: $n") + + implicit val F: GenConcurrent[F, E] = this + + MiniSemaphore[F](n).flatMap { sem => ta.parTraverse_ { a => sem.withPermit(f(a)) } } + } + override def racePair[A, B](fa: F[A], fb: F[B]) : F[Either[(Outcome[F, E, A], Fiber[F, E, B]), (Fiber[F, E, A], Outcome[F, E, B])]] = { implicit val F: GenConcurrent[F, E] = this diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/syntax/GenConcurrentSyntax.scala b/kernel/shared/src/main/scala/cats/effect/kernel/syntax/GenConcurrentSyntax.scala index 7eab7d4a8d..4fceb6c91a 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/syntax/GenConcurrentSyntax.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/syntax/GenConcurrentSyntax.scala @@ -16,7 +16,7 @@ package cats.effect.kernel.syntax -import cats.Traverse +import cats.{Foldable, Traverse} import cats.effect.kernel.GenConcurrent trait GenConcurrentSyntax { @@ -52,6 +52,11 @@ final class ConcurrentParTraverseNOps[T[_], A] private[syntax] ( f: A => F[B] )(implicit T: Traverse[T], F: GenConcurrent[F, _]): F[T[B]] = F.parTraverseN(n)(wrapped)(f) + + def parTraverseN_[F[_], B](n: Int)( + f: A => F[B] + )(implicit T: Foldable[T], F: GenConcurrent[F, _]): F[Unit] = + F.parTraverseN_(n)(wrapped)(f) } final class ConcurrentParSequenceNOps[T[_], F[_], A] private[syntax] ( @@ -59,4 +64,7 @@ final class ConcurrentParSequenceNOps[T[_], F[_], A] private[syntax] ( ) extends AnyVal { def parSequenceN(n: Int)(implicit T: Traverse[T], F: GenConcurrent[F, _]): F[T[A]] = F.parSequenceN(n)(wrapped) + + def parSequenceN_(n: Int)(implicit T: Foldable[T], F: GenConcurrent[F, _]): F[Unit] = + F.parSequenceN_(n)(wrapped) } diff --git a/kernel/shared/src/test/scala/cats/effect/kernel/SyntaxSpec.scala b/kernel/shared/src/test/scala/cats/effect/kernel/SyntaxSpec.scala index 2563ffbefd..78f614db3f 100644 --- a/kernel/shared/src/test/scala/cats/effect/kernel/SyntaxSpec.scala +++ b/kernel/shared/src/test/scala/cats/effect/kernel/SyntaxSpec.scala @@ -46,11 +46,21 @@ class SyntaxSpec extends Specification { result: F[List[Int]] } + { + val result = List(1).parTraverseN_(3)(F.pure) + result: F[Unit] + } + { val result = List(target).parSequenceN(3) result: F[List[A]] } + { + val result = List(target).parSequenceN_(3) + result: F[Unit] + } + { val result = target.parReplicateAN(3)(5) result: F[List[A]] diff --git a/tests/shared/src/test/scala/cats/effect/IOPropSpec.scala b/tests/shared/src/test/scala/cats/effect/IOPropSpec.scala index 321f0958ac..3d3c5181b6 100644 --- a/tests/shared/src/test/scala/cats/effect/IOPropSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/IOPropSpec.scala @@ -67,6 +67,33 @@ class IOPropSpec extends BaseSpec with Discipline { } } + "parTraverseN_" should { + + "never exceed the maximum bound of concurrent tasks" in realProp { + for { + length <- Gen.chooseNum(0, 50) + limit <- Gen.chooseNum(1, 15, 2, 5) + } yield length -> limit + } { + case (length, limit) => + Queue.unbounded[IO, Int].flatMap { q => + val task = q.offer(1) >> IO.sleep(7.millis) >> q.offer(-1) + val testRun = List.fill(length)(task).parSequenceN_(limit) + def check(acc: Int = 0): IO[Unit] = + q.tryTake.flatMap { + case None => IO.unit + case Some(n) => + val newAcc = acc + n + if (newAcc > limit) + IO.raiseError(new Exception(s"Limit of $limit exceeded, was $newAcc")) + else check(newAcc) + } + + testRun >> check().mustEqual(()) + } + } + } + "parSequenceN" should { "give the same result as parSequence" in realProp( Gen.posNum[Int].flatMap(n => arbitrary[List[Int]].map(n -> _))) { diff --git a/tests/shared/src/test/scala/cats/effect/IOSpec.scala b/tests/shared/src/test/scala/cats/effect/IOSpec.scala index 34134247a0..02171a567f 100644 --- a/tests/shared/src/test/scala/cats/effect/IOSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/IOSpec.scala @@ -1556,6 +1556,34 @@ class IOSpec extends BaseSpec with Discipline with IOPlatformSpecification { } + "parTraverseN_" should { + + "throw when n < 1" in real { + IO.defer { + List.empty[Int].parTraverseN_(0)(_.pure[IO]) + }.mustFailWith[IllegalArgumentException] + } + + "propagate errors" in real { + List(1, 2, 3) + .parTraverseN_(2) { (n: Int) => + if (n == 2) IO.raiseError(new RuntimeException) else n.pure[IO] + } + .mustFailWith[RuntimeException] + } + + "be cancelable" in ticked { implicit ticker => + val p = for { + f <- List(1, 2, 3).parTraverseN_(2)(_ => IO.never).start + _ <- IO.sleep(100.millis) + _ <- f.cancel + } yield true + + p must completeAs(true) + } + + } + "parallel" should { "run parallel actually in parallel" in real { val x = IO.sleep(2.seconds) >> IO.pure(1) From 76ef4abfcd2bdc99cbd7f0613c3231c182b278e3 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 19 Dec 2023 07:48:49 +0000 Subject: [PATCH 289/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/1cf38c7377dbf9e2cb0d20bbbe9e4d994a228343' (2023-12-06) → 'github:typelevel/typelevel-nix/5995e0e045c0f17cffde7d0b6dfcde3de8aabb29' (2023-12-18) • Updated input 'typelevel-nix/devshell': 'github:numtide/devshell/7ad1c417c87e98e56dcef7ecd0e0a2f2e5669d51' (2023-11-24) → 'github:numtide/devshell/44ddedcbcfc2d52a76b64fb6122f209881bd3e1e' (2023-12-05) • Updated input 'typelevel-nix/flake-utils': 'github:numtide/flake-utils/ff7b65b44d01cf9ba6a71320833626af21126384' (2023-09-12) → 'github:numtide/flake-utils/4022d587cbbfd70fe950c1e2083a02621806a725' (2023-12-04) • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/0c6d8c783336a59f4c59d4a6daed6ab269c4b361' (2023-12-03) → 'github:nixos/nixpkgs/aa9d4729cbc99dabacb50e3994dcefb3ea0f7447' (2023-12-14) --- flake.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/flake.lock b/flake.lock index af3f06123b..7d1046e863 100644 --- a/flake.lock +++ b/flake.lock @@ -6,11 +6,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1700815693, - "narHash": "sha256-JtKZEQUzosrCwDsLgm+g6aqbP1aseUl1334OShEAS3s=", + "lastModified": 1701787589, + "narHash": "sha256-ce+oQR4Zq9VOsLoh9bZT8Ip9PaMLcjjBUHVPzW5d7Cw=", "owner": "numtide", "repo": "devshell", - "rev": "7ad1c417c87e98e56dcef7ecd0e0a2f2e5669d51", + "rev": "44ddedcbcfc2d52a76b64fb6122f209881bd3e1e", "type": "github" }, "original": { @@ -24,11 +24,11 @@ "systems": "systems_2" }, "locked": { - "lastModified": 1694529238, - "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "lastModified": 1701680307, + "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", "owner": "numtide", "repo": "flake-utils", - "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", "type": "github" }, "original": { @@ -55,11 +55,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1701626906, - "narHash": "sha256-ugr1QyzzwNk505ICE4VMQzonHQ9QS5W33xF2FXzFQ00=", + "lastModified": 1702539185, + "narHash": "sha256-KnIRG5NMdLIpEkZTnN5zovNYc0hhXjAgv6pfd5Z4c7U=", "owner": "nixos", "repo": "nixpkgs", - "rev": "0c6d8c783336a59f4c59d4a6daed6ab269c4b361", + "rev": "aa9d4729cbc99dabacb50e3994dcefb3ea0f7447", "type": "github" }, "original": { @@ -119,11 +119,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1701880040, - "narHash": "sha256-7yKDDltsZHdF8xKYHKUGl6C3Sfcsb7NDOTAOAxkjgTo=", + "lastModified": 1702939640, + "narHash": "sha256-+C98ZDpHCsrRCwNchrM8oCTAfVphElYst+2E3p6H1jU=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "1cf38c7377dbf9e2cb0d20bbbe9e4d994a228343", + "rev": "5995e0e045c0f17cffde7d0b6dfcde3de8aabb29", "type": "github" }, "original": { From 931cd64891d925f2e576e40a7f116143073af369 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 24 Dec 2023 21:44:25 +0000 Subject: [PATCH 290/429] Register example apps on Native --- tests/native/src/main/scala/catseffect/examplesplatform.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/native/src/main/scala/catseffect/examplesplatform.scala b/tests/native/src/main/scala/catseffect/examplesplatform.scala index c74289db06..937aca6685 100644 --- a/tests/native/src/main/scala/catseffect/examplesplatform.scala +++ b/tests/native/src/main/scala/catseffect/examplesplatform.scala @@ -41,6 +41,10 @@ package examples { register(Arguments) register(NonFatalError) register(FatalError) + register(RaiseFatalErrorAttempt) + register(RaiseFatalErrorHandle) + register(RaiseFatalErrorMap) + register(RaiseFatalErrorFlatMap) registerRaw(FatalErrorRaw) register(Canceled) registerLazy("catseffect.examples.GlobalRacingInit", GlobalRacingInit) From f2c848efaf9965ebc54c7331f735943c01f58c6c Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 25 Dec 2023 00:20:31 +0000 Subject: [PATCH 291/429] Only run mdoc on Temurin 17 --- .github/workflows/ci.yml | 2 +- build.sbt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 827710ff20..0ddf1b7888 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -249,7 +249,7 @@ jobs: - shell: bash run: sbt '++ ${{ matrix.scala }}' '${{ matrix.ci }}' - - if: (matrix.scala == '2.13.12' || matrix.scala == '3.3.1') && matrix.ci == 'ciJVM' + - if: (matrix.scala == '2.13.12' || matrix.scala == '3.3.1') && matrix.ci == 'ciJVM' && matrix.java == 'temurin@17 shell: bash run: sbt '++ ${{ matrix.scala }}' docs/mdoc diff --git a/build.sbt b/build.sbt index 85eefff4a9..3a45f9cc62 100644 --- a/build.sbt +++ b/build.sbt @@ -190,7 +190,8 @@ ThisBuild / githubWorkflowBuild := Seq("JVM", "JS", "Native").map { platform => WorkflowStep.Sbt( List("docs/mdoc"), cond = Some( - s"(matrix.scala == '$Scala213' || matrix.scala == '$Scala3') && matrix.ci == 'ciJVM'")), + s"(matrix.scala == '$Scala213' || matrix.scala == '$Scala3') && matrix.ci == 'ciJVM' && matrix.java == '${LatestJava.render}") + ), WorkflowStep.Run( List("example/test-jvm.sh ${{ matrix.scala }}"), name = Some("Test Example JVM App Within Sbt"), From 0134b3a1d1c3ca2932fe387b35c0fc20859f8d15 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 25 Dec 2023 12:25:55 +0000 Subject: [PATCH 292/429] Fix workflow --- .github/workflows/ci.yml | 2 +- build.sbt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ddf1b7888..e53f860d93 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -249,7 +249,7 @@ jobs: - shell: bash run: sbt '++ ${{ matrix.scala }}' '${{ matrix.ci }}' - - if: (matrix.scala == '2.13.12' || matrix.scala == '3.3.1') && matrix.ci == 'ciJVM' && matrix.java == 'temurin@17 + - if: (matrix.scala == '2.13.12' || matrix.scala == '3.3.1') && matrix.ci == 'ciJVM' && matrix.java == 'temurin@17' shell: bash run: sbt '++ ${{ matrix.scala }}' docs/mdoc diff --git a/build.sbt b/build.sbt index 3a45f9cc62..cadc5b33d9 100644 --- a/build.sbt +++ b/build.sbt @@ -190,7 +190,7 @@ ThisBuild / githubWorkflowBuild := Seq("JVM", "JS", "Native").map { platform => WorkflowStep.Sbt( List("docs/mdoc"), cond = Some( - s"(matrix.scala == '$Scala213' || matrix.scala == '$Scala3') && matrix.ci == 'ciJVM' && matrix.java == '${LatestJava.render}") + s"(matrix.scala == '$Scala213' || matrix.scala == '$Scala3') && matrix.ci == 'ciJVM' && matrix.java == '${LatestJava.render}'") ), WorkflowStep.Run( List("example/test-jvm.sh ${{ matrix.scala }}"), From 9780cff8813f11a074439998de6135555e981853 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 2 Jan 2024 07:48:51 +0000 Subject: [PATCH 293/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/5995e0e045c0f17cffde7d0b6dfcde3de8aabb29' (2023-12-18) → 'github:typelevel/typelevel-nix/8eb51654dc21054929b0ae6883c0382c9f51b1a2' (2024-01-01) • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/aa9d4729cbc99dabacb50e3994dcefb3ea0f7447' (2023-12-14) → 'github:nixos/nixpkgs/d44d59d2b5bd694cd9d996fd8c51d03e3e9ba7f7' (2023-12-31) --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 7d1046e863..e19ed5fdaa 100644 --- a/flake.lock +++ b/flake.lock @@ -55,11 +55,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1702539185, - "narHash": "sha256-KnIRG5NMdLIpEkZTnN5zovNYc0hhXjAgv6pfd5Z4c7U=", + "lastModified": 1704008649, + "narHash": "sha256-rGPSWjXTXTurQN9beuHdyJhB8O761w1Zc5BqSSmHvoM=", "owner": "nixos", "repo": "nixpkgs", - "rev": "aa9d4729cbc99dabacb50e3994dcefb3ea0f7447", + "rev": "d44d59d2b5bd694cd9d996fd8c51d03e3e9ba7f7", "type": "github" }, "original": { @@ -119,11 +119,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1702939640, - "narHash": "sha256-+C98ZDpHCsrRCwNchrM8oCTAfVphElYst+2E3p6H1jU=", + "lastModified": 1704087752, + "narHash": "sha256-PTM++FBin2Xd4kc2HyQ74Wm0PdjSheCcnX45xhH1fOw=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "5995e0e045c0f17cffde7d0b6dfcde3de8aabb29", + "rev": "8eb51654dc21054929b0ae6883c0382c9f51b1a2", "type": "github" }, "original": { From 7cc6d719e26a069354281cbe95adb728e90487b1 Mon Sep 17 00:00:00 2001 From: Kamil Kloch Date: Thu, 13 Apr 2023 17:32:51 +0200 Subject: [PATCH 294/429] Add IOOps from cats.implicits. Reuse existing syntax Update TestIOOps. Revert back to cherry-picked implementation, add missing methods. Add missing header. Add missing implementation. Remove unused import. Revert to Scala 2 import syntax. Reuse existing extension classes. Remove IO.parReplicateA, IOparReplicateA_. Add tests for adaptError and attemptTap. Remove TestIOOps. Cleanup. scalafmt. Add tests. Update tests. Fix type constraints. Add IO.traverse and IO.traverse_. Reorganize tests. --- .../benchmarks/AtomicCellBenchmark.scala | 1 - .../effect/benchmarks/MutexBenchmark.scala | 1 - .../src/main/scala/cats/effect/IO.scala | 142 ++++++++++++++++-- .../test/scala/cats/effect/SelectorSpec.scala | 1 - .../cats/effect/IOImplicitSpec.scala | 53 +++++++ .../cats/effect/IOParImplicitSpec.scala | 0 .../src/test/scala/cats/effect/IOSpec.scala | 26 ++++ 7 files changed, 206 insertions(+), 18 deletions(-) create mode 100644 tests/shared/src/test/scala-2.13+/cats/effect/IOImplicitSpec.scala rename tests/shared/src/test/scala-2.13+/{ => not}/cats/effect/IOParImplicitSpec.scala (100%) diff --git a/benchmarks/src/main/scala/cats/effect/benchmarks/AtomicCellBenchmark.scala b/benchmarks/src/main/scala/cats/effect/benchmarks/AtomicCellBenchmark.scala index 7695905298..610208e91c 100644 --- a/benchmarks/src/main/scala/cats/effect/benchmarks/AtomicCellBenchmark.scala +++ b/benchmarks/src/main/scala/cats/effect/benchmarks/AtomicCellBenchmark.scala @@ -19,7 +19,6 @@ package cats.effect.benchmarks import cats.effect.IO import cats.effect.std._ import cats.effect.unsafe.implicits.global -import cats.syntax.all._ import org.openjdk.jmh.annotations._ diff --git a/benchmarks/src/main/scala/cats/effect/benchmarks/MutexBenchmark.scala b/benchmarks/src/main/scala/cats/effect/benchmarks/MutexBenchmark.scala index 9161a4277d..2b77a5d7fe 100644 --- a/benchmarks/src/main/scala/cats/effect/benchmarks/MutexBenchmark.scala +++ b/benchmarks/src/main/scala/cats/effect/benchmarks/MutexBenchmark.scala @@ -19,7 +19,6 @@ package cats.effect.benchmarks import cats.effect.IO import cats.effect.std._ import cats.effect.unsafe.implicits.global -import cats.syntax.all._ import org.openjdk.jmh.annotations._ diff --git a/core/shared/src/main/scala/cats/effect/IO.scala b/core/shared/src/main/scala/cats/effect/IO.scala index a54724a9e0..88d0729e99 100644 --- a/core/shared/src/main/scala/cats/effect/IO.scala +++ b/core/shared/src/main/scala/cats/effect/IO.scala @@ -27,6 +27,7 @@ import cats.{ Id, Monad, Monoid, + NonEmptyParallel, Now, Parallel, Semigroup, @@ -42,16 +43,11 @@ import cats.effect.kernel.GenTemporal.handleDuration import cats.effect.std.{Backpressure, Console, Env, Supervisor, UUIDGen} import cats.effect.tracing.{Tracing, TracingEvent} import cats.effect.unsafe.IORuntime +import cats.syntax._ import cats.syntax.all._ import scala.annotation.unchecked.uncheckedVariance -import scala.concurrent.{ - CancellationException, - ExecutionContext, - Future, - Promise, - TimeoutException -} +import scala.concurrent._ import scala.concurrent.duration._ import scala.util.{Failure, Success, Try} import scala.util.control.NonFatal @@ -162,6 +158,15 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] { def <&[B](that: IO[B]): IO[A] = both(that).map { case (a, _) => a } + /** + * Transform certain errors using `pf` and rethrow them. Non matching errors and successful + * values are not affected by this function. + * + * Implements `ApplicativeError.adaptError`. + */ + def adaptError[E](pf: PartialFunction[Throwable, Throwable]): IO[A] = + recoverWith(pf.andThen(IO.raiseError[A] _)) + /** * Replaces the result of this IO with the given value. */ @@ -186,6 +191,15 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] { def attempt: IO[Either[Throwable, A]] = IO.Attempt(this) + /** + * Reifies the value or error of the source and performs an effect on the result, then + * recovers the original value or error back into `IO`. + * + * Implements `MonadError.attemptTap`. + */ + def attemptTap[B](f: Either[Throwable, A] => IO[B]): IO[A] = + attempt.flatTap(f).rethrow + /** * Replaces failures in this IO with an empty Option. */ @@ -575,6 +589,36 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] { def onError(f: Throwable => IO[Unit]): IO[A] = handleErrorWith(t => f(t).voidError *> IO.raiseError(t)) + /** + * Like `Parallel.parProductL` + */ + def parProductL[B](iob: IO[B])(implicit P: NonEmptyParallel[IO]): IO[A] = + P.parProductL[A, B](this)(iob) + + /** + * Like `Parallel.parProductR` + */ + def parProductR[B](iob: IO[B])(implicit P: NonEmptyParallel[IO]): IO[B] = + P.parProductR[A, B](this)(iob) + + /** + * Like `Parallel.parProduct` + */ + def parProduct[B](iob: IO[B])(implicit P: NonEmptyParallel[IO]): IO[(A, B)] = + Parallel.parProduct(this, iob)(P) + + /** + * Like `Parallel.parReplicateA` + */ + def parReplicateA(n: Int): IO[List[A]] = + List.fill(n)(this).parSequence + + /** + * Like `Parallel.parReplicateA_` + */ + def parReplicateA_(n: Int): IO[Unit] = + List.fill(n)(this).parSequence_ + def race[B](that: IO[B]): IO[Either[A, B]] = IO.race(this, that) @@ -1107,7 +1151,45 @@ private[effect] trait IOLowPriorityImplicits { } } -object IO extends IOCompanionPlatform with IOLowPriorityImplicits { +object IO extends IOCompanionPlatform with IOLowPriorityImplicits with TupleParallelSyntax { + + implicit final def catsSyntaxParallelSequence1[T[_], A]( + toia: T[IO[A]]): ParallelSequenceOps1[T, IO, A] = new ParallelSequenceOps1(toia) + + implicit final def catsSyntaxParallelSequence_[T[_], A]( + tioa: T[IO[A]]): ParallelSequence_Ops[T, IO, A] = + new ParallelSequence_Ops(tioa) + + implicit final def catsSyntaxParallelUnorderedSequence[T[_], A]( + tioa: T[IO[A]]): ParallelUnorderedSequenceOps[T, IO, A] = + new ParallelUnorderedSequenceOps(tioa) + + implicit final def catsSyntaxParallelFlatSequence1[T[_], A]( + tioa: T[IO[T[A]]]): ParallelFlatSequenceOps1[T, IO, A] = + new ParallelFlatSequenceOps1(tioa) + + implicit final def catsSyntaxParallelUnorderedFlatSequence[T[_], A]( + tiota: T[IO[T[A]]]): ParallelUnorderedFlatSequenceOps[T, IO, A] = + new ParallelUnorderedFlatSequenceOps(tiota) + + implicit final def catsSyntaxParallelSequenceFilter[T[_], A]( + x: T[IO[Option[A]]]): ParallelSequenceFilterOps[T, IO, A] = + new ParallelSequenceFilterOps(x) + + implicit class IOFlatSequenceOps[T[_], A](tiota: T[IO[T[A]]]) { + def flatSequence( + implicit T: Traverse[T], + G: Applicative[IO], + F: cats.FlatMap[T]): IO[T[A]] = { + tiota.sequence(T, G).map(F.flatten) + } + } + + implicit class IOSequenceOps[T[_], A](tioa: T[IO[A]]) { + def sequence(implicit T: Traverse[T], G: Applicative[IO]): IO[T[A]] = T.sequence(tioa)(G) + + def sequence_(implicit F: Foldable[T], G: Applicative[IO]): IO[Unit] = F.sequence_(tioa)(G) + } /** * Newtype encoding for an `IO` datatype that has a `cats.Applicative` capable of doing @@ -1415,6 +1497,18 @@ object IO extends IOCompanionPlatform with IOLowPriorityImplicits { */ def some[A](a: A): IO[Option[A]] = pure(Some(a)) + /** + * Like `Parallel.parTraverse` + */ + def parTraverse[T[_]: Traverse, A, B](ta: T[A])(f: A => IO[B]): IO[T[B]] = + ta.parTraverse(f) + + /** + * Like `Parallel.parTraverse_` + */ + def parTraverse_[T[_]: Foldable, A, B](ta: T[A])(f: A => IO[B]): IO[Unit] = + ta.parTraverse_(f) + /** * Like `Parallel.parTraverse`, but limits the degree of parallelism. */ @@ -1428,22 +1522,34 @@ object IO extends IOCompanionPlatform with IOLowPriorityImplicits { _asyncForIO.parTraverseN_(n)(ta)(f) /** - * Like `Parallel.parSequence_`, but limits the degree of parallelism. + * Like `Parallel.parSequence` */ - def parSequenceN_[T[_]: Foldable, A](n: Int)(tma: T[IO[A]]): IO[Unit] = - _asyncForIO.parSequenceN_(n)(tma) + def parSequence[T[_]: Traverse, A](tioa: T[IO[A]]): IO[T[A]] = + tioa.parSequence + + /** + * Like `Parallel.parSequence_` + */ + def parSequence_[T[_]: Foldable, A](tioa: T[IO[A]]): IO[Unit] = + tioa.parSequence_ /** * Like `Parallel.parSequence`, but limits the degree of parallelism. */ - def parSequenceN[T[_]: Traverse, A](n: Int)(tma: T[IO[A]]): IO[T[A]] = - _asyncForIO.parSequenceN(n)(tma) + def parSequenceN[T[_]: Traverse, A](n: Int)(tioa: T[IO[A]]): IO[T[A]] = + _asyncForIO.parSequenceN(n)(tioa) + + /** + * Like `Parallel.parSequence_`, but limits the degree of parallelism. + */ + def parSequenceN_[T[_]: Foldable, A](n: Int)(tma: T[IO[A]]): IO[Unit] = + _asyncForIO.parSequenceN_(n)(tma) /** * Like `Parallel.parReplicateA`, but limits the degree of parallelism. */ - def parReplicateAN[A](n: Int)(replicas: Int, ma: IO[A]): IO[List[A]] = - _asyncForIO.parReplicateAN(n)(replicas, ma) + def parReplicateAN[A](n: Int)(replicas: Int, ioa: IO[A]): IO[List[A]] = + _asyncForIO.parReplicateAN(n)(replicas, ioa) /** * Lifts a pure value into `IO`. @@ -1514,6 +1620,12 @@ object IO extends IOCompanionPlatform with IOLowPriorityImplicits { def trace: IO[Trace] = IOTrace + def traverse[T[_]: Traverse, A, B](ta: T[A])(f: A => IO[B]): IO[T[B]] = + ta.traverse(f)(_asyncForIO) + + def traverse_[T[_]: Foldable, A, B](ta: T[A])(f: A => IO[B]): IO[Unit] = + ta.traverse_(f)(_asyncForIO) + private[effect] def runtime: IO[IORuntime] = ReadRT def pollers: IO[List[Any]] = diff --git a/tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala b/tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala index d977c048ba..79685bc4d9 100644 --- a/tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala @@ -17,7 +17,6 @@ package cats.effect import cats.effect.unsafe.IORuntime -import cats.syntax.all._ import scala.concurrent.duration._ diff --git a/tests/shared/src/test/scala-2.13+/cats/effect/IOImplicitSpec.scala b/tests/shared/src/test/scala-2.13+/cats/effect/IOImplicitSpec.scala new file mode 100644 index 0000000000..530cfd5f58 --- /dev/null +++ b/tests/shared/src/test/scala-2.13+/cats/effect/IOImplicitSpec.scala @@ -0,0 +1,53 @@ +/* + * Copyright 2020-2023 Typelevel + * + * 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 + +class IOImplicitSpec extends BaseSpec { + + "Can resolve IO sequence ops without import of cats.syntax.all" in { // compilation test + for { + _ <- List(IO(1)).sequence_ + _ <- Option(IO(1)).sequence + _ <- Option(IO(1)).sequence_ + _ <- List(IO(List(1))).flatSequence + } yield () + true + } + + "Can resolve IO.Par ops without import of cats.syntax.all" in { // compilation test + for { + _ <- Option(IO(1)).parSequence + _ <- Option(IO(1)).parSequence_ + _ <- IO(1).parReplicateA(2) + _ <- IO(1).parReplicateA_(2) + _ <- IO(1).parProduct(IO(2)) + _ <- IO(1).parProductL(IO(2)) + _ <- IO(1).parProductR(IO(2)) + _ <- List(IO(Option(1))).parSequenceFilter + _ <- List(IO(1)).parUnorderedSequence + _ <- List(IO(List(1))).parFlatSequence + _ <- List(IO(List(1))).parUnorderedFlatSequence + _ <- (IO(1), IO(2)).parMapN(_ + _) + _ <- (IO(1), IO(2)).parTupled + _ <- (IO(1), IO(2)).parFlatMapN { case (x, y) => IO.pure(x + y) } + _ <- (IO(1), IO(2), IO(3)).parMapN(_ + _ + _) + _ <- (IO(1), IO(2), IO(3)).parTupled + _ <- (IO(1), IO(2), IO(3)).parFlatMapN { case (x, y, z) => IO.pure(x + y + z) } + } yield () + true + } +} diff --git a/tests/shared/src/test/scala-2.13+/cats/effect/IOParImplicitSpec.scala b/tests/shared/src/test/scala-2.13+/not/cats/effect/IOParImplicitSpec.scala similarity index 100% rename from tests/shared/src/test/scala-2.13+/cats/effect/IOParImplicitSpec.scala rename to tests/shared/src/test/scala-2.13+/not/cats/effect/IOParImplicitSpec.scala diff --git a/tests/shared/src/test/scala/cats/effect/IOSpec.scala b/tests/shared/src/test/scala/cats/effect/IOSpec.scala index 02171a567f..c3c55d3690 100644 --- a/tests/shared/src/test/scala/cats/effect/IOSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/IOSpec.scala @@ -115,6 +115,26 @@ class IOSpec extends BaseSpec with Discipline with IOPlatformSpecification { (IO.pure(42) orElse IO.raiseError[Int](TestException)) must completeAs(42) } + "adaptError is a no-op for a successful effect" in ticked { implicit ticker => + IO(42).adaptError { case x => x } must completeAs(42) + } + + "adaptError is a no-op for a non-matching error" in ticked { implicit ticker => + case object TestException1 extends RuntimeException + case object TestException2 extends RuntimeException + IO.raiseError[Unit](TestException1).adaptError { + case TestException2 => TestException2 + } must failAs(TestException1) + } + + "adaptError transforms the error in a failed effect" in ticked { implicit ticker => + case object TestException1 extends RuntimeException + case object TestException2 extends RuntimeException + IO.raiseError[Unit](TestException1).adaptError { + case TestException1 => TestException2 + } must failAs(TestException2) + } + "attempt is redeem with Left(_) for recover and Right(_) for map" in ticked { implicit ticker => forAll { (io: IO[Int]) => io.attempt eqv io.redeem(Left(_), Right(_)) } @@ -126,6 +146,12 @@ class IOSpec extends BaseSpec with Discipline with IOPlatformSpecification { } } + "attemptTap(f) is an alias for attempt.flatTap(f).rethrow" in ticked { implicit ticker => + forAll { (io: IO[Int], f: Either[Throwable, Int] => IO[Int]) => + io.attemptTap(f) eqv io.attempt.flatTap(f).rethrow + } + } + "rethrow is inverse of attempt" in ticked { implicit ticker => forAll { (io: IO[Int]) => io.attempt.rethrow eqv io } } From 4bc17b80ebbf67b72aa958b615e0d11410bb61b9 Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Wed, 3 Jan 2024 12:31:02 -0600 Subject: [PATCH 295/429] Skip starvation test (which is breaking CI) --- ioapp-tests/src/test/scala/IOAppSpec.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ioapp-tests/src/test/scala/IOAppSpec.scala b/ioapp-tests/src/test/scala/IOAppSpec.scala index 82efb5c9ee..21c0a36a60 100644 --- a/ioapp-tests/src/test/scala/IOAppSpec.scala +++ b/ioapp-tests/src/test/scala/IOAppSpec.scala @@ -233,7 +233,8 @@ class IOAppSpec extends Specification { h.stderr() must not(contain("boom")) } - "warn on cpu starvation" in { + // TODO reenable this test (#3919) + "warn on cpu starvation" in skipped { val h = platform("CpuStarvation", List.empty) h.awaitStatus() val err = h.stderr() From 5b7f46895ec271ef60266a9ae0863937753e0dd3 Mon Sep 17 00:00:00 2001 From: Neo Lin Date: Thu, 4 Jan 2024 13:21:48 -0500 Subject: [PATCH 296/429] fix: Added priority queue sink and source extending queue --- std/shared/src/main/scala/cats/effect/std/PQueue.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/std/shared/src/main/scala/cats/effect/std/PQueue.scala b/std/shared/src/main/scala/cats/effect/std/PQueue.scala index a8f1864950..a7602a9a0f 100644 --- a/std/shared/src/main/scala/cats/effect/std/PQueue.scala +++ b/std/shared/src/main/scala/cats/effect/std/PQueue.scala @@ -214,7 +214,7 @@ object PQueue { else () } -trait PQueueSource[F[_], A] { +trait PQueueSource[F[_], A] extends QueueSource[F, A] { /** * Dequeues the least element from the PQueue, possibly fiber blocking until an element @@ -260,7 +260,7 @@ trait PQueueSource[F[_], A] { * Note: If there are multiple elements with least priority, the order in which they are * dequeued is undefined. */ - def tryTakeN(maxN: Option[Int])(implicit F: Monad[F]): F[List[A]] = { + override def tryTakeN(maxN: Option[Int])(implicit F: Monad[F]): F[List[A]] = { PQueueSource.assertMaxNPositive(maxN) def loop(i: Int, limit: Int, acc: List[A]): F[List[A]] = @@ -302,7 +302,7 @@ object PQueueSource { } } -trait PQueueSink[F[_], A] { +trait PQueueSink[F[_], A] extends QueueSink[F, A] { /** * Enqueues the given element, possibly fiber blocking until sufficient capacity becomes @@ -339,7 +339,7 @@ trait PQueueSink[F[_], A] { * @return * an effect that contains the remaining valus that could not be offered. */ - def tryOfferN(list: List[A])(implicit F: Monad[F]): F[List[A]] = list match { + override def tryOfferN(list: List[A])(implicit F: Monad[F]): F[List[A]] = list match { case Nil => F.pure(list) case h :: t => tryOffer(h).ifM( From 30856832a056f56511e694b2cafc17edfab7ee31 Mon Sep 17 00:00:00 2001 From: Neo Lin Date: Fri, 5 Jan 2024 09:55:48 -0500 Subject: [PATCH 297/429] fix: Added priority queue sink and source extending queue, cleanup --- .../main/scala/cats/effect/std/PQueue.scala | 119 +----------------- 1 file changed, 2 insertions(+), 117 deletions(-) diff --git a/std/shared/src/main/scala/cats/effect/std/PQueue.scala b/std/shared/src/main/scala/cats/effect/std/PQueue.scala index a7602a9a0f..9ecf4ad30f 100644 --- a/std/shared/src/main/scala/cats/effect/std/PQueue.scala +++ b/std/shared/src/main/scala/cats/effect/std/PQueue.scala @@ -214,79 +214,9 @@ object PQueue { else () } -trait PQueueSource[F[_], A] extends QueueSource[F, A] { - - /** - * Dequeues the least element from the PQueue, possibly fiber blocking until an element - * becomes available. - * - * O(log(n)) - * - * Note: If there are multiple elements with least priority, the order in which they are - * dequeued is undefined. If you want to break ties with FIFO order you will need an - * additional `Ref[F, Long]` to track insertion, and embed that information into your instance - * for `Order[A]`. - */ - def take: F[A] - - /** - * Attempts to dequeue the least element from the PQueue, if one is available without fiber - * blocking. - * - * O(log(n)) - * - * @return - * an effect that describes whether the dequeueing of an element from the PQueue succeeded - * without blocking, with `None` denoting that no element was available - * - * Note: If there are multiple elements with least priority, the order in which they are - * dequeued is undefined. If you want to break ties with FIFO order you will need an - * additional `Ref[F, Long]` to track insertion, and embed that information into your instance - * for `Order[A]`. - */ - def tryTake: F[Option[A]] - - /** - * Attempts to dequeue elements from the PQueue, if they are available without semantically - * blocking. This is a convenience method that recursively runs `tryTake`. It does not provide - * any additional performance benefits. - * - * @param maxN - * The max elements to dequeue. Passing `None` will try to dequeue the whole queue. - * - * @return - * an effect that contains the dequeued elements from the PQueue - * - * Note: If there are multiple elements with least priority, the order in which they are - * dequeued is undefined. - */ - override def tryTakeN(maxN: Option[Int])(implicit F: Monad[F]): F[List[A]] = { - PQueueSource.assertMaxNPositive(maxN) - - def loop(i: Int, limit: Int, acc: List[A]): F[List[A]] = - if (i >= limit) - F.pure(acc.reverse) - else - tryTake flatMap { - case Some(a) => loop(i + 1, limit, a :: acc) - case None => F.pure(acc.reverse) - } - - maxN match { - case Some(limit) => loop(0, limit, Nil) - case None => loop(0, Int.MaxValue, Nil) - } - } - - def size: F[Int] -} +trait PQueueSource[F[_], A] extends QueueSource[F, A] object PQueueSource { - private def assertMaxNPositive(maxN: Option[Int]): Unit = maxN match { - case Some(n) if n <= 0 => - throw new IllegalArgumentException(s"Provided maxN parameter must be positive, was $n") - case _ => () - } implicit def catsFunctorForPQueueSource[F[_]: Functor]: Functor[PQueueSource[F, *]] = new Functor[PQueueSource[F, *]] { @@ -302,52 +232,7 @@ object PQueueSource { } } -trait PQueueSink[F[_], A] extends QueueSink[F, A] { - - /** - * Enqueues the given element, possibly fiber blocking until sufficient capacity becomes - * available. - * - * O(log(n)) - * - * @param a - * the element to be put in the PQueue - */ - def offer(a: A): F[Unit] - - /** - * Attempts to enqueue the given element without fiber blocking. - * - * O(log(n)) - * - * @param a - * the element to be put in the PQueue - * @return - * an effect that describes whether the enqueuing of the given element succeeded without - * blocking - */ - def tryOffer(a: A): F[Boolean] - - /** - * Attempts to enqueue the given elements without semantically blocking. If an item in the - * list cannot be enqueued, the remaining elements will be returned. This is a convenience - * method that recursively runs `tryOffer` and does not offer any additional performance - * benefits. - * - * @param list - * the elements to be put in the PQueue - * @return - * an effect that contains the remaining valus that could not be offered. - */ - override def tryOfferN(list: List[A])(implicit F: Monad[F]): F[List[A]] = list match { - case Nil => F.pure(list) - case h :: t => - tryOffer(h).ifM( - tryOfferN(t), - F.pure(list) - ) - } -} +trait PQueueSink[F[_], A] extends QueueSink[F, A] object PQueueSink { implicit def catsContravariantForPQueueSink[F[_]]: Contravariant[PQueueSink[F, *]] = From fb356b75ea75b13dcd0e9f775989ea298f0fdd96 Mon Sep 17 00:00:00 2001 From: Neo Lin Date: Fri, 5 Jan 2024 10:26:52 -0500 Subject: [PATCH 298/429] fix: Added priority queue sink and source extending queue, fixing mima error check --- .../main/scala/cats/effect/std/PQueue.scala | 54 ++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/std/shared/src/main/scala/cats/effect/std/PQueue.scala b/std/shared/src/main/scala/cats/effect/std/PQueue.scala index 9ecf4ad30f..b9204fd7b4 100644 --- a/std/shared/src/main/scala/cats/effect/std/PQueue.scala +++ b/std/shared/src/main/scala/cats/effect/std/PQueue.scala @@ -214,9 +214,48 @@ object PQueue { else () } -trait PQueueSource[F[_], A] extends QueueSource[F, A] +trait PQueueSource[F[_], A] extends QueueSource[F, A] { + + /** + * Attempts to dequeue elements from the PQueue, if they are available without semantically + * blocking. This is a convenience method that recursively runs `tryTake`. It does not provide + * any additional performance benefits. + * + * @param maxN + * The max elements to dequeue. Passing `None` will try to dequeue the whole queue. + * + * @return + * an effect that contains the dequeued elements from the PQueue + * + * Note: If there are multiple elements with least priority, the order in which they are + * dequeued is undefined. + */ + override def tryTakeN(maxN: Option[Int])(implicit F: Monad[F]): F[List[A]] = { + PQueueSource.assertMaxNPositive(maxN) + + def loop(i: Int, limit: Int, acc: List[A]): F[List[A]] = + if (i >= limit) + F.pure(acc.reverse) + else + tryTake flatMap { + case Some(a) => loop(i + 1, limit, a :: acc) + case None => F.pure(acc.reverse) + } + + maxN match { + case Some(limit) => loop(0, limit, Nil) + case None => loop(0, Int.MaxValue, Nil) + } + } + +} object PQueueSource { + private def assertMaxNPositive(maxN: Option[Int]): Unit = maxN match { + case Some(n) if n <= 0 => + throw new IllegalArgumentException(s"Provided maxN parameter must be positive, was $n") + case _ => () + } implicit def catsFunctorForPQueueSource[F[_]: Functor]: Functor[PQueueSource[F, *]] = new Functor[PQueueSource[F, *]] { @@ -232,7 +271,18 @@ object PQueueSource { } } -trait PQueueSink[F[_], A] extends QueueSink[F, A] +trait PQueueSink[F[_], A] extends QueueSink[F, A] { + + override def tryOfferN(list: List[A])(implicit F: Monad[F]): F[List[A]] = list match { + case Nil => F.pure(list) + case h :: t => + tryOffer(h).ifM( + tryOfferN(t), + F.pure(list) + ) + } + +} object PQueueSink { implicit def catsContravariantForPQueueSink[F[_]]: Contravariant[PQueueSink[F, *]] = From 31a1d66b63df37cef3e702021f42aaa2bb63af31 Mon Sep 17 00:00:00 2001 From: Neo Lin Date: Fri, 5 Jan 2024 20:48:57 -0500 Subject: [PATCH 299/429] breaking the build to show the build error of ReversedMissingMethodProblem --- .../main/scala/cats/effect/std/PQueue.scala | 45 +++++++------------ 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/std/shared/src/main/scala/cats/effect/std/PQueue.scala b/std/shared/src/main/scala/cats/effect/std/PQueue.scala index b9204fd7b4..da1fee18c7 100644 --- a/std/shared/src/main/scala/cats/effect/std/PQueue.scala +++ b/std/shared/src/main/scala/cats/effect/std/PQueue.scala @@ -230,32 +230,12 @@ trait PQueueSource[F[_], A] extends QueueSource[F, A] { * Note: If there are multiple elements with least priority, the order in which they are * dequeued is undefined. */ - override def tryTakeN(maxN: Option[Int])(implicit F: Monad[F]): F[List[A]] = { - PQueueSource.assertMaxNPositive(maxN) - - def loop(i: Int, limit: Int, acc: List[A]): F[List[A]] = - if (i >= limit) - F.pure(acc.reverse) - else - tryTake flatMap { - case Some(a) => loop(i + 1, limit, a :: acc) - case None => F.pure(acc.reverse) - } - - maxN match { - case Some(limit) => loop(0, limit, Nil) - case None => loop(0, Int.MaxValue, Nil) - } - } + override def tryTakeN(maxN: Option[Int])(implicit F: Monad[F]): F[List[A]] = + super.tryTakeN(maxN) } object PQueueSource { - private def assertMaxNPositive(maxN: Option[Int]): Unit = maxN match { - case Some(n) if n <= 0 => - throw new IllegalArgumentException(s"Provided maxN parameter must be positive, was $n") - case _ => () - } implicit def catsFunctorForPQueueSource[F[_]: Functor]: Functor[PQueueSource[F, *]] = new Functor[PQueueSource[F, *]] { @@ -273,14 +253,19 @@ object PQueueSource { trait PQueueSink[F[_], A] extends QueueSink[F, A] { - override def tryOfferN(list: List[A])(implicit F: Monad[F]): F[List[A]] = list match { - case Nil => F.pure(list) - case h :: t => - tryOffer(h).ifM( - tryOfferN(t), - F.pure(list) - ) - } + /** + * Attempts to enqueue the given elements at the back of the queue without semantically + * blocking. If an item in the list cannot be enqueued, the remaining elements will be + * returned. This is a convenience method that recursively runs `tryOffer` and does not offer + * any additional performance benefits. + * + * @param list + * the elements to be put at the back of the queue + * @return + * an effect that contains the remaining valus that could not be offered. + */ + override def tryOfferN(list: List[A])(implicit F: Monad[F]): F[List[A]] = + super.tryOfferN(list) } From 67071563d1fd50acafd33a839c36d08e582a072f Mon Sep 17 00:00:00 2001 From: Neo Lin Date: Sat, 6 Jan 2024 18:59:24 -0500 Subject: [PATCH 300/429] shifting shared logic to companion object --- .../main/scala/cats/effect/std/PQueue.scala | 4 +-- .../main/scala/cats/effect/std/Queue.scala | 29 +++++++++++++------ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/std/shared/src/main/scala/cats/effect/std/PQueue.scala b/std/shared/src/main/scala/cats/effect/std/PQueue.scala index da1fee18c7..62b645a6d4 100644 --- a/std/shared/src/main/scala/cats/effect/std/PQueue.scala +++ b/std/shared/src/main/scala/cats/effect/std/PQueue.scala @@ -231,7 +231,7 @@ trait PQueueSource[F[_], A] extends QueueSource[F, A] { * dequeued is undefined. */ override def tryTakeN(maxN: Option[Int])(implicit F: Monad[F]): F[List[A]] = - super.tryTakeN(maxN) + QueueSource.tryTakeN[F, A](maxN, tryTake) } @@ -265,7 +265,7 @@ trait PQueueSink[F[_], A] extends QueueSink[F, A] { * an effect that contains the remaining valus that could not be offered. */ override def tryOfferN(list: List[A])(implicit F: Monad[F]): F[List[A]] = - super.tryOfferN(list) + QueueSink.tryOfferN(list, tryOffer) } diff --git a/std/shared/src/main/scala/cats/effect/std/Queue.scala b/std/shared/src/main/scala/cats/effect/std/Queue.scala index 635e626113..81e5c827eb 100644 --- a/std/shared/src/main/scala/cats/effect/std/Queue.scala +++ b/std/shared/src/main/scala/cats/effect/std/Queue.scala @@ -1135,7 +1135,16 @@ trait QueueSource[F[_], A] { * @return * an effect that contains the dequeued elements */ - def tryTakeN(maxN: Option[Int])(implicit F: Monad[F]): F[List[A]] = { + def tryTakeN(maxN: Option[Int])(implicit F: Monad[F]): F[List[A]] = + QueueSource.tryTakeN[F, A](maxN, tryTake) + + def size: F[Int] +} + +object QueueSource { + + private[std] def tryTakeN[F[_], A](maxN: Option[Int], tryTake: F[Option[A]])( + implicit F: Monad[F]): F[List[A]] = { QueueSource.assertMaxNPositive(maxN) def loop(i: Int, limit: Int, acc: List[A]): F[List[A]] = @@ -1153,10 +1162,6 @@ trait QueueSource[F[_], A] { } } - def size: F[Int] -} - -object QueueSource { private[std] def assertMaxNPositive(maxN: Option[Int]): Unit = maxN match { case Some(n) if n <= 0 => throw new IllegalArgumentException(s"Provided maxN parameter must be positive, was $n") @@ -1212,17 +1217,23 @@ trait QueueSink[F[_], A] { * @return * an effect that contains the remaining valus that could not be offered. */ - def tryOfferN(list: List[A])(implicit F: Monad[F]): F[List[A]] = list match { + def tryOfferN(list: List[A])(implicit F: Monad[F]): F[List[A]] = + QueueSink.tryOfferN[F, A](list, tryOffer) + +} + +object QueueSink { + + private[std] def tryOfferN[F[_], A](list: List[A], tryOffer: A => F[Boolean])( + implicit F: Monad[F]): F[List[A]] = list match { case Nil => F.pure(list) case h :: t => tryOffer(h).ifM( - tryOfferN(t), + tryOfferN(t, tryOffer), F.pure(list) ) } -} -object QueueSink { implicit def catsContravariantForQueueSink[F[_]]: Contravariant[QueueSink[F, *]] = new Contravariant[QueueSink[F, *]] { override def contramap[A, B](fa: QueueSink[F, A])(f: B => A): QueueSink[F, B] = From 6df6dea3ecdc058724c9297062aff4ba6e0b6231 Mon Sep 17 00:00:00 2001 From: Neo Lin Date: Sat, 6 Jan 2024 19:20:29 -0500 Subject: [PATCH 301/429] shifting shared logic to companion object --- std/shared/src/main/scala/cats/effect/std/PQueue.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/std/shared/src/main/scala/cats/effect/std/PQueue.scala b/std/shared/src/main/scala/cats/effect/std/PQueue.scala index 62b645a6d4..8e74b2eea5 100644 --- a/std/shared/src/main/scala/cats/effect/std/PQueue.scala +++ b/std/shared/src/main/scala/cats/effect/std/PQueue.scala @@ -270,6 +270,7 @@ trait PQueueSink[F[_], A] extends QueueSink[F, A] { } object PQueueSink { + implicit def catsContravariantForPQueueSink[F[_]]: Contravariant[PQueueSink[F, *]] = new Contravariant[PQueueSink[F, *]] { override def contramap[A, B](fa: PQueueSink[F, A])(f: B => A): PQueueSink[F, B] = From 98aae1155749f306043ee5cb54d48e6f7a7f737f Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Sun, 7 Jan 2024 16:05:04 +0000 Subject: [PATCH 302/429] Update sbt-mdoc to 2.5.2 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 02317e9e3c..34b997f6d5 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,7 +6,7 @@ addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.16") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.6") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.1") +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.2") addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.1") From facc6798da518c95e9d74230a945a9fb5edc45ea Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 11:18:53 +0000 Subject: [PATCH 303/429] Update sbt-typelevel to 0.6.5 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 02317e9e3c..97ed7edd8e 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.6.4") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.6.5") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.16") From 423e9e2e2d778eb029472b83f9bdfd02cc1717d4 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 11:20:06 +0000 Subject: [PATCH 304/429] Run prePR with sbt-typelevel Executed command: sbt tlPrePrBotHook --- .github/workflows/ci.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e53f860d93..aecfcaaffb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -292,7 +292,7 @@ jobs: - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: target-${{ matrix.os }}-${{ matrix.java }}-${{ matrix.scala }}-${{ matrix.ci }} path: targets.tar @@ -382,7 +382,7 @@ jobs: run: sbt +update - name: Download target directories (3.3.1, ciJVM) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.1-ciJVM @@ -392,7 +392,7 @@ jobs: rm targets.tar - name: Download target directories (3.3.1, ciNative) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.1-ciNative @@ -402,7 +402,7 @@ jobs: rm targets.tar - name: Download target directories (3.3.1, ciJS) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.1-ciJS @@ -412,7 +412,7 @@ jobs: rm targets.tar - name: Download target directories (2.12.18, ciJVM) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.18-ciJVM @@ -422,7 +422,7 @@ jobs: rm targets.tar - name: Download target directories (2.12.18, ciNative) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.18-ciNative @@ -432,7 +432,7 @@ jobs: rm targets.tar - name: Download target directories (2.12.18, ciJS) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.18-ciJS @@ -442,7 +442,7 @@ jobs: rm targets.tar - name: Download target directories (2.13.12, ciJVM) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.12-ciJVM @@ -452,7 +452,7 @@ jobs: rm targets.tar - name: Download target directories (2.13.12, ciNative) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.12-ciNative @@ -462,7 +462,7 @@ jobs: rm targets.tar - name: Download target directories (2.13.12, ciJS) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.12-ciJS @@ -472,7 +472,7 @@ jobs: rm targets.tar - name: Download target directories (2.13.12, ciFirefox) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.12-ciFirefox @@ -482,7 +482,7 @@ jobs: rm targets.tar - name: Download target directories (2.13.12, ciChrome) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.12-ciChrome From 8927283b20c457055878c280d6cd2317a0794f3c Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 8 Jan 2024 12:17:47 +0000 Subject: [PATCH 305/429] Apply `@static` annotation internally --- .../scala/cats/effect/CallbackStack.scala | 3 - .../main/scala/cats/effect/ArrayStack.scala | 6 +- .../main/scala/cats/effect/ByteStack.scala | 24 +++++--- .../scala/cats/effect/CallbackStack.scala | 3 - .../main/scala/cats/effect/ContState.scala | 4 +- .../src/main/scala/cats/effect/IO.scala | 10 ++- .../src/main/scala/cats/effect/IOFiber.scala | 10 +-- .../src/main/scala/cats/effect/SyncIO.scala | 61 ++++++++++--------- .../cats/effect/tracing/RingBuffer.scala | 4 +- .../scala/cats/effect/tracing/Tracing.scala | 31 ++++++---- 10 files changed, 89 insertions(+), 67 deletions(-) diff --git a/core/js/src/main/scala/cats/effect/CallbackStack.scala b/core/js/src/main/scala/cats/effect/CallbackStack.scala index faf744cbfb..defac2f5d9 100644 --- a/core/js/src/main/scala/cats/effect/CallbackStack.scala +++ b/core/js/src/main/scala/cats/effect/CallbackStack.scala @@ -62,9 +62,6 @@ private final class CallbackStackOps[A](private val callbacks: js.Array[A => Uni } private object CallbackStack { - @inline def apply[A](cb: A => Unit): CallbackStack[A] = - js.Array(cb).asInstanceOf[CallbackStack[A]] - @inline implicit def ops[A](stack: CallbackStack[A]): CallbackStackOps[A] = new CallbackStackOps(stack.asInstanceOf[js.Array[A => Unit]]) diff --git a/core/jvm-native/src/main/scala/cats/effect/ArrayStack.scala b/core/jvm-native/src/main/scala/cats/effect/ArrayStack.scala index 81e312717c..48ca533df5 100644 --- a/core/jvm-native/src/main/scala/cats/effect/ArrayStack.scala +++ b/core/jvm-native/src/main/scala/cats/effect/ArrayStack.scala @@ -16,6 +16,8 @@ package cats.effect +import org.typelevel.scalaccompat.annotation._ + private final class ArrayStack[A <: AnyRef]( private[this] var buffer: Array[AnyRef], private[this] var index: Int) { @@ -78,8 +80,8 @@ private final class ArrayStack[A <: AnyRef]( private object ArrayStack { - def apply[A <: AnyRef](): ArrayStack[A] = new ArrayStack() + @static3 def apply[A <: AnyRef](): ArrayStack[A] = new ArrayStack() - def apply[A <: AnyRef](size: Int): ArrayStack[A] = new ArrayStack(size) + @static3 def apply[A <: AnyRef](size: Int): ArrayStack[A] = new ArrayStack(size) } diff --git a/core/jvm-native/src/main/scala/cats/effect/ByteStack.scala b/core/jvm-native/src/main/scala/cats/effect/ByteStack.scala index 4d2bc16ef4..e50131abf2 100644 --- a/core/jvm-native/src/main/scala/cats/effect/ByteStack.scala +++ b/core/jvm-native/src/main/scala/cats/effect/ByteStack.scala @@ -16,11 +16,17 @@ package cats.effect +import org.typelevel.scalaccompat.annotation._ + +private final class ByteStack private () + private object ByteStack { type T = Array[Int] - final def toDebugString(stack: Array[Int], translate: Byte => String = _.toString): String = { + @static3 final def toDebugString( + stack: Array[Int], + translate: Byte => String = _.toString): String = { val count = size(stack) ((count - 1) to 0 by -1) .foldLeft( @@ -38,10 +44,10 @@ private object ByteStack { .toString } - final def create(initialMaxOps: Int): Array[Int] = + @static3 final def create(initialMaxOps: Int): Array[Int] = new Array[Int](1 + 1 + ((initialMaxOps - 1) >> 3)) // count-slot + 1 for each set of 8 ops - final def growIfNeeded(stack: Array[Int], count: Int): Array[Int] = { + @static3 final def growIfNeeded(stack: Array[Int], count: Int): Array[Int] = { if ((1 + ((count + 1) >> 3)) < stack.length) { stack } else { @@ -51,7 +57,7 @@ private object ByteStack { } } - final def push(stack: Array[Int], op: Byte): Array[Int] = { + @static3 final def push(stack: Array[Int], op: Byte): Array[Int] = { val c = stack(0) // current count of elements val use = growIfNeeded(stack, c) // alias so we add to the right place val s = (c >> 3) + 1 // current slot in `use` @@ -61,24 +67,24 @@ private object ByteStack { use } - final def size(stack: Array[Int]): Int = + @static3 final def size(stack: Array[Int]): Int = stack(0) - final def isEmpty(stack: Array[Int]): Boolean = + @static3 final def isEmpty(stack: Array[Int]): Boolean = stack(0) < 1 - final def read(stack: Array[Int], pos: Int): Byte = { + @static3 final def read(stack: Array[Int], pos: Int): Byte = { if (pos < 0 || pos >= stack(0)) throw new ArrayIndexOutOfBoundsException() ((stack((pos >> 3) + 1) >>> ((pos & 7) << 2)) & 0x0000000f).toByte } - final def peek(stack: Array[Int]): Byte = { + @static3 final def peek(stack: Array[Int]): Byte = { val c = stack(0) - 1 if (c < 0) throw new ArrayIndexOutOfBoundsException() ((stack((c >> 3) + 1) >>> ((c & 7) << 2)) & 0x0000000f).toByte } - final def pop(stack: Array[Int]): Byte = { + @static3 final def pop(stack: Array[Int]): Byte = { val op = peek(stack) stack(0) -= 1 op diff --git a/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala b/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala index 8ba7b08a2d..ef8f2ed4ac 100644 --- a/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala +++ b/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala @@ -149,8 +149,5 @@ private final class CallbackStack[A](private[this] var callback: A => Unit) } private object CallbackStack { - def apply[A](cb: A => Unit): CallbackStack[A] = - new CallbackStack(cb) - type Handle = Byte } diff --git a/core/shared/src/main/scala/cats/effect/ContState.scala b/core/shared/src/main/scala/cats/effect/ContState.scala index 86a76ceae5..5f65ee184f 100644 --- a/core/shared/src/main/scala/cats/effect/ContState.scala +++ b/core/shared/src/main/scala/cats/effect/ContState.scala @@ -18,6 +18,8 @@ package cats.effect import cats.effect.unsafe.WeakBag +import org.typelevel.scalaccompat.annotation._ + import java.util.concurrent.atomic.AtomicReference /** @@ -49,6 +51,6 @@ private object ContState { * important. It must be private (so that no user code can access it), and it mustn't be used * for any other purpose. */ - private val waitingSentinel: Either[Throwable, Any] = + @static3 private val waitingSentinel: Either[Throwable, Any] = new Right(null) } diff --git a/core/shared/src/main/scala/cats/effect/IO.scala b/core/shared/src/main/scala/cats/effect/IO.scala index a54724a9e0..feea41d332 100644 --- a/core/shared/src/main/scala/cats/effect/IO.scala +++ b/core/shared/src/main/scala/cats/effect/IO.scala @@ -44,6 +44,8 @@ import cats.effect.tracing.{Tracing, TracingEvent} import cats.effect.unsafe.IORuntime import cats.syntax.all._ +import org.typelevel.scalaccompat.annotation._ + import scala.annotation.unchecked.uncheckedVariance import scala.concurrent.{ CancellationException, @@ -1109,6 +1111,9 @@ private[effect] trait IOLowPriorityImplicits { object IO extends IOCompanionPlatform with IOLowPriorityImplicits { + @static3 private[this] val _alignForIO = new IOAlign + @static3 private[this] val _asyncForIO: kernel.Async[IO] = new IOAsync + /** * Newtype encoding for an `IO` datatype that has a `cats.Applicative` capable of doing * parallel processing in `ap` and `map2`, needed for implementing `cats.Parallel`. @@ -1787,7 +1792,7 @@ object IO extends IOCompanionPlatform with IOLowPriorityImplicits { implicit def alignForIO: Align[IO] = _alignForIO - private[this] val _alignForIO = new Align[IO] { + private[this] final class IOAlign extends Align[IO] { def align[A, B](fa: IO[A], fb: IO[B]): IO[Ior[A, B]] = alignWith(fa, fb)(identity) @@ -1800,8 +1805,7 @@ object IO extends IOCompanionPlatform with IOLowPriorityImplicits { def functor: Functor[IO] = Functor[IO] } - private[this] val _asyncForIO: kernel.Async[IO] = new kernel.Async[IO] - with StackSafeMonad[IO] { + private[this] final class IOAsync extends kernel.Async[IO] with StackSafeMonad[IO] { override def asyncCheckAttempt[A]( k: (Either[Throwable, A] => Unit) => IO[Either[Option[IO[Unit]], A]]): IO[A] = diff --git a/core/shared/src/main/scala/cats/effect/IOFiber.scala b/core/shared/src/main/scala/cats/effect/IOFiber.scala index 0fdda58bb0..3950aaa123 100644 --- a/core/shared/src/main/scala/cats/effect/IOFiber.scala +++ b/core/shared/src/main/scala/cats/effect/IOFiber.scala @@ -20,6 +20,8 @@ import cats.arrow.FunctionK import cats.effect.tracing._ import cats.effect.unsafe._ +import org.typelevel.scalaccompat.annotation._ + import scala.annotation.{switch, tailrec} import scala.concurrent.ExecutionContext import scala.concurrent.duration._ @@ -1569,11 +1571,11 @@ private final class IOFiber[A]( private object IOFiber { /* prefetch */ - private[IOFiber] val TypeBlocking = Sync.Type.Blocking - private[IOFiber] val OutcomeCanceled = Outcome.Canceled() - private[effect] val RightUnit = Right(()) + @static3 private[IOFiber] val TypeBlocking = Sync.Type.Blocking + @static3 private[IOFiber] val OutcomeCanceled = Outcome.Canceled() + @static3 private[effect] val RightUnit = Right(()) - def onFatalFailure(t: Throwable): Nothing = { + @static3 def onFatalFailure(t: Throwable): Nothing = { val interrupted = Thread.interrupted() if (IORuntime.globalFatalFailureHandled.compareAndSet(false, true)) { diff --git a/core/shared/src/main/scala/cats/effect/SyncIO.scala b/core/shared/src/main/scala/cats/effect/SyncIO.scala index aad9095eaf..06f2475f1c 100644 --- a/core/shared/src/main/scala/cats/effect/SyncIO.scala +++ b/core/shared/src/main/scala/cats/effect/SyncIO.scala @@ -22,6 +22,8 @@ import cats.effect.syntax.monadCancel._ import cats.kernel.{Monoid, Semigroup} import cats.syntax.all._ +import org.typelevel.scalaccompat.annotation._ + import scala.annotation.{switch, tailrec} import scala.annotation.unchecked.uncheckedVariance import scala.concurrent.duration._ @@ -396,7 +398,8 @@ private[effect] trait SyncIOLowPriorityImplicits { object SyncIO extends SyncIOCompanionPlatform with SyncIOLowPriorityImplicits { - private[this] val Delay = Sync.Type.Delay + @static3 private[this] val Delay = Sync.Type.Delay + @static3 private[this] val _syncForSyncIO: Sync[SyncIO] = new SyncIOSync // constructors @@ -576,48 +579,48 @@ object SyncIO extends SyncIOCompanionPlatform with SyncIOLowPriorityImplicits { def functor: Functor[SyncIO] = Functor[SyncIO] } - private[this] val _syncForSyncIO: Sync[SyncIO] = - new Sync[SyncIO] + private[this] final class SyncIOSync + extends Sync[SyncIO] with StackSafeMonad[SyncIO] with MonadCancel.Uncancelable[SyncIO, Throwable] { - def pure[A](x: A): SyncIO[A] = - SyncIO.pure(x) + def pure[A](x: A): SyncIO[A] = + SyncIO.pure(x) - def raiseError[A](e: Throwable): SyncIO[A] = - SyncIO.raiseError(e) + def raiseError[A](e: Throwable): SyncIO[A] = + SyncIO.raiseError(e) - def handleErrorWith[A](fa: SyncIO[A])(f: Throwable => SyncIO[A]): SyncIO[A] = - fa.handleErrorWith(f) + def handleErrorWith[A](fa: SyncIO[A])(f: Throwable => SyncIO[A]): SyncIO[A] = + fa.handleErrorWith(f) - def flatMap[A, B](fa: SyncIO[A])(f: A => SyncIO[B]): SyncIO[B] = - fa.flatMap(f) + def flatMap[A, B](fa: SyncIO[A])(f: A => SyncIO[B]): SyncIO[B] = + fa.flatMap(f) - def monotonic: SyncIO[FiniteDuration] = - SyncIO.monotonic + def monotonic: SyncIO[FiniteDuration] = + SyncIO.monotonic - def realTime: SyncIO[FiniteDuration] = - SyncIO.realTime + def realTime: SyncIO[FiniteDuration] = + SyncIO.realTime - def suspend[A](hint: Sync.Type)(thunk: => A): SyncIO[A] = - Suspend(hint, () => thunk) + def suspend[A](hint: Sync.Type)(thunk: => A): SyncIO[A] = + Suspend(hint, () => thunk) - override def attempt[A](fa: SyncIO[A]): SyncIO[Either[Throwable, A]] = - fa.attempt + override def attempt[A](fa: SyncIO[A]): SyncIO[Either[Throwable, A]] = + fa.attempt - override def redeem[A, B](fa: SyncIO[A])(recover: Throwable => B, f: A => B): SyncIO[B] = - fa.redeem(recover, f) + override def redeem[A, B](fa: SyncIO[A])(recover: Throwable => B, f: A => B): SyncIO[B] = + fa.redeem(recover, f) - override def redeemWith[A, B]( - fa: SyncIO[A])(recover: Throwable => SyncIO[B], bind: A => SyncIO[B]): SyncIO[B] = - fa.redeemWith(recover, bind) + override def redeemWith[A, B]( + fa: SyncIO[A])(recover: Throwable => SyncIO[B], bind: A => SyncIO[B]): SyncIO[B] = + fa.redeemWith(recover, bind) - override def unit: SyncIO[Unit] = - SyncIO.unit + override def unit: SyncIO[Unit] = + SyncIO.unit - def forceR[A, B](fa: SyncIO[A])(fb: SyncIO[B]): SyncIO[B] = - fa.attempt.productR(fb) - } + def forceR[A, B](fa: SyncIO[A])(fb: SyncIO[B]): SyncIO[B] = + fa.attempt.productR(fb) + } implicit def syncForSyncIO: Sync[SyncIO] with MonadCancel[SyncIO, Throwable] = _syncForSyncIO diff --git a/core/shared/src/main/scala/cats/effect/tracing/RingBuffer.scala b/core/shared/src/main/scala/cats/effect/tracing/RingBuffer.scala index 36b51007da..f4985305d0 100644 --- a/core/shared/src/main/scala/cats/effect/tracing/RingBuffer.scala +++ b/core/shared/src/main/scala/cats/effect/tracing/RingBuffer.scala @@ -16,6 +16,8 @@ package cats.effect.tracing +import org.typelevel.scalaccompat.annotation._ + private[effect] final class RingBuffer private (logSize: Int) { private[this] val length = 1 << logSize @@ -59,6 +61,6 @@ private[effect] final class RingBuffer private (logSize: Int) { } private[effect] object RingBuffer { - def empty(logSize: Int): RingBuffer = + @static3 def empty(logSize: Int): RingBuffer = new RingBuffer(logSize) } diff --git a/core/shared/src/main/scala/cats/effect/tracing/Tracing.scala b/core/shared/src/main/scala/cats/effect/tracing/Tracing.scala index adecec18d9..56b650b099 100644 --- a/core/shared/src/main/scala/cats/effect/tracing/Tracing.scala +++ b/core/shared/src/main/scala/cats/effect/tracing/Tracing.scala @@ -18,29 +18,33 @@ package cats.effect.tracing import cats.effect.{IOFiber, Trace} +import org.typelevel.scalaccompat.annotation._ + import scala.collection.mutable.ArrayBuffer +private[effect] final class Tracing private () + private[effect] object Tracing extends TracingPlatform { import TracingConstants._ - private[this] final val TurnRight = "╰" + @static3 private[this] final val TurnRight = "╰" // private[this] final val InverseTurnRight = "╭" - private[this] final val Junction = "├" + @static3 private[this] final val Junction = "├" // private[this] final val Line = "│" - private[tracing] def buildEvent(): TracingEvent = { + @static3 private[tracing] def buildEvent(): TracingEvent = { new TracingEvent.StackTrace() } - private[this] final val runLoopFilter: Array[String] = + @static3 private[this] final val runLoopFilter: Array[String] = Array( "cats.effect.", "scala.runtime.", "scala.scalajs.runtime.", "scala.scalanative.runtime.") - private[tracing] final val stackTraceClassNameFilter: Array[String] = Array( + @static3 private[tracing] final val stackTraceClassNameFilter: Array[String] = Array( "cats.", "sbt.", "java.", @@ -50,7 +54,7 @@ private[effect] object Tracing extends TracingPlatform { "org.scalajs." ) - private[tracing] def combineOpAndCallSite( + @static3 private[tracing] def combineOpAndCallSite( methodSite: StackTraceElement, callSite: StackTraceElement): StackTraceElement = { val methodSiteMethodName = methodSite.getMethodName @@ -64,7 +68,7 @@ private[effect] object Tracing extends TracingPlatform { ) } - private[tracing] def isInternalClass(className: String): Boolean = { + @static3 private[tracing] def isInternalClass(className: String): Boolean = { var i = 0 val len = stackTraceClassNameFilter.length while (i < len) { @@ -75,7 +79,7 @@ private[effect] object Tracing extends TracingPlatform { false } - private[this] def getOpAndCallSite( + @static3 private[this] def getOpAndCallSite( stackTrace: Array[StackTraceElement]): StackTraceElement = { val len = stackTrace.length var idx = 1 @@ -98,7 +102,10 @@ private[effect] object Tracing extends TracingPlatform { null } - def augmentThrowable(enhancedExceptions: Boolean, t: Throwable, events: RingBuffer): Unit = { + @static3 def augmentThrowable( + enhancedExceptions: Boolean, + t: Throwable, + events: RingBuffer): Unit = { def applyRunLoopFilter(ste: StackTraceElement): Boolean = { val name = ste.getClassName var i = 0 @@ -144,13 +151,13 @@ private[effect] object Tracing extends TracingPlatform { } } - def getFrames(events: RingBuffer): List[StackTraceElement] = + @static3 def getFrames(events: RingBuffer): List[StackTraceElement] = events .toList() .collect { case ev: TracingEvent.StackTrace => getOpAndCallSite(ev.getStackTrace) } .filter(_ ne null) - def prettyPrint(trace: Trace): String = { + @static3 def prettyPrint(trace: Trace): String = { val frames = trace.toList frames @@ -163,7 +170,7 @@ private[effect] object Tracing extends TracingPlatform { .mkString(System.lineSeparator()) } - def captureTrace(runnable: Runnable): Option[(Runnable, Trace)] = { + @static3 def captureTrace(runnable: Runnable): Option[(Runnable, Trace)] = { runnable match { case f: IOFiber[_] if f.isDone => None case f: IOFiber[_] => Some(runnable -> f.captureTrace()) From 2309e40e1c148a5356882bf41508ead3144f10c2 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 8 Jan 2024 12:26:44 +0000 Subject: [PATCH 306/429] Fix `CallbackStack` ctor --- core/js/src/main/scala/cats/effect/CallbackStack.scala | 3 +++ .../src/main/scala/cats/effect/CallbackStack.scala | 5 +++++ core/shared/src/main/scala/cats/effect/IOFiber.scala | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/core/js/src/main/scala/cats/effect/CallbackStack.scala b/core/js/src/main/scala/cats/effect/CallbackStack.scala index defac2f5d9..cebf61d63f 100644 --- a/core/js/src/main/scala/cats/effect/CallbackStack.scala +++ b/core/js/src/main/scala/cats/effect/CallbackStack.scala @@ -62,6 +62,9 @@ private final class CallbackStackOps[A](private val callbacks: js.Array[A => Uni } private object CallbackStack { + @inline def of[A](cb: A => Unit): CallbackStack[A] = + js.Array(cb).asInstanceOf[CallbackStack[A]] + @inline implicit def ops[A](stack: CallbackStack[A]): CallbackStackOps[A] = new CallbackStackOps(stack.asInstanceOf[js.Array[A => Unit]]) diff --git a/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala b/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala index ef8f2ed4ac..5389fd55cd 100644 --- a/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala +++ b/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala @@ -16,6 +16,8 @@ package cats.effect +import org.typelevel.scalaccompat.annotation._ + import scala.annotation.tailrec import java.util.concurrent.atomic.AtomicReference @@ -149,5 +151,8 @@ private final class CallbackStack[A](private[this] var callback: A => Unit) } private object CallbackStack { + @static3 def of[A](cb: A => Unit): CallbackStack[A] = + new CallbackStack(cb) + type Handle = Byte } diff --git a/core/shared/src/main/scala/cats/effect/IOFiber.scala b/core/shared/src/main/scala/cats/effect/IOFiber.scala index 3950aaa123..757d5759fe 100644 --- a/core/shared/src/main/scala/cats/effect/IOFiber.scala +++ b/core/shared/src/main/scala/cats/effect/IOFiber.scala @@ -87,7 +87,7 @@ private final class IOFiber[A]( private[this] var currentCtx: ExecutionContext = startEC private[this] val objectState: ArrayStack[AnyRef] = ArrayStack() private[this] val finalizers: ArrayStack[IO[Unit]] = ArrayStack() - private[this] val callbacks: CallbackStack[OutcomeIO[A]] = CallbackStack(cb) + private[this] val callbacks: CallbackStack[OutcomeIO[A]] = CallbackStack.of(cb) private[this] var resumeTag: Byte = ExecR private[this] var resumeIO: IO[Any] = startIO private[this] val runtime: IORuntime = rt From 1d000764283a03fe8f061c880177c04dda1d1426 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 8 Jan 2024 12:32:11 +0000 Subject: [PATCH 307/429] Fix compile --- core/shared/src/main/scala/cats/effect/IODeferred.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shared/src/main/scala/cats/effect/IODeferred.scala b/core/shared/src/main/scala/cats/effect/IODeferred.scala index dd97abb27e..382a3db6cf 100644 --- a/core/shared/src/main/scala/cats/effect/IODeferred.scala +++ b/core/shared/src/main/scala/cats/effect/IODeferred.scala @@ -56,7 +56,7 @@ private final class IODeferred[A] extends Deferred[IO, A] { } private[this] val cell = new AtomicReference(initial) - private[this] val callbacks = CallbackStack[Right[Nothing, A]](null) + private[this] val callbacks = CallbackStack.of[Right[Nothing, A]](null) private[this] val clearCounter = new AtomicInteger def complete(a: A): IO[Boolean] = IO { From 001c635ce8fbc8d293371512fb72c5d630b80000 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 8 Jan 2024 12:55:31 +0000 Subject: [PATCH 308/429] Fix compile --- core/jvm-native/src/main/scala/cats/effect/ByteStack.scala | 2 +- core/shared/src/main/scala/cats/effect/IOFiber.scala | 2 +- core/shared/src/main/scala/cats/effect/package.scala | 2 -- core/shared/src/main/scala/cats/effect/tracing/Tracing.scala | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/core/jvm-native/src/main/scala/cats/effect/ByteStack.scala b/core/jvm-native/src/main/scala/cats/effect/ByteStack.scala index e50131abf2..fe0dc678af 100644 --- a/core/jvm-native/src/main/scala/cats/effect/ByteStack.scala +++ b/core/jvm-native/src/main/scala/cats/effect/ByteStack.scala @@ -18,7 +18,7 @@ package cats.effect import org.typelevel.scalaccompat.annotation._ -private final class ByteStack private () +private[effect] final class ByteStack private object ByteStack { diff --git a/core/shared/src/main/scala/cats/effect/IOFiber.scala b/core/shared/src/main/scala/cats/effect/IOFiber.scala index 757d5759fe..e9e742f947 100644 --- a/core/shared/src/main/scala/cats/effect/IOFiber.scala +++ b/core/shared/src/main/scala/cats/effect/IOFiber.scala @@ -98,7 +98,7 @@ private final class IOFiber[A]( * Ideally these would be on the stack, but they can't because we sometimes need to * relocate our runloop to another fiber. */ - private[this] var conts: ByteStack = _ + private[this] var conts: ByteStack.T = _ private[this] var canceled: Boolean = false private[this] var masks: Int = 0 diff --git a/core/shared/src/main/scala/cats/effect/package.scala b/core/shared/src/main/scala/cats/effect/package.scala index 8231b24620..0af80f188e 100644 --- a/core/shared/src/main/scala/cats/effect/package.scala +++ b/core/shared/src/main/scala/cats/effect/package.scala @@ -80,6 +80,4 @@ package object effect { val Ref = cekernel.Ref private[effect] type IOLocalState = scala.collection.immutable.Map[IOLocal[_], Any] - - private[effect] type ByteStack = ByteStack.T } diff --git a/core/shared/src/main/scala/cats/effect/tracing/Tracing.scala b/core/shared/src/main/scala/cats/effect/tracing/Tracing.scala index 56b650b099..808e759574 100644 --- a/core/shared/src/main/scala/cats/effect/tracing/Tracing.scala +++ b/core/shared/src/main/scala/cats/effect/tracing/Tracing.scala @@ -22,7 +22,7 @@ import org.typelevel.scalaccompat.annotation._ import scala.collection.mutable.ArrayBuffer -private[effect] final class Tracing private () +private[effect] final class Tracing private[effect] object Tracing extends TracingPlatform { From 416bf4f7e6ff487dfb0c7fe0629f2bd6335886ab Mon Sep 17 00:00:00 2001 From: Neo Lin Date: Tue, 9 Jan 2024 18:18:26 -0500 Subject: [PATCH 309/429] reverting the comments in PQueueSink --- std/shared/src/main/scala/cats/effect/std/PQueue.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/std/shared/src/main/scala/cats/effect/std/PQueue.scala b/std/shared/src/main/scala/cats/effect/std/PQueue.scala index 8e74b2eea5..4ef532a570 100644 --- a/std/shared/src/main/scala/cats/effect/std/PQueue.scala +++ b/std/shared/src/main/scala/cats/effect/std/PQueue.scala @@ -254,13 +254,13 @@ object PQueueSource { trait PQueueSink[F[_], A] extends QueueSink[F, A] { /** - * Attempts to enqueue the given elements at the back of the queue without semantically - * blocking. If an item in the list cannot be enqueued, the remaining elements will be - * returned. This is a convenience method that recursively runs `tryOffer` and does not offer - * any additional performance benefits. + * Attempts to enqueue the given elements without semantically blocking. If an item in the + * list cannot be enqueued, the remaining elements will be returned. This is a convenience + * method that recursively runs `tryOffer` and does not offer any additional performance + * benefits. * * @param list - * the elements to be put at the back of the queue + * the elements to be put in the PQueue * @return * an effect that contains the remaining valus that could not be offered. */ From 8e31fcd5e0c75080d08b97b8823441860614bb66 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Fri, 12 Jan 2024 00:15:47 +0000 Subject: [PATCH 310/429] Update sbt-scalajs, scalajs-compiler, ... to 1.15.0 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index d0cfa1c188..0d6ae5c97d 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,7 +2,7 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.6.5") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.15.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.16") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.6") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") From b7228d5e09380c187ee66fd774fbc172a4048150 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 13 Jan 2024 16:19:52 +0000 Subject: [PATCH 311/429] Try out Scala 3.4 snapshot --- build.sbt | 2 +- core/js/src/main/scala/cats/effect/CallbackStack.scala | 2 +- .../jvm-native/src/main/scala/cats/effect/CallbackStack.scala | 4 ++-- core/shared/src/main/scala/cats/effect/IODeferred.scala | 2 +- core/shared/src/main/scala/cats/effect/IOFiber.scala | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build.sbt b/build.sbt index cadc5b33d9..06636fd702 100644 --- a/build.sbt +++ b/build.sbt @@ -114,7 +114,7 @@ val MacOS = "macos-latest" val Scala212 = "2.12.18" val Scala213 = "2.13.12" -val Scala3 = "3.3.1" +val Scala3 = "3.4.0-RC1-bin-20240112-c50f2ff-NIGHTLY" ThisBuild / crossScalaVersions := Seq(Scala3, Scala212, Scala213) ThisBuild / githubWorkflowScalaVersions := crossScalaVersions.value diff --git a/core/js/src/main/scala/cats/effect/CallbackStack.scala b/core/js/src/main/scala/cats/effect/CallbackStack.scala index cebf61d63f..faf744cbfb 100644 --- a/core/js/src/main/scala/cats/effect/CallbackStack.scala +++ b/core/js/src/main/scala/cats/effect/CallbackStack.scala @@ -62,7 +62,7 @@ private final class CallbackStackOps[A](private val callbacks: js.Array[A => Uni } private object CallbackStack { - @inline def of[A](cb: A => Unit): CallbackStack[A] = + @inline def apply[A](cb: A => Unit): CallbackStack[A] = js.Array(cb).asInstanceOf[CallbackStack[A]] @inline implicit def ops[A](stack: CallbackStack[A]): CallbackStackOps[A] = diff --git a/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala b/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala index 5389fd55cd..087e2865c5 100644 --- a/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala +++ b/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala @@ -22,7 +22,7 @@ import scala.annotation.tailrec import java.util.concurrent.atomic.AtomicReference -private final class CallbackStack[A](private[this] var callback: A => Unit) +private final class CallbackStack[A] private (private[this] var callback: A => Unit) extends AtomicReference[CallbackStack[A]] { def push(next: A => Unit): CallbackStack[A] = { @@ -151,7 +151,7 @@ private final class CallbackStack[A](private[this] var callback: A => Unit) } private object CallbackStack { - @static3 def of[A](cb: A => Unit): CallbackStack[A] = + @static3 def apply[A](cb: A => Unit): CallbackStack[A] = new CallbackStack(cb) type Handle = Byte diff --git a/core/shared/src/main/scala/cats/effect/IODeferred.scala b/core/shared/src/main/scala/cats/effect/IODeferred.scala index 382a3db6cf..dd97abb27e 100644 --- a/core/shared/src/main/scala/cats/effect/IODeferred.scala +++ b/core/shared/src/main/scala/cats/effect/IODeferred.scala @@ -56,7 +56,7 @@ private final class IODeferred[A] extends Deferred[IO, A] { } private[this] val cell = new AtomicReference(initial) - private[this] val callbacks = CallbackStack.of[Right[Nothing, A]](null) + private[this] val callbacks = CallbackStack[Right[Nothing, A]](null) private[this] val clearCounter = new AtomicInteger def complete(a: A): IO[Boolean] = IO { diff --git a/core/shared/src/main/scala/cats/effect/IOFiber.scala b/core/shared/src/main/scala/cats/effect/IOFiber.scala index e9e742f947..bd3f570f33 100644 --- a/core/shared/src/main/scala/cats/effect/IOFiber.scala +++ b/core/shared/src/main/scala/cats/effect/IOFiber.scala @@ -87,7 +87,7 @@ private final class IOFiber[A]( private[this] var currentCtx: ExecutionContext = startEC private[this] val objectState: ArrayStack[AnyRef] = ArrayStack() private[this] val finalizers: ArrayStack[IO[Unit]] = ArrayStack() - private[this] val callbacks: CallbackStack[OutcomeIO[A]] = CallbackStack.of(cb) + private[this] val callbacks: CallbackStack[OutcomeIO[A]] = CallbackStack(cb) private[this] var resumeTag: Byte = ExecR private[this] var resumeIO: IO[Any] = startIO private[this] val runtime: IORuntime = rt From df2b7b850329f1342db76d4f1c206ff136518d2c Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 13 Jan 2024 16:24:26 +0000 Subject: [PATCH 312/429] Actually still need `CallbackStack.of` ctor --- core/js/src/main/scala/cats/effect/CallbackStack.scala | 2 +- .../jvm-native/src/main/scala/cats/effect/CallbackStack.scala | 4 ++-- core/shared/src/main/scala/cats/effect/IODeferred.scala | 2 +- core/shared/src/main/scala/cats/effect/IOFiber.scala | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/js/src/main/scala/cats/effect/CallbackStack.scala b/core/js/src/main/scala/cats/effect/CallbackStack.scala index faf744cbfb..cebf61d63f 100644 --- a/core/js/src/main/scala/cats/effect/CallbackStack.scala +++ b/core/js/src/main/scala/cats/effect/CallbackStack.scala @@ -62,7 +62,7 @@ private final class CallbackStackOps[A](private val callbacks: js.Array[A => Uni } private object CallbackStack { - @inline def apply[A](cb: A => Unit): CallbackStack[A] = + @inline def of[A](cb: A => Unit): CallbackStack[A] = js.Array(cb).asInstanceOf[CallbackStack[A]] @inline implicit def ops[A](stack: CallbackStack[A]): CallbackStackOps[A] = diff --git a/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala b/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala index 087e2865c5..5389fd55cd 100644 --- a/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala +++ b/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala @@ -22,7 +22,7 @@ import scala.annotation.tailrec import java.util.concurrent.atomic.AtomicReference -private final class CallbackStack[A] private (private[this] var callback: A => Unit) +private final class CallbackStack[A](private[this] var callback: A => Unit) extends AtomicReference[CallbackStack[A]] { def push(next: A => Unit): CallbackStack[A] = { @@ -151,7 +151,7 @@ private final class CallbackStack[A] private (private[this] var callback: A => U } private object CallbackStack { - @static3 def apply[A](cb: A => Unit): CallbackStack[A] = + @static3 def of[A](cb: A => Unit): CallbackStack[A] = new CallbackStack(cb) type Handle = Byte diff --git a/core/shared/src/main/scala/cats/effect/IODeferred.scala b/core/shared/src/main/scala/cats/effect/IODeferred.scala index dd97abb27e..382a3db6cf 100644 --- a/core/shared/src/main/scala/cats/effect/IODeferred.scala +++ b/core/shared/src/main/scala/cats/effect/IODeferred.scala @@ -56,7 +56,7 @@ private final class IODeferred[A] extends Deferred[IO, A] { } private[this] val cell = new AtomicReference(initial) - private[this] val callbacks = CallbackStack[Right[Nothing, A]](null) + private[this] val callbacks = CallbackStack.of[Right[Nothing, A]](null) private[this] val clearCounter = new AtomicInteger def complete(a: A): IO[Boolean] = IO { diff --git a/core/shared/src/main/scala/cats/effect/IOFiber.scala b/core/shared/src/main/scala/cats/effect/IOFiber.scala index bd3f570f33..e9e742f947 100644 --- a/core/shared/src/main/scala/cats/effect/IOFiber.scala +++ b/core/shared/src/main/scala/cats/effect/IOFiber.scala @@ -87,7 +87,7 @@ private final class IOFiber[A]( private[this] var currentCtx: ExecutionContext = startEC private[this] val objectState: ArrayStack[AnyRef] = ArrayStack() private[this] val finalizers: ArrayStack[IO[Unit]] = ArrayStack() - private[this] val callbacks: CallbackStack[OutcomeIO[A]] = CallbackStack(cb) + private[this] val callbacks: CallbackStack[OutcomeIO[A]] = CallbackStack.of(cb) private[this] var resumeTag: Byte = ExecR private[this] var resumeIO: IO[Any] = startIO private[this] val runtime: IORuntime = rt From 29f3e4da1d5e912c9da203d5006825e991e0d429 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 13 Jan 2024 20:51:38 +0000 Subject: [PATCH 313/429] Optimize `TimerHeap` packing strategy --- .../scala/cats/effect/unsafe/TimerHeap.scala | 103 ++++++++++++++---- .../cats/effect/unsafe/TimerHeapSpec.scala | 3 +- 2 files changed, 82 insertions(+), 24 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index ffc470660b..9aebef0032 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -48,12 +48,21 @@ import java.util.concurrent.atomic.AtomicInteger * The only explicit synchronization is the `canceledCounter` atomic, which is used to track and * publish cancelations from other threads. Because other threads cannot safely remove a node, * they `null` the callback, toggle the `canceled` flag, and increment the counter to indicate - * that the owner thread should iterate the heap to properly remove these nodes. + * that the owner thread should iterate the heap to remove these nodes a.k.a. "packing". + * + * To amortize the cost of packing, we only do so if canceled nodes make up at least half of the + * heap. In an ideal world, cancelation from external threads is relatively rare and those nodes + * are removed naturally as they surface to the top of the heap, such that we never exceed the + * packing threshold. */ private final class TimerHeap extends AtomicInteger { - // at most this many nodes are externally canceled and waiting to be removed from the heap + // At most this many nodes are externally canceled and waiting to be removed from the heap. canceledCounter => + // And this is how many of those externally canceled nodes were already removed. + // We track this separately so we can increment on owner thread without overhead of the atomic. + private[this] var removedCanceledCounter = 0 + // The index 0 is not used; the root is at index 1. // This is standard practice in binary heaps, to simplify arithmetics. private[this] var heap: Array[Node] = new Array(8) // TODO what initial value @@ -64,20 +73,37 @@ private final class TimerHeap extends AtomicInteger { /** * only called by owner thread */ + @tailrec def peekFirstTriggerTime(): Long = if (size > 0) { - val tt = heap(1).triggerTime - if (tt != Long.MinValue) { - tt - } else { - // in the VERY unlikely case when - // the trigger time is exactly our - // sentinel, we just cheat a little - // (this could cause threads to wake - // up 1 ns too early): - Long.MaxValue + val root = heap(1) + + if (root.isDeleted()) { // DOA. Remove it and loop. + + removeAt(1) + if (root.isCanceled()) + removedCanceledCounter += 1 + + peekFirstTriggerTime() // loop + + } else { // We got a live one! + + val tt = root.triggerTime + if (tt != Long.MinValue) { // tt != sentinel + tt + } else { + // in the VERY unlikely case when + // the trigger time is exactly our + // sentinel, we just cheat a little + // (this could cause threads to wake + // up 1 ns too early): + Long.MaxValue + } + } - } else Long.MinValue + } else { // size == 0 + Long.MinValue // sentinel + } /** * for testing @@ -107,6 +133,9 @@ private final class TimerHeap extends AtomicInteger { heap(size) = null size -= 1 + if (root.isCanceled()) + removedCanceledCounter += 1 + val back = root.getAndClear() if (rootExpired && (back ne null)) back else loop() } else null @@ -156,6 +185,7 @@ private final class TimerHeap extends AtomicInteger { val rootExpired = !rootDeleted && isExpired(root, now) if (rootDeleted || rootExpired) { // see if we can just replace the root root.index = -1 + if (root.isCanceled()) removedCanceledCounter += 1 if (rootExpired) out(0) = root.getAndClear() val node = new Node(triggerTime, callback, 1) heap(1) = node @@ -179,21 +209,50 @@ private final class TimerHeap extends AtomicInteger { /** * only called by owner thread */ + @tailrec def packIfNeeded(): Unit = { - val canceled = canceledCounter.getAndSet(0) // we now see all external cancelations + + val back = canceledCounter.get() + + // Account for canceled nodes that were already removed. + val canceledCount = back - removedCanceledCounter + + if (canceledCount >= size / 2) { // We have exceeded the packing threshold. + + // We will attempt to remove this many nodes. + val removeCount = // First try to use our current value but get latest if it is stale. + if (canceledCounter.compareAndSet(back, 0)) canceledCount + else canceledCounter.getAndSet(0) - removedCanceledCounter + + removedCanceledCounter = 0 // Reset, these have now been accounted for. + + // All external cancelations are now visible (published via canceledCounter). + pack(removeCount) + + } else { // canceledCounter will eventually overflow if we do not subtract removedCanceledCounter. + + if (canceledCounter.compareAndSet(back, canceledCount)) { + removedCanceledCounter = 0 // Reset, these have now been accounted for. + } else { + packIfNeeded() // canceledCounter was externally incremented, loop. + } + } + } + + private[this] def pack(removeCount: Int): Unit = { val heap = this.heap // local copy - // we track how many canceled nodes we found so we can try to exit the loop early + // We track how many canceled nodes we removed so we can try to exit the loop early. var i = 1 - var c = 0 - while (c < canceled && i <= size) { - // we are careful to consider only *canceled* nodes, which increment the canceledCounter - // a node may be deleted b/c it was stolen, but this does not increment the canceledCounter - // to avoid leaks we must attempt to find a canceled node for every increment + var r = 0 + while (r < removeCount && i <= size) { + // We are careful to consider only *canceled* nodes, which increment the canceledCounter. + // A node may be deleted b/c it was stolen, but this does not increment the canceledCounter. + // To avoid leaks we must attempt to find a canceled node for every increment. if (heap(i).isCanceled()) { removeAt(i) - c += 1 - // don't increment i, the new i may be canceled too + r += 1 + // Don't increment i, the new i may be canceled too. } else { i += 1 } diff --git a/tests/jvm/src/test/scala/cats/effect/unsafe/TimerHeapSpec.scala b/tests/jvm/src/test/scala/cats/effect/unsafe/TimerHeapSpec.scala index efd694e4e5..711de0a040 100644 --- a/tests/jvm/src/test/scala/cats/effect/unsafe/TimerHeapSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/unsafe/TimerHeapSpec.scala @@ -89,9 +89,8 @@ class TimerHeapSpec extends Specification { m.peekFirstQuiescent() mustEqual cb0 m.peekFirstTriggerTime() mustEqual 1L r0.run() - m.packIfNeeded() - m.peekFirstQuiescent() mustEqual cb1 m.peekFirstTriggerTime() mustEqual 2L + m.peekFirstQuiescent() mustEqual cb1 m.pollFirstIfTriggered(Long.MaxValue) mustEqual cb1 m.peekFirstQuiescent() mustEqual cb2 m.peekFirstTriggerTime() mustEqual 3L From f8824818e91e59f0b528bdf41c8710dbbd4ccda8 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 15 Jan 2024 16:32:10 +0000 Subject: [PATCH 314/429] Only use `@static` on JVM --- build.sbt | 2 +- .../src/main/scala/cats/effect/Platform.scala | 2 ++ .../main/scala/cats/effect/ArrayStack.scala | 6 ++-- .../main/scala/cats/effect/ByteStack.scala | 20 +++++------ .../scala/cats/effect/CallbackStack.scala | 6 ++-- .../src/main/scala/cats/effect/Platform.scala | 2 ++ .../src/main/scala/cats/effect/Platform.scala | 2 ++ .../main/scala/cats/effect/ContState.scala | 6 ++-- .../src/main/scala/cats/effect/IO.scala | 8 ++--- .../src/main/scala/cats/effect/IOFiber.scala | 12 +++---- .../src/main/scala/cats/effect/SyncIO.scala | 8 ++--- .../cats/effect/tracing/RingBuffer.scala | 7 ++-- .../scala/cats/effect/tracing/Tracing.scala | 33 +++++++++---------- 13 files changed, 60 insertions(+), 54 deletions(-) diff --git a/build.sbt b/build.sbt index 06636fd702..cadc5b33d9 100644 --- a/build.sbt +++ b/build.sbt @@ -114,7 +114,7 @@ val MacOS = "macos-latest" val Scala212 = "2.12.18" val Scala213 = "2.13.12" -val Scala3 = "3.4.0-RC1-bin-20240112-c50f2ff-NIGHTLY" +val Scala3 = "3.3.1" ThisBuild / crossScalaVersions := Seq(Scala3, Scala212, Scala213) ThisBuild / githubWorkflowScalaVersions := crossScalaVersions.value diff --git a/core/js/src/main/scala/cats/effect/Platform.scala b/core/js/src/main/scala/cats/effect/Platform.scala index 3ea79f540d..f7792a16ce 100644 --- a/core/js/src/main/scala/cats/effect/Platform.scala +++ b/core/js/src/main/scala/cats/effect/Platform.scala @@ -20,4 +20,6 @@ private object Platform { final val isJs = true final val isJvm = false final val isNative = false + + class static extends scala.annotation.Annotation } diff --git a/core/jvm-native/src/main/scala/cats/effect/ArrayStack.scala b/core/jvm-native/src/main/scala/cats/effect/ArrayStack.scala index 48ca533df5..97529d501e 100644 --- a/core/jvm-native/src/main/scala/cats/effect/ArrayStack.scala +++ b/core/jvm-native/src/main/scala/cats/effect/ArrayStack.scala @@ -16,7 +16,7 @@ package cats.effect -import org.typelevel.scalaccompat.annotation._ +import Platform.static private final class ArrayStack[A <: AnyRef]( private[this] var buffer: Array[AnyRef], @@ -80,8 +80,8 @@ private final class ArrayStack[A <: AnyRef]( private object ArrayStack { - @static3 def apply[A <: AnyRef](): ArrayStack[A] = new ArrayStack() + @static def apply[A <: AnyRef](): ArrayStack[A] = new ArrayStack() - @static3 def apply[A <: AnyRef](size: Int): ArrayStack[A] = new ArrayStack(size) + @static def apply[A <: AnyRef](size: Int): ArrayStack[A] = new ArrayStack(size) } diff --git a/core/jvm-native/src/main/scala/cats/effect/ByteStack.scala b/core/jvm-native/src/main/scala/cats/effect/ByteStack.scala index fe0dc678af..9bdbdaf1f6 100644 --- a/core/jvm-native/src/main/scala/cats/effect/ByteStack.scala +++ b/core/jvm-native/src/main/scala/cats/effect/ByteStack.scala @@ -16,7 +16,7 @@ package cats.effect -import org.typelevel.scalaccompat.annotation._ +import Platform.static private[effect] final class ByteStack @@ -24,7 +24,7 @@ private object ByteStack { type T = Array[Int] - @static3 final def toDebugString( + @static final def toDebugString( stack: Array[Int], translate: Byte => String = _.toString): String = { val count = size(stack) @@ -44,10 +44,10 @@ private object ByteStack { .toString } - @static3 final def create(initialMaxOps: Int): Array[Int] = + @static final def create(initialMaxOps: Int): Array[Int] = new Array[Int](1 + 1 + ((initialMaxOps - 1) >> 3)) // count-slot + 1 for each set of 8 ops - @static3 final def growIfNeeded(stack: Array[Int], count: Int): Array[Int] = { + @static final def growIfNeeded(stack: Array[Int], count: Int): Array[Int] = { if ((1 + ((count + 1) >> 3)) < stack.length) { stack } else { @@ -57,7 +57,7 @@ private object ByteStack { } } - @static3 final def push(stack: Array[Int], op: Byte): Array[Int] = { + @static final def push(stack: Array[Int], op: Byte): Array[Int] = { val c = stack(0) // current count of elements val use = growIfNeeded(stack, c) // alias so we add to the right place val s = (c >> 3) + 1 // current slot in `use` @@ -67,24 +67,24 @@ private object ByteStack { use } - @static3 final def size(stack: Array[Int]): Int = + @static final def size(stack: Array[Int]): Int = stack(0) - @static3 final def isEmpty(stack: Array[Int]): Boolean = + @static final def isEmpty(stack: Array[Int]): Boolean = stack(0) < 1 - @static3 final def read(stack: Array[Int], pos: Int): Byte = { + @static final def read(stack: Array[Int], pos: Int): Byte = { if (pos < 0 || pos >= stack(0)) throw new ArrayIndexOutOfBoundsException() ((stack((pos >> 3) + 1) >>> ((pos & 7) << 2)) & 0x0000000f).toByte } - @static3 final def peek(stack: Array[Int]): Byte = { + @static final def peek(stack: Array[Int]): Byte = { val c = stack(0) - 1 if (c < 0) throw new ArrayIndexOutOfBoundsException() ((stack((c >> 3) + 1) >>> ((c & 7) << 2)) & 0x0000000f).toByte } - @static3 final def pop(stack: Array[Int]): Byte = { + @static final def pop(stack: Array[Int]): Byte = { val op = peek(stack) stack(0) -= 1 op diff --git a/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala b/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala index 5389fd55cd..c2a5fe2d18 100644 --- a/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala +++ b/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala @@ -16,12 +16,12 @@ package cats.effect -import org.typelevel.scalaccompat.annotation._ - import scala.annotation.tailrec import java.util.concurrent.atomic.AtomicReference +import Platform.static + private final class CallbackStack[A](private[this] var callback: A => Unit) extends AtomicReference[CallbackStack[A]] { @@ -151,7 +151,7 @@ private final class CallbackStack[A](private[this] var callback: A => Unit) } private object CallbackStack { - @static3 def of[A](cb: A => Unit): CallbackStack[A] = + @static def of[A](cb: A => Unit): CallbackStack[A] = new CallbackStack(cb) type Handle = Byte diff --git a/core/jvm/src/main/scala/cats/effect/Platform.scala b/core/jvm/src/main/scala/cats/effect/Platform.scala index f07617685b..4f166221dc 100644 --- a/core/jvm/src/main/scala/cats/effect/Platform.scala +++ b/core/jvm/src/main/scala/cats/effect/Platform.scala @@ -20,4 +20,6 @@ private object Platform { final val isJs = false final val isJvm = true final val isNative = false + + type static = org.typelevel.scalaccompat.annotation.static3 } diff --git a/core/native/src/main/scala/cats/effect/Platform.scala b/core/native/src/main/scala/cats/effect/Platform.scala index 31c900ae57..9fbedaf859 100644 --- a/core/native/src/main/scala/cats/effect/Platform.scala +++ b/core/native/src/main/scala/cats/effect/Platform.scala @@ -20,4 +20,6 @@ private object Platform { final val isJs = false final val isJvm = false final val isNative = true + + class static extends scala.annotation.Annotation } diff --git a/core/shared/src/main/scala/cats/effect/ContState.scala b/core/shared/src/main/scala/cats/effect/ContState.scala index 5f65ee184f..e6519f6c26 100644 --- a/core/shared/src/main/scala/cats/effect/ContState.scala +++ b/core/shared/src/main/scala/cats/effect/ContState.scala @@ -18,10 +18,10 @@ package cats.effect import cats.effect.unsafe.WeakBag -import org.typelevel.scalaccompat.annotation._ - import java.util.concurrent.atomic.AtomicReference +import Platform.static + /** * Possible states (held in the `AtomicReference`): * - "initial": `get() == null` @@ -51,6 +51,6 @@ private object ContState { * important. It must be private (so that no user code can access it), and it mustn't be used * for any other purpose. */ - @static3 private val waitingSentinel: Either[Throwable, Any] = + @static private val waitingSentinel: Either[Throwable, Any] = new Right(null) } diff --git a/core/shared/src/main/scala/cats/effect/IO.scala b/core/shared/src/main/scala/cats/effect/IO.scala index feea41d332..60846e9dbb 100644 --- a/core/shared/src/main/scala/cats/effect/IO.scala +++ b/core/shared/src/main/scala/cats/effect/IO.scala @@ -44,8 +44,6 @@ import cats.effect.tracing.{Tracing, TracingEvent} import cats.effect.unsafe.IORuntime import cats.syntax.all._ -import org.typelevel.scalaccompat.annotation._ - import scala.annotation.unchecked.uncheckedVariance import scala.concurrent.{ CancellationException, @@ -61,6 +59,8 @@ import scala.util.control.NonFatal import java.util.UUID import java.util.concurrent.Executor +import Platform.static + /** * A pure abstraction representing the intention to perform a side effect, where the result of * that side effect may be obtained synchronously (via return) or asynchronously (via callback). @@ -1111,8 +1111,8 @@ private[effect] trait IOLowPriorityImplicits { object IO extends IOCompanionPlatform with IOLowPriorityImplicits { - @static3 private[this] val _alignForIO = new IOAlign - @static3 private[this] val _asyncForIO: kernel.Async[IO] = new IOAsync + @static private[this] val _alignForIO = new IOAlign + @static private[this] val _asyncForIO: kernel.Async[IO] = new IOAsync /** * Newtype encoding for an `IO` datatype that has a `cats.Applicative` capable of doing diff --git a/core/shared/src/main/scala/cats/effect/IOFiber.scala b/core/shared/src/main/scala/cats/effect/IOFiber.scala index e9e742f947..dcec731815 100644 --- a/core/shared/src/main/scala/cats/effect/IOFiber.scala +++ b/core/shared/src/main/scala/cats/effect/IOFiber.scala @@ -20,8 +20,6 @@ import cats.arrow.FunctionK import cats.effect.tracing._ import cats.effect.unsafe._ -import org.typelevel.scalaccompat.annotation._ - import scala.annotation.{switch, tailrec} import scala.concurrent.ExecutionContext import scala.concurrent.duration._ @@ -30,6 +28,8 @@ import scala.util.control.NonFatal import java.util.concurrent.RejectedExecutionException import java.util.concurrent.atomic.AtomicBoolean +import Platform.static + /* * Rationale on memory barrier exploitation in this class... * @@ -1571,11 +1571,11 @@ private final class IOFiber[A]( private object IOFiber { /* prefetch */ - @static3 private[IOFiber] val TypeBlocking = Sync.Type.Blocking - @static3 private[IOFiber] val OutcomeCanceled = Outcome.Canceled() - @static3 private[effect] val RightUnit = Right(()) + @static private[IOFiber] val TypeBlocking = Sync.Type.Blocking + @static private[IOFiber] val OutcomeCanceled = Outcome.Canceled() + @static private[effect] val RightUnit = Right(()) - @static3 def onFatalFailure(t: Throwable): Nothing = { + @static def onFatalFailure(t: Throwable): Nothing = { val interrupted = Thread.interrupted() if (IORuntime.globalFatalFailureHandled.compareAndSet(false, true)) { diff --git a/core/shared/src/main/scala/cats/effect/SyncIO.scala b/core/shared/src/main/scala/cats/effect/SyncIO.scala index 06f2475f1c..38d57b643c 100644 --- a/core/shared/src/main/scala/cats/effect/SyncIO.scala +++ b/core/shared/src/main/scala/cats/effect/SyncIO.scala @@ -22,14 +22,14 @@ import cats.effect.syntax.monadCancel._ import cats.kernel.{Monoid, Semigroup} import cats.syntax.all._ -import org.typelevel.scalaccompat.annotation._ - import scala.annotation.{switch, tailrec} import scala.annotation.unchecked.uncheckedVariance import scala.concurrent.duration._ import scala.util.Try import scala.util.control.NonFatal +import Platform.static + /** * A pure abstraction representing the intention to perform a side effect, where the result of * that side effect is obtained synchronously. @@ -398,8 +398,8 @@ private[effect] trait SyncIOLowPriorityImplicits { object SyncIO extends SyncIOCompanionPlatform with SyncIOLowPriorityImplicits { - @static3 private[this] val Delay = Sync.Type.Delay - @static3 private[this] val _syncForSyncIO: Sync[SyncIO] = new SyncIOSync + @static private[this] val Delay = Sync.Type.Delay + @static private[this] val _syncForSyncIO: Sync[SyncIO] = new SyncIOSync // constructors diff --git a/core/shared/src/main/scala/cats/effect/tracing/RingBuffer.scala b/core/shared/src/main/scala/cats/effect/tracing/RingBuffer.scala index f4985305d0..2a7ac48387 100644 --- a/core/shared/src/main/scala/cats/effect/tracing/RingBuffer.scala +++ b/core/shared/src/main/scala/cats/effect/tracing/RingBuffer.scala @@ -14,9 +14,10 @@ * limitations under the License. */ -package cats.effect.tracing +package cats.effect +package tracing -import org.typelevel.scalaccompat.annotation._ +import Platform.static private[effect] final class RingBuffer private (logSize: Int) { @@ -61,6 +62,6 @@ private[effect] final class RingBuffer private (logSize: Int) { } private[effect] object RingBuffer { - @static3 def empty(logSize: Int): RingBuffer = + @static def empty(logSize: Int): RingBuffer = new RingBuffer(logSize) } diff --git a/core/shared/src/main/scala/cats/effect/tracing/Tracing.scala b/core/shared/src/main/scala/cats/effect/tracing/Tracing.scala index 808e759574..e92ac74c66 100644 --- a/core/shared/src/main/scala/cats/effect/tracing/Tracing.scala +++ b/core/shared/src/main/scala/cats/effect/tracing/Tracing.scala @@ -14,37 +14,36 @@ * limitations under the License. */ -package cats.effect.tracing - -import cats.effect.{IOFiber, Trace} - -import org.typelevel.scalaccompat.annotation._ +package cats.effect +package tracing import scala.collection.mutable.ArrayBuffer +import Platform.static + private[effect] final class Tracing private[effect] object Tracing extends TracingPlatform { import TracingConstants._ - @static3 private[this] final val TurnRight = "╰" + @static private[this] final val TurnRight = "╰" // private[this] final val InverseTurnRight = "╭" - @static3 private[this] final val Junction = "├" + @static private[this] final val Junction = "├" // private[this] final val Line = "│" - @static3 private[tracing] def buildEvent(): TracingEvent = { + @static private[tracing] def buildEvent(): TracingEvent = { new TracingEvent.StackTrace() } - @static3 private[this] final val runLoopFilter: Array[String] = + @static private[this] final val runLoopFilter: Array[String] = Array( "cats.effect.", "scala.runtime.", "scala.scalajs.runtime.", "scala.scalanative.runtime.") - @static3 private[tracing] final val stackTraceClassNameFilter: Array[String] = Array( + @static private[tracing] final val stackTraceClassNameFilter: Array[String] = Array( "cats.", "sbt.", "java.", @@ -54,7 +53,7 @@ private[effect] object Tracing extends TracingPlatform { "org.scalajs." ) - @static3 private[tracing] def combineOpAndCallSite( + @static private[tracing] def combineOpAndCallSite( methodSite: StackTraceElement, callSite: StackTraceElement): StackTraceElement = { val methodSiteMethodName = methodSite.getMethodName @@ -68,7 +67,7 @@ private[effect] object Tracing extends TracingPlatform { ) } - @static3 private[tracing] def isInternalClass(className: String): Boolean = { + @static private[tracing] def isInternalClass(className: String): Boolean = { var i = 0 val len = stackTraceClassNameFilter.length while (i < len) { @@ -79,7 +78,7 @@ private[effect] object Tracing extends TracingPlatform { false } - @static3 private[this] def getOpAndCallSite( + @static private[this] def getOpAndCallSite( stackTrace: Array[StackTraceElement]): StackTraceElement = { val len = stackTrace.length var idx = 1 @@ -102,7 +101,7 @@ private[effect] object Tracing extends TracingPlatform { null } - @static3 def augmentThrowable( + @static def augmentThrowable( enhancedExceptions: Boolean, t: Throwable, events: RingBuffer): Unit = { @@ -151,13 +150,13 @@ private[effect] object Tracing extends TracingPlatform { } } - @static3 def getFrames(events: RingBuffer): List[StackTraceElement] = + @static def getFrames(events: RingBuffer): List[StackTraceElement] = events .toList() .collect { case ev: TracingEvent.StackTrace => getOpAndCallSite(ev.getStackTrace) } .filter(_ ne null) - @static3 def prettyPrint(trace: Trace): String = { + @static def prettyPrint(trace: Trace): String = { val frames = trace.toList frames @@ -170,7 +169,7 @@ private[effect] object Tracing extends TracingPlatform { .mkString(System.lineSeparator()) } - @static3 def captureTrace(runnable: Runnable): Option[(Runnable, Trace)] = { + @static def captureTrace(runnable: Runnable): Option[(Runnable, Trace)] = { runnable match { case f: IOFiber[_] if f.isDone => None case f: IOFiber[_] => Some(runnable -> f.captureTrace()) From 381779ddc35db6b64623a3748dc4c3414ff9694c Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 15 Jan 2024 16:45:59 +0000 Subject: [PATCH 315/429] More static --- .../src/main/scala/cats/effect/unsafe/IORuntime.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala b/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala index b0e96c9c5c..416fd342c3 100644 --- a/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala +++ b/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala @@ -21,6 +21,8 @@ import scala.concurrent.ExecutionContext import java.util.concurrent.atomic.AtomicBoolean +import Platform.static + @annotation.implicitNotFound("""Could not find an implicit IORuntime. Instead of calling unsafe methods directly, consider using cats.effect.IOApp, which @@ -110,9 +112,9 @@ object IORuntime extends IORuntimeCompanionPlatform { private[effect] def testRuntime(ec: ExecutionContext, scheduler: Scheduler): IORuntime = new IORuntime(ec, ec, scheduler, Nil, new NoOpFiberMonitor(), () => (), IORuntimeConfig()) - private[effect] final val allRuntimes: ThreadSafeHashtable[IORuntime] = + @static private[effect] final val allRuntimes: ThreadSafeHashtable[IORuntime] = new ThreadSafeHashtable(4) - private[effect] final val globalFatalFailureHandled: AtomicBoolean = + @static private[effect] final val globalFatalFailureHandled: AtomicBoolean = new AtomicBoolean(false) } From da8bb4333d5e9a8223e094f5c440278db7d0f982 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 15 Jan 2024 17:15:43 +0000 Subject: [PATCH 316/429] Hush MiMa --- build.sbt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index cadc5b33d9..ce1eac1ef2 100644 --- a/build.sbt +++ b/build.sbt @@ -767,7 +767,12 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) ProblemFilters.exclude[DirectMissingMethodProblem]( "cats.effect.unsafe.WorkStealingThreadPool.this"), // annoying consequence of reverting #2473 - ProblemFilters.exclude[AbstractClassProblem]("cats.effect.ExitCode") + ProblemFilters.exclude[AbstractClassProblem]("cats.effect.ExitCode"), + // #3934 which made these internal vals into proper static fields + ProblemFilters.exclude[DirectMissingMethodProblem]( + "cats.effect.unsafe.IORuntime.allRuntimes"), + ProblemFilters.exclude[DirectMissingMethodProblem]( + "cats.effect.unsafe.IORuntime.globalFatalFailureHandled") ) } else Seq() } From 07d0b9cc26f054e7f0a8bd1fa8fc4f295dc5b736 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 16 Jan 2024 02:15:43 +0000 Subject: [PATCH 317/429] Fix compile --- .../main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index e62d1d6147..aad24a5479 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -527,7 +527,7 @@ private[effect] final class WorkStealingThreadPool[P]( */ private[effect] def prepareForBlocking(): Unit = { val thread = Thread.currentThread() - val worker = thread.asInstanceOf[WorkerThread] + val worker = thread.asInstanceOf[WorkerThread[_]] worker.prepareForBlocking() } From 3b5325d2df0cb856cdad09e94b95d75351093912 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 16 Jan 2024 02:21:18 +0000 Subject: [PATCH 318/429] Fix compile --- .../src/test/scala/cats/effect/CallbackStackSpec.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/shared/src/test/scala/cats/effect/CallbackStackSpec.scala b/tests/shared/src/test/scala/cats/effect/CallbackStackSpec.scala index fdf5a5ab41..151d5bf3d0 100644 --- a/tests/shared/src/test/scala/cats/effect/CallbackStackSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/CallbackStackSpec.scala @@ -22,7 +22,7 @@ class CallbackStackSpec extends BaseSpec with DetectPlatform { "CallbackStack" should { "correctly report the number removed" in { - val stack = CallbackStack[Unit](null) + val stack = CallbackStack.of[Unit](null) val handle = stack.push(_ => ()) stack.push(_ => ()) val removed = stack.clearHandle(handle) @@ -34,7 +34,7 @@ class CallbackStackSpec extends BaseSpec with DetectPlatform { "handle race conditions in pack" in real { - IO(CallbackStack[Unit](null)).flatMap { stack => + IO(CallbackStack.of[Unit](null)).flatMap { stack => val pushClearPack = for { handle <- IO(stack.push(_ => ())) removed <- IO(stack.clearHandle(handle)) @@ -54,7 +54,7 @@ class CallbackStackSpec extends BaseSpec with DetectPlatform { "pack runs concurrently with clear" in real { IO { - val stack = CallbackStack[Unit](null) + val stack = CallbackStack.of[Unit](null) val handle = stack.push(_ => ()) stack.clearHandle(handle) stack From 311d406d9adc7143206533ca88d9242a99f47938 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 16 Jan 2024 02:33:32 +0000 Subject: [PATCH 319/429] Organize imports --- core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala b/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala index 7043b5de4a..37c10ced94 100644 --- a/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala +++ b/core/jvm-native/src/main/scala/cats/effect/CallbackStack.scala @@ -22,7 +22,6 @@ import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference} import CallbackStack.Handle import CallbackStack.Node - import Platform.static private final class CallbackStack[A](private[this] var callback: A => Unit) From a608390ea642b7ba114bc5bc8a18e56c2be884c4 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 16 Jan 2024 02:46:10 +0000 Subject: [PATCH 320/429] Fix warning --- core/shared/src/main/scala/cats/effect/IODeferred.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/shared/src/main/scala/cats/effect/IODeferred.scala b/core/shared/src/main/scala/cats/effect/IODeferred.scala index 13c05d2226..a64481b910 100644 --- a/core/shared/src/main/scala/cats/effect/IODeferred.scala +++ b/core/shared/src/main/scala/cats/effect/IODeferred.scala @@ -29,9 +29,10 @@ private final class IODeferred[A] extends Deferred[IO, A] { val removed = callbacks.clearHandle(handle) if (!removed) { val clearCount = clearCounter.incrementAndGet() - if ((clearCount & (clearCount - 1)) == 0) // power of 2 + if ((clearCount & (clearCount - 1)) == 0) { // power of 2 clearCounter.addAndGet(-callbacks.pack(clearCount)) - () + () + } } } From 3329fc516c86736df1a3f274c8d1495b9540c48d Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 20:05:29 +0000 Subject: [PATCH 321/429] Update nscplugin, sbt-scala-native, ... to 0.4.17 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 0d6ae5c97d..d4b55709d5 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -3,7 +3,7 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.6.5") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.15.0") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.16") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.6") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.2") From aa908063c6744185a68f5e6d2a0795fec9d0ab8f Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Sat, 20 Jan 2024 00:16:18 +0000 Subject: [PATCH 322/429] Update sbt-jmh to 0.4.7 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index d4b55709d5..5da40853a5 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -4,7 +4,7 @@ addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.6.5") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.15.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") -addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.6") +addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.2") addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0") From 2cc84cb759550da7f02a6ca5fe35760e78e13095 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 23 Jan 2024 07:48:57 +0000 Subject: [PATCH 323/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/8eb51654dc21054929b0ae6883c0382c9f51b1a2' (2024-01-01) → 'github:typelevel/typelevel-nix/3ff868a8830e7513a17ac7d2f4f071411c96b35e' (2024-01-17) • Updated input 'typelevel-nix/devshell': 'github:numtide/devshell/44ddedcbcfc2d52a76b64fb6122f209881bd3e1e' (2023-12-05) → 'github:numtide/devshell/ca1ff587c602b934afe830ea3cb26d0fbde4c395' (2024-01-14) • Added input 'typelevel-nix/devshell/flake-utils': 'github:numtide/flake-utils/4022d587cbbfd70fe950c1e2083a02621806a725' (2023-12-04) • Added input 'typelevel-nix/devshell/flake-utils/systems': 'github:nix-systems/default/da67096a3b9bf56a91d16901293e51ba5b49a27e' (2023-04-09) • Updated input 'typelevel-nix/devshell/nixpkgs': 'github:NixOS/nixpkgs/9952d6bc395f5841262b006fbace8dd7e143b634' (2023-02-26) → 'github:NixOS/nixpkgs/63143ac2c9186be6d9da6035fa22620018c85932' (2024-01-02) • Removed input 'typelevel-nix/devshell/systems' • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/d44d59d2b5bd694cd9d996fd8c51d03e3e9ba7f7' (2023-12-31) → 'github:nixos/nixpkgs/ea780f3de2d169f982564128804841500e85e373' (2024-01-14) --- flake.lock | 48 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/flake.lock b/flake.lock index e19ed5fdaa..128cbd7dab 100644 --- a/flake.lock +++ b/flake.lock @@ -2,15 +2,15 @@ "nodes": { "devshell": { "inputs": { - "nixpkgs": "nixpkgs", - "systems": "systems" + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1701787589, - "narHash": "sha256-ce+oQR4Zq9VOsLoh9bZT8Ip9PaMLcjjBUHVPzW5d7Cw=", + "lastModified": 1705240333, + "narHash": "sha256-s9h2h44fCi54sSIT9ktd3eDik9JDpQE9DeYuXcA44u4=", "owner": "numtide", "repo": "devshell", - "rev": "44ddedcbcfc2d52a76b64fb6122f209881bd3e1e", + "rev": "ca1ff587c602b934afe830ea3cb26d0fbde4c395", "type": "github" }, "original": { @@ -20,6 +20,24 @@ } }, "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1701680307, + "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { "inputs": { "systems": "systems_2" }, @@ -39,11 +57,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1677383253, - "narHash": "sha256-UfpzWfSxkfXHnb4boXZNaKsAcUrZT9Hw+tao1oZxd08=", + "lastModified": 1704161960, + "narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9952d6bc395f5841262b006fbace8dd7e143b634", + "rev": "63143ac2c9186be6d9da6035fa22620018c85932", "type": "github" }, "original": { @@ -55,11 +73,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1704008649, - "narHash": "sha256-rGPSWjXTXTurQN9beuHdyJhB8O761w1Zc5BqSSmHvoM=", + "lastModified": 1705242415, + "narHash": "sha256-a8DRYrNrzTudvO7XHUPNJD89Wbf1ZZT0VbwCsPnHWaE=", "owner": "nixos", "repo": "nixpkgs", - "rev": "d44d59d2b5bd694cd9d996fd8c51d03e3e9ba7f7", + "rev": "ea780f3de2d169f982564128804841500e85e373", "type": "github" }, "original": { @@ -115,15 +133,15 @@ "typelevel-nix": { "inputs": { "devshell": "devshell", - "flake-utils": "flake-utils", + "flake-utils": "flake-utils_2", "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1704087752, - "narHash": "sha256-PTM++FBin2Xd4kc2HyQ74Wm0PdjSheCcnX45xhH1fOw=", + "lastModified": 1705527810, + "narHash": "sha256-bl1CkA40UgejXV+ZMAaIqkPvkfl/5aqAgQP09TTrbiQ=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "8eb51654dc21054929b0ae6883c0382c9f51b1a2", + "rev": "3ff868a8830e7513a17ac7d2f4f071411c96b35e", "type": "github" }, "original": { From e39910587d5c0ba21a740674e7b2c7cafacb85df Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 23 Jan 2024 20:04:46 +0000 Subject: [PATCH 324/429] Update specs2-core, specs2-scalacheck to 4.20.5 in series/3.x --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 51cf679d70..5c86728d63 100644 --- a/build.sbt +++ b/build.sbt @@ -317,7 +317,7 @@ ThisBuild / apiURL := Some(url("https://typelevel.org/cats-effect/api/3.x/")) ThisBuild / autoAPIMappings := true val CatsVersion = "2.10.0" -val Specs2Version = "4.20.3" +val Specs2Version = "4.20.5" val ScalaCheckVersion = "1.17.0" val DisciplineVersion = "1.4.0" val CoopVersion = "1.2.0" From 2718ae9ceb2a6a686de12ef175a6321073f002c6 Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Sat, 27 Jan 2024 15:53:55 -0600 Subject: [PATCH 325/429] Quick and dirty unsafe queue API --- .../main/scala/cats/effect/std/Queue.scala | 168 +++--------------- .../scala/cats/effect/std/QueueSink.scala | 82 +++++++++ .../scala/cats/effect/std/QueueSource.scala | 98 ++++++++++ .../effect/std/unsafe/BoundedQueueSink.scala | 39 ++++ .../std/unsafe/UnboundedQueueSink.scala | 44 +++++ .../cats/effect/std/DeferredJVMSpec.scala | 8 +- 6 files changed, 290 insertions(+), 149 deletions(-) create mode 100644 std/shared/src/main/scala/cats/effect/std/QueueSink.scala create mode 100644 std/shared/src/main/scala/cats/effect/std/QueueSource.scala create mode 100644 std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueueSink.scala create mode 100644 std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueueSink.scala diff --git a/std/shared/src/main/scala/cats/effect/std/Queue.scala b/std/shared/src/main/scala/cats/effect/std/Queue.scala index 81e5c827eb..1696b7ad03 100644 --- a/std/shared/src/main/scala/cats/effect/std/Queue.scala +++ b/std/shared/src/main/scala/cats/effect/std/Queue.scala @@ -105,6 +105,12 @@ object Queue { private[effect] def unboundedForAsync[F[_], A](implicit F: Async[F]): F[Queue[F, A]] = F.delay(new UnboundedAsyncQueue()) + def unsafeBounded[F[_], A](capacity: Int)( + implicit F: Async[F]): F[Queue[F, A] with unsafe.BoundedQueueSink[F, A]] = { + require(capacity > 1 && capacity < Short.MaxValue.toInt * 2) + F.delay(new BoundedAsyncQueue(capacity)) + } + /** * Constructs a queue through which a single element can pass only in the case when there are * at least one taking fiber and at least one offering fiber for `F` data types that are @@ -135,6 +141,10 @@ object Queue { unboundedForConcurrent } + def unsafeUnbounded[F[_], A]( + implicit F: Async[F]): F[Queue[F, A] with unsafe.UnboundedQueueSink[F, A]] = + F.delay(new UnboundedAsyncQueue()) + /** * Constructs an empty, bounded, dropping queue holding up to `capacity` elements for `F` data * types that are [[cats.effect.kernel.GenConcurrent]]. When the queue is full (contains @@ -542,7 +552,9 @@ object Queue { * Does not correctly handle bound = 0 because take waiters are async[Unit] */ private final class BoundedAsyncQueue[F[_], A](capacity: Int)(implicit F: Async[F]) - extends Queue[F, A] { + extends Queue[F, A] + with unsafe.BoundedQueueSink[F, A] { + require(capacity > 1) private[this] val buffer = new UnsafeBounded[A](capacity) @@ -625,7 +637,7 @@ object Queue { } } - def tryOffer(a: A): F[Boolean] = F delay { + def unsafeTryOffer(a: A): Boolean = { try { buffer.put(a) notifyOne(takers) @@ -636,6 +648,8 @@ object Queue { } } + def tryOffer(a: A): F[Boolean] = F.delay(unsafeTryOffer(a)) + val size: F[Int] = F.delay(buffer.size()) val take: F[A] = @@ -802,16 +816,21 @@ object Queue { } } - private final class UnboundedAsyncQueue[F[_], A]()(implicit F: Async[F]) extends Queue[F, A] { + private final class UnboundedAsyncQueue[F[_], A]()(implicit F: Async[F]) + extends Queue[F, A] + with unsafe.UnboundedQueueSink[F, A] { + private[this] val buffer = new UnsafeUnbounded[A]() private[this] val takers = new UnsafeUnbounded[Either[Throwable, Unit] => Unit]() private[this] val FailureSignal = cats.effect.std.FailureSignal // prefetch - def offer(a: A): F[Unit] = F delay { + def unsafeOffer(a: A): Unit = { buffer.put(a) notifyOne() } + def offer(a: A): F[Unit] = F.delay(unsafeOffer(a)) + def tryOffer(a: A): F[Boolean] = F delay { buffer.put(a) notifyOne() @@ -1104,144 +1123,3 @@ object Queue { } } } - -trait QueueSource[F[_], A] { - - /** - * Dequeues an element from the front of the queue, possibly fiber blocking until an element - * becomes available. - */ - def take: F[A] - - /** - * Attempts to dequeue an element from the front of the queue, if one is available without - * fiber blocking. - * - * @return - * an effect that describes whether the dequeueing of an element from the queue succeeded - * without blocking, with `None` denoting that no element was available - */ - def tryTake: F[Option[A]] - - /** - * Attempts to dequeue elements from the front of the queue, if they are available without - * semantically blocking. This method does not guarantee any additional performance benefits - * beyond simply recursively calling [[tryTake]], though some implementations will provide a - * more efficient implementation. - * - * @param maxN - * The max elements to dequeue. Passing `None` will try to dequeue the whole queue. - * - * @return - * an effect that contains the dequeued elements - */ - def tryTakeN(maxN: Option[Int])(implicit F: Monad[F]): F[List[A]] = - QueueSource.tryTakeN[F, A](maxN, tryTake) - - def size: F[Int] -} - -object QueueSource { - - private[std] def tryTakeN[F[_], A](maxN: Option[Int], tryTake: F[Option[A]])( - implicit F: Monad[F]): F[List[A]] = { - QueueSource.assertMaxNPositive(maxN) - - def loop(i: Int, limit: Int, acc: List[A]): F[List[A]] = - if (i >= limit) - F.pure(acc.reverse) - else - tryTake flatMap { - case Some(a) => loop(i + 1, limit, a :: acc) - case None => F.pure(acc.reverse) - } - - maxN match { - case Some(limit) => loop(0, limit, Nil) - case None => loop(0, Int.MaxValue, Nil) - } - } - - private[std] def assertMaxNPositive(maxN: Option[Int]): Unit = maxN match { - case Some(n) if n <= 0 => - throw new IllegalArgumentException(s"Provided maxN parameter must be positive, was $n") - case _ => () - } - - implicit def catsFunctorForQueueSource[F[_]: Functor]: Functor[QueueSource[F, *]] = - new Functor[QueueSource[F, *]] { - override def map[A, B](fa: QueueSource[F, A])(f: A => B): QueueSource[F, B] = - new QueueSource[F, B] { - override def take: F[B] = - fa.take.map(f) - override def tryTake: F[Option[B]] = { - fa.tryTake.map(_.map(f)) - } - override def size: F[Int] = - fa.size - } - } -} - -trait QueueSink[F[_], A] { - - /** - * Enqueues the given element at the back of the queue, possibly fiber blocking until - * sufficient capacity becomes available. - * - * @param a - * the element to be put at the back of the queue - */ - def offer(a: A): F[Unit] - - /** - * Attempts to enqueue the given element at the back of the queue without semantically - * blocking. - * - * @param a - * the element to be put at the back of the queue - * @return - * an effect that describes whether the enqueuing of the given element succeeded without - * blocking - */ - def tryOffer(a: A): F[Boolean] - - /** - * Attempts to enqueue the given elements at the back of the queue without semantically - * blocking. If an item in the list cannot be enqueued, the remaining elements will be - * returned. This is a convenience method that recursively runs `tryOffer` and does not offer - * any additional performance benefits. - * - * @param list - * the elements to be put at the back of the queue - * @return - * an effect that contains the remaining valus that could not be offered. - */ - def tryOfferN(list: List[A])(implicit F: Monad[F]): F[List[A]] = - QueueSink.tryOfferN[F, A](list, tryOffer) - -} - -object QueueSink { - - private[std] def tryOfferN[F[_], A](list: List[A], tryOffer: A => F[Boolean])( - implicit F: Monad[F]): F[List[A]] = list match { - case Nil => F.pure(list) - case h :: t => - tryOffer(h).ifM( - tryOfferN(t, tryOffer), - F.pure(list) - ) - } - - implicit def catsContravariantForQueueSink[F[_]]: Contravariant[QueueSink[F, *]] = - new Contravariant[QueueSink[F, *]] { - override def contramap[A, B](fa: QueueSink[F, A])(f: B => A): QueueSink[F, B] = - new QueueSink[F, B] { - override def offer(b: B): F[Unit] = - fa.offer(f(b)) - override def tryOffer(b: B): F[Boolean] = - fa.tryOffer(f(b)) - } - } -} diff --git a/std/shared/src/main/scala/cats/effect/std/QueueSink.scala b/std/shared/src/main/scala/cats/effect/std/QueueSink.scala new file mode 100644 index 0000000000..310f5f9fd7 --- /dev/null +++ b/std/shared/src/main/scala/cats/effect/std/QueueSink.scala @@ -0,0 +1,82 @@ +/* + * Copyright 2020-2023 Typelevel + * + * 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 +package effect +package std + +import cats.syntax.all._ + +trait QueueSink[F[_], A] { + + /** + * Enqueues the given element at the back of the queue, possibly fiber blocking until + * sufficient capacity becomes available. + * + * @param a + * the element to be put at the back of the queue + */ + def offer(a: A): F[Unit] + + /** + * Attempts to enqueue the given element at the back of the queue without semantically + * blocking. + * + * @param a + * the element to be put at the back of the queue + * @return + * an effect that describes whether the enqueuing of the given element succeeded without + * blocking + */ + def tryOffer(a: A): F[Boolean] + + /** + * Attempts to enqueue the given elements at the back of the queue without semantically + * blocking. If an item in the list cannot be enqueued, the remaining elements will be + * returned. This is a convenience method that recursively runs `tryOffer` and does not offer + * any additional performance benefits. + * + * @param list + * the elements to be put at the back of the queue + * @return + * an effect that contains the remaining valus that could not be offered. + */ + def tryOfferN(list: List[A])(implicit F: Monad[F]): F[List[A]] = + QueueSink.tryOfferN(list, tryOffer) +} + +object QueueSink { + + private[std] def tryOfferN[F[_], A](list: List[A], tryOffer: A => F[Boolean])(implicit F: Monad[F]): F[List[A]] = list match { + case Nil => F.pure(list) + case h :: t => + tryOffer(h).ifM( + tryOfferN(t, tryOffer), + F.pure(list) + ) + } + + implicit def catsContravariantForQueueSink[F[_]]: Contravariant[QueueSink[F, *]] = + new Contravariant[QueueSink[F, *]] { + override def contramap[A, B](fa: QueueSink[F, A])(f: B => A): QueueSink[F, B] = + new QueueSink[F, B] { + override def offer(b: B): F[Unit] = + fa.offer(f(b)) + override def tryOffer(b: B): F[Boolean] = + fa.tryOffer(f(b)) + } + } +} diff --git a/std/shared/src/main/scala/cats/effect/std/QueueSource.scala b/std/shared/src/main/scala/cats/effect/std/QueueSource.scala new file mode 100644 index 0000000000..8745019d4f --- /dev/null +++ b/std/shared/src/main/scala/cats/effect/std/QueueSource.scala @@ -0,0 +1,98 @@ +/* + * Copyright 2020-2023 Typelevel + * + * 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 +package effect +package std + +import cats.syntax.all._ + +trait QueueSource[F[_], A] { + + /** + * Dequeues an element from the front of the queue, possibly fiber blocking until an element + * becomes available. + */ + def take: F[A] + + /** + * Attempts to dequeue an element from the front of the queue, if one is available without + * fiber blocking. + * + * @return + * an effect that describes whether the dequeueing of an element from the queue succeeded + * without blocking, with `None` denoting that no element was available + */ + def tryTake: F[Option[A]] + + /** + * Attempts to dequeue elements from the front of the queue, if they are available without + * semantically blocking. This method does not guarantee any additional performance benefits + * beyond simply recursively calling [[tryTake]], though some implementations will provide a + * more efficient implementation. + * + * @param maxN + * The max elements to dequeue. Passing `None` will try to dequeue the whole queue. + * + * @return + * an effect that contains the dequeued elements + */ + def tryTakeN(maxN: Option[Int])(implicit F: Monad[F]): F[List[A]] = + QueueSource.tryTakeN(maxN, tryTake) + + def size: F[Int] +} + +object QueueSource { + + private[std] def tryTakeN[F[_], A](maxN: Option[Int], tryTake: F[Option[A]])(implicit F: Monad[F]): F[List[A]] = { + QueueSource.assertMaxNPositive(maxN) + + def loop(i: Int, limit: Int, acc: List[A]): F[List[A]] = + if (i >= limit) + F.pure(acc.reverse) + else + tryTake flatMap { + case Some(a) => loop(i + 1, limit, a :: acc) + case None => F.pure(acc.reverse) + } + + maxN match { + case Some(limit) => loop(0, limit, Nil) + case None => loop(0, Int.MaxValue, Nil) + } + } + + private[std] def assertMaxNPositive(maxN: Option[Int]): Unit = maxN match { + case Some(n) if n <= 0 => + throw new IllegalArgumentException(s"Provided maxN parameter must be positive, was $n") + case _ => () + } + + implicit def catsFunctorForQueueSource[F[_]: Functor]: Functor[QueueSource[F, *]] = + new Functor[QueueSource[F, *]] { + override def map[A, B](fa: QueueSource[F, A])(f: A => B): QueueSource[F, B] = + new QueueSource[F, B] { + override def take: F[B] = + fa.take.map(f) + override def tryTake: F[Option[B]] = { + fa.tryTake.map(_.map(f)) + } + override def size: F[Int] = + fa.size + } + } +} diff --git a/std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueueSink.scala b/std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueueSink.scala new file mode 100644 index 0000000000..dbcf64ba70 --- /dev/null +++ b/std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueueSink.scala @@ -0,0 +1,39 @@ +/* + * Copyright 2020-2023 Typelevel + * + * 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 +package effect +package std +package unsafe + +trait BoundedQueueSink[F[_], A] extends QueueSink[F, A] { + def unsafeTryOffer(a: A): Boolean +} + +object BoundedQueueSink { + implicit def catsContravariantForBoundedQueueSink[F[_]]: Contravariant[BoundedQueueSink[F, *]] = + new Contravariant[BoundedQueueSink[F, *]] { + override def contramap[A, B](fa: BoundedQueueSink[F, A])(f: B => A): BoundedQueueSink[F, B] = + new BoundedQueueSink[F, B] { + override def unsafeTryOffer(b: B): Boolean = + fa.unsafeTryOffer(f(b)) + override def offer(b: B): F[Unit] = + fa.offer(f(b)) + override def tryOffer(b: B): F[Boolean] = + fa.tryOffer(f(b)) + } + } +} diff --git a/std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueueSink.scala b/std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueueSink.scala new file mode 100644 index 0000000000..6a8589d713 --- /dev/null +++ b/std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueueSink.scala @@ -0,0 +1,44 @@ +/* + * Copyright 2020-2023 Typelevel + * + * 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 +package effect +package std +package unsafe + +trait UnboundedQueueSink[F[_], A] extends QueueSink[F, A] with BoundedQueueSink[F, A] { + def unsafeOffer(a: A): Unit + + def unsafeTryOffer(a: A): Boolean = { + unsafeOffer(a) + true + } +} + +object UnboundedQueueSink { + implicit def catsContravariantForUnboundedQueueSink[F[_]]: Contravariant[UnboundedQueueSink[F, *]] = + new Contravariant[UnboundedQueueSink[F, *]] { + override def contramap[A, B](fa: UnboundedQueueSink[F, A])(f: B => A): UnboundedQueueSink[F, B] = + new UnboundedQueueSink[F, B] { + override def unsafeOffer(b: B): Unit = + fa.unsafeOffer(f(b)) + override def offer(b: B): F[Unit] = + fa.offer(f(b)) + override def tryOffer(b: B): F[Boolean] = + fa.tryOffer(f(b)) + } + } +} diff --git a/tests/jvm/src/test/scala/cats/effect/std/DeferredJVMSpec.scala b/tests/jvm/src/test/scala/cats/effect/std/DeferredJVMSpec.scala index 6d045b6822..aa92037a0e 100644 --- a/tests/jvm/src/test/scala/cats/effect/std/DeferredJVMSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/std/DeferredJVMSpec.scala @@ -18,7 +18,7 @@ package cats.effect package std import cats.effect.kernel.Deferred -import cats.effect.unsafe.IORuntime +import cats.effect.unsafe.{IORuntime, IORuntimeConfig, Scheduler} import org.specs2.mutable.Specification import org.specs2.specification.BeforeAfterEach @@ -232,12 +232,12 @@ abstract class BaseDeferredJVMTests(parallelism: Int) try { ioa.unsafeRunTimed(10.seconds)( - unsafe.IORuntime( + IORuntime( ctx, ctx, - unsafe.Scheduler.fromScheduledExecutor(scheduler), + Scheduler.fromScheduledExecutor(scheduler), () => (), - unsafe.IORuntimeConfig())) + IORuntimeConfig())) } finally { executor.shutdown() scheduler.shutdown() From e3cc4cf15d0a422b42f79229e2e5c05825c99b31 Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Sun, 28 Jan 2024 17:05:23 -0600 Subject: [PATCH 326/429] Added nominative types for the unsafe queues and some tests --- .../main/scala/cats/effect/std/Queue.scala | 9 ++-- .../scala/cats/effect/std/QueueSink.scala | 3 +- .../scala/cats/effect/std/QueueSource.scala | 3 +- .../cats/effect/std/unsafe/BoundedQueue.scala | 47 +++++++++++++++++ .../effect/std/unsafe/BoundedQueueSink.scala | 6 ++- .../effect/std/unsafe/UnboundedQueue.scala | 50 +++++++++++++++++++ .../std/unsafe/UnboundedQueueSink.scala | 6 ++- .../scala/cats/effect/std/QueueSpec.scala | 35 +++++++++++++ 8 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueue.scala create mode 100644 std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueue.scala diff --git a/std/shared/src/main/scala/cats/effect/std/Queue.scala b/std/shared/src/main/scala/cats/effect/std/Queue.scala index 1696b7ad03..ca0f1dc7ef 100644 --- a/std/shared/src/main/scala/cats/effect/std/Queue.scala +++ b/std/shared/src/main/scala/cats/effect/std/Queue.scala @@ -106,7 +106,7 @@ object Queue { F.delay(new UnboundedAsyncQueue()) def unsafeBounded[F[_], A](capacity: Int)( - implicit F: Async[F]): F[Queue[F, A] with unsafe.BoundedQueueSink[F, A]] = { + implicit F: Async[F]): F[unsafe.BoundedQueue[F, A]] = { require(capacity > 1 && capacity < Short.MaxValue.toInt * 2) F.delay(new BoundedAsyncQueue(capacity)) } @@ -141,8 +141,7 @@ object Queue { unboundedForConcurrent } - def unsafeUnbounded[F[_], A]( - implicit F: Async[F]): F[Queue[F, A] with unsafe.UnboundedQueueSink[F, A]] = + def unsafeUnbounded[F[_], A](implicit F: Async[F]): F[unsafe.UnboundedQueue[F, A]] = F.delay(new UnboundedAsyncQueue()) /** @@ -553,7 +552,7 @@ object Queue { */ private final class BoundedAsyncQueue[F[_], A](capacity: Int)(implicit F: Async[F]) extends Queue[F, A] - with unsafe.BoundedQueueSink[F, A] { + with unsafe.BoundedQueue[F, A] { require(capacity > 1) @@ -818,7 +817,7 @@ object Queue { private final class UnboundedAsyncQueue[F[_], A]()(implicit F: Async[F]) extends Queue[F, A] - with unsafe.UnboundedQueueSink[F, A] { + with unsafe.UnboundedQueue[F, A] { private[this] val buffer = new UnsafeUnbounded[A]() private[this] val takers = new UnsafeUnbounded[Either[Throwable, Unit] => Unit]() diff --git a/std/shared/src/main/scala/cats/effect/std/QueueSink.scala b/std/shared/src/main/scala/cats/effect/std/QueueSink.scala index 310f5f9fd7..e9549ce59d 100644 --- a/std/shared/src/main/scala/cats/effect/std/QueueSink.scala +++ b/std/shared/src/main/scala/cats/effect/std/QueueSink.scala @@ -60,7 +60,8 @@ trait QueueSink[F[_], A] { object QueueSink { - private[std] def tryOfferN[F[_], A](list: List[A], tryOffer: A => F[Boolean])(implicit F: Monad[F]): F[List[A]] = list match { + private[std] def tryOfferN[F[_], A](list: List[A], tryOffer: A => F[Boolean])( + implicit F: Monad[F]): F[List[A]] = list match { case Nil => F.pure(list) case h :: t => tryOffer(h).ifM( diff --git a/std/shared/src/main/scala/cats/effect/std/QueueSource.scala b/std/shared/src/main/scala/cats/effect/std/QueueSource.scala index 8745019d4f..8ad9126508 100644 --- a/std/shared/src/main/scala/cats/effect/std/QueueSource.scala +++ b/std/shared/src/main/scala/cats/effect/std/QueueSource.scala @@ -58,7 +58,8 @@ trait QueueSource[F[_], A] { object QueueSource { - private[std] def tryTakeN[F[_], A](maxN: Option[Int], tryTake: F[Option[A]])(implicit F: Monad[F]): F[List[A]] = { + private[std] def tryTakeN[F[_], A](maxN: Option[Int], tryTake: F[Option[A]])( + implicit F: Monad[F]): F[List[A]] = { QueueSource.assertMaxNPositive(maxN) def loop(i: Int, limit: Int, acc: List[A]): F[List[A]] = diff --git a/std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueue.scala b/std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueue.scala new file mode 100644 index 0000000000..1a15d71196 --- /dev/null +++ b/std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueue.scala @@ -0,0 +1,47 @@ +/* + * Copyright 2020-2023 Typelevel + * + * 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 +package effect +package std +package unsafe + +import cats.syntax.all._ + +trait BoundedQueue[F[_], A] extends Queue[F, A] with BoundedQueueSink[F, A] + +object BoundedQueue { + + def apply[F[_]: kernel.Async, A](bound: Int): F[BoundedQueue[F, A]] = + Queue.unsafeBounded[F, A](bound) + + implicit def catsInvariantForBoundedQueue[F[_]: Functor]: Invariant[Queue[F, *]] = + new Invariant[Queue[F, *]] { + override def imap[A, B](fa: Queue[F, A])(f: A => B)(g: B => A): Queue[F, B] = + new Queue[F, B] { + override def offer(b: B): F[Unit] = + fa.offer(g(b)) + override def tryOffer(b: B): F[Boolean] = + fa.tryOffer(g(b)) + override def take: F[B] = + fa.take.map(f) + override def tryTake: F[Option[B]] = + fa.tryTake.map(_.map(f)) + override def size: F[Int] = + fa.size + } + } +} diff --git a/std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueueSink.scala b/std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueueSink.scala index dbcf64ba70..b739ade7da 100644 --- a/std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueueSink.scala +++ b/std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueueSink.scala @@ -24,9 +24,11 @@ trait BoundedQueueSink[F[_], A] extends QueueSink[F, A] { } object BoundedQueueSink { - implicit def catsContravariantForBoundedQueueSink[F[_]]: Contravariant[BoundedQueueSink[F, *]] = + implicit def catsContravariantForBoundedQueueSink[F[_]] + : Contravariant[BoundedQueueSink[F, *]] = new Contravariant[BoundedQueueSink[F, *]] { - override def contramap[A, B](fa: BoundedQueueSink[F, A])(f: B => A): BoundedQueueSink[F, B] = + override def contramap[A, B](fa: BoundedQueueSink[F, A])( + f: B => A): BoundedQueueSink[F, B] = new BoundedQueueSink[F, B] { override def unsafeTryOffer(b: B): Boolean = fa.unsafeTryOffer(f(b)) diff --git a/std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueue.scala b/std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueue.scala new file mode 100644 index 0000000000..b07688f6b7 --- /dev/null +++ b/std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueue.scala @@ -0,0 +1,50 @@ +/* + * Copyright 2020-2023 Typelevel + * + * 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 +package effect +package std +package unsafe + +import cats.syntax.all._ + +trait UnboundedQueue[F[_], A] + extends Queue[F, A] + with BoundedQueue[F, A] + with UnboundedQueueSink[F, A] + +object UnboundedQueue { + + def apply[F[_]: kernel.Async, A]: F[UnboundedQueue[F, A]] = + Queue.unsafeUnbounded[F, A] + + implicit def catsInvariantForUnboundedQueue[F[_]: Functor]: Invariant[Queue[F, *]] = + new Invariant[Queue[F, *]] { + override def imap[A, B](fa: Queue[F, A])(f: A => B)(g: B => A): Queue[F, B] = + new Queue[F, B] { + override def offer(b: B): F[Unit] = + fa.offer(g(b)) + override def tryOffer(b: B): F[Boolean] = + fa.tryOffer(g(b)) + override def take: F[B] = + fa.take.map(f) + override def tryTake: F[Option[B]] = + fa.tryTake.map(_.map(f)) + override def size: F[Int] = + fa.size + } + } +} diff --git a/std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueueSink.scala b/std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueueSink.scala index 6a8589d713..23f2727b98 100644 --- a/std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueueSink.scala +++ b/std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueueSink.scala @@ -29,9 +29,11 @@ trait UnboundedQueueSink[F[_], A] extends QueueSink[F, A] with BoundedQueueSink[ } object UnboundedQueueSink { - implicit def catsContravariantForUnboundedQueueSink[F[_]]: Contravariant[UnboundedQueueSink[F, *]] = + implicit def catsContravariantForUnboundedQueueSink[F[_]] + : Contravariant[UnboundedQueueSink[F, *]] = new Contravariant[UnboundedQueueSink[F, *]] { - override def contramap[A, B](fa: UnboundedQueueSink[F, A])(f: B => A): UnboundedQueueSink[F, B] = + override def contramap[A, B](fa: UnboundedQueueSink[F, A])( + f: B => A): UnboundedQueueSink[F, B] = new UnboundedQueueSink[F, B] { override def unsafeOffer(b: B): Unit = fa.unsafeOffer(f(b)) diff --git a/tests/shared/src/test/scala/cats/effect/std/QueueSpec.scala b/tests/shared/src/test/scala/cats/effect/std/QueueSpec.scala index 965cd6a35d..b7b147fd4b 100644 --- a/tests/shared/src/test/scala/cats/effect/std/QueueSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/std/QueueSpec.scala @@ -41,6 +41,29 @@ class BoundedQueueSpec extends BaseSpec with QueueTests[Queue] with DetectPlatfo boundedQueueTests(Queue.bounded) } + "BoundedQueue (unsafe)" should { + "permit tryOffer when empty" in real { + Queue.unsafeBounded[IO, Int](1024) flatMap { q => + for { + attempt <- IO(q.unsafeTryOffer(42)) + _ <- IO(attempt must beTrue) + i <- q.take + _ <- IO(i mustEqual 42) + } yield ok + } + } + + "forbid tryOffer when full" in real { + Queue.unsafeBounded[IO, Int](8) flatMap { q => + for { + _ <- 0.until(8).toList.traverse_(q.offer(_)) + attempt <- IO(q.unsafeTryOffer(42)) + _ <- IO(attempt must beFalse) + } yield ok + } + } + } + "BoundedQueue constructor" should { "not OOM" in real { Queue.bounded[IO, Unit](Int.MaxValue).as(true) @@ -366,6 +389,18 @@ class UnboundedQueueSpec extends BaseSpec with QueueTests[Queue] { unboundedQueueTests(Queue.unboundedForAsync) } + "UnboundedQueue (unsafe)" should { + "pass a value from unsafeOffer to take" in real { + Queue.unsafeUnbounded[IO, Int] flatMap { q => + for { + _ <- IO(q.unsafeOffer(42)) + i <- q.take + _ <- IO(i mustEqual 42) + } yield ok + } + } + } + "UnboundedQueue mapk" should { unboundedQueueTests(Queue.unbounded[IO, Int].map(_.mapK(FunctionK.id))) } From bf9937c23e8f873cc7369ba1f5efd534bd9e7bd6 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 07:48:46 +0000 Subject: [PATCH 327/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/3ff868a8830e7513a17ac7d2f4f071411c96b35e' (2024-01-17) → 'github:typelevel/typelevel-nix/2bbdddbc81a9da0052a22d7d8ebd17b88cd55a8f' (2024-01-29) • Updated input 'typelevel-nix/devshell': 'github:numtide/devshell/ca1ff587c602b934afe830ea3cb26d0fbde4c395' (2024-01-14) → 'github:numtide/devshell/83cb93d6d063ad290beee669f4badf9914cc16ec' (2024-01-15) • Updated input 'typelevel-nix/flake-utils': 'github:numtide/flake-utils/4022d587cbbfd70fe950c1e2083a02621806a725' (2023-12-04) → 'github:numtide/flake-utils/1ef2e671c3b0c19053962c07dbda38332dcebf26' (2024-01-15) • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/ea780f3de2d169f982564128804841500e85e373' (2024-01-14) → 'github:nixos/nixpkgs/160b762eda6d139ac10ae081f8f78d640dd523eb' (2024-01-27) --- flake.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/flake.lock b/flake.lock index 128cbd7dab..700d3efd8e 100644 --- a/flake.lock +++ b/flake.lock @@ -6,11 +6,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1705240333, - "narHash": "sha256-s9h2h44fCi54sSIT9ktd3eDik9JDpQE9DeYuXcA44u4=", + "lastModified": 1705332421, + "narHash": "sha256-USpGLPme1IuqG78JNqSaRabilwkCyHmVWY0M9vYyqEA=", "owner": "numtide", "repo": "devshell", - "rev": "ca1ff587c602b934afe830ea3cb26d0fbde4c395", + "rev": "83cb93d6d063ad290beee669f4badf9914cc16ec", "type": "github" }, "original": { @@ -42,11 +42,11 @@ "systems": "systems_2" }, "locked": { - "lastModified": 1701680307, - "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", "owner": "numtide", "repo": "flake-utils", - "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", "type": "github" }, "original": { @@ -73,11 +73,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1705242415, - "narHash": "sha256-a8DRYrNrzTudvO7XHUPNJD89Wbf1ZZT0VbwCsPnHWaE=", + "lastModified": 1706367331, + "narHash": "sha256-AqgkGHRrI6h/8FWuVbnkfFmXr4Bqsr4fV23aISqj/xg=", "owner": "nixos", "repo": "nixpkgs", - "rev": "ea780f3de2d169f982564128804841500e85e373", + "rev": "160b762eda6d139ac10ae081f8f78d640dd523eb", "type": "github" }, "original": { @@ -137,11 +137,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1705527810, - "narHash": "sha256-bl1CkA40UgejXV+ZMAaIqkPvkfl/5aqAgQP09TTrbiQ=", + "lastModified": 1706547644, + "narHash": "sha256-NGQ34yoAKu5jykVJtQ8dWsEh2MF88SpWXkT/ldhvzn0=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "3ff868a8830e7513a17ac7d2f4f071411c96b35e", + "rev": "2bbdddbc81a9da0052a22d7d8ebd17b88cd55a8f", "type": "github" }, "original": { From 5ebbf9f9014325b8fe2b3b8071346c2c8189da33 Mon Sep 17 00:00:00 2001 From: Seth Tisue Date: Tue, 30 Jan 2024 22:11:37 -0500 Subject: [PATCH 328/429] fix typo in docs --- docs/std/async-await.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/std/async-await.md b/docs/std/async-await.md index ded96c006e..031f6c756b 100644 --- a/docs/std/async-await.md +++ b/docs/std/async-await.md @@ -9,7 +9,7 @@ Syntactic sugar that allows for direct-style programming. This feature is provided by an incubating side-library that lives in another repository ([typelevel/cats-effect-cps](https://github.com/typelevel/cats-effect-cps)), so that it can undergo updates at a higher pace than Cats Effect if required. -Because it relies on experimental functionality from the compiler, cats-effec-cps ought to be considered experimental until upstream support stabilises, at which point it will be folded into Cats Effect itself. +Because it relies on experimental functionality from the compiler, cats-effect-cps ought to be considered experimental until upstream support stabilises, at which point it will be folded into Cats Effect itself. ## Installation From 62f3529d885b288f3cea13845aac311660b3f713 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 07:48:37 +0000 Subject: [PATCH 329/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/2bbdddbc81a9da0052a22d7d8ebd17b88cd55a8f' (2024-01-29) → 'github:typelevel/typelevel-nix/518374c569c63fdcae8eede4b41dfdc63da80ee3' (2024-02-05) • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/160b762eda6d139ac10ae081f8f78d640dd523eb' (2024-01-27) → 'github:nixos/nixpkgs/79a13f1437e149dc7be2d1290c74d378dad60814' (2024-02-03) --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 700d3efd8e..12c74fd000 100644 --- a/flake.lock +++ b/flake.lock @@ -73,11 +73,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1706367331, - "narHash": "sha256-AqgkGHRrI6h/8FWuVbnkfFmXr4Bqsr4fV23aISqj/xg=", + "lastModified": 1706925685, + "narHash": "sha256-hVInjWMmgH4yZgA4ZtbgJM1qEAel72SYhP5nOWX4UIM=", "owner": "nixos", "repo": "nixpkgs", - "rev": "160b762eda6d139ac10ae081f8f78d640dd523eb", + "rev": "79a13f1437e149dc7be2d1290c74d378dad60814", "type": "github" }, "original": { @@ -137,11 +137,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1706547644, - "narHash": "sha256-NGQ34yoAKu5jykVJtQ8dWsEh2MF88SpWXkT/ldhvzn0=", + "lastModified": 1707140733, + "narHash": "sha256-CyMonYdVvXJSIdcaecL1+q8ni57QAUduaZ0bBWWDsZs=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "2bbdddbc81a9da0052a22d7d8ebd17b88cd55a8f", + "rev": "518374c569c63fdcae8eede4b41dfdc63da80ee3", "type": "github" }, "original": { From bb17ea21e415afa3a2bfd971f62784b3871286f0 Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Sat, 10 Feb 2024 13:29:59 -0600 Subject: [PATCH 330/429] Added some explanatory scaladoc --- .../main/scala/cats/effect/std/Queue.scala | 26 +++++++++++++++++++ .../cats/effect/std/unsafe/BoundedQueue.scala | 10 +++++++ .../effect/std/unsafe/UnboundedQueue.scala | 7 +++++ 3 files changed, 43 insertions(+) diff --git a/std/shared/src/main/scala/cats/effect/std/Queue.scala b/std/shared/src/main/scala/cats/effect/std/Queue.scala index ca0f1dc7ef..5c6db4e46a 100644 --- a/std/shared/src/main/scala/cats/effect/std/Queue.scala +++ b/std/shared/src/main/scala/cats/effect/std/Queue.scala @@ -105,6 +105,20 @@ object Queue { private[effect] def unboundedForAsync[F[_], A](implicit F: Async[F]): F[Queue[F, A]] = F.delay(new UnboundedAsyncQueue()) + /** + * Creates a new `Queue` subject to some `capacity` bound which supports a side-effecting + * `tryOffer` function, allowing impure code to directly add values to the queue without + * indirecting through something like [[Dispatcher]]. This can improve performance + * significantly in some common cases. Note that the queue produced by this constructor can be + * used as a perfectly conventional [[Queue]] (as it is a subtype). + * + * @param capacity + * the maximum capacity of the queue (must be strictly greater than 1 and less than 32768) + * @return + * an empty bounded queue + * @see + * [[cats.effect.std.unsafe.BoundedQueue]] + */ def unsafeBounded[F[_], A](capacity: Int)( implicit F: Async[F]): F[unsafe.BoundedQueue[F, A]] = { require(capacity > 1 && capacity < Short.MaxValue.toInt * 2) @@ -141,6 +155,18 @@ object Queue { unboundedForConcurrent } + /** + * Creates a new `Queue` subject to some `capacity` bound which supports a side-effecting + * `offer` function, allowing impure code to directly add values to the queue without + * indirecting through something like [[Dispatcher]]. This can improve performance + * significantly in some common cases. Note that the queue produced by this constructor can be + * used as a perfectly conventional [[Queue]] (as it is a subtype). + * + * @return + * an empty unbounded queue + * @see + * [[cats.effect.std.unsafe.UnboundedQueue]] + */ def unsafeUnbounded[F[_], A](implicit F: Async[F]): F[unsafe.UnboundedQueue[F, A]] = F.delay(new UnboundedAsyncQueue()) diff --git a/std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueue.scala b/std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueue.scala index 1a15d71196..cfda39be88 100644 --- a/std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueue.scala +++ b/std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueue.scala @@ -21,6 +21,16 @@ package unsafe import cats.syntax.all._ +/** + * A [[Queue]] which supports a side-effecting variant of `tryOffer`, allowing impure code to + * add elements to the queue without having to indirect through something like [[Dispatcher]]. + * Note that a side-effecting variant of `offer` is impossible without hard-blocking a thread + * (when the queue is full), and thus is not supported by this API. For a variant of the same + * queue which supports a side-effecting `offer`, see [[UnboundedQueue]]. + * + * @see + * [[Queue.unsafeBounded]] + */ trait BoundedQueue[F[_], A] extends Queue[F, A] with BoundedQueueSink[F, A] object BoundedQueue { diff --git a/std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueue.scala b/std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueue.scala index b07688f6b7..f9e6bd92a7 100644 --- a/std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueue.scala +++ b/std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueue.scala @@ -21,6 +21,13 @@ package unsafe import cats.syntax.all._ +/** + * A [[Queue]] which supports a side-effecting variant of `offer`, allowing impure code to add + * elements to the queue without having to indirect through something like [[Dispatcher]]. + * + * @see + * [[Queue.unsafeUnbounded]] + */ trait UnboundedQueue[F[_], A] extends Queue[F, A] with BoundedQueue[F, A] From b1ab13ba583b3e805bb04700bfdf581eee9f5694 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 13 Feb 2024 07:48:49 +0000 Subject: [PATCH 331/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/518374c569c63fdcae8eede4b41dfdc63da80ee3' (2024-02-05) → 'github:typelevel/typelevel-nix/85318b2ace46ee59560d5a5ce1a56ee08b46b15e' (2024-02-12) • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/79a13f1437e149dc7be2d1290c74d378dad60814' (2024-02-03) → 'github:nixos/nixpkgs/f3a93440fbfff8a74350f4791332a19282cc6dc8' (2024-02-11) --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 12c74fd000..105ffb7458 100644 --- a/flake.lock +++ b/flake.lock @@ -73,11 +73,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1706925685, - "narHash": "sha256-hVInjWMmgH4yZgA4ZtbgJM1qEAel72SYhP5nOWX4UIM=", + "lastModified": 1707619277, + "narHash": "sha256-vKnYD5GMQbNQyyQm4wRlqi+5n0/F1hnvqSQgaBy4BqY=", "owner": "nixos", "repo": "nixpkgs", - "rev": "79a13f1437e149dc7be2d1290c74d378dad60814", + "rev": "f3a93440fbfff8a74350f4791332a19282cc6dc8", "type": "github" }, "original": { @@ -137,11 +137,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1707140733, - "narHash": "sha256-CyMonYdVvXJSIdcaecL1+q8ni57QAUduaZ0bBWWDsZs=", + "lastModified": 1707745269, + "narHash": "sha256-0mwnDOYtq6TNfKKOhMa66o4gDOZXTjY5kho43tTeQ24=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "518374c569c63fdcae8eede4b41dfdc63da80ee3", + "rev": "85318b2ace46ee59560d5a5ce1a56ee08b46b15e", "type": "github" }, "original": { From 3b0e1f9bb9e121030ebd0572442ebc195f518261 Mon Sep 17 00:00:00 2001 From: Chidi Nweke <88949660+ChidiRnweke@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:18:28 +0100 Subject: [PATCH 332/429] Use dep instead of lib in FAQ docs The directive "lib" has been deprecated in favour of the directive "dep". Using the lib directive "lib" results in a deprecation warning. See https://scala-cli.virtuslab.org/docs/release_notes#rename-using-lib-to-using-dep for more information. --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index 4a7c1b40e6..6235ec4cf3 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -9,7 +9,7 @@ title: FAQ ```scala-cli //> using scala "2.13.8" -//> using lib "org.typelevel::cats-effect::3.5.3" +//> using dep "org.typelevel::cats-effect::3.5.3" import cats.effect._ From 0856fa1b712fa802b8c773114fe16b6233b4fa76 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 15 Feb 2024 19:53:43 +0000 Subject: [PATCH 333/429] Regenerate workflow --- .github/workflows/ci.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efd58a93d6..7747f29c48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -283,20 +283,12 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) shell: bash -<<<<<<< HEAD - run: mkdir -p benchmarks/target testkit/native/target target std/jvm/target example/js/target kernel-testkit/jvm/target testkit/js/target rootJS/target core/native/target site-docs/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target graalvm-example/target kernel-testkit/js/target core/jvm/target rootJVM/target rootNative/target example/native/target kernel/native/target example/jvm/target laws/jvm/target std/native/target testkit/jvm/target project/target -======= run: mkdir -p testkit/native/target std/jvm/target kernel-testkit/jvm/target testkit/js/target core/native/target site-docs/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target kernel-testkit/js/target core/jvm/target kernel/native/target laws/jvm/target std/native/target testkit/jvm/target project/target ->>>>>>> series/3.x - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) shell: bash -<<<<<<< HEAD - run: tar cf targets.tar benchmarks/target testkit/native/target target std/jvm/target example/js/target kernel-testkit/jvm/target testkit/js/target rootJS/target core/native/target site-docs/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target graalvm-example/target kernel-testkit/js/target core/jvm/target rootJVM/target rootNative/target example/native/target kernel/native/target example/jvm/target laws/jvm/target std/native/target testkit/jvm/target project/target -======= run: tar cf targets.tar testkit/native/target std/jvm/target kernel-testkit/jvm/target testkit/js/target core/native/target site-docs/target std/js/target laws/native/target kernel-testkit/native/target kernel/jvm/target core/js/target kernel/js/target laws/js/target kernel-testkit/js/target core/jvm/target kernel/native/target laws/jvm/target std/native/target testkit/jvm/target project/target ->>>>>>> series/3.x - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) @@ -617,5 +609,5 @@ jobs: - name: Submit Dependencies uses: scalacenter/sbt-dependency-submission@v2 with: - modules-ignore: cats-effect-benchmarks_3 cats-effect-benchmarks_2.12 cats-effect-benchmarks_2.13 cats-effect_3 cats-effect_2.12 cats-effect_2.13 cats-effect-stress-tests_3 cats-effect-stress-tests_2.12 cats-effect-stress-tests_2.13 cats-effect-example_sjs1_3 cats-effect-example_sjs1_2.12 cats-effect-example_sjs1_2.13 rootjs_3 rootjs_2.12 rootjs_2.13 ioapptestsnative_3 ioapptestsnative_2.12 ioapptestsnative_2.13 cats-effect-graalvm-example_3 cats-effect-graalvm-example_2.12 cats-effect-graalvm-example_2.13 cats-effect-tests_sjs1_3 cats-effect-tests_sjs1_2.12 cats-effect-tests_sjs1_2.13 rootjvm_3 rootjvm_2.12 rootjvm_2.13 rootnative_3 rootnative_2.12 rootnative_2.13 cats-effect-example_native0.4_3 cats-effect-example_native0.4_2.12 cats-effect-example_native0.4_2.13 cats-effect-example_3 cats-effect-example_2.12 cats-effect-example_2.13 cats-effect-tests_3 cats-effect-tests_2.12 cats-effect-tests_2.13 ioapptestsjvm_3 ioapptestsjvm_2.12 ioapptestsjvm_2.13 ioapptestsjs_3 ioapptestsjs_2.12 ioapptestsjs_2.13 cats-effect-tests_native0.4_3 cats-effect-tests_native0.4_2.12 cats-effect-tests_native0.4_2.13 + modules-ignore: cats-effect-benchmarks_3 cats-effect-benchmarks_2.12 cats-effect-benchmarks_2.13 cats-effect_3 cats-effect_2.12 cats-effect_2.13 cats-effect-example_sjs1_3 cats-effect-example_sjs1_2.12 cats-effect-example_sjs1_2.13 rootjs_3 rootjs_2.12 rootjs_2.13 ioapptestsnative_3 ioapptestsnative_2.12 ioapptestsnative_2.13 cats-effect-graalvm-example_3 cats-effect-graalvm-example_2.12 cats-effect-graalvm-example_2.13 cats-effect-tests_sjs1_3 cats-effect-tests_sjs1_2.12 cats-effect-tests_sjs1_2.13 rootjvm_3 rootjvm_2.12 rootjvm_2.13 rootnative_3 rootnative_2.12 rootnative_2.13 cats-effect-example_native0.4_3 cats-effect-example_native0.4_2.12 cats-effect-example_native0.4_2.13 cats-effect-example_3 cats-effect-example_2.12 cats-effect-example_2.13 cats-effect-tests_3 cats-effect-tests_2.12 cats-effect-tests_2.13 ioapptestsjvm_3 ioapptestsjvm_2.12 ioapptestsjvm_2.13 ioapptestsjs_3 ioapptestsjs_2.12 ioapptestsjs_2.13 cats-effect-tests_native0.4_3 cats-effect-tests_native0.4_2.12 cats-effect-tests_native0.4_2.13 configs-ignore: test scala-tool scala-doc-tool test-internal From 3050e1808c2d3cc33295f9981640feee6e825466 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Sun, 18 Feb 2024 04:09:57 +0000 Subject: [PATCH 334/429] Update sbt-typelevel to 0.6.6 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 5da40853a5..5ab53a5642 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.6.5") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.6.6") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.15.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") From 19086730968debd0b4e7b30e0f429a961cab0c04 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 19 Feb 2024 02:26:05 +0000 Subject: [PATCH 335/429] Bump copyright year --- NOTICE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NOTICE.txt b/NOTICE.txt index eebb95040d..d3c1e1a527 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1,5 +1,5 @@ cats-effect -Copyright 2020-2023 Typelevel +Copyright 2020-2024 Typelevel Licensed under Apache License 2.0 (see LICENSE) This software contains portions of code derived from scala-js From 02f1c9d9a6ff71e4e14f88648e41d0c5b8497375 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 19 Feb 2024 02:34:52 +0000 Subject: [PATCH 336/429] Handle exceptions in externally submitted sleeps --- .../cats/effect/unsafe/WorkStealingThreadPool.scala | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index 11df189c59..631f1a45bb 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -36,6 +36,7 @@ import cats.effect.tracing.TracingConstants import scala.collection.mutable import scala.concurrent.ExecutionContextExecutor import scala.concurrent.duration.{Duration, FiniteDuration} +import scala.util.control.NonFatal import java.time.Instant import java.time.temporal.ChronoField @@ -662,7 +663,14 @@ private[effect] final class WorkStealingThreadPool[P]( override def sleep(delay: FiniteDuration, task: Runnable): Runnable = { val cb = new AtomicBoolean with (Right[Nothing, Unit] => Unit) { // run at most once - def apply(ru: Right[Nothing, Unit]) = if (compareAndSet(false, true)) task.run() + def apply(ru: Right[Nothing, Unit]) = if (compareAndSet(false, true)) { + try { + task.run() + } catch { + case ex if NonFatal(ex) => + reportFailure(ex) + } + } } val cancel = sleepInternal(delay, cb) From 0927d34240a28a0ed6f2f2065cdc1a497e95150c Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 19 Feb 2024 02:37:03 +0000 Subject: [PATCH 337/429] Account for possible null heap in TimerHeap#steal --- core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index 6e0a73360b..963712cae2 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -164,8 +164,10 @@ private final class TimerHeap extends AtomicInteger { } else false val heap = this.heap // local copy - val size = Math.min(this.size, heap.length - 1) - go(heap, size, 1) + if (heap ne null) { + val size = Math.min(this.size, heap.length - 1) + go(heap, size, 1) + } else false } /** From 91e12342c767cfb76b6884233bbff5a8b7034e66 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 20:05:42 +0000 Subject: [PATCH 338/429] Update scalafmt-core to 3.8.0 in series/3.x --- .scalafmt.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index 90853fc664..0a5c3b9999 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = 3.7.17 +version = 3.8.0 runner.dialect = Scala213Source3 fileOverride { From 41bc9b94a85d8e8c460348e08b2072a4088d235e Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Fri, 23 Feb 2024 08:08:27 +0000 Subject: [PATCH 339/429] Update sbt to 1.9.9 in series/3.x --- project/build.properties | 2 +- scalafix/project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project/build.properties b/project/build.properties index abbbce5da4..04267b14af 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.8 +sbt.version=1.9.9 diff --git a/scalafix/project/build.properties b/scalafix/project/build.properties index abbbce5da4..04267b14af 100644 --- a/scalafix/project/build.properties +++ b/scalafix/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.8 +sbt.version=1.9.9 From c2712f2a549437f85b43c93137d4a222356dcfcf Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 08:25:39 +0000 Subject: [PATCH 340/429] Update sbt-typelevel to 0.6.7 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 46c420e835..5eb8eb1f05 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.6.6") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.6.7") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.15.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") From 4f3a5de81a3bd570cf72820eaa573de730fc3abd Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 08:26:54 +0000 Subject: [PATCH 341/429] Run prePR with sbt-typelevel Executed command: sbt tlPrePrBotHook --- .github/workflows/ci.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7747f29c48..011f0c1705 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,6 +123,11 @@ jobs: runs-on: ${{ matrix.os }} timeout-minutes: 60 steps: + - name: Install sbt + if: contains(runner.os, 'macos') + shell: bash + run: brew install sbt + - name: Ignore line ending differences in git if: contains(runner.os, 'windows') shell: bash @@ -307,6 +312,10 @@ jobs: java: [temurin@8] runs-on: ${{ matrix.os }} steps: + - name: Install sbt + if: contains(runner.os, 'macos') + run: brew install sbt + - name: Ignore line ending differences in git if: contains(runner.os, 'windows') run: git config --global core.autocrlf false @@ -532,6 +541,10 @@ jobs: java: [temurin@8] runs-on: ${{ matrix.os }} steps: + - name: Install sbt + if: contains(runner.os, 'macos') + run: brew install sbt + - name: Ignore line ending differences in git if: contains(runner.os, 'windows') run: git config --global core.autocrlf false From 554e46b3147f2cb15cb9b148e7ce35f6afeba080 Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Sat, 3 Feb 2024 10:13:17 -0600 Subject: [PATCH 342/429] Updated macos GHA runners to use the new M series --- .github/workflows/ci.yml | 14 +++++++------- build.sbt | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 011f0c1705..714b8437c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: name: Build and Test strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest, windows-latest, macos-14] scala: [3.3.1, 2.12.18, 2.13.12] java: - temurin@8 @@ -52,13 +52,13 @@ jobs: - os: windows-latest scala: 3.3.1 ci: ciJVM - - os: macos-latest + - os: macos-14 scala: 3.3.1 ci: ciJVM - os: windows-latest scala: 2.12.18 ci: ciJVM - - os: macos-latest + - os: macos-14 scala: 2.12.18 ci: ciJVM - ci: ciFirefox @@ -79,7 +79,7 @@ jobs: java: graalvm@17 - os: windows-latest ci: ciJS - - os: macos-latest + - os: macos-14 ci: ciJS - ci: ciFirefox java: temurin@11 @@ -91,7 +91,7 @@ jobs: java: graalvm@17 - os: windows-latest ci: ciFirefox - - os: macos-latest + - os: macos-14 ci: ciFirefox - ci: ciChrome java: temurin@11 @@ -103,7 +103,7 @@ jobs: java: graalvm@17 - os: windows-latest ci: ciChrome - - os: macos-latest + - os: macos-14 ci: ciChrome - ci: ciNative java: temurin@11 @@ -115,7 +115,7 @@ jobs: java: graalvm@17 - os: windows-latest ci: ciNative - - os: macos-latest + - os: macos-14 ci: ciNative scala: 2.12.18 - os: windows-latest diff --git a/build.sbt b/build.sbt index 283b4e706d..51ead65cd9 100644 --- a/build.sbt +++ b/build.sbt @@ -110,7 +110,7 @@ ThisBuild / developers := List( val PrimaryOS = "ubuntu-latest" val Windows = "windows-latest" -val MacOS = "macos-latest" +val MacOS = "macos-14" val Scala212 = "2.12.18" val Scala213 = "2.13.12" From 495c520ad7ba4a0366a549156914d9909d4629ac Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 26 Feb 2024 14:37:26 +0000 Subject: [PATCH 343/429] Exclude macOS / Java 8 jobs --- .github/workflows/ci.yml | 2 ++ build.sbt | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 714b8437c5..94d3d9ea8f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,8 @@ jobs: - os: macos-14 scala: 2.12.18 ci: ciJVM + - os: macos-14 + java: temurin@8 - ci: ciFirefox scala: 3.3.1 - ci: ciChrome diff --git a/build.sbt b/build.sbt index 51ead65cd9..97f38e222f 100644 --- a/build.sbt +++ b/build.sbt @@ -244,7 +244,7 @@ ThisBuild / githubWorkflowBuildMatrixExclusions := { Seq( MatrixExclude(Map("os" -> Windows, "scala" -> scala, "ci" -> CI.JVM.command)), MatrixExclude(Map("os" -> MacOS, "scala" -> scala, "ci" -> CI.JVM.command))) - } + } :+ MatrixExclude(Map("os" -> MacOS, "java" -> OldGuardJava.render)) val jsScalaFilters = for { scala <- (ThisBuild / githubWorkflowScalaVersions).value.filterNot(Set(Scala213)) From f1b8fce3928f31a3ca34fa076d243e77b941ed67 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 00:20:55 +0000 Subject: [PATCH 344/429] Update scala-library to 2.12.19 in series/3.x --- .github/workflows/ci.yml | 20 ++++++++++---------- build.sbt | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94d3d9ea8f..0d14906428 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-14] - scala: [3.3.1, 2.12.18, 2.13.12] + scala: [3.3.1, 2.12.19, 2.13.12] java: - temurin@8 - temurin@11 @@ -41,13 +41,13 @@ jobs: java: temurin@11 - scala: 3.3.1 java: temurin@21 - - scala: 2.12.18 + - scala: 2.12.19 java: temurin@11 - - scala: 2.12.18 + - scala: 2.12.19 java: temurin@17 - - scala: 2.12.18 + - scala: 2.12.19 java: temurin@21 - - scala: 2.12.18 + - scala: 2.12.19 java: graalvm@17 - os: windows-latest scala: 3.3.1 @@ -56,10 +56,10 @@ jobs: scala: 3.3.1 ci: ciJVM - os: windows-latest - scala: 2.12.18 + scala: 2.12.19 ci: ciJVM - os: macos-14 - scala: 2.12.18 + scala: 2.12.19 ci: ciJVM - os: macos-14 java: temurin@8 @@ -68,9 +68,9 @@ jobs: - ci: ciChrome scala: 3.3.1 - ci: ciFirefox - scala: 2.12.18 + scala: 2.12.19 - ci: ciChrome - scala: 2.12.18 + scala: 2.12.19 - ci: ciJS java: temurin@11 - ci: ciJS @@ -119,7 +119,7 @@ jobs: ci: ciNative - os: macos-14 ci: ciNative - scala: 2.12.18 + scala: 2.12.19 - os: windows-latest java: graalvm@17 runs-on: ${{ matrix.os }} diff --git a/build.sbt b/build.sbt index 97f38e222f..a967fdf8ca 100644 --- a/build.sbt +++ b/build.sbt @@ -112,7 +112,7 @@ val PrimaryOS = "ubuntu-latest" val Windows = "windows-latest" val MacOS = "macos-14" -val Scala212 = "2.12.18" +val Scala212 = "2.12.19" val Scala213 = "2.13.12" val Scala3 = "3.3.1" From fa5a5ef9602944ee05cb2d7d7e0f0ed59990f557 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 00:21:24 +0000 Subject: [PATCH 345/429] Regenerate GitHub Actions workflow Executed command: sbt githubWorkflowGenerate --- .github/workflows/ci.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d14906428..1be9e7bc82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -422,32 +422,32 @@ jobs: tar xf targets.tar rm targets.tar - - name: Download target directories (2.12.18, ciJVM) + - name: Download target directories (2.12.19, ciJVM) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.18-ciJVM + name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.19-ciJVM - - name: Inflate target directories (2.12.18, ciJVM) + - name: Inflate target directories (2.12.19, ciJVM) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.12.18, ciNative) + - name: Download target directories (2.12.19, ciNative) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.18-ciNative + name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.19-ciNative - - name: Inflate target directories (2.12.18, ciNative) + - name: Inflate target directories (2.12.19, ciNative) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.12.18, ciJS) + - name: Download target directories (2.12.19, ciJS) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.18-ciJS + name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.19-ciJS - - name: Inflate target directories (2.12.18, ciJS) + - name: Inflate target directories (2.12.19, ciJS) run: | tar xf targets.tar rm targets.tar From c6528a53cb61c927c657ceee459d3e57ba92aa11 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 07:48:47 +0000 Subject: [PATCH 346/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/85318b2ace46ee59560d5a5ce1a56ee08b46b15e' (2024-02-12) → 'github:typelevel/typelevel-nix/035bec68f65a213bf6080c7aeb18ba88b9fe9b5f' (2024-02-26) • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/f3a93440fbfff8a74350f4791332a19282cc6dc8' (2024-02-11) → 'github:nixos/nixpkgs/2a34566b67bef34c551f204063faeecc444ae9da' (2024-02-25) --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 105ffb7458..6c68848764 100644 --- a/flake.lock +++ b/flake.lock @@ -73,11 +73,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1707619277, - "narHash": "sha256-vKnYD5GMQbNQyyQm4wRlqi+5n0/F1hnvqSQgaBy4BqY=", + "lastModified": 1708847675, + "narHash": "sha256-RUZ7KEs/a4EzRELYDGnRB6i7M1Izii3JD/LyzH0c6Tg=", "owner": "nixos", "repo": "nixpkgs", - "rev": "f3a93440fbfff8a74350f4791332a19282cc6dc8", + "rev": "2a34566b67bef34c551f204063faeecc444ae9da", "type": "github" }, "original": { @@ -137,11 +137,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1707745269, - "narHash": "sha256-0mwnDOYtq6TNfKKOhMa66o4gDOZXTjY5kho43tTeQ24=", + "lastModified": 1708920671, + "narHash": "sha256-mzSNb+bwOKtbgf2d/ypxR+mHsnzBPOXzssrckpr7RU0=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "85318b2ace46ee59560d5a5ce1a56ee08b46b15e", + "rev": "035bec68f65a213bf6080c7aeb18ba88b9fe9b5f", "type": "github" }, "original": { From 370018f253139ae41c7c1f892f6ea9d533c49594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Tue, 27 Feb 2024 16:21:58 +0100 Subject: [PATCH 347/429] Fix Gavin's name --- docs/third-party-resources.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/third-party-resources.md b/docs/third-party-resources.md index c0ef43c99c..387a79070e 100644 --- a/docs/third-party-resources.md +++ b/docs/third-party-resources.md @@ -31,5 +31,5 @@ These are some selected videos on various topics in Cats Effect: * [Concurrency with Cats Effect](https://www.youtube.com/watch?v=Gig-f_HXvLI&ab_channel=FunctionalTV), Michael Pilquist * [How do Fibers work?](https://www.youtube.com/watch?v=x5_MmZVLiSM&ab_channel=ScalaWorld), Fabio Labella * [Cancellation in Cats Effect](https://www.youtube.com/watch?v=X9u3rgPz_zE&t=1002s&ab_channel=ScalaintheCity), Daniel Ciocîrlan -* [Intro to Cats Effect](https://www.youtube.com/watch?v=83pXEdCpY4A&ab_channel=thoughtbot), Gavin Biesi +* [Intro to Cats Effect](https://www.youtube.com/watch?v=83pXEdCpY4A&ab_channel=thoughtbot), Gavin Bisesi * [The IO Monad for Scala](https://www.youtube.com/watch?v=8_TWM2t97r4&t=811s&ab_channel=ScalaIOFR), Gabriel Volpe From 7f011b108f4817e9ac4be33610ccb113eb981622 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Thu, 29 Feb 2024 20:10:16 +0000 Subject: [PATCH 348/429] Update scala3-library, ... to 3.3.3 in series/3.x --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index a967fdf8ca..ae6ee25665 100644 --- a/build.sbt +++ b/build.sbt @@ -114,7 +114,7 @@ val MacOS = "macos-14" val Scala212 = "2.12.19" val Scala213 = "2.13.12" -val Scala3 = "3.3.1" +val Scala3 = "3.3.3" ThisBuild / crossScalaVersions := Seq(Scala3, Scala212, Scala213) ThisBuild / githubWorkflowScalaVersions := crossScalaVersions.value From 55ce360744135b153dc24fb3f456c8b699689562 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Thu, 29 Feb 2024 20:10:45 +0000 Subject: [PATCH 349/429] Regenerate GitHub Actions workflow Executed command: sbt githubWorkflowGenerate --- .github/workflows/ci.yml | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1be9e7bc82..16113d852e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-14] - scala: [3.3.1, 2.12.19, 2.13.12] + scala: [3.3.3, 2.12.19, 2.13.12] java: - temurin@8 - temurin@11 @@ -37,9 +37,9 @@ jobs: - graalvm@17 ci: [ciJVM, ciNative, ciJS, ciFirefox, ciChrome] exclude: - - scala: 3.3.1 + - scala: 3.3.3 java: temurin@11 - - scala: 3.3.1 + - scala: 3.3.3 java: temurin@21 - scala: 2.12.19 java: temurin@11 @@ -50,10 +50,10 @@ jobs: - scala: 2.12.19 java: graalvm@17 - os: windows-latest - scala: 3.3.1 + scala: 3.3.3 ci: ciJVM - os: macos-14 - scala: 3.3.1 + scala: 3.3.3 ci: ciJVM - os: windows-latest scala: 2.12.19 @@ -64,9 +64,9 @@ jobs: - os: macos-14 java: temurin@8 - ci: ciFirefox - scala: 3.3.1 + scala: 3.3.3 - ci: ciChrome - scala: 3.3.1 + scala: 3.3.3 - ci: ciFirefox scala: 2.12.19 - ci: ciChrome @@ -239,24 +239,24 @@ jobs: run: sbt githubWorkflowCheck - name: Check that scalafix has been run on JVM - if: matrix.ci == 'ciJVM' && matrix.scala != '3.3.1' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' + if: matrix.ci == 'ciJVM' && matrix.scala != '3.3.3' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' shell: bash run: sbt '++ ${{ matrix.scala }}' 'rootJVM/scalafixAll --check' - name: Check that scalafix has been run on JS - if: matrix.ci == 'ciJS' && matrix.scala != '3.3.1' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' + if: matrix.ci == 'ciJS' && matrix.scala != '3.3.3' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' shell: bash run: sbt '++ ${{ matrix.scala }}' 'rootJS/scalafixAll --check' - name: Check that scalafix has been run on Native - if: matrix.ci == 'ciNative' && matrix.scala != '3.3.1' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' + if: matrix.ci == 'ciNative' && matrix.scala != '3.3.3' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' shell: bash run: sbt '++ ${{ matrix.scala }}' 'rootNative/scalafixAll --check' - shell: bash run: sbt '++ ${{ matrix.scala }}' '${{ matrix.ci }}' - - if: (matrix.scala == '2.13.12' || matrix.scala == '3.3.1') && matrix.ci == 'ciJVM' && matrix.java == 'temurin@17' + - if: (matrix.scala == '2.13.12' || matrix.scala == '3.3.3') && matrix.ci == 'ciJVM' && matrix.java == 'temurin@17' shell: bash run: sbt '++ ${{ matrix.scala }}' docs/mdoc @@ -392,32 +392,32 @@ jobs: if: matrix.java == 'graalvm@17' && steps.setup-java-graalvm-17.outputs.cache-hit == 'false' run: sbt +update - - name: Download target directories (3.3.1, ciJVM) + - name: Download target directories (3.3.3, ciJVM) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.1-ciJVM + name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.3-ciJVM - - name: Inflate target directories (3.3.1, ciJVM) + - name: Inflate target directories (3.3.3, ciJVM) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (3.3.1, ciNative) + - name: Download target directories (3.3.3, ciNative) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.1-ciNative + name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.3-ciNative - - name: Inflate target directories (3.3.1, ciNative) + - name: Inflate target directories (3.3.3, ciNative) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (3.3.1, ciJS) + - name: Download target directories (3.3.3, ciJS) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.1-ciJS + name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.3-ciJS - - name: Inflate target directories (3.3.1, ciJS) + - name: Inflate target directories (3.3.3, ciJS) run: | tar xf targets.tar rm targets.tar From 58dcfc081114d1ab4ff58fbfc3c0b3300db496c1 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 5 Mar 2024 07:48:57 +0000 Subject: [PATCH 350/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/035bec68f65a213bf6080c7aeb18ba88b9fe9b5f' (2024-02-26) → 'github:typelevel/typelevel-nix/52169f0a21ffe51a16598423290ebf7d0d6cc2b1' (2024-03-04) • Updated input 'typelevel-nix/devshell': 'github:numtide/devshell/83cb93d6d063ad290beee669f4badf9914cc16ec' (2024-01-15) → 'github:numtide/devshell/5ddecd67edbd568ebe0a55905273e56cc82aabe3' (2024-02-26) • Updated input 'typelevel-nix/flake-utils': 'github:numtide/flake-utils/1ef2e671c3b0c19053962c07dbda38332dcebf26' (2024-01-15) → 'github:numtide/flake-utils/d465f4819400de7c8d874d50b982301f28a84605' (2024-02-28) • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/2a34566b67bef34c551f204063faeecc444ae9da' (2024-02-25) → 'github:nixos/nixpkgs/fa9a51752f1b5de583ad5213eb621be071806663' (2024-03-02) --- flake.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/flake.lock b/flake.lock index 6c68848764..0769b3f9e3 100644 --- a/flake.lock +++ b/flake.lock @@ -6,11 +6,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1705332421, - "narHash": "sha256-USpGLPme1IuqG78JNqSaRabilwkCyHmVWY0M9vYyqEA=", + "lastModified": 1708939976, + "narHash": "sha256-O5+nFozxz2Vubpdl1YZtPrilcIXPcRAjqNdNE8oCRoA=", "owner": "numtide", "repo": "devshell", - "rev": "83cb93d6d063ad290beee669f4badf9914cc16ec", + "rev": "5ddecd67edbd568ebe0a55905273e56cc82aabe3", "type": "github" }, "original": { @@ -42,11 +42,11 @@ "systems": "systems_2" }, "locked": { - "lastModified": 1705309234, - "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "lastModified": 1709126324, + "narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=", "owner": "numtide", "repo": "flake-utils", - "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "rev": "d465f4819400de7c8d874d50b982301f28a84605", "type": "github" }, "original": { @@ -73,11 +73,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1708847675, - "narHash": "sha256-RUZ7KEs/a4EzRELYDGnRB6i7M1Izii3JD/LyzH0c6Tg=", + "lastModified": 1709386671, + "narHash": "sha256-VPqfBnIJ+cfa78pd4Y5Cr6sOWVW8GYHRVucxJGmRf8Q=", "owner": "nixos", "repo": "nixpkgs", - "rev": "2a34566b67bef34c551f204063faeecc444ae9da", + "rev": "fa9a51752f1b5de583ad5213eb621be071806663", "type": "github" }, "original": { @@ -137,11 +137,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1708920671, - "narHash": "sha256-mzSNb+bwOKtbgf2d/ypxR+mHsnzBPOXzssrckpr7RU0=", + "lastModified": 1709579932, + "narHash": "sha256-6FXB4+iqwPAoYr1nbUpP8wtV09cPpnZyOIq5z58IhOs=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "035bec68f65a213bf6080c7aeb18ba88b9fe9b5f", + "rev": "52169f0a21ffe51a16598423290ebf7d0d6cc2b1", "type": "github" }, "original": { From fefe1f204c7fe355d6e478323d82f8c0edc4fdfc Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Wed, 6 Mar 2024 14:22:31 -0600 Subject: [PATCH 351/429] Fixed older headers --- .../main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala | 2 +- .../src/main/scala/cats/effect/unsafe/PollingSystem.scala | 2 +- core/jvm/src/main/scala/cats/effect/Selector.scala | 2 +- core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala | 2 +- core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala | 2 +- core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala | 2 +- .../src/main/scala/cats/effect/FileDescriptorPoller.scala | 2 +- core/native/src/main/scala/cats/effect/Signal.scala | 2 +- core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala | 2 +- .../scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala | 2 +- .../native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala | 2 +- .../src/main/scala/catseffect/examplesjvmnative.scala | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/core/js/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala b/core/js/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala index 0e60f17760..a9cce1cb31 100644 --- a/core/js/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala +++ b/core/js/src/main/scala/cats/effect/unsafe/FiberMonitorPlatform.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala index c4aa0d2b2d..cc36ef2b8d 100644 --- a/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/PollingSystem.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/jvm/src/main/scala/cats/effect/Selector.scala b/core/jvm/src/main/scala/cats/effect/Selector.scala index 586c448342..da39e74d90 100644 --- a/core/jvm/src/main/scala/cats/effect/Selector.scala +++ b/core/jvm/src/main/scala/cats/effect/Selector.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala index a958076cea..bbb853b947 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SelectorSystem.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala index 541335e62d..46ffa909e3 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/SleepSystem.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala index 963712cae2..4ae344085c 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/TimerHeap.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala b/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala index 28deff9bbf..b4a99feb47 100644 --- a/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala +++ b/core/native/src/main/scala/cats/effect/FileDescriptorPoller.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/native/src/main/scala/cats/effect/Signal.scala b/core/native/src/main/scala/cats/effect/Signal.scala index b380dea64e..6d678211df 100644 --- a/core/native/src/main/scala/cats/effect/Signal.scala +++ b/core/native/src/main/scala/cats/effect/Signal.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index 28c7f4afb3..9874edb58e 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala index f0be84c399..e9ea74a5e3 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EventLoopExecutorScheduler.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index 8f5febc439..3a26a4eb6d 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/tests/jvm-native/src/main/scala/catseffect/examplesjvmnative.scala b/tests/jvm-native/src/main/scala/catseffect/examplesjvmnative.scala index 6e5b4a5d49..878e515ea3 100644 --- a/tests/jvm-native/src/main/scala/catseffect/examplesjvmnative.scala +++ b/tests/jvm-native/src/main/scala/catseffect/examplesjvmnative.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 98d65d5904f701783fe394987026b7593a92762f Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 6 Mar 2024 20:34:06 +0000 Subject: [PATCH 352/429] Fix import --- core/jvm/src/main/scala/cats/effect/unsafe/FiberMonitor.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/FiberMonitor.scala b/core/jvm/src/main/scala/cats/effect/unsafe/FiberMonitor.scala index 78fb342e21..d2990b44e0 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/FiberMonitor.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/FiberMonitor.scala @@ -18,7 +18,6 @@ package cats.effect package unsafe import cats.effect.tracing.TracingConstants -import cats.effect.unsafe.ref.WeakReference import scala.concurrent.ExecutionContext From b9d3ac8efe4cb4beeeb278c63e9b9f2c8a03cc27 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 6 Mar 2024 20:36:30 +0000 Subject: [PATCH 353/429] Delete deleted specs --- .../test/scala/cats/effect/IOAppSpec.scala | 381 ------------------ .../effect/unsafe/TimerSkipListIOSpec.scala | 146 ------- 2 files changed, 527 deletions(-) delete mode 100644 tests/jvm/src/test/scala/cats/effect/IOAppSpec.scala delete mode 100644 tests/jvm/src/test/scala/cats/effect/unsafe/TimerSkipListIOSpec.scala diff --git a/tests/jvm/src/test/scala/cats/effect/IOAppSpec.scala b/tests/jvm/src/test/scala/cats/effect/IOAppSpec.scala deleted file mode 100644 index be555ea617..0000000000 --- a/tests/jvm/src/test/scala/cats/effect/IOAppSpec.scala +++ /dev/null @@ -1,381 +0,0 @@ -/* - * Copyright 2020-2024 Typelevel - * - * 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 - -import org.specs2.mutable.Specification - -import scala.io.Source -import scala.sys.process.{BasicIO, Process, ProcessBuilder} - -import java.io.File - -class IOAppSpec extends Specification { - - abstract class Platform(val id: String) { outer => - def builder(proto: AnyRef, args: List[String]): ProcessBuilder - def pid(proto: AnyRef): Option[Int] - - def dumpSignal: String - - def sendSignal(pid: Int): Unit = { - Runtime.getRuntime().exec(s"kill -$dumpSignal $pid") - () - } - - def apply(proto: AnyRef, args: List[String]): Handle = { - val stdoutBuffer = new StringBuffer() - val stderrBuffer = new StringBuffer() - val p = builder(proto, args).run(BasicIO(false, stdoutBuffer, None).withError { in => - val err = Source.fromInputStream(in).getLines().mkString(System.lineSeparator()) - stderrBuffer.append(err) - () - }) - - new Handle { - def awaitStatus() = p.exitValue() - def term() = p.destroy() // TODO probably doesn't work - def stderr() = stderrBuffer.toString - def stdout() = stdoutBuffer.toString - def pid() = outer.pid(proto) - } - } - } - - object JVM extends Platform("jvm") { - val ClassPath = System.getProperty("sbt.classpath") - - val JavaHome = { - val path = sys.env.get("JAVA_HOME").orElse(sys.props.get("java.home")).get - if (path.endsWith("/jre")) { - // handle JDK 8 installations - path.replace("/jre", "") - } else { - path - } - } - - val dumpSignal = "USR1" - - def builder(proto: AnyRef, args: List[String]) = Process( - s"$JavaHome/bin/java", - List("-cp", ClassPath, proto.getClass.getName.replaceAll("\\$$", "")) ::: args) - - // scala.sys.process.Process and java.lang.Process lack getting PID support. Java 9+ introduced it but - // whatever because it's very hard to obtain a java.lang.Process from scala.sys.process.Process. - def pid(proto: AnyRef): Option[Int] = { - val mainName = proto.getClass.getSimpleName.replace("$", "") - val jpsStdoutBuffer = new StringBuffer() - val jpsProcess = - Process(s"$JavaHome/bin/jps", List.empty).run(BasicIO(false, jpsStdoutBuffer, None)) - jpsProcess.exitValue() - - val output = jpsStdoutBuffer.toString - Source.fromString(output).getLines().find(_.contains(mainName)).map(_.split(" ")(0).toInt) - } - - } - - object Node extends Platform("node") { - val dumpSignal = "USR2" - - def builder(proto: AnyRef, args: List[String]) = - Process( - s"node", - "--enable-source-maps" :: BuildInfo - .jsRunner - .getAbsolutePath :: proto.getClass.getName.init :: args) - - def pid(proto: AnyRef): Option[Int] = { - val mainName = proto.getClass.getName.init - val stdoutBuffer = new StringBuffer() - val process = - Process("ps", List("aux")).run(BasicIO(false, stdoutBuffer, None)) - process.exitValue() - - val output = stdoutBuffer.toString - Source - .fromString(output) - .getLines() - .find(l => l.contains(BuildInfo.jsRunner.getAbsolutePath) && l.contains(mainName)) - .map(_.split(" +")(1).toInt) - } - } - - if (BuildInfo.testJSIOApp) - test(Node) - else - test(JVM) - - def test(platform: Platform): Unit = { - s"IOApp (${platform.id})" should { - import catseffect.examples._ - - val isWindows = System.getProperty("os.name").toLowerCase.contains("windows") - - if (isWindows) { - // these tests have all been emperically flaky on Windows CI builds, so they're disabled - "evaluate and print hello world" in skipped("this test is unreliable on Windows") - "pass all arguments to child" in skipped("this test is unreliable on Windows") - - "exit with leaked fibers" in skipped("this test is unreliable on Windows") - - "exit on non-fatal error" in skipped("this test is unreliable on Windows") - "exit on fatal error" in skipped("this test is unreliable on Windows") - - "exit on fatal error with other unsafe runs" in skipped( - "this test is unreliable on Windows") - - "warn on global runtime collision" in skipped("this test is unreliable on Windows") - "abort awaiting shutdown hooks" in skipped("this test is unreliable on Windows") - - // The jvm cannot gracefully terminate processes on Windows, so this - // test cannot be carried out properly. Same for testing IOApp in sbt. - "run finalizers on TERM" in skipped( - "cannot observe graceful process termination on Windows") - } else { - "evaluate and print hello world" in { - val h = platform(HelloWorld, Nil) - h.awaitStatus() mustEqual 0 - h.stdout() mustEqual s"Hello, World!${System.lineSeparator()}" - } - - "pass all arguments to child" in { - val expected = List("the", "quick", "brown", "fox jumped", "over") - val h = platform(Arguments, expected) - h.awaitStatus() mustEqual 0 - h.stdout() mustEqual expected.mkString( - "", - System.lineSeparator(), - System.lineSeparator()) - } - - "exit on non-fatal error" in { - val h = platform(NonFatalError, List.empty) - h.awaitStatus() mustEqual 1 - h.stderr() must contain("Boom!") - } - - "exit with leaked fibers" in { - val h = platform(LeakedFiber, List.empty) - h.awaitStatus() mustEqual 0 - } - - "exit on fatal error" in { - val h = platform(FatalError, List.empty) - h.awaitStatus() mustEqual 1 - h.stderr() must contain("Boom!") - h.stdout() must not(contain("sadness")) - } - - "exit on fatal error with other unsafe runs" in { - val h = platform(FatalErrorUnsafeRun, List.empty) - h.awaitStatus() mustEqual 1 - h.stderr() must contain("Boom!") - } - - "exit on raising a fatal error with attempt" in { - val h = platform(RaiseFatalErrorAttempt, List.empty) - h.awaitStatus() mustEqual 1 - h.stderr() must contain("Boom!") - h.stdout() must not(contain("sadness")) - } - - "exit on raising a fatal error with handleError" in { - val h = platform(RaiseFatalErrorHandle, List.empty) - h.awaitStatus() mustEqual 1 - h.stderr() must contain("Boom!") - h.stdout() must not(contain("sadness")) - } - - "exit on raising a fatal error inside a map" in { - val h = platform(RaiseFatalErrorMap, List.empty) - h.awaitStatus() mustEqual 1 - h.stderr() must contain("Boom!") - h.stdout() must not(contain("sadness")) - } - - "exit on raising a fatal error inside a flatMap" in { - val h = platform(RaiseFatalErrorFlatMap, List.empty) - h.awaitStatus() mustEqual 1 - h.stderr() must contain("Boom!") - h.stdout() must not(contain("sadness")) - } - - "warn on global runtime collision" in { - val h = platform(GlobalRacingInit, List.empty) - h.awaitStatus() mustEqual 0 - h.stderr() must contain( - "Cats Effect global runtime already initialized; custom configurations will be ignored") - h.stderr() must not(contain("boom")) - } - - "reset global runtime on shutdown" in { - val h = platform(GlobalShutdown, List.empty) - h.awaitStatus() mustEqual 0 - h.stderr() must not contain - "Cats Effect global runtime already initialized; custom configurations will be ignored" - h.stderr() must not(contain("boom")) - } - - "custom runtime installed as global" in { - val h = platform(CustomRuntime, List.empty) - h.awaitStatus() mustEqual 0 - } - - "abort awaiting shutdown hooks" in { - val h = platform(ShutdownHookImmediateTimeout, List.empty) - h.awaitStatus() mustEqual 0 - } - - "run finalizers on TERM" in { - import _root_.java.io.{BufferedReader, FileReader} - - // we have to resort to this convoluted approach because Process#destroy kills listeners before killing the process - val test = File.createTempFile("cats-effect", "finalizer-test") - def readTest(): String = { - val reader = new BufferedReader(new FileReader(test)) - try { - reader.readLine() - } finally { - reader.close() - } - } - - val h = platform(Finalizers, test.getAbsolutePath() :: Nil) - - var i = 0 - while (!h.stdout().contains("Started") && i < 100) { - Thread.sleep(100) - i += 1 - } - - Thread.sleep( - 100 - ) // give thread scheduling just a sec to catch up and get us into the latch.await() - - h.term() - h.awaitStatus() mustEqual 143 - - i = 0 - while (readTest() == null && i < 100) { - i += 1 - } - readTest() must contain("canceled") - } - } - - "exit on fatal error without IOApp" in { - val h = platform(FatalErrorRaw, List.empty) - h.awaitStatus() - h.stdout() must not(contain("sadness")) - h.stderr() must not(contain("Promise already completed")) - } - - "exit on canceled" in { - val h = platform(Canceled, List.empty) - h.awaitStatus() mustEqual 1 - } - - if (BuildInfo.testJSIOApp) { - "gracefully ignore undefined process.exit" in { - val h = platform(UndefinedProcessExit, List.empty) - h.awaitStatus() mustEqual 0 - } - - "support main thread evaluation" in skipped( - "JavaScript is all main thread, all the time") - - "warn on cpu starvation" in skipped( - "starvation detection works on Node, but the test struggles with determinism") - } else { - val isJava8 = sys.props.get("java.version").filter(_.startsWith("1.8")).isDefined - - if (isJava8) { - "live fiber snapshot" in skipped( - "JDK 8 does not have free signals for live fiber snapshots") - } else if (isWindows) { - "live fiber snapshot" in skipped( - "cannot observe signals sent to process termination on Windows") - } else { - "live fiber snapshot" in { - val h = platform(LiveFiberSnapshot, List.empty) - - // wait for the application to fully start before trying to send the signal - while (!h.stdout().contains("ready")) { - Thread.sleep(100L) - } - - val pid = h.pid() - pid must beSome - pid.foreach(platform.sendSignal) - h.awaitStatus() - val stderr = h.stderr() - stderr must contain("cats.effect.IOFiber") - } - } - - "shutdown on worker thread interruption" in { - val h = platform(WorkerThreadInterrupt, List.empty) - h.awaitStatus() mustEqual 1 - h.stderr() must contain("java.lang.InterruptedException") - ok - } - - "support main thread evaluation" in { - val h = platform(EvalOnMainThread, List.empty) - h.awaitStatus() mustEqual 0 - } - - "use configurable reportFailure for MainThread" in { - val h = platform(MainThreadReportFailure, List.empty) - h.awaitStatus() mustEqual 0 - } - - "warn on blocked threads" in { - val h = platform(BlockedThreads, List.empty) - h.awaitStatus() - val err = h.stderr() - err must contain( - "[WARNING] A Cats Effect worker thread was detected to be in a blocked state") - } - - "warn on cpu starvation" in { - val h = platform(CpuStarvation, List.empty) - h.awaitStatus() - val err = h.stderr() - err must not(contain("[WARNING] Failed to register Cats Effect CPU")) - err must contain("[WARNING] Your app's responsiveness") - // we use a regex because time has too many corner cases - a test run at just the wrong - // moment on new year's eve, etc - err must beMatching( - // (?s) allows matching across line breaks - """(?s)^\d{4}-[01]\d-[0-3]\dT[012]\d:[0-6]\d:[0-6]\d(?:\.\d{1,3})?Z \[WARNING\] Your app's responsiveness.*""" - ) - } - } - } - () - } - - trait Handle { - def awaitStatus(): Int - def term(): Unit - def stderr(): String - def stdout(): String - def pid(): Option[Int] - } -} diff --git a/tests/jvm/src/test/scala/cats/effect/unsafe/TimerSkipListIOSpec.scala b/tests/jvm/src/test/scala/cats/effect/unsafe/TimerSkipListIOSpec.scala deleted file mode 100644 index 3eecd476d0..0000000000 --- a/tests/jvm/src/test/scala/cats/effect/unsafe/TimerSkipListIOSpec.scala +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2020-2024 Typelevel - * - * 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 -package unsafe - -import cats.syntax.all._ - -import scala.concurrent.duration._ - -import java.util.concurrent.{ConcurrentSkipListSet, ThreadLocalRandom} -import java.util.concurrent.atomic.AtomicLong - -class TimerSkipListIOSpec extends BaseSpec { - - final val N = 50000 - final val DELAY = 10000L // ns - - private def drainUntilDone(m: TimerSkipList, done: Ref[IO, Boolean]): IO[Unit] = { - val pollSome: IO[Long] = IO { - while ({ - val cb = m.pollFirstIfTriggered(System.nanoTime()) - if (cb ne null) { - cb(Right(())) - true - } else false - }) {} - m.peekFirstTriggerTime() - } - def go(lastOne: Boolean): IO[Unit] = pollSome.flatMap { next => - if (next == Long.MinValue) IO.cede - else { - IO.defer { - val now = System.nanoTime() - val delay = next - now - if (delay > 0L) IO.sleep(delay.nanos) - else IO.unit - } - } - } *> { - if (lastOne) IO.unit - else done.get.ifM(go(lastOne = true), IO.cede *> go(lastOne = false)) - } - - go(lastOne = false) - } - - "TimerSkipList" should { - - "insert/pollFirstIfTriggered concurrently" in real { - IO.ref(false).flatMap { done => - IO { (new TimerSkipList, new AtomicLong) }.flatMap { - case (m, ctr) => - val insert = IO { - m.insert( - now = System.nanoTime(), - delay = DELAY, - callback = { _ => ctr.getAndIncrement; () }, - tlr = ThreadLocalRandom.current() - ) - } - val inserts = - (insert.parReplicateA_(N) *> IO.sleep(2 * DELAY.nanos)).guarantee(done.set(true)) - - val polls = drainUntilDone(m, done).parReplicateA_(2) - - IO.both(inserts, polls).flatMap { _ => - IO.sleep(0.5.second) *> IO { - m.pollFirstIfTriggered(System.nanoTime()) must beNull - ctr.get() mustEqual N.toLong - } - } - } - } - } - - "insert/cancel concurrently" in real { - IO.ref(false).flatMap { done => - IO { (new TimerSkipList, new ConcurrentSkipListSet[Int]) }.flatMap { - case (m, called) => - def insert(id: Int): IO[Runnable] = IO { - val now = System.nanoTime() - val canceller = m.insert( - now = now, - delay = DELAY, - callback = { _ => called.add(id); () }, - tlr = ThreadLocalRandom.current() - ) - canceller - } - - def cancel(c: Runnable): IO[Unit] = IO { - c.run() - } - - val firstBatch = (0 until N).toList - val secondBatch = (N until (2 * N)).toList - - for { - // add the first N callbacks: - cancellers <- firstBatch.traverse(insert) - // then race removing those, and adding another N: - _ <- IO.both( - cancellers.parTraverse(cancel), - secondBatch.parTraverse(insert) - ) - // since the fibers calling callbacks - // are not running yet, the cancelled - // ones must never be invoked - _ <- IO.both( - IO.sleep(2 * DELAY.nanos).guarantee(done.set(true)), - drainUntilDone(m, done).parReplicateA_(2) - ) - _ <- IO { - assert(m.pollFirstIfTriggered(System.nanoTime()) eq null) - // no cancelled callback should've been called, - // and all the other ones must've been called: - val calledIds = { - val b = Set.newBuilder[Int] - val it = called.iterator() - while (it.hasNext()) { - b += it.next() - } - b.result() - } - calledIds mustEqual secondBatch.toSet - } - } yield ok - } - } - } - } -} From 4ea8613f8323bfca813cd99d91d40e92fcb7f006 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 6 Mar 2024 20:48:50 +0000 Subject: [PATCH 354/429] Remove unused import --- tests/shared/src/test/scala/cats/effect/CallbackStackSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/shared/src/test/scala/cats/effect/CallbackStackSpec.scala b/tests/shared/src/test/scala/cats/effect/CallbackStackSpec.scala index ee1f8f880e..902a9837c1 100644 --- a/tests/shared/src/test/scala/cats/effect/CallbackStackSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/CallbackStackSpec.scala @@ -16,7 +16,7 @@ package cats.effect -import cats.syntax.all._ + class CallbackStackSpec extends BaseSpec with DetectPlatform { From 10e925bf4c1f458f7d3848bf0951ba85ee2fbd62 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 6 Mar 2024 20:59:59 +0000 Subject: [PATCH 355/429] Formatting --- tests/shared/src/test/scala/cats/effect/CallbackStackSpec.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/shared/src/test/scala/cats/effect/CallbackStackSpec.scala b/tests/shared/src/test/scala/cats/effect/CallbackStackSpec.scala index 902a9837c1..8f0dd5899d 100644 --- a/tests/shared/src/test/scala/cats/effect/CallbackStackSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/CallbackStackSpec.scala @@ -16,8 +16,6 @@ package cats.effect - - class CallbackStackSpec extends BaseSpec with DetectPlatform { "CallbackStack" should { From 237f22bd2996da32ff527b9652aad2f764528cee Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Wed, 20 Mar 2024 08:05:21 +0000 Subject: [PATCH 356/429] Update sbt-buildinfo to 0.12.0 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 5eb8eb1f05..eae2a9033f 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -7,6 +7,6 @@ addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.2") addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0") -addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.12.0") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.1") addSbtPlugin("org.scalameta" % "sbt-native-image" % "0.3.4") From 8bcec15b45ad9d5588fe44439ca2ba0c97f7fe72 Mon Sep 17 00:00:00 2001 From: Andrew Valencik Date: Mon, 25 Mar 2024 19:40:45 -0400 Subject: [PATCH 357/429] Remove CODE_OF_CONDUCT override, use org default --- CODE_OF_CONDUCT.md | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index f38f936e7e..0000000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,18 +0,0 @@ -# Code of Conduct - -We are committed to providing a friendly, safe and welcoming environment for all, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other such characteristics. - -Everyone is expected to follow the [Scala Code of Conduct] when discussing the project on the available communication channels. - -## Moderation - -Any questions, concerns, or moderation requests please contact a member of the project. - -- Ross A. Baker | [twitter](https://twitter.com/rossabaker) | [email](mailto:ross@rossabaker.com) -- Christopher Davenport | [twitter](https://twitter.com/davenpcm) | [email](mailto:chris@christopherdavenport.tech) -- Alexandru Nedelcu | [twitter](https://twitter.com/alexelcu) | [email](mailto:y20+coc@alexn.org) -- Daniel Spiewak | [twitter](https://twitter.com/djspiewak) | [email](mailto:djspiewak@gmail.com) - -And of course, always feel free to reach out to anyone with the **mods** role in [Discord](https://discord.gg/QNnHKHq5Ts). - -[Scala Code of Conduct]: https://typelevel.org/code-of-conduct.html From d5c278f80fbf7f48c8bb5fd3b6e35fbde4037783 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 26 Mar 2024 07:48:45 +0000 Subject: [PATCH 358/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/52169f0a21ffe51a16598423290ebf7d0d6cc2b1' (2024-03-04) → 'github:typelevel/typelevel-nix/60c3868688cb8f5f7ebc781f6e122c061ae35d4d' (2024-03-11) • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/fa9a51752f1b5de583ad5213eb621be071806663' (2024-03-02) → 'github:nixos/nixpkgs/d40e866b1f98698d454dad8f592fe7616ff705a4' (2024-03-10) --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 0769b3f9e3..80c450b76b 100644 --- a/flake.lock +++ b/flake.lock @@ -73,11 +73,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1709386671, - "narHash": "sha256-VPqfBnIJ+cfa78pd4Y5Cr6sOWVW8GYHRVucxJGmRf8Q=", + "lastModified": 1710097495, + "narHash": "sha256-B7Ea7q7hU7SE8wOPJ9oXEBjvB89yl2csaLjf5v/7jr8=", "owner": "nixos", "repo": "nixpkgs", - "rev": "fa9a51752f1b5de583ad5213eb621be071806663", + "rev": "d40e866b1f98698d454dad8f592fe7616ff705a4", "type": "github" }, "original": { @@ -137,11 +137,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1709579932, - "narHash": "sha256-6FXB4+iqwPAoYr1nbUpP8wtV09cPpnZyOIq5z58IhOs=", + "lastModified": 1710188850, + "narHash": "sha256-KbNmyxEvcnq5h/wfeL1ZxO9RwoNRjJ0IgYlUZpdSlLo=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "52169f0a21ffe51a16598423290ebf7d0d6cc2b1", + "rev": "60c3868688cb8f5f7ebc781f6e122c061ae35d4d", "type": "github" }, "original": { From b8d3e2b5d5213c7ac15f2e452ea90c8152d10e12 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Fri, 29 Mar 2024 08:06:17 +0000 Subject: [PATCH 359/429] Update scalafmt-core to 3.8.1 in series/3.x --- .scalafmt.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index 0a5c3b9999..fc5f4f6fa5 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = 3.8.0 +version = 3.8.1 runner.dialect = Scala213Source3 fileOverride { From 5d1b8ed01524b8dbc398fc2bfb5a610b57268ca6 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Sun, 31 Mar 2024 16:14:06 +0000 Subject: [PATCH 360/429] Update sbt-scalajs, scalajs-compiler, ... to 1.16.0 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 5eb8eb1f05..f6923b890b 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,7 +2,7 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.6.7") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.15.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.2") From 069a2864ee60d5d33de969b8e19d70ad0376aad3 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Wed, 17 Apr 2024 04:06:07 +0000 Subject: [PATCH 361/429] Update scalacheck to 1.17.1 in series/3.x --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 52893353a9..1451d123f2 100644 --- a/build.sbt +++ b/build.sbt @@ -318,7 +318,7 @@ ThisBuild / autoAPIMappings := true val CatsVersion = "2.10.0" val Specs2Version = "4.20.5" -val ScalaCheckVersion = "1.17.0" +val ScalaCheckVersion = "1.17.1" val DisciplineVersion = "1.4.0" val CoopVersion = "1.2.0" From 6e076f81774f9026591aab0a3640efa77965a666 Mon Sep 17 00:00:00 2001 From: Eugene K <50055047+geny200@users.noreply.github.com> Date: Fri, 19 Apr 2024 20:55:57 +0300 Subject: [PATCH 362/429] Update copyright dates --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bbabf866ec..b3ab4dc27a 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ If everything goes well, your browser will open at the end of this. ## License ``` -Copyright 2017-2022 Typelevel +Copyright 2017-2024 Typelevel Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 690abc4f73eea6e92376624be61986214c279b16 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Sat, 20 Apr 2024 20:10:01 +0000 Subject: [PATCH 363/429] Update sbt-typelevel to 0.7.0 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 5b2aed1d17..b251e1b36f 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.6.7") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.0") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") From 17653ee03bdf84bd46e4f953be566d11aa974087 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Sat, 20 Apr 2024 20:11:08 +0000 Subject: [PATCH 364/429] Run prePR with sbt-typelevel Executed command: sbt tlPrePrBotHook --- ioapp-tests/src/test/scala/IOAppSpec.scala | 2 +- tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala | 2 +- .../src/test/scala/cats/effect/FileDescriptorPollerSpec.scala | 2 +- .../src/test/scala-2.13+/cats/effect/IOImplicitSpec.scala | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ioapp-tests/src/test/scala/IOAppSpec.scala b/ioapp-tests/src/test/scala/IOAppSpec.scala index 21c0a36a60..5724ada7de 100644 --- a/ioapp-tests/src/test/scala/IOAppSpec.scala +++ b/ioapp-tests/src/test/scala/IOAppSpec.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala b/tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala index 79685bc4d9..78ea55d4aa 100644 --- a/tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/SelectorSpec.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala index b06450cdcc..aa40fb650c 100644 --- a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala +++ b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/tests/shared/src/test/scala-2.13+/cats/effect/IOImplicitSpec.scala b/tests/shared/src/test/scala-2.13+/cats/effect/IOImplicitSpec.scala index 530cfd5f58..1e98bf7b5e 100644 --- a/tests/shared/src/test/scala-2.13+/cats/effect/IOImplicitSpec.scala +++ b/tests/shared/src/test/scala-2.13+/cats/effect/IOImplicitSpec.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 440f3006a1d91fff3bb24a0a6296d29bd5845564 Mon Sep 17 00:00:00 2001 From: Daniel Urban Date: Sun, 21 Apr 2024 07:54:54 +0200 Subject: [PATCH 365/429] Fix versions in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b3ab4dc27a..1dfc0819ec 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ libraryDependencies += "org.typelevel" %% "cats-effect" % "3.5.4" ``` -The above represents the core, stable dependency which brings in the entirety of Cats Effect. This is *most likely* what you want. All current Cats Effect releases are published for Scala 2.12, 2.13, 3.0, and Scala.js 1.7. +The above represents the core, stable dependency which brings in the entirety of Cats Effect. This is *most likely* what you want. All current Cats Effect releases are published for Scala 2.12, 2.13, 3.2, and Scala.js 1.13. Or, if you prefer a less bare-bones starting point, you can try [the Giter8 template](https://github.com/typelevel/ce3.g8): From 0ed2e08ecdcf33a0bdb3a421324f21714f08ad7e Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Thu, 2 May 2024 00:20:04 +0000 Subject: [PATCH 366/429] Update sbt-scalafix to 0.12.1 in series/3.x --- project/plugins.sbt | 2 +- scalafix/project/plugins.sbt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index b251e1b36f..2b0f0d6ea2 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -8,5 +8,5 @@ addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.2") addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.12.0") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.1") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.12.1") addSbtPlugin("org.scalameta" % "sbt-native-image" % "0.3.4") diff --git a/scalafix/project/plugins.sbt b/scalafix/project/plugins.sbt index 00d82eb2cc..44a6e78843 100644 --- a/scalafix/project/plugins.sbt +++ b/scalafix/project/plugins.sbt @@ -1 +1 @@ -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.1") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.12.1") From 659ae9380c16412828cf90119fe301f5f26c10b3 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Thu, 2 May 2024 00:20:39 +0000 Subject: [PATCH 367/429] Update scala-library to 2.13.14 in series/3.x --- .github/workflows/ci.yml | 8 ++++---- build.sbt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2678ec28ac..ed4195cc6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-14] - scala: [3.3.3, 2.12.19, 2.13.12] + scala: [3.3.3, 2.12.19, 2.13.14] java: - temurin@8 - temurin@11 @@ -256,7 +256,7 @@ jobs: - shell: bash run: sbt '++ ${{ matrix.scala }}' '${{ matrix.ci }}' - - if: (matrix.scala == '2.13.12' || matrix.scala == '3.3.3') && matrix.ci == 'ciJVM' && matrix.java == 'temurin@17' + - if: (matrix.scala == '2.13.14' || matrix.scala == '3.3.3') && matrix.ci == 'ciJVM' && matrix.java == 'temurin@17' shell: bash run: sbt '++ ${{ matrix.scala }}' docs/mdoc @@ -271,7 +271,7 @@ jobs: run: example/test-js.sh ${{ matrix.scala }} - name: Test GraalVM Native Image - if: matrix.scala == '2.13.12' && matrix.java == 'graalvm@17' && matrix.os == 'ubuntu-latest' + if: matrix.scala == '2.13.14' && matrix.java == 'graalvm@17' && matrix.os == 'ubuntu-latest' shell: bash run: sbt '++ ${{ matrix.scala }}' graalVMExample/nativeImage graalVMExample/nativeImageRun @@ -281,7 +281,7 @@ jobs: run: example/test-native.sh ${{ matrix.scala }} - name: Scalafix tests - if: matrix.scala == '2.13.12' && matrix.ci == 'ciJVM' && matrix.os == 'ubuntu-latest' + if: matrix.scala == '2.13.14' && matrix.ci == 'ciJVM' && matrix.os == 'ubuntu-latest' shell: bash run: | cd scalafix diff --git a/build.sbt b/build.sbt index 1451d123f2..17213b74de 100644 --- a/build.sbt +++ b/build.sbt @@ -113,7 +113,7 @@ val Windows = "windows-latest" val MacOS = "macos-14" val Scala212 = "2.12.19" -val Scala213 = "2.13.12" +val Scala213 = "2.13.14" val Scala3 = "3.3.3" ThisBuild / crossScalaVersions := Seq(Scala3, Scala212, Scala213) From e7937d3eb4bd23de590b1bbacf14bb606abb56fa Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Thu, 2 May 2024 00:21:06 +0000 Subject: [PATCH 368/429] Regenerate GitHub Actions workflow Executed command: sbt githubWorkflowGenerate --- .github/workflows/ci.yml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed4195cc6e..eebe88e177 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -452,52 +452,52 @@ jobs: tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.12, ciJVM) + - name: Download target directories (2.13.14, ciJVM) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.12-ciJVM + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.14-ciJVM - - name: Inflate target directories (2.13.12, ciJVM) + - name: Inflate target directories (2.13.14, ciJVM) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.12, ciNative) + - name: Download target directories (2.13.14, ciNative) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.12-ciNative + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.14-ciNative - - name: Inflate target directories (2.13.12, ciNative) + - name: Inflate target directories (2.13.14, ciNative) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.12, ciJS) + - name: Download target directories (2.13.14, ciJS) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.12-ciJS + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.14-ciJS - - name: Inflate target directories (2.13.12, ciJS) + - name: Inflate target directories (2.13.14, ciJS) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.12, ciFirefox) + - name: Download target directories (2.13.14, ciFirefox) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.12-ciFirefox + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.14-ciFirefox - - name: Inflate target directories (2.13.12, ciFirefox) + - name: Inflate target directories (2.13.14, ciFirefox) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.12, ciChrome) + - name: Download target directories (2.13.14, ciChrome) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.12-ciChrome + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.14-ciChrome - - name: Inflate target directories (2.13.12, ciChrome) + - name: Inflate target directories (2.13.14, ciChrome) run: | tar xf targets.tar rm targets.tar From 5b62d994b9b1f7ead54376250400f51bc16975db Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 04:08:55 +0000 Subject: [PATCH 369/429] Update sbt to 1.10.0 in series/3.x --- project/build.properties | 2 +- scalafix/project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project/build.properties b/project/build.properties index 04267b14af..081fdbbc76 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.9 +sbt.version=1.10.0 diff --git a/scalafix/project/build.properties b/scalafix/project/build.properties index 04267b14af..081fdbbc76 100644 --- a/scalafix/project/build.properties +++ b/scalafix/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.9 +sbt.version=1.10.0 From d11e1435a30d2214c756d77b1ba455c44943d2b7 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 20:09:54 +0000 Subject: [PATCH 370/429] Update sbt-typelevel to 0.7.1 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index b251e1b36f..afe60b7704 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.0") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.1") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") From ad55f0e312167eb668f59c0fe474696773135537 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 20 May 2024 17:45:32 +0000 Subject: [PATCH 371/429] Fix warning --- kernel/shared/src/main/scala/cats/effect/kernel/Outcome.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/Outcome.scala b/kernel/shared/src/main/scala/cats/effect/kernel/Outcome.scala index 2b633dce37..3c7a195559 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/Outcome.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/Outcome.scala @@ -229,8 +229,8 @@ object Outcome extends LowPriorityImplicits { } @tailrec - def tailRecM[A, B](a: A)(f: A => Outcome[F, E, Either[A, B]]): Outcome[F, E, B] = - f(a) match { + def tailRecM[A, B](a0: A)(f: A => Outcome[F, E, Either[A, B]]): Outcome[F, E, B] = + f(a0) match { case Succeeded(fa) => Traverse[F].sequence[Either[A, *], B](fa) match { // Dotty can't infer this case Left(a) => tailRecM(a)(f) From 73315cb983dde8a777b3ccc28b643d2cca30567b Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 20 May 2024 18:28:57 +0000 Subject: [PATCH 372/429] More fixing --- .../test/scala/cats/effect/unsafe/WorkerThreadNameSpec.scala | 2 +- tests/shared/src/test/scala/cats/effect/IOSpec.scala | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/jvm/src/test/scala/cats/effect/unsafe/WorkerThreadNameSpec.scala b/tests/jvm/src/test/scala/cats/effect/unsafe/WorkerThreadNameSpec.scala index 21845da279..c99b390abf 100644 --- a/tests/jvm/src/test/scala/cats/effect/unsafe/WorkerThreadNameSpec.scala +++ b/tests/jvm/src/test/scala/cats/effect/unsafe/WorkerThreadNameSpec.scala @@ -16,7 +16,7 @@ package cats.effect.unsafe -import cats.effect.{BaseSpec /*, IO*/} +import cats.effect.BaseSpec import cats.effect.testkit.TestInstances import scala.concurrent.duration._ diff --git a/tests/shared/src/test/scala/cats/effect/IOSpec.scala b/tests/shared/src/test/scala/cats/effect/IOSpec.scala index a4e2359f6b..a9842f7dbd 100644 --- a/tests/shared/src/test/scala/cats/effect/IOSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/IOSpec.scala @@ -1882,9 +1882,9 @@ class IOSpec extends BaseSpec with Discipline with IOPlatformSpecification { "timeoutAndForget" should { "terminate on an uncancelable fiber" in real { - IO.never.uncancelable.timeoutAndForget(1.second).attempt flatMap { e => + IO.never.uncancelable.timeoutAndForget(1.second).attempt flatMap { r => IO { - e must beLike { case Left(e) => e must haveClass[TimeoutException] } + r must beLike { case Left(e) => e must haveClass[TimeoutException] } } } } From 5ef1a6826582c10a2d10d9f54492661ca5951fa0 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 20 May 2024 18:46:44 +0000 Subject: [PATCH 373/429] Organize imports --- core/js/src/main/scala/cats/effect/process.scala | 1 - std/js/src/main/scala/cats/effect/std/EnvCompanionPlatform.scala | 1 - 2 files changed, 2 deletions(-) diff --git a/core/js/src/main/scala/cats/effect/process.scala b/core/js/src/main/scala/cats/effect/process.scala index efc9994802..4616d05c57 100644 --- a/core/js/src/main/scala/cats/effect/process.scala +++ b/core/js/src/main/scala/cats/effect/process.scala @@ -18,7 +18,6 @@ package cats.effect import cats.data.OptionT import cats.effect.std.Env -import cats.syntax.all._ import scala.scalajs.js import scala.util.Try diff --git a/std/js/src/main/scala/cats/effect/std/EnvCompanionPlatform.scala b/std/js/src/main/scala/cats/effect/std/EnvCompanionPlatform.scala index b55a26e7a6..89db9e7d83 100644 --- a/std/js/src/main/scala/cats/effect/std/EnvCompanionPlatform.scala +++ b/std/js/src/main/scala/cats/effect/std/EnvCompanionPlatform.scala @@ -18,7 +18,6 @@ package cats.effect.std import cats.data.OptionT import cats.effect.kernel.Sync -import cats.syntax.all._ import scala.collection.immutable.Iterable import scala.scalajs.js From d9fa9cc5579eba987d791856a0c62804ad26602c Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 20 May 2024 18:55:33 +0000 Subject: [PATCH 374/429] Address warning --- .../src/main/scala/cats/effect/syntax/DispatcherSyntax.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/js/src/main/scala/cats/effect/syntax/DispatcherSyntax.scala b/core/js/src/main/scala/cats/effect/syntax/DispatcherSyntax.scala index 339ee0e169..a7dcc14f38 100644 --- a/core/js/src/main/scala/cats/effect/syntax/DispatcherSyntax.scala +++ b/core/js/src/main/scala/cats/effect/syntax/DispatcherSyntax.scala @@ -45,7 +45,7 @@ final class DispatcherOps[F[_]] private[syntax] (private[syntax] val wrapped: Di def unsafeRunSyncToFuture[A](fa: F[A], syncLimit: Int)(implicit F: Async[F]): Future[A] = F.syncStep[SyncIO, A](fa, syncLimit).attempt.unsafeRunSync() match { case Left(t) => Future.failed(t) - case Right(Left(fa)) => wrapped.unsafeToFuture(fa) + case Right(Left(fa1)) => wrapped.unsafeToFuture(fa1) case Right(Right(a)) => Future.successful(a) } @@ -63,7 +63,7 @@ final class DispatcherOps[F[_]] private[syntax] (private[syntax] val wrapped: Di def unsafeRunSyncToPromise[A](fa: F[A], syncLimit: Int)(implicit F: Async[F]): Promise[A] = F.syncStep[SyncIO, A](fa, syncLimit).attempt.unsafeRunSync() match { case Left(t) => Promise.reject(t) - case Right(Left(fa)) => wrapped.unsafeToPromise(fa) + case Right(Left(fa1)) => wrapped.unsafeToPromise(fa1) case Right(Right(a)) => Promise.resolve[A](a) } From b61470e07099e377a9fed9b37d8d5d3b314f4e13 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 27 May 2024 15:25:16 +0000 Subject: [PATCH 375/429] Update headers --- std/shared/src/main/scala/cats/effect/std/QueueSink.scala | 2 +- std/shared/src/main/scala/cats/effect/std/QueueSource.scala | 2 +- .../src/main/scala/cats/effect/std/unsafe/BoundedQueue.scala | 2 +- .../main/scala/cats/effect/std/unsafe/BoundedQueueSink.scala | 2 +- .../src/main/scala/cats/effect/std/unsafe/UnboundedQueue.scala | 2 +- .../main/scala/cats/effect/std/unsafe/UnboundedQueueSink.scala | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/std/shared/src/main/scala/cats/effect/std/QueueSink.scala b/std/shared/src/main/scala/cats/effect/std/QueueSink.scala index e9549ce59d..c590b13f77 100644 --- a/std/shared/src/main/scala/cats/effect/std/QueueSink.scala +++ b/std/shared/src/main/scala/cats/effect/std/QueueSink.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/std/shared/src/main/scala/cats/effect/std/QueueSource.scala b/std/shared/src/main/scala/cats/effect/std/QueueSource.scala index 8ad9126508..c8ecae3ce0 100644 --- a/std/shared/src/main/scala/cats/effect/std/QueueSource.scala +++ b/std/shared/src/main/scala/cats/effect/std/QueueSource.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueue.scala b/std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueue.scala index cfda39be88..d341b69d14 100644 --- a/std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueue.scala +++ b/std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueue.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueueSink.scala b/std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueueSink.scala index b739ade7da..9e38779e35 100644 --- a/std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueueSink.scala +++ b/std/shared/src/main/scala/cats/effect/std/unsafe/BoundedQueueSink.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueue.scala b/std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueue.scala index f9e6bd92a7..5627a36fc8 100644 --- a/std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueue.scala +++ b/std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueue.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueueSink.scala b/std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueueSink.scala index 23f2727b98..428ee60c88 100644 --- a/std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueueSink.scala +++ b/std/shared/src/main/scala/cats/effect/std/unsafe/UnboundedQueueSink.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Typelevel + * Copyright 2020-2024 Typelevel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 582568d6af381b7ab899d58dd1f171674187f356 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 28 May 2024 07:48:45 +0000 Subject: [PATCH 376/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/60c3868688cb8f5f7ebc781f6e122c061ae35d4d?narHash=sha256-KbNmyxEvcnq5h/wfeL1ZxO9RwoNRjJ0IgYlUZpdSlLo%3D' (2024-03-11) → 'github:typelevel/typelevel-nix/e494632f444ab0a45c7294e6231a0e7e13053e64?narHash=sha256-HYj9yoYOogKvC4lFx20mdrIkn0UBbYxkrp/1cycNrFM%3D' (2024-05-28) • Updated input 'typelevel-nix/devshell': 'github:numtide/devshell/5ddecd67edbd568ebe0a55905273e56cc82aabe3?narHash=sha256-O5%2BnFozxz2Vubpdl1YZtPrilcIXPcRAjqNdNE8oCRoA%3D' (2024-02-26) → 'github:numtide/devshell/12e914740a25ea1891ec619bb53cf5e6ca922e40?narHash=sha256-wtBhsdMJA3Wa32Wtm1eeo84GejtI43pMrFrmwLXrsEc%3D' (2024-04-19) • Updated input 'typelevel-nix/devshell/nixpkgs': 'github:NixOS/nixpkgs/63143ac2c9186be6d9da6035fa22620018c85932?narHash=sha256-QGua89Pmq%2BFBAro8NriTuoO/wNaUtugt29/qqA8zeeM%3D' (2024-01-02) → follows 'typelevel-nix/nixpkgs' • Updated input 'typelevel-nix/flake-utils': 'github:numtide/flake-utils/d465f4819400de7c8d874d50b982301f28a84605?narHash=sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8%3D' (2024-02-28) → 'github:numtide/flake-utils/b1d9ab70662946ef0850d488da1c9019f3a9752a?narHash=sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ%3D' (2024-03-11) • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/d40e866b1f98698d454dad8f592fe7616ff705a4?narHash=sha256-B7Ea7q7hU7SE8wOPJ9oXEBjvB89yl2csaLjf5v/7jr8%3D' (2024-03-10) → 'github:nixos/nixpkgs/e2dd4e18cc1c7314e24154331bae07df76eb582f?narHash=sha256-usk0vE7VlxPX8jOavrtpOqphdfqEQpf9lgedlY/r66c%3D' (2024-05-26) --- flake.lock | 47 +++++++++++++++++------------------------------ 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/flake.lock b/flake.lock index 80c450b76b..51a7013079 100644 --- a/flake.lock +++ b/flake.lock @@ -3,14 +3,17 @@ "devshell": { "inputs": { "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" + "nixpkgs": [ + "typelevel-nix", + "nixpkgs" + ] }, "locked": { - "lastModified": 1708939976, - "narHash": "sha256-O5+nFozxz2Vubpdl1YZtPrilcIXPcRAjqNdNE8oCRoA=", + "lastModified": 1713532798, + "narHash": "sha256-wtBhsdMJA3Wa32Wtm1eeo84GejtI43pMrFrmwLXrsEc=", "owner": "numtide", "repo": "devshell", - "rev": "5ddecd67edbd568ebe0a55905273e56cc82aabe3", + "rev": "12e914740a25ea1891ec619bb53cf5e6ca922e40", "type": "github" }, "original": { @@ -42,11 +45,11 @@ "systems": "systems_2" }, "locked": { - "lastModified": 1709126324, - "narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=", + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "d465f4819400de7c8d874d50b982301f28a84605", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", "type": "github" }, "original": { @@ -57,27 +60,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1704161960, - "narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "63143ac2c9186be6d9da6035fa22620018c85932", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_2": { - "locked": { - "lastModified": 1710097495, - "narHash": "sha256-B7Ea7q7hU7SE8wOPJ9oXEBjvB89yl2csaLjf5v/7jr8=", + "lastModified": 1716715802, + "narHash": "sha256-usk0vE7VlxPX8jOavrtpOqphdfqEQpf9lgedlY/r66c=", "owner": "nixos", "repo": "nixpkgs", - "rev": "d40e866b1f98698d454dad8f592fe7616ff705a4", + "rev": "e2dd4e18cc1c7314e24154331bae07df76eb582f", "type": "github" }, "original": { @@ -134,14 +121,14 @@ "inputs": { "devshell": "devshell", "flake-utils": "flake-utils_2", - "nixpkgs": "nixpkgs_2" + "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1710188850, - "narHash": "sha256-KbNmyxEvcnq5h/wfeL1ZxO9RwoNRjJ0IgYlUZpdSlLo=", + "lastModified": 1716858680, + "narHash": "sha256-HYj9yoYOogKvC4lFx20mdrIkn0UBbYxkrp/1cycNrFM=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "60c3868688cb8f5f7ebc781f6e122c061ae35d4d", + "rev": "e494632f444ab0a45c7294e6231a0e7e13053e64", "type": "github" }, "original": { From 6fd78c558c2d67e06215b39282751aae20656c12 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 28 May 2024 16:06:26 +0000 Subject: [PATCH 377/429] Update cats-core, cats-free, ... to 2.11.0 in series/3.x --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 17213b74de..ae0753449b 100644 --- a/build.sbt +++ b/build.sbt @@ -316,7 +316,7 @@ ThisBuild / apiURL := Some(url("https://typelevel.org/cats-effect/api/3.x/")) ThisBuild / autoAPIMappings := true -val CatsVersion = "2.10.0" +val CatsVersion = "2.11.0" val Specs2Version = "4.20.5" val ScalaCheckVersion = "1.17.1" val DisciplineVersion = "1.4.0" From 0572831e7c53408edf86883cf222a8ec3b4f08a8 Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Wed, 29 May 2024 10:13:06 -0500 Subject: [PATCH 378/429] Fixed copy/paste error in docs --- std/shared/src/main/scala/cats/effect/std/Queue.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/std/shared/src/main/scala/cats/effect/std/Queue.scala b/std/shared/src/main/scala/cats/effect/std/Queue.scala index 574cd00036..18e6ca312d 100644 --- a/std/shared/src/main/scala/cats/effect/std/Queue.scala +++ b/std/shared/src/main/scala/cats/effect/std/Queue.scala @@ -156,11 +156,11 @@ object Queue { } /** - * Creates a new `Queue` subject to some `capacity` bound which supports a side-effecting - * `offer` function, allowing impure code to directly add values to the queue without - * indirecting through something like [[Dispatcher]]. This can improve performance - * significantly in some common cases. Note that the queue produced by this constructor can be - * used as a perfectly conventional [[Queue]] (as it is a subtype). + * Creates a new unbounded `Queue` which supports a side-effecting `offer` function, allowing + * impure code to directly add values to the queue without indirecting through something like + * [[Dispatcher]]. This can improve performance significantly in some common cases. Note that + * the queue produced by this constructor can be used as a perfectly conventional [[Queue]] + * (as it is a subtype). * * @return * an empty unbounded queue From 733cbc9598963009c884d31d7b1a6faa8376373b Mon Sep 17 00:00:00 2001 From: Paul J Thordarson Date: Sun, 2 Jun 2024 09:03:53 -0400 Subject: [PATCH 379/429] Added new fork gotcha to the docs When forking on GitHub, it is easy to make a shallow copy, which causes issues when running sbt due to missing git tags. This PR adds this as a gotcha to CONTRIBUTING.md. --- CONTRIBUTING.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f1b6816e64..fa4ec8f799 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,18 @@ There's always lots to do! This is an incredibly exciting project used by countl Anything which is marked with [**good first issue**](https://github.com/typelevel/cats-effect/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) is something that the Cats Effect maintainers have evaluated and determined is likely to not require a significant amount of time or prior knowledge of the code in order to fix. If you want to take on one of these tasks, just leave a comment on the issue and we'll assign it to you! Additionally, whenever we mark issues with this label, we are committing to doing our best to be extra-responsive to questions and pull requests against the issue so as to best help you find your feet as a contributor. +When creating a fork on GitHub, be sure to uncheck the box marked _Copy the branch only_, or you may have see the following error when running [sbt](https://github.com/sbt/sbt) due to missing git tags: +```sbt +[error] fatal: No names found, cannot describe anything. +[error] Nonzero exit code (128) running git. +``` +If you do encounter this error, make sure you have the `typelevel/cats-effect` repo as a remote with `git remote -v`. To add it as a remote, run one of the following: +* `git remote add upstream git@github.com:typelevel/cats-effect.git` for ssh +* `git remote add upstream https://github.com/typelevel/cats-effect.git` for https + +When you are done run `git fetch --tags `, for the above case this would be `git fetch --tags upstream`. + + ## Tooling Cats Effect is built with [sbt](https://github.com/sbt/sbt), and you should be able to jump right in by running `sbt test`. I will note, however, that `sbt +test` takes about two hours on my laptop, so you probably *shouldn't* start there... From 644ec1ffbbf3b77b69d361701f27dad2362fb420 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 4 Jun 2024 07:48:42 +0000 Subject: [PATCH 380/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/e494632f444ab0a45c7294e6231a0e7e13053e64?narHash=sha256-HYj9yoYOogKvC4lFx20mdrIkn0UBbYxkrp/1cycNrFM%3D' (2024-05-28) → 'github:typelevel/typelevel-nix/2c291f9ae8bdeeba19186c07ba23f326c2684a0d?narHash=sha256-tQpoiekTytMktbztATOaZdit2yvjexJ331/0yGKiELs%3D' (2024-06-03) • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/e2dd4e18cc1c7314e24154331bae07df76eb582f?narHash=sha256-usk0vE7VlxPX8jOavrtpOqphdfqEQpf9lgedlY/r66c%3D' (2024-05-26) → 'github:nixos/nixpkgs/6132b0f6e344ce2fe34fc051b72fb46e34f668e0?narHash=sha256-7R2ZvOnvd9h8fDd65p0JnB7wXfUvreox3xFdYWd1BnY%3D' (2024-05-30) --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 51a7013079..d9d42daa1f 100644 --- a/flake.lock +++ b/flake.lock @@ -60,11 +60,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1716715802, - "narHash": "sha256-usk0vE7VlxPX8jOavrtpOqphdfqEQpf9lgedlY/r66c=", + "lastModified": 1717112898, + "narHash": "sha256-7R2ZvOnvd9h8fDd65p0JnB7wXfUvreox3xFdYWd1BnY=", "owner": "nixos", "repo": "nixpkgs", - "rev": "e2dd4e18cc1c7314e24154331bae07df76eb582f", + "rev": "6132b0f6e344ce2fe34fc051b72fb46e34f668e0", "type": "github" }, "original": { @@ -124,11 +124,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1716858680, - "narHash": "sha256-HYj9yoYOogKvC4lFx20mdrIkn0UBbYxkrp/1cycNrFM=", + "lastModified": 1717443572, + "narHash": "sha256-tQpoiekTytMktbztATOaZdit2yvjexJ331/0yGKiELs=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "e494632f444ab0a45c7294e6231a0e7e13053e64", + "rev": "2c291f9ae8bdeeba19186c07ba23f326c2684a0d", "type": "github" }, "original": { From 49d3331725df2aa639c7baba23d1664fe7883071 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Fri, 14 Jun 2024 16:07:23 +0000 Subject: [PATCH 381/429] Update scalafmt-core to 3.8.2 in series/3.x --- .scalafmt.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index fc5f4f6fa5..c56da8f43e 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = 3.8.1 +version = 3.8.2 runner.dialect = Scala213Source3 fileOverride { From 52c37a1daca1b118556ceebff63b445c0c8f0053 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 18 Jun 2024 07:48:40 +0000 Subject: [PATCH 382/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/2c291f9ae8bdeeba19186c07ba23f326c2684a0d?narHash=sha256-tQpoiekTytMktbztATOaZdit2yvjexJ331/0yGKiELs%3D' (2024-06-03) → 'github:typelevel/typelevel-nix/9b93f7d1710b6a1fd187c3ddf2f37ebd7eff0555?narHash=sha256-ad3SkqwMmVDJP2%2BfOXo1LlRw93fqRF2lYdo3jGtWDgE%3D' (2024-06-17) • Updated input 'typelevel-nix/devshell': 'github:numtide/devshell/12e914740a25ea1891ec619bb53cf5e6ca922e40?narHash=sha256-wtBhsdMJA3Wa32Wtm1eeo84GejtI43pMrFrmwLXrsEc%3D' (2024-04-19) → 'github:numtide/devshell/1ebbe68d57457c8cae98145410b164b5477761f4?narHash=sha256-Q0OEFqe35fZbbRPPRdrjTUUChKVhhWXz3T9ZSKmaoVY%3D' (2024-06-03) • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/6132b0f6e344ce2fe34fc051b72fb46e34f668e0?narHash=sha256-7R2ZvOnvd9h8fDd65p0JnB7wXfUvreox3xFdYWd1BnY%3D' (2024-05-30) → 'github:nixos/nixpkgs/3f84a279f1a6290ce154c5531378acc827836fbb?narHash=sha256-u1fA0DYQYdeG%2B5kDm1bOoGcHtX0rtC7qs2YA2N1X%2B%2BI%3D' (2024-06-13) --- flake.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/flake.lock b/flake.lock index d9d42daa1f..9e2fb48291 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ ] }, "locked": { - "lastModified": 1713532798, - "narHash": "sha256-wtBhsdMJA3Wa32Wtm1eeo84GejtI43pMrFrmwLXrsEc=", + "lastModified": 1717408969, + "narHash": "sha256-Q0OEFqe35fZbbRPPRdrjTUUChKVhhWXz3T9ZSKmaoVY=", "owner": "numtide", "repo": "devshell", - "rev": "12e914740a25ea1891ec619bb53cf5e6ca922e40", + "rev": "1ebbe68d57457c8cae98145410b164b5477761f4", "type": "github" }, "original": { @@ -60,11 +60,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1717112898, - "narHash": "sha256-7R2ZvOnvd9h8fDd65p0JnB7wXfUvreox3xFdYWd1BnY=", + "lastModified": 1718276985, + "narHash": "sha256-u1fA0DYQYdeG+5kDm1bOoGcHtX0rtC7qs2YA2N1X++I=", "owner": "nixos", "repo": "nixpkgs", - "rev": "6132b0f6e344ce2fe34fc051b72fb46e34f668e0", + "rev": "3f84a279f1a6290ce154c5531378acc827836fbb", "type": "github" }, "original": { @@ -124,11 +124,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1717443572, - "narHash": "sha256-tQpoiekTytMktbztATOaZdit2yvjexJ331/0yGKiELs=", + "lastModified": 1718635259, + "narHash": "sha256-ad3SkqwMmVDJP2+fOXo1LlRw93fqRF2lYdo3jGtWDgE=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "2c291f9ae8bdeeba19186c07ba23f326c2684a0d", + "rev": "9b93f7d1710b6a1fd187c3ddf2f37ebd7eff0555", "type": "github" }, "original": { From adf5ce76d46007f75e81baf66e9755bb3b78bcc9 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Wed, 19 Jun 2024 16:05:54 +0000 Subject: [PATCH 383/429] Update sbt-mdoc to 2.5.3 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 61b93b61c6..a3f64b1978 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -5,7 +5,7 @@ addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.1") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7") -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.2") +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.3") addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.12.0") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.12.1") From 174251d056764c2bc0578bad60a065d48bfa6044 Mon Sep 17 00:00:00 2001 From: Rafal Sietko-Sierkiewicz Date: Mon, 24 Jun 2024 17:47:53 +0200 Subject: [PATCH 384/429] Remove IORuntime from allRuntimes collection --- .../src/main/scala/cats/effect/unsafe/IORuntime.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala b/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala index d32caa0c47..99051bedcf 100644 --- a/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala +++ b/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala @@ -18,6 +18,7 @@ package cats.effect package unsafe import scala.concurrent.ExecutionContext +import scala.util.chaining._ import java.util.concurrent.atomic.AtomicBoolean @@ -42,7 +43,7 @@ final class IORuntime private[unsafe] ( val scheduler: Scheduler, private[effect] val pollers: List[Any], private[effect] val fiberMonitor: FiberMonitor, - val shutdown: () => Unit, + defaultShutdown: () => Unit, val config: IORuntimeConfig ) { @@ -56,6 +57,9 @@ final class IORuntime private[unsafe] ( private[effect] val enhancedExceptions: Boolean = config.enhancedExceptions private[effect] val traceBufferLogSize: Int = config.traceBufferLogSize + val shutdown: () => Unit = + (() => IORuntime.allRuntimes.remove(this, this.hashCode())).pipe(_ => defaultShutdown) + override def toString: String = s"IORuntime($compute, $scheduler, $config)" } From 41ff9eba364ee86f21256b181ad64e0a39772b4c Mon Sep 17 00:00:00 2001 From: Rafal Sietko-Sierkiewicz Date: Mon, 24 Jun 2024 17:57:52 +0200 Subject: [PATCH 385/429] Remove scala util chaining as it is not available on scala 2.12 --- .../src/main/scala/cats/effect/unsafe/IORuntime.scala | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala b/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala index 99051bedcf..9f636faaa6 100644 --- a/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala +++ b/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala @@ -17,12 +17,10 @@ package cats.effect package unsafe -import scala.concurrent.ExecutionContext -import scala.util.chaining._ +import cats.effect.Platform.static import java.util.concurrent.atomic.AtomicBoolean - -import Platform.static +import scala.concurrent.ExecutionContext @annotation.implicitNotFound("""Could not find an implicit IORuntime. @@ -58,7 +56,10 @@ final class IORuntime private[unsafe] ( private[effect] val traceBufferLogSize: Int = config.traceBufferLogSize val shutdown: () => Unit = - (() => IORuntime.allRuntimes.remove(this, this.hashCode())).pipe(_ => defaultShutdown) + () => { + IORuntime.allRuntimes.remove(this, this.hashCode()) + defaultShutdown() + } override def toString: String = s"IORuntime($compute, $scheduler, $config)" } From 92a2ea386c07b4d7d33d852867fdd4272a2a5de4 Mon Sep 17 00:00:00 2001 From: Rafal Sietko-Sierkiewicz Date: Mon, 24 Jun 2024 18:01:42 +0200 Subject: [PATCH 386/429] Run formatting after second commit --- core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala b/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala index 9f636faaa6..c3aad4b1bd 100644 --- a/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala +++ b/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala @@ -19,9 +19,10 @@ package unsafe import cats.effect.Platform.static -import java.util.concurrent.atomic.AtomicBoolean import scala.concurrent.ExecutionContext +import java.util.concurrent.atomic.AtomicBoolean + @annotation.implicitNotFound("""Could not find an implicit IORuntime. Instead of calling unsafe methods directly, consider using cats.effect.IOApp, which From dc0f7327fd2c49000ec78fdba31d1feff8df6df4 Mon Sep 17 00:00:00 2001 From: Rafal Sietko-Sierkiewicz Date: Wed, 26 Jun 2024 07:56:53 +0200 Subject: [PATCH 387/429] Remove IORuntime in shutdown hook --- .../scala/cats/effect/unsafe/IORuntime.scala | 13 ++++------- .../cats/effect/unsafe/IORuntimeSpec.scala | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 9 deletions(-) create mode 100644 tests/shared/src/test/scala/cats/effect/unsafe/IORuntimeSpec.scala diff --git a/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala b/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala index c3aad4b1bd..0aa6b79cb5 100644 --- a/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala +++ b/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala @@ -42,7 +42,7 @@ final class IORuntime private[unsafe] ( val scheduler: Scheduler, private[effect] val pollers: List[Any], private[effect] val fiberMonitor: FiberMonitor, - defaultShutdown: () => Unit, + val shutdown: () => Unit, val config: IORuntimeConfig ) { @@ -56,12 +56,6 @@ final class IORuntime private[unsafe] ( private[effect] val enhancedExceptions: Boolean = config.enhancedExceptions private[effect] val traceBufferLogSize: Int = config.traceBufferLogSize - val shutdown: () => Unit = - () => { - IORuntime.allRuntimes.remove(this, this.hashCode()) - defaultShutdown() - } - override def toString: String = s"IORuntime($compute, $scheduler, $config)" } @@ -76,12 +70,13 @@ object IORuntime extends IORuntimeCompanionPlatform { config: IORuntimeConfig): IORuntime = { val fiberMonitor = FiberMonitor(compute) val unregister = registerFiberMonitorMBean(fiberMonitor) - val unregisterAndShutdown = () => { + def unregisterAndShutdown: () => Unit = () => { unregister() shutdown() + allRuntimes.remove(runtime, runtime.hashCode()) } - val runtime = + lazy val runtime = new IORuntime( compute, blocking, diff --git a/tests/shared/src/test/scala/cats/effect/unsafe/IORuntimeSpec.scala b/tests/shared/src/test/scala/cats/effect/unsafe/IORuntimeSpec.scala new file mode 100644 index 0000000000..df68966ea9 --- /dev/null +++ b/tests/shared/src/test/scala/cats/effect/unsafe/IORuntimeSpec.scala @@ -0,0 +1,22 @@ +package cats.effect.unsafe + +import cats.effect.BaseSpec + +class IORuntimeSpec extends BaseSpec { + + "IORuntimeSpec" should { + "cleanup allRuntimes collection on shutdown" in { + val (defaultScheduler, closeScheduler) = Scheduler.createDefaultScheduler() + + val runtime = IORuntime(null, null, defaultScheduler, closeScheduler, IORuntimeConfig()) + + IORuntime.allRuntimes.unsafeHashtable().find(_ == runtime) must beEqualTo(Some(runtime)) + + val _ = runtime.shutdown() + + IORuntime.allRuntimes.unsafeHashtable().find(_ == runtime) must beEqualTo(None) + } + + } + +} From 12b6c54d7c79a707b8328315762e93c2aacb016b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Sierkiewicz?= <30866803+RafalSierkiewicz@users.noreply.github.com> Date: Thu, 27 Jun 2024 06:41:18 +0200 Subject: [PATCH 388/429] Apply change suggestion Co-authored-by: Daniel Spiewak --- core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala b/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala index 0aa6b79cb5..17601df013 100644 --- a/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala +++ b/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala @@ -70,7 +70,7 @@ object IORuntime extends IORuntimeCompanionPlatform { config: IORuntimeConfig): IORuntime = { val fiberMonitor = FiberMonitor(compute) val unregister = registerFiberMonitorMBean(fiberMonitor) - def unregisterAndShutdown: () => Unit = () => { + def unregisterAndShutdown(): Unit = { unregister() shutdown() allRuntimes.remove(runtime, runtime.hashCode()) From 2a7513a06a1d6b221a05f065499a6ed5f86a98ca Mon Sep 17 00:00:00 2001 From: Rafal Sietko-Sierkiewicz Date: Thu, 27 Jun 2024 06:58:56 +0200 Subject: [PATCH 389/429] Revert "Apply change suggestion" This reverts commit 12b6c54d7c79a707b8328315762e93c2aacb016b. --- core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala b/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala index 17601df013..0aa6b79cb5 100644 --- a/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala +++ b/core/shared/src/main/scala/cats/effect/unsafe/IORuntime.scala @@ -70,7 +70,7 @@ object IORuntime extends IORuntimeCompanionPlatform { config: IORuntimeConfig): IORuntime = { val fiberMonitor = FiberMonitor(compute) val unregister = registerFiberMonitorMBean(fiberMonitor) - def unregisterAndShutdown(): Unit = { + def unregisterAndShutdown: () => Unit = () => { unregister() shutdown() allRuntimes.remove(runtime, runtime.hashCode()) From ad236eba6608016444cd9912bbb73ff72a0b2943 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Fri, 28 Jun 2024 07:46:39 +0000 Subject: [PATCH 390/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/9b93f7d1710b6a1fd187c3ddf2f37ebd7eff0555?narHash=sha256-ad3SkqwMmVDJP2%2BfOXo1LlRw93fqRF2lYdo3jGtWDgE%3D' (2024-06-17) → 'github:typelevel/typelevel-nix/fde01a54440beacf4c40e4b4d87c6201732016cf?narHash=sha256-hCyvkhjRk6VynNGxp6MkyfXwT1UquA6%2B%2BWclsmWRy7w%3D' (2024-06-25) • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/3f84a279f1a6290ce154c5531378acc827836fbb?narHash=sha256-u1fA0DYQYdeG%2B5kDm1bOoGcHtX0rtC7qs2YA2N1X%2B%2BI%3D' (2024-06-13) → 'github:nixos/nixpkgs/9693852a2070b398ee123a329e68f0dab5526681?narHash=sha256-jHJSUH619zBQ6WdC21fFAlDxHErKVDJ5fpN0Hgx4sjs%3D' (2024-06-22) --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 9e2fb48291..9408d3681a 100644 --- a/flake.lock +++ b/flake.lock @@ -60,11 +60,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1718276985, - "narHash": "sha256-u1fA0DYQYdeG+5kDm1bOoGcHtX0rtC7qs2YA2N1X++I=", + "lastModified": 1719082008, + "narHash": "sha256-jHJSUH619zBQ6WdC21fFAlDxHErKVDJ5fpN0Hgx4sjs=", "owner": "nixos", "repo": "nixpkgs", - "rev": "3f84a279f1a6290ce154c5531378acc827836fbb", + "rev": "9693852a2070b398ee123a329e68f0dab5526681", "type": "github" }, "original": { @@ -124,11 +124,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1718635259, - "narHash": "sha256-ad3SkqwMmVDJP2+fOXo1LlRw93fqRF2lYdo3jGtWDgE=", + "lastModified": 1719336674, + "narHash": "sha256-hCyvkhjRk6VynNGxp6MkyfXwT1UquA6++WclsmWRy7w=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "9b93f7d1710b6a1fd187c3ddf2f37ebd7eff0555", + "rev": "fde01a54440beacf4c40e4b4d87c6201732016cf", "type": "github" }, "original": { From 64f03631657c2cccab2a37248774f7b20c18c180 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 00:20:32 +0000 Subject: [PATCH 391/429] Update sbt to 1.10.1 in series/3.x --- project/build.properties | 2 +- scalafix/project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project/build.properties b/project/build.properties index 081fdbbc76..ee4c672cd0 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.0 +sbt.version=1.10.1 diff --git a/scalafix/project/build.properties b/scalafix/project/build.properties index 081fdbbc76..ee4c672cd0 100644 --- a/scalafix/project/build.properties +++ b/scalafix/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.0 +sbt.version=1.10.1 From 484c2adcd0eae4a02308084975c6dae5dd974cf8 Mon Sep 17 00:00:00 2001 From: Brian Wignall Date: Mon, 8 Jul 2024 16:42:32 -0400 Subject: [PATCH 392/429] Fix typos --- .../scala/cats/effect/unsafe/WorkStealingThreadPool.scala | 2 +- core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala | 2 +- docs/tutorial.md | 2 +- .../main/scala/cats/effect/kernel/testkit/Generators.scala | 4 ++-- .../shared/src/main/scala/cats/effect/kernel/GenSpawn.scala | 4 ++-- .../src/test/scala/cats/effect/FileDescriptorPollerSpec.scala | 2 +- tests/shared/src/test/scala/cats/effect/ResourceSpec.scala | 4 ++-- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index fa8628919c..074b0429fa 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -424,7 +424,7 @@ private[effect] final class WorkStealingThreadPool[P]( * Updates the internal state to mark the given worker thread as parked. * * @note - * This method is intentionally duplicated, to accomodate the unconditional code paths in + * This method is intentionally duplicated, to accommodate the unconditional code paths in * the [[WorkerThread]] runloop. */ private[unsafe] def transitionWorkerToParked(): Unit = { diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala index 9c31f4a448..c42d71c97f 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala @@ -577,7 +577,7 @@ private final class WorkerThread[P]( // The dequeued element was a batch of fibers. Enqueue the whole // batch on the local queue and execute the first fiber. - // Make room for the batch if the local queue cannot accomodate + // Make room for the batch if the local queue cannot accommodate // all of the fibers as is. queue.drainBatch(external, rnd) diff --git a/docs/tutorial.md b/docs/tutorial.md index b48bab5a8f..17e68d06f4 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -689,7 +689,7 @@ running its `queueR.getAndUpdate` call than the consumer is running its update the `queueR` content. Can we alleviate this? Sure! There are a few options you can implement: -1. Making the producer artifically slower by introducing a call to `Async[F].sleep` +1. Making the producer artificially slower by introducing a call to `Async[F].sleep` (_e.g._ for 1 microsecond). Truth is, in real world scenarios a producer will not be as fast as in our example so this tweak is not that 'strange'. Note that to be able to use `sleep` now `F` requires an implicit `Async[F]` instance. The diff --git a/kernel-testkit/shared/src/main/scala/cats/effect/kernel/testkit/Generators.scala b/kernel-testkit/shared/src/main/scala/cats/effect/kernel/testkit/Generators.scala index bc2bbb796b..3ce90df4c4 100644 --- a/kernel-testkit/shared/src/main/scala/cats/effect/kernel/testkit/Generators.scala +++ b/kernel-testkit/shared/src/main/scala/cats/effect/kernel/testkit/Generators.scala @@ -40,7 +40,7 @@ trait Generators1[F[_]] extends Serializable { // Generators of base cases, with no recursion protected def baseGen[A: Arbitrary: Cogen]: List[(String, Gen[F[A]])] = { // prevent unused implicit param warnings, the params need to stay because - // this method is overriden in subtraits + // this method is overridden in subtraits val _ = (implicitly[Arbitrary[A]], implicitly[Cogen[A]]) Nil } @@ -49,7 +49,7 @@ trait Generators1[F[_]] extends Serializable { protected def recursiveGen[A: Arbitrary: Cogen]( deeper: GenK[F]): List[(String, Gen[F[A]])] = { // prevent unused params warnings, the params need to stay because - // this method is overriden in subtraits + // this method is overridden in subtraits val _ = (deeper, implicitly[Arbitrary[A]], implicitly[Cogen[A]]) Nil } diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/GenSpawn.scala b/kernel/shared/src/main/scala/cats/effect/kernel/GenSpawn.scala index d4d8d9ab6e..71afab4526 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/GenSpawn.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/GenSpawn.scala @@ -131,7 +131,7 @@ import cats.syntax.all._ * * {{{ * - * // Starts a fiber that continously prints "A". + * // Starts a fiber that continuously prints "A". * // After 10 seconds, the resource scope exits so the fiber is canceled. * F.background(F.delay(println("A")).foreverM).use { _ => * F.sleep(10.seconds) @@ -202,7 +202,7 @@ trait GenSpawn[F[_], E] extends MonadCancel[F, E] with Unique[F] { * * {{{ * - * // Starts a fiber that continously prints "A". + * // Starts a fiber that continuously prints "A". * // After 10 seconds, the resource scope exits so the fiber is canceled. * F.background(F.delay(println("A")).foreverM).use { _ => * F.sleep(10.seconds) diff --git a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala index aa40fb650c..543d71b0a6 100644 --- a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala +++ b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala @@ -130,7 +130,7 @@ class FileDescriptorPollerSpec extends BaseSpec { } } - // multiples of 64 to excercise ready queue draining logic + // multiples of 64 to exercise ready queue draining logic test(64) *> test(128) *> test(1000) // a big, non-64-multiple } diff --git a/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala b/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala index d48272f98a..b917818531 100644 --- a/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala @@ -603,7 +603,7 @@ class ResourceSpec extends BaseSpec with ScalaCheck with Discipline { "propagate the exit case" in { import Resource.ExitCase - "use succesfully, test left" >> ticked { implicit ticker => + "use successfully, test left" >> ticked { implicit ticker => var got: ExitCase = null val r = Resource.onFinalizeCase(ec => IO { got = ec }) r.both(Resource.unit).use(_ => IO.unit) must completeAs(()) @@ -725,7 +725,7 @@ class ResourceSpec extends BaseSpec with ScalaCheck with Discipline { "propagate the exit case" in { import Resource.ExitCase - "use succesfully, test left" >> ticked { implicit ticker => + "use successfully, test left" >> ticked { implicit ticker => var got: ExitCase = null val r = Resource.onFinalizeCase(ec => IO { got = ec }) r.combineK(Resource.unit).use(_ => IO.unit) must completeAs(()) From 698306a68bfe385b6a01be4e201aad974a3a3e62 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Tue, 9 Jul 2024 16:04:39 +0000 Subject: [PATCH 393/429] Update sbt-mdoc to 2.5.4 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index a3f64b1978..984f3bd4ae 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -5,7 +5,7 @@ addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.1") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7") -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.3") +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.4") addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.12.0") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.12.1") From 8f238861f615fc0f8fdbc7234fbe38d060b223d4 Mon Sep 17 00:00:00 2001 From: satorg Date: Wed, 10 Jul 2024 15:17:32 -0700 Subject: [PATCH 394/429] fix scaladocs for `async` and `async_` --- .../src/main/scala/cats/effect/IO.scala | 29 +++++++++++++++++-- .../main/scala/cats/effect/kernel/Async.scala | 11 +++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/core/shared/src/main/scala/cats/effect/IO.scala b/core/shared/src/main/scala/cats/effect/IO.scala index 7138a0b915..711c7b8d4c 100644 --- a/core/shared/src/main/scala/cats/effect/IO.scala +++ b/core/shared/src/main/scala/cats/effect/IO.scala @@ -1354,7 +1354,12 @@ object IO extends IOCompanionPlatform with IOLowPriorityImplicits with TuplePara * } * }}} * - * Note that `async` is uncancelable during its registration. + * @note + * `async` is always uncancelable during its registration. The created effect will be + * uncancelable during its execution if the registration callback provides no finalizer + * (i.e. evaluates to `None`). If you need the created task to be cancelable, return a + * finalizer effect upon the registration. In a rare case when there's nothing to finalize, + * you can return `Some(IO.unit)` for that. * * @see * [[async_]] for a simplified variant without a finalizer @@ -1407,7 +1412,9 @@ object IO extends IOCompanionPlatform with IOLowPriorityImplicits with TuplePara * This function can be thought of as a safer, lexically-constrained version of `Promise`, * where `IO` is like a safer, lazy version of `Future`. * - * Also, note that `async` is uncancelable during its registration. + * @note + * `async_` is uncancelable during both its registration and execution. If you need an + * asyncronous effect to be cancelable, consider using `async` instead. * * @see * [[async]] for more generic version providing a finalizer @@ -1425,6 +1432,24 @@ object IO extends IOCompanionPlatform with IOLowPriorityImplicits with TuplePara IOCont(body, Tracing.calculateTracingEvent(k)) } + /** + * An effect that requests self-cancelation on the current fiber. + * + * `canceled` has a return type of `IO[Unit]` instead of `IO[Nothing]` due to execution + * continuing in a masked region. In the following example, the fiber requests + * self-cancelation in a masked region, so cancelation is suppressed until the fiber is + * completely unmasked. `fa` will run but `fb` will not. If `canceled` had a return type of + * `IO[Nothing]`, then it would not be possible to continue execution to `fa` (there would be + * no `Nothing` value to pass to the `flatMap`). + * + * {{{ + * + * IO.uncancelable { _ => + * IO.canceled *> fa + * } *> fb + * + * }}} + */ def canceled: IO[Unit] = Canceled /** diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/Async.scala b/kernel/shared/src/main/scala/cats/effect/kernel/Async.scala index 655f7018f1..bd2f3d9e76 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/Async.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/Async.scala @@ -117,7 +117,12 @@ trait Async[F[_]] extends AsyncPlatform[F] with Sync[F] with Temporal[F] { * The effect returns `Option[F[Unit]]` which is an optional finalizer to be run in the event * that the fiber running `async(k)` is canceled. * - * Also, note that `async` is uncancelable during its registration. + * @note + * `async` is always uncancelable during its registration. The created effect will be + * uncancelable during its execution if the registration callback provides no finalizer + * (i.e. evaluates to `None`). If you need the created task to be cancelable, return a + * finalizer effect upon the registration. In a rare case when there's nothing to finalize, + * you can return `Some(F.unit)` for that. * * @see * [[async_]] for a simplified variant without a finalizer @@ -139,7 +144,9 @@ trait Async[F[_]] extends AsyncPlatform[F] with Sync[F] with Temporal[F] { * This function can be thought of as a safer, lexically-constrained version of `Promise`, * where `IO` is like a safer, lazy version of `Future`. * - * Also, note that `async_` is uncancelable during its registration. + * @note + * `async_` is uncancelable during both its registration and execution. If you need an + * asyncronous effect to be cancelable, consider using `async` instead. * * @see * [[async]] for more generic version providing a finalizer From 6c15a2879fecc08ce596d9dc3ef0dafb8fb9d36a Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 15 Jul 2024 20:10:20 +0000 Subject: [PATCH 395/429] Update sbt-typelevel to 0.7.2 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 984f3bd4ae..2e533524cc 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.1") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.2") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") From 280c999e93931ecd72b6cc1168eeed2084cc22c4 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 15 Jul 2024 20:11:30 +0000 Subject: [PATCH 396/429] Run prePR with sbt-typelevel Executed command: sbt tlPrePrBotHook --- .../scala/cats/effect/unsafe/IORuntimeSpec.scala | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/shared/src/test/scala/cats/effect/unsafe/IORuntimeSpec.scala b/tests/shared/src/test/scala/cats/effect/unsafe/IORuntimeSpec.scala index df68966ea9..a7db35fe3e 100644 --- a/tests/shared/src/test/scala/cats/effect/unsafe/IORuntimeSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/unsafe/IORuntimeSpec.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2020-2024 Typelevel + * + * 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.unsafe import cats.effect.BaseSpec From 4ae7b25a355a041e98f5536a7b68b7f899323615 Mon Sep 17 00:00:00 2001 From: Jason Pickens Date: Thu, 25 Jul 2024 10:22:41 +1200 Subject: [PATCH 397/429] Use correct typelevel-nix overlay --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 2d5487987c..0d6febd0ce 100644 --- a/flake.nix +++ b/flake.nix @@ -12,7 +12,7 @@ let pkgs = import nixpkgs { inherit system; - overlays = [ typelevel-nix.overlay ]; + overlays = [ typelevel-nix.overlays.default ]; }; mkShell = jdk: pkgs.devshell.mkShell { From 0a578751d628e9fa16f767e5d5ac54f07d840083 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Thu, 25 Jul 2024 12:08:20 +0000 Subject: [PATCH 398/429] Update scalafmt-core to 3.8.3 in series/3.x --- .scalafmt.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index c56da8f43e..8a156a2732 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = 3.8.2 +version = 3.8.3 runner.dialect = Scala213Source3 fileOverride { From 54d44deeb40284f1f23edc945470e2368fff7380 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Sun, 28 Jul 2024 07:46:42 +0000 Subject: [PATCH 399/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/fde01a54440beacf4c40e4b4d87c6201732016cf?narHash=sha256-hCyvkhjRk6VynNGxp6MkyfXwT1UquA6%2B%2BWclsmWRy7w%3D' (2024-06-25) → 'github:typelevel/typelevel-nix/56599042bf39e03933e81fae63d9515ec6bc3542?narHash=sha256-%2BuZUK1fRJp9Pqab41Bez0Ox1HCuolofVsdBxN2MK/j4%3D' (2024-07-08) • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/9693852a2070b398ee123a329e68f0dab5526681?narHash=sha256-jHJSUH619zBQ6WdC21fFAlDxHErKVDJ5fpN0Hgx4sjs%3D' (2024-06-22) → 'github:nixos/nixpkgs/ab82a9612aa45284d4adf69ee81871a389669a9e?narHash=sha256-5r0pInVo5d6Enti0YwUSQK4TebITypB42bWy5su3MrQ%3D' (2024-07-07) --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 9408d3681a..f78eef0b5e 100644 --- a/flake.lock +++ b/flake.lock @@ -60,11 +60,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1719082008, - "narHash": "sha256-jHJSUH619zBQ6WdC21fFAlDxHErKVDJ5fpN0Hgx4sjs=", + "lastModified": 1720368505, + "narHash": "sha256-5r0pInVo5d6Enti0YwUSQK4TebITypB42bWy5su3MrQ=", "owner": "nixos", "repo": "nixpkgs", - "rev": "9693852a2070b398ee123a329e68f0dab5526681", + "rev": "ab82a9612aa45284d4adf69ee81871a389669a9e", "type": "github" }, "original": { @@ -124,11 +124,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1719336674, - "narHash": "sha256-hCyvkhjRk6VynNGxp6MkyfXwT1UquA6++WclsmWRy7w=", + "lastModified": 1720469922, + "narHash": "sha256-+uZUK1fRJp9Pqab41Bez0Ox1HCuolofVsdBxN2MK/j4=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "fde01a54440beacf4c40e4b4d87c6201732016cf", + "rev": "56599042bf39e03933e81fae63d9515ec6bc3542", "type": "github" }, "original": { From 9fce81fb759217774c390ef67e8135271bbd9efc Mon Sep 17 00:00:00 2001 From: satorg Date: Thu, 1 Aug 2024 01:07:34 -0700 Subject: [PATCH 400/429] fix/improve scaladoc for Outcome --- .../src/main/scala/cats/effect/kernel/Outcome.scala | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/Outcome.scala b/kernel/shared/src/main/scala/cats/effect/kernel/Outcome.scala index 3c7a195559..fe65675076 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/Outcome.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/Outcome.scala @@ -41,7 +41,7 @@ import scala.util.{Either, Left, Right} * `A`. This is to support monad transformers. Consider * * {{{ - * val oc: OutcomeIO[Int] = + * val oc: OptionT[IO, Outcome[OptionT[IO, *], Throwable, Int]] = * for { * fiber <- Spawn[OptionT[IO, *]].start(OptionT.none[IO, Int]) * oc <- fiber.join @@ -49,7 +49,15 @@ import scala.util.{Either, Left, Right} * }}} * * If the fiber succeeds then there is no value of type `Int` to be wrapped in `Succeeded`, - * hence `Succeeded` contains a value of type `OptionT[IO, Int]` instead. + * hence `Succeeded` contains a value of type `OptionT[IO, Int]` instead: + * + * {{{ + * def run: IO[Unit] = + * for { + * res <- oc.flatMap(_.embedNever).value // `res` is `Option[Int]` here + * _ <- Console[IO].println(res) // prints "None" + * } yield () + * }}} * * In general you can assume that binding on the value of type `F[A]` contained in `Succeeded` * does not perform further effects. In the case of `IO` that means that the outcome has been From 95f3d2ffea709e59c06321c5656e2079c2ae54a8 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 13 Aug 2024 22:41:16 +0000 Subject: [PATCH 401/429] Refactor `Ref#mapK` to take `Functor[G]` constraint --- .../main/scala/cats/effect/kernel/Ref.scala | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/Ref.scala b/kernel/shared/src/main/scala/cats/effect/kernel/Ref.scala index b04cd3c469..b57c75fc81 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/Ref.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/Ref.scala @@ -19,7 +19,7 @@ package effect package kernel import cats.data.State -import cats.effect.kernel.Ref.TransformedRef +import cats.effect.kernel.Ref.{TransformedRef, TransformedRef2} import cats.syntax.all._ /** @@ -186,8 +186,12 @@ abstract class Ref[F[_], A] extends RefSource[F, A] with RefSink[F, A] { /** * Modify the context `F` using transformation `f`. */ - def mapK[G[_]](f: F ~> G)(implicit F: Functor[F]): Ref[G, A] = - new TransformedRef(this, f) + def mapK[G[_]](f: F ~> G)(implicit G: Functor[G], dummy: DummyImplicit): Ref[G, A] = + new TransformedRef2(this, f) + + @deprecated("Use mapK with Functor[G] constraint", "3.6.0") + def mapK[G[_]](f: F ~> G, F: Functor[F]): Ref[G, A] = + new TransformedRef(this, f)(F) } object Ref { @@ -361,6 +365,27 @@ object Ref { def empty[A: Monoid]: F[Ref[F, A]] = of(Monoid[A].empty) } + final private[kernel] class TransformedRef2[F[_], G[_], A]( + underlying: Ref[F, A], + trans: F ~> G)( + implicit G: Functor[G] + ) extends Ref[G, A] { + override def get: G[A] = trans(underlying.get) + override def set(a: A): G[Unit] = trans(underlying.set(a)) + override def getAndSet(a: A): G[A] = trans(underlying.getAndSet(a)) + override def tryUpdate(f: A => A): G[Boolean] = trans(underlying.tryUpdate(f)) + override def tryModify[B](f: A => (A, B)): G[Option[B]] = trans(underlying.tryModify(f)) + override def update(f: A => A): G[Unit] = trans(underlying.update(f)) + override def modify[B](f: A => (A, B)): G[B] = trans(underlying.modify(f)) + override def tryModifyState[B](state: State[A, B]): G[Option[B]] = + trans(underlying.tryModifyState(state)) + override def modifyState[B](state: State[A, B]): G[B] = trans(underlying.modifyState(state)) + + override def access: G[(A, A => G[Boolean])] = + G.compose[(A, *)].compose[A => *].map(trans(underlying.access))(trans(_)) + } + + @deprecated("Use TransformedRef2 with Functor[G] constraint", "3.6.0") final private[kernel] class TransformedRef[F[_], G[_], A]( underlying: Ref[F, A], trans: F ~> G)( From f51f240ed67c1b30fda5cbfdcb82943833b043f9 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 14 Aug 2024 19:30:31 +0000 Subject: [PATCH 402/429] `register` -> `accessPoller` --- .../cats/effect/unsafe/IORuntimeCompanionPlatform.scala | 2 +- .../scala/cats/effect/unsafe/WorkStealingThreadPool.scala | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala index 9412fbdbfe..cd321ab187 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala @@ -152,7 +152,7 @@ private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type ( threadPool, - pollingSystem.makeApi(threadPool.register), + pollingSystem.makeApi(threadPool.accessPoller), { () => unregisterMBeans() threadPool.shutdown() diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index 074b0429fa..38ead18cb2 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/jvm/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -87,7 +87,7 @@ private[effect] final class WorkStealingThreadPool[P]( private[unsafe] val pollers: Array[P] = new Array[AnyRef](threadCount).asInstanceOf[Array[P]] - private[unsafe] def register(cb: P => Unit): Unit = { + private[unsafe] def accessPoller(cb: P => Unit): Unit = { // figure out where we are val thread = Thread.currentThread() @@ -97,8 +97,8 @@ private[effect] final class WorkStealingThreadPool[P]( if (worker.isOwnedBy(pool)) // we're good cb(worker.poller()) else // possibly a blocking worker thread, possibly on another wstp - scheduleExternal(() => register(cb)) - } else scheduleExternal(() => register(cb)) + scheduleExternal(() => accessPoller(cb)) + } else scheduleExternal(() => accessPoller(cb)) } /** From 932fa7d5312d85ef1f1da46eba8f47a1b302f9d0 Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Tue, 20 Aug 2024 17:47:06 +0200 Subject: [PATCH 403/429] Make IO.onError consistent with Applicative.onError This also deprecates the old `IO.onError` function. --- core/shared/src/main/scala/cats/effect/IO.scala | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/shared/src/main/scala/cats/effect/IO.scala b/core/shared/src/main/scala/cats/effect/IO.scala index 711c7b8d4c..27cde94301 100644 --- a/core/shared/src/main/scala/cats/effect/IO.scala +++ b/core/shared/src/main/scala/cats/effect/IO.scala @@ -588,9 +588,19 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] { def onCancel(fin: IO[Unit]): IO[A] = IO.OnCancel(this, fin) + @deprecated("Use onError with PartialFunction argument", "3.6.0") def onError(f: Throwable => IO[Unit]): IO[A] = handleErrorWith(t => f(t).voidError *> IO.raiseError(t)) + /** + * Execute a callback on certain errors, then rethrow them. Any non matching error is rethrown + * as well. + * + * Implements `ApplicativeError.onError`. + */ + def onError(pf: PartialFunction[Throwable, IO[Unit]]): IO[A] = + handleErrorWith(t => pf.applyOrElse(t, (_: Throwable) => IO.unit) *> IO.raiseError(t)) + /** * Like `Parallel.parProductL` */ From b4cd5e54d98a8a9ed56509d3c23ffef6cb27ba7f Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Tue, 20 Aug 2024 17:51:28 +0200 Subject: [PATCH 404/429] Replace usage of the old onError with the new one --- core/shared/src/main/scala/cats/effect/IO.scala | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/core/shared/src/main/scala/cats/effect/IO.scala b/core/shared/src/main/scala/cats/effect/IO.scala index 27cde94301..c4100eda04 100644 --- a/core/shared/src/main/scala/cats/effect/IO.scala +++ b/core/shared/src/main/scala/cats/effect/IO.scala @@ -493,7 +493,7 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] { IO.executionContext.flatMap(ec => IO(ec.reportFailure(t))) } - poll(this).onCancel(finalizer).onError(_ => handled).flatTap(_ => finalizer) + poll(this).onCancel(finalizer).onError { case _ => handled }.flatTap(_ => finalizer) } /** @@ -519,10 +519,11 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] { def guaranteeCase(finalizer: OutcomeIO[A @uncheckedVariance] => IO[Unit]): IO[A] = IO.uncancelable { poll => val finalized = poll(this).onCancel(finalizer(Outcome.canceled)) - val handled = finalized.onError { e => - finalizer(Outcome.errored(e)).handleErrorWith { t => - IO.executionContext.flatMap(ec => IO(ec.reportFailure(t))) - } + val handled = finalized.onError { + case e => + finalizer(Outcome.errored(e)).handleErrorWith { t => + IO.executionContext.flatMap(ec => IO(ec.reportFailure(t))) + } } handled.flatTap(a => finalizer(Outcome.succeeded(IO.pure(a)))) } From 78252747a740acb26d85c1609d348152a4cd7ec7 Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Tue, 20 Aug 2024 18:59:28 +0200 Subject: [PATCH 405/429] Make scala 2.12 happy As it requires to annotate anonymous function: The argument types of an anonymous function must be fully known. (SLS 8.5) --- core/shared/src/main/scala/cats/effect/IO.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/shared/src/main/scala/cats/effect/IO.scala b/core/shared/src/main/scala/cats/effect/IO.scala index c4100eda04..3f57ab8e4d 100644 --- a/core/shared/src/main/scala/cats/effect/IO.scala +++ b/core/shared/src/main/scala/cats/effect/IO.scala @@ -492,8 +492,8 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] { val handled = finalizer handleErrorWith { t => IO.executionContext.flatMap(ec => IO(ec.reportFailure(t))) } - - poll(this).onCancel(finalizer).onError { case _ => handled }.flatTap(_ => finalizer) + val onError: PartialFunction[Throwable, IO[Unit]] = { case _ => handled } + poll(this).onCancel(finalizer).onError(onError).flatTap(_ => finalizer) } /** @@ -519,13 +519,13 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] { def guaranteeCase(finalizer: OutcomeIO[A @uncheckedVariance] => IO[Unit]): IO[A] = IO.uncancelable { poll => val finalized = poll(this).onCancel(finalizer(Outcome.canceled)) - val handled = finalized.onError { + val onError: PartialFunction[Throwable, IO[Unit]] = { case e => - finalizer(Outcome.errored(e)).handleErrorWith { t => + finalizer(Outcome.errored(e)).handleErrorWith { (t: Throwable) => IO.executionContext.flatMap(ec => IO(ec.reportFailure(t))) } } - handled.flatTap(a => finalizer(Outcome.succeeded(IO.pure(a)))) + finalized.onError(onError).flatTap { (a: A) => finalizer(Outcome.succeeded(IO.pure(a))) } } def handleError[B >: A](f: Throwable => B): IO[B] = From f4f505c8e70adb930662d33a22e0415ff6018e07 Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Wed, 21 Aug 2024 09:46:14 +0200 Subject: [PATCH 406/429] Reuse logic between IO.onError --- core/shared/src/main/scala/cats/effect/IO.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/shared/src/main/scala/cats/effect/IO.scala b/core/shared/src/main/scala/cats/effect/IO.scala index 3f57ab8e4d..de7c332a42 100644 --- a/core/shared/src/main/scala/cats/effect/IO.scala +++ b/core/shared/src/main/scala/cats/effect/IO.scala @@ -590,8 +590,10 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] { IO.OnCancel(this, fin) @deprecated("Use onError with PartialFunction argument", "3.6.0") - def onError(f: Throwable => IO[Unit]): IO[A] = - handleErrorWith(t => f(t).voidError *> IO.raiseError(t)) + def onError(f: Throwable => IO[Unit]): IO[A] = { + val pf: PartialFunction[Throwable, IO[Unit]] = { case t => f(t) } + onError(pf) + } /** * Execute a callback on certain errors, then rethrow them. Any non matching error is rethrown From 40a7dde79141bbcd2781bc08a14492afbb1f63f9 Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Thu, 22 Aug 2024 19:45:05 +0200 Subject: [PATCH 407/429] Add reportError helper and use it for onError With this change, if the onError callback raises any error, We'll report that error and then re-raise the original error. --- core/shared/src/main/scala/cats/effect/IO.scala | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/core/shared/src/main/scala/cats/effect/IO.scala b/core/shared/src/main/scala/cats/effect/IO.scala index de7c332a42..2d7a18384c 100644 --- a/core/shared/src/main/scala/cats/effect/IO.scala +++ b/core/shared/src/main/scala/cats/effect/IO.scala @@ -602,7 +602,8 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] { * Implements `ApplicativeError.onError`. */ def onError(pf: PartialFunction[Throwable, IO[Unit]]): IO[A] = - handleErrorWith(t => pf.applyOrElse(t, (_: Throwable) => IO.unit) *> IO.raiseError(t)) + handleErrorWith(t => + pf.applyOrElse(t, (_: Throwable) => IO.unit).reportError *> IO.raiseError(t)) /** * Like `Parallel.parProductL` @@ -941,6 +942,19 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] { def void: IO[Unit] = map(_ => ()) + /** + * Similar to [[IO.voidError]], but also reports the error. + */ + private[effect] def reportError(implicit ev: A <:< Unit): IO[Unit] = { + val _ = ev + asInstanceOf[IO[Unit]].handleErrorWith { t => + IO.executionContext.flatMap(ec => IO(ec.reportFailure(t))) + } + } + + /** + * Discard any error raised by the source. + */ def voidError(implicit ev: A <:< Unit): IO[Unit] = { val _ = ev asInstanceOf[IO[Unit]].handleError(_ => ()) From 4764d05e48451e8e8bea85aba6c35633731c0fff Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Thu, 22 Aug 2024 20:54:12 +0200 Subject: [PATCH 408/429] Use reportError when applicable --- core/shared/src/main/scala/cats/effect/IO.scala | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/core/shared/src/main/scala/cats/effect/IO.scala b/core/shared/src/main/scala/cats/effect/IO.scala index 2d7a18384c..c2a84bfa1c 100644 --- a/core/shared/src/main/scala/cats/effect/IO.scala +++ b/core/shared/src/main/scala/cats/effect/IO.scala @@ -489,10 +489,7 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] { def guarantee(finalizer: IO[Unit]): IO[A] = // this is a little faster than the default implementation, which helps Resource IO uncancelable { poll => - val handled = finalizer handleErrorWith { t => - IO.executionContext.flatMap(ec => IO(ec.reportFailure(t))) - } - val onError: PartialFunction[Throwable, IO[Unit]] = { case _ => handled } + val onError: PartialFunction[Throwable, IO[Unit]] = { case _ => finalizer.reportError } poll(this).onCancel(finalizer).onError(onError).flatTap(_ => finalizer) } @@ -520,10 +517,7 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] { IO.uncancelable { poll => val finalized = poll(this).onCancel(finalizer(Outcome.canceled)) val onError: PartialFunction[Throwable, IO[Unit]] = { - case e => - finalizer(Outcome.errored(e)).handleErrorWith { (t: Throwable) => - IO.executionContext.flatMap(ec => IO(ec.reportFailure(t))) - } + case e => finalizer(Outcome.errored(e)).reportError } finalized.onError(onError).flatTap { (a: A) => finalizer(Outcome.succeeded(IO.pure(a))) } } From 868451b090e407094d01eaa953ee8b4fde69ac5c Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Thu, 22 Aug 2024 20:58:47 +0200 Subject: [PATCH 409/429] Fix onError callback error handling semantic For the ApplicativeError.onError method, We let the callback's error propagate to match with cats's ApplicativeError semantic. For the old onError, We report callback's error and raise the original error instead. --- core/shared/src/main/scala/cats/effect/IO.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/shared/src/main/scala/cats/effect/IO.scala b/core/shared/src/main/scala/cats/effect/IO.scala index c2a84bfa1c..4d1f39c86c 100644 --- a/core/shared/src/main/scala/cats/effect/IO.scala +++ b/core/shared/src/main/scala/cats/effect/IO.scala @@ -585,7 +585,7 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] { @deprecated("Use onError with PartialFunction argument", "3.6.0") def onError(f: Throwable => IO[Unit]): IO[A] = { - val pf: PartialFunction[Throwable, IO[Unit]] = { case t => f(t) } + val pf: PartialFunction[Throwable, IO[Unit]] = { case t => f(t).reportError } onError(pf) } @@ -596,8 +596,7 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] { * Implements `ApplicativeError.onError`. */ def onError(pf: PartialFunction[Throwable, IO[Unit]]): IO[A] = - handleErrorWith(t => - pf.applyOrElse(t, (_: Throwable) => IO.unit).reportError *> IO.raiseError(t)) + handleErrorWith(t => pf.applyOrElse(t, (_: Throwable) => IO.unit) *> IO.raiseError(t)) /** * Like `Parallel.parProductL` From c6026b3ee343f4d854b6cd96ebc95a5a499f8755 Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Mon, 26 Aug 2024 19:13:02 +0200 Subject: [PATCH 410/429] Override onError for IOAsync --- core/shared/src/main/scala/cats/effect/IO.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/shared/src/main/scala/cats/effect/IO.scala b/core/shared/src/main/scala/cats/effect/IO.scala index 4d1f39c86c..ee3a9da1fc 100644 --- a/core/shared/src/main/scala/cats/effect/IO.scala +++ b/core/shared/src/main/scala/cats/effect/IO.scala @@ -1995,6 +1995,9 @@ object IO extends IOCompanionPlatform with IOLowPriorityImplicits with TuplePara override def handleError[A](fa: IO[A])(f: Throwable => A): IO[A] = fa.handleError(f) + override def onError[A](fa: IO[A])(pf: PartialFunction[Throwable, IO[Unit]]): IO[A] = + fa.onError(pf) + override def timeout[A](fa: IO[A], duration: FiniteDuration)( implicit ev: TimeoutException <:< Throwable): IO[A] = { fa.timeout(duration) From d1b42465a9b39468cc8f924afed54091852b499a Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Wed, 28 Aug 2024 07:46:39 +0000 Subject: [PATCH 411/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/56599042bf39e03933e81fae63d9515ec6bc3542?narHash=sha256-%2BuZUK1fRJp9Pqab41Bez0Ox1HCuolofVsdBxN2MK/j4%3D' (2024-07-08) → 'github:typelevel/typelevel-nix/520d28a7da74c17df16bd105b63fe0ff145fc531?narHash=sha256-gH/RNFAB0X6Z53iFde6JQA4XbE92JbGf2t7hWKRlqPA%3D' (2024-08-20) • Updated input 'typelevel-nix/devshell': 'github:numtide/devshell/1ebbe68d57457c8cae98145410b164b5477761f4?narHash=sha256-Q0OEFqe35fZbbRPPRdrjTUUChKVhhWXz3T9ZSKmaoVY%3D' (2024-06-03) → 'github:numtide/devshell/67cce7359e4cd3c45296fb4aaf6a19e2a9c757ae?narHash=sha256-Yo/3loq572A8Su6aY5GP56knpuKYRvM2a1meP9oJZCw%3D' (2024-07-27) • Removed input 'typelevel-nix/devshell/flake-utils' • Removed input 'typelevel-nix/devshell/flake-utils/systems' • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/ab82a9612aa45284d4adf69ee81871a389669a9e?narHash=sha256-5r0pInVo5d6Enti0YwUSQK4TebITypB42bWy5su3MrQ%3D' (2024-07-07) → 'github:nixos/nixpkgs/ff1c2669bbb4d0dd9e62cc94f0968cfa652ceec1?narHash=sha256-MGtXhZHLZGKhtZT/MYXBJEuMkZB5DLYjY679EYNL7Es%3D' (2024-08-18) --- flake.lock | 54 ++++++++++-------------------------------------------- 1 file changed, 10 insertions(+), 44 deletions(-) diff --git a/flake.lock b/flake.lock index f78eef0b5e..189a79bd6e 100644 --- a/flake.lock +++ b/flake.lock @@ -2,18 +2,17 @@ "nodes": { "devshell": { "inputs": { - "flake-utils": "flake-utils", "nixpkgs": [ "typelevel-nix", "nixpkgs" ] }, "locked": { - "lastModified": 1717408969, - "narHash": "sha256-Q0OEFqe35fZbbRPPRdrjTUUChKVhhWXz3T9ZSKmaoVY=", + "lastModified": 1722113426, + "narHash": "sha256-Yo/3loq572A8Su6aY5GP56knpuKYRvM2a1meP9oJZCw=", "owner": "numtide", "repo": "devshell", - "rev": "1ebbe68d57457c8cae98145410b164b5477761f4", + "rev": "67cce7359e4cd3c45296fb4aaf6a19e2a9c757ae", "type": "github" }, "original": { @@ -26,24 +25,6 @@ "inputs": { "systems": "systems" }, - "locked": { - "lastModified": 1701680307, - "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_2": { - "inputs": { - "systems": "systems_2" - }, "locked": { "lastModified": 1710146030, "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", @@ -60,11 +41,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1720368505, - "narHash": "sha256-5r0pInVo5d6Enti0YwUSQK4TebITypB42bWy5su3MrQ=", + "lastModified": 1723985069, + "narHash": "sha256-MGtXhZHLZGKhtZT/MYXBJEuMkZB5DLYjY679EYNL7Es=", "owner": "nixos", "repo": "nixpkgs", - "rev": "ab82a9612aa45284d4adf69ee81871a389669a9e", + "rev": "ff1c2669bbb4d0dd9e62cc94f0968cfa652ceec1", "type": "github" }, "original": { @@ -102,33 +83,18 @@ "type": "github" } }, - "systems_2": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, "typelevel-nix": { "inputs": { "devshell": "devshell", - "flake-utils": "flake-utils_2", + "flake-utils": "flake-utils", "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1720469922, - "narHash": "sha256-+uZUK1fRJp9Pqab41Bez0Ox1HCuolofVsdBxN2MK/j4=", + "lastModified": 1724193442, + "narHash": "sha256-gH/RNFAB0X6Z53iFde6JQA4XbE92JbGf2t7hWKRlqPA=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "56599042bf39e03933e81fae63d9515ec6bc3542", + "rev": "520d28a7da74c17df16bd105b63fe0ff145fc531", "type": "github" }, "original": { From deee6dc8d792e3ffc617c3d5774a6f08fca0f2e0 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Wed, 4 Sep 2024 16:09:12 +0000 Subject: [PATCH 412/429] Update scala-library to 2.12.20 in series/3.x --- .github/workflows/ci.yml | 20 ++++++++++---------- build.sbt | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eebe88e177..b11cf46987 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-14] - scala: [3.3.3, 2.12.19, 2.13.14] + scala: [3.3.3, 2.12.20, 2.13.14] java: - temurin@8 - temurin@11 @@ -41,13 +41,13 @@ jobs: java: temurin@11 - scala: 3.3.3 java: temurin@21 - - scala: 2.12.19 + - scala: 2.12.20 java: temurin@11 - - scala: 2.12.19 + - scala: 2.12.20 java: temurin@17 - - scala: 2.12.19 + - scala: 2.12.20 java: temurin@21 - - scala: 2.12.19 + - scala: 2.12.20 java: graalvm@17 - os: windows-latest scala: 3.3.3 @@ -56,10 +56,10 @@ jobs: scala: 3.3.3 ci: ciJVM - os: windows-latest - scala: 2.12.19 + scala: 2.12.20 ci: ciJVM - os: macos-14 - scala: 2.12.19 + scala: 2.12.20 ci: ciJVM - os: macos-14 java: temurin@8 @@ -68,9 +68,9 @@ jobs: - ci: ciChrome scala: 3.3.3 - ci: ciFirefox - scala: 2.12.19 + scala: 2.12.20 - ci: ciChrome - scala: 2.12.19 + scala: 2.12.20 - ci: ciJS java: temurin@11 - ci: ciJS @@ -119,7 +119,7 @@ jobs: ci: ciNative - os: macos-14 ci: ciNative - scala: 2.12.19 + scala: 2.12.20 - os: windows-latest java: graalvm@17 runs-on: ${{ matrix.os }} diff --git a/build.sbt b/build.sbt index ae0753449b..8bda350aa1 100644 --- a/build.sbt +++ b/build.sbt @@ -112,7 +112,7 @@ val PrimaryOS = "ubuntu-latest" val Windows = "windows-latest" val MacOS = "macos-14" -val Scala212 = "2.12.19" +val Scala212 = "2.12.20" val Scala213 = "2.13.14" val Scala3 = "3.3.3" From 25666c5aaa8cff20319cb2ec201d1edebae89d2c Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Wed, 4 Sep 2024 16:09:42 +0000 Subject: [PATCH 413/429] Regenerate GitHub Actions workflow Executed command: sbt githubWorkflowGenerate --- .github/workflows/ci.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b11cf46987..354721206f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -422,32 +422,32 @@ jobs: tar xf targets.tar rm targets.tar - - name: Download target directories (2.12.19, ciJVM) + - name: Download target directories (2.12.20, ciJVM) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.19-ciJVM + name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.20-ciJVM - - name: Inflate target directories (2.12.19, ciJVM) + - name: Inflate target directories (2.12.20, ciJVM) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.12.19, ciNative) + - name: Download target directories (2.12.20, ciNative) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.19-ciNative + name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.20-ciNative - - name: Inflate target directories (2.12.19, ciNative) + - name: Inflate target directories (2.12.20, ciNative) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.12.19, ciJS) + - name: Download target directories (2.12.20, ciJS) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.19-ciJS + name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.20-ciJS - - name: Inflate target directories (2.12.19, ciJS) + - name: Inflate target directories (2.12.20, ciJS) run: | tar xf targets.tar rm targets.tar From 4e3509dd1921fde5ef09686531cd69baa51517c7 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2024 12:11:12 +0000 Subject: [PATCH 414/429] Update sbt-typelevel to 0.7.3 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 2e533524cc..a5d14c511d 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.2") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.3") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") From 20be7b6098aca2db3501a7708922b0632e84860c Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2024 12:12:25 +0000 Subject: [PATCH 415/429] Run prePR with sbt-typelevel Executed command: sbt tlPrePrBotHook --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 354721206f..5acff1b74a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -536,7 +536,7 @@ jobs: dependency-submission: name: Submit Dependencies - if: github.event_name != 'pull_request' + if: github.event.repository.fork == false && github.event_name != 'pull_request' strategy: matrix: os: [ubuntu-latest] From 81fc80cabf5e1c049453749002d0ba64874d55d8 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 00:22:30 +0000 Subject: [PATCH 416/429] Update sbt to 1.10.2 in series/3.x --- project/build.properties | 2 +- scalafix/project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project/build.properties b/project/build.properties index ee4c672cd0..0b699c3052 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.1 +sbt.version=1.10.2 diff --git a/scalafix/project/build.properties b/scalafix/project/build.properties index ee4c672cd0..0b699c3052 100644 --- a/scalafix/project/build.properties +++ b/scalafix/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.1 +sbt.version=1.10.2 From 37c77c8a9ec8fde4fadd0e2f0e64f30481bec381 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:05:01 +0000 Subject: [PATCH 417/429] Update sbt-mdoc to 2.6.1 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index a5d14c511d..c1079067fc 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -5,7 +5,7 @@ addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.3") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7") -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.4") +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.6.1") addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.12.0") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.12.1") From 8fe95e684752a1a8d53856380235f070abfc6d50 Mon Sep 17 00:00:00 2001 From: Maksym Ochenashko Date: Wed, 25 Sep 2024 11:46:43 +0300 Subject: [PATCH 418/429] Implement async dropping queue --- .../effect/benchmarks/QueueBenchmark.scala | 16 ++ .../main/scala/cats/effect/std/Queue.scala | 237 +++++++++++------- .../scala/cats/effect/std/QueueSpec.scala | 6 +- 3 files changed, 163 insertions(+), 96 deletions(-) diff --git a/benchmarks/src/main/scala/cats/effect/benchmarks/QueueBenchmark.scala b/benchmarks/src/main/scala/cats/effect/benchmarks/QueueBenchmark.scala index 6a34c7fcca..cf932f6f80 100644 --- a/benchmarks/src/main/scala/cats/effect/benchmarks/QueueBenchmark.scala +++ b/benchmarks/src/main/scala/cats/effect/benchmarks/QueueBenchmark.scala @@ -115,6 +115,22 @@ class QueueBenchmark { def unboundedAsyncEnqueueDequeueContended(): Unit = Queue.unboundedForAsync[IO, Unit].flatMap(enqueueDequeueContended(_)).unsafeRunSync() + @Benchmark + def droppingConcurrentEnqueueDequeueOne(): Unit = + Queue.droppingForConcurrent[IO, Unit](size).flatMap(enqueueDequeueOne(_)).unsafeRunSync() + + @Benchmark + def droppingConcurrentEnqueueDequeueMany(): Unit = + Queue.droppingForConcurrent[IO, Unit](size).flatMap(enqueueDequeueMany(_)).unsafeRunSync() + + @Benchmark + def droppingAsyncEnqueueDequeueOne(): Unit = + Queue.droppingForAsync[IO, Unit](size).flatMap(enqueueDequeueOne(_)).unsafeRunSync() + + @Benchmark + def droppingAsyncEnqueueDequeueMany(): Unit = + Queue.droppingForAsync[IO, Unit](size).flatMap(enqueueDequeueMany(_)).unsafeRunSync() + private[this] def enqueueDequeueOne(q: Queue[IO, Unit]): IO[Unit] = { def loop(i: Int): IO[Unit] = if (i > 0) diff --git a/std/shared/src/main/scala/cats/effect/std/Queue.scala b/std/shared/src/main/scala/cats/effect/std/Queue.scala index 18e6ca312d..1ca998591d 100644 --- a/std/shared/src/main/scala/cats/effect/std/Queue.scala +++ b/std/shared/src/main/scala/cats/effect/std/Queue.scala @@ -105,6 +105,14 @@ object Queue { private[effect] def unboundedForAsync[F[_], A](implicit F: Async[F]): F[Queue[F, A]] = F.delay(new UnboundedAsyncQueue()) + private[effect] def droppingForConcurrent[F[_], A](capacity: Int)( + implicit F: GenConcurrent[F, _]): F[Queue[F, A]] = + F.ref(State.empty[F, A]).map(new DroppingQueue(capacity, _)) + + private[effect] def droppingForAsync[F[_], A](capacity: Int)( + implicit F: Async[F]): F[Queue[F, A]] = + F.delay(new DroppingAsyncQueue(capacity)) + /** * Creates a new `Queue` subject to some `capacity` bound which supports a side-effecting * `tryOffer` function, allowing impure code to directly add values to the queue without @@ -184,7 +192,18 @@ object Queue { */ def dropping[F[_], A](capacity: Int)(implicit F: GenConcurrent[F, _]): F[Queue[F, A]] = { assertPositive(capacity, "Dropping") - F.ref(State.empty[F, A]).map(new DroppingQueue(capacity, _)) + // async queue can't handle capacity == 1 and allocates eagerly, so cap at 64k + if (1 < capacity && capacity < Short.MaxValue.toInt * 2) { + F match { + case f0: Async[F] => + droppingForAsync[F, A](capacity)(f0) + + case _ => + droppingForConcurrent[F, A](capacity) + } + } else { + droppingForConcurrent[F, A](capacity) + } } /** @@ -573,108 +592,21 @@ object Queue { private val EitherUnit: Either[Nothing, Unit] = Right(()) - /* - * Does not correctly handle bound = 0 because take waiters are async[Unit] - */ - private final class BoundedAsyncQueue[F[_], A](capacity: Int)(implicit F: Async[F]) - extends Queue[F, A] - with unsafe.BoundedQueue[F, A] { + private abstract class BaseBoundedAsyncQueue[F[_], A](capacity: Int)(implicit F: Async[F]) + extends Queue[F, A] { require(capacity > 1) - private[this] val buffer = new UnsafeBounded[A](capacity) + protected[this] val buffer = new UnsafeBounded[A](capacity) - private[this] val takers = new UnsafeUnbounded[Either[Throwable, Unit] => Unit]() - private[this] val offerers = new UnsafeUnbounded[Either[Throwable, Unit] => Unit]() + protected[this] val takers = new UnsafeUnbounded[Either[Throwable, Unit] => Unit]() + protected[this] val offerers = new UnsafeUnbounded[Either[Throwable, Unit] => Unit]() - private[this] val FailureSignal = cats.effect.std.FailureSignal // prefetch + protected[this] val FailureSignal = cats.effect.std.FailureSignal // prefetch // private[this] val takers = new ConcurrentLinkedQueue[AtomicReference[Either[Throwable, Unit] => Unit]]() // private[this] val offerers = new ConcurrentLinkedQueue[AtomicReference[Either[Throwable, Unit] => Unit]]() - def offer(a: A): F[Unit] = - F uncancelable { poll => - F defer { - try { - // attempt to put into the buffer; if the buffer is full, it will raise an exception - buffer.put(a) - // println(s"offered: size = ${buffer.size()}") - - // we successfully put, if there are any takers, grab the first one and wake it up - notifyOne(takers) - F.unit - } catch { - case FailureSignal => - // capture whether or not we were successful in our retry - var succeeded = false - - // a latch blocking until some taker notifies us - val wait = F.async[Unit] { k => - F delay { - // add ourselves to the listeners queue - val clear = offerers.put(k) - - try { - // now that we're listening, re-attempt putting - buffer.put(a) - - // it worked! clear ourselves out of the queue - clear() - // our retry succeeded - succeeded = true - - // manually complete our own callback - // note that we could have a race condition here where we're already completed - // async will deduplicate these calls for us - // additionally, the continuation (below) is held until the registration completes - k(EitherUnit) - - // we *might* have negated a notification by succeeding here - // unnecessary wake-ups are mostly harmless (only slight fairness loss) - notifyOne(offerers) - - // technically it's possible to already have waiting takers. notify one of them - notifyOne(takers) - - // we're immediately complete, so no point in creating a finalizer - None - } catch { - case FailureSignal => - // our retry failed, meaning the queue is still full and we're listening, so suspend - // println(s"failed offer size = ${buffer.size()}") - Some(F.delay(clear())) - } - } - } - - val notifyAnyway = F delay { - // we might have been awakened and canceled simultaneously - // try waking up another offerer just in case - notifyOne(offerers) - } - - // suspend until the buffer put can succeed - // if succeeded is true then we've *already* put - // if it's false, then some taker woke us up, so race the retry with other offers - (poll(wait) *> F.defer(if (succeeded) F.unit else poll(offer(a)))) - .onCancel(notifyAnyway) - } - } - } - - def unsafeTryOffer(a: A): Boolean = { - try { - buffer.put(a) - notifyOne(takers) - true - } catch { - case FailureSignal => - false - } - } - - def tryOffer(a: A): F[Boolean] = F.delay(unsafeTryOffer(a)) - val size: F[Int] = F.delay(buffer.size()) val take: F[A] = @@ -808,7 +740,7 @@ object Queue { // TODO could optimize notifications by checking if buffer is completely empty on put @tailrec - private[this] def notifyOne( + protected[this] final def notifyOne( waiters: UnsafeUnbounded[Either[Throwable, Unit] => Unit]): Unit = { // capture whether or not we should loop (structured in this way to avoid nested try/catch, which has a performance cost) val retry = @@ -841,6 +773,98 @@ object Queue { } } + /* + * Does not correctly handle bound = 0 because take waiters are async[Unit] + */ + private final class BoundedAsyncQueue[F[_], A](capacity: Int)(implicit F: Async[F]) + extends BaseBoundedAsyncQueue[F, A](capacity) + with unsafe.BoundedQueue[F, A] { + + def offer(a: A): F[Unit] = + F uncancelable { poll => + F defer { + try { + // attempt to put into the buffer; if the buffer is full, it will raise an exception + buffer.put(a) + // println(s"offered: size = ${buffer.size()}") + + // we successfully put, if there are any takers, grab the first one and wake it up + notifyOne(takers) + F.unit + } catch { + case FailureSignal => + // capture whether or not we were successful in our retry + var succeeded = false + + // a latch blocking until some taker notifies us + val wait = F.async[Unit] { k => + F delay { + // add ourselves to the listeners queue + val clear = offerers.put(k) + + try { + // now that we're listening, re-attempt putting + buffer.put(a) + + // it worked! clear ourselves out of the queue + clear() + // our retry succeeded + succeeded = true + + // manually complete our own callback + // note that we could have a race condition here where we're already completed + // async will deduplicate these calls for us + // additionally, the continuation (below) is held until the registration completes + k(EitherUnit) + + // we *might* have negated a notification by succeeding here + // unnecessary wake-ups are mostly harmless (only slight fairness loss) + notifyOne(offerers) + + // technically it's possible to already have waiting takers. notify one of them + notifyOne(takers) + + // we're immediately complete, so no point in creating a finalizer + None + } catch { + case FailureSignal => + // our retry failed, meaning the queue is still full and we're listening, so suspend + // println(s"failed offer size = ${buffer.size()}") + Some(F.delay(clear())) + } + } + } + + val notifyAnyway = F delay { + // we might have been awakened and canceled simultaneously + // try waking up another offerer just in case + notifyOne(offerers) + } + + // suspend until the buffer put can succeed + // if succeeded is true then we've *already* put + // if it's false, then some taker woke us up, so race the retry with other offers + (poll(wait) *> F.defer(if (succeeded) F.unit else poll(offer(a)))) + .onCancel(notifyAnyway) + } + } + } + + def unsafeTryOffer(a: A): Boolean = { + try { + buffer.put(a) + notifyOne(takers) + true + } catch { + case FailureSignal => + false + } + } + + def tryOffer(a: A): F[Boolean] = F.delay(unsafeTryOffer(a)) + + } + private final class UnboundedAsyncQueue[F[_], A]()(implicit F: Async[F]) extends Queue[F, A] with unsafe.UnboundedQueue[F, A] { @@ -960,6 +984,29 @@ object Queue { } } + private final class DroppingAsyncQueue[F[_], A](capacity: Int)(implicit F: Async[F]) + extends BaseBoundedAsyncQueue[F, A](capacity) { + + def offer(a: A): F[Unit] = + F.delay { + tryOfferUnsafe(a) + () + } + + def tryOffer(a: A): F[Boolean] = + F.delay(tryOfferUnsafe(a)) + + private def tryOfferUnsafe(a: A): Boolean = + try { + buffer.put(a) + notifyOne(takers) + true + } catch { + case FailureSignal => + false + } + } + // ported with love from https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/MpmcArrayQueue.java private[effect] final class UnsafeBounded[A](bound: Int) { require(bound > 1) diff --git a/tests/shared/src/test/scala/cats/effect/std/QueueSpec.scala b/tests/shared/src/test/scala/cats/effect/std/QueueSpec.scala index 231e584088..c2dc7e6e8f 100644 --- a/tests/shared/src/test/scala/cats/effect/std/QueueSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/std/QueueSpec.scala @@ -418,7 +418,11 @@ class UnboundedQueueSpec extends BaseSpec with QueueTests[Queue] { class DroppingQueueSpec extends BaseSpec with QueueTests[Queue] { sequential - "DroppingQueue" should { + "DroppingQueue (concurrent)" should { + droppingQueueTests(i => if (i < 1) Queue.dropping(i) else Queue.droppingForConcurrent(i)) + } + + "DroppingQueue (async)" should { droppingQueueTests(Queue.dropping) } From fb838419e636b7ac917ab26d0713b82ab09c8a50 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Sat, 28 Sep 2024 00:18:53 +0000 Subject: [PATCH 419/429] Update sbt-scalafix to 0.13.0 in series/3.x --- project/plugins.sbt | 2 +- scalafix/project/plugins.sbt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index c1079067fc..b71a01054f 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -8,5 +8,5 @@ addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.6.1") addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.8.0") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.12.0") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.12.1") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.13.0") addSbtPlugin("org.scalameta" % "sbt-native-image" % "0.3.4") diff --git a/scalafix/project/plugins.sbt b/scalafix/project/plugins.sbt index 44a6e78843..d8c6965c5c 100644 --- a/scalafix/project/plugins.sbt +++ b/scalafix/project/plugins.sbt @@ -1 +1 @@ -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.12.1") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.13.0") From 6c596fba024ab476aeb7309c2bb9d07da2be5e30 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Sat, 28 Sep 2024 07:46:34 +0000 Subject: [PATCH 420/429] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'typelevel-nix': 'github:typelevel/typelevel-nix/520d28a7da74c17df16bd105b63fe0ff145fc531?narHash=sha256-gH/RNFAB0X6Z53iFde6JQA4XbE92JbGf2t7hWKRlqPA%3D' (2024-08-20) → 'github:typelevel/typelevel-nix/d4fe497c6a619962584f5dc4b2ca9d4f824e68c6?narHash=sha256-ciaPDMAtj8hsYtHAXL0fP2UNo4JDKKxSb0bfR%2BATs2s%3D' (2024-09-23) • Updated input 'typelevel-nix/flake-utils': 'github:numtide/flake-utils/b1d9ab70662946ef0850d488da1c9019f3a9752a?narHash=sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ%3D' (2024-03-11) → 'github:numtide/flake-utils/c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a?narHash=sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ%3D' (2024-09-17) • Updated input 'typelevel-nix/nixpkgs': 'github:nixos/nixpkgs/ff1c2669bbb4d0dd9e62cc94f0968cfa652ceec1?narHash=sha256-MGtXhZHLZGKhtZT/MYXBJEuMkZB5DLYjY679EYNL7Es%3D' (2024-08-18) → 'github:nixos/nixpkgs/a1d92660c6b3b7c26fb883500a80ea9d33321be2?narHash=sha256-V5LpfdHyQkUF7RfOaDPrZDP%2Boqz88lTJrMT1%2BstXNwo%3D' (2024-09-20) --- flake.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/flake.lock b/flake.lock index 189a79bd6e..cd2c510763 100644 --- a/flake.lock +++ b/flake.lock @@ -26,11 +26,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", "type": "github" }, "original": { @@ -41,11 +41,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1723985069, - "narHash": "sha256-MGtXhZHLZGKhtZT/MYXBJEuMkZB5DLYjY679EYNL7Es=", + "lastModified": 1726871744, + "narHash": "sha256-V5LpfdHyQkUF7RfOaDPrZDP+oqz88lTJrMT1+stXNwo=", "owner": "nixos", "repo": "nixpkgs", - "rev": "ff1c2669bbb4d0dd9e62cc94f0968cfa652ceec1", + "rev": "a1d92660c6b3b7c26fb883500a80ea9d33321be2", "type": "github" }, "original": { @@ -90,11 +90,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1724193442, - "narHash": "sha256-gH/RNFAB0X6Z53iFde6JQA4XbE92JbGf2t7hWKRlqPA=", + "lastModified": 1727106412, + "narHash": "sha256-ciaPDMAtj8hsYtHAXL0fP2UNo4JDKKxSb0bfR+ATs2s=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "520d28a7da74c17df16bd105b63fe0ff145fc531", + "rev": "d4fe497c6a619962584f5dc4b2ca9d4f824e68c6", "type": "github" }, "original": { From 8e054c5295026b131d5e758cf5eddcadb1c9264f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Zu=CC=88hlke?= Date: Sun, 29 Sep 2024 22:18:57 +0200 Subject: [PATCH 421/429] Update to Scala 2.13.15 and fix Any inference --- .github/workflows/ci.yml | 38 +++++++++---------- build.sbt | 2 +- .../test/scala/cats/effect/ResourceSpec.scala | 2 +- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5acff1b74a..fda1ed4d66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-14] - scala: [3.3.3, 2.12.20, 2.13.14] + scala: [3.3.3, 2.12.20, 2.13.15] java: - temurin@8 - temurin@11 @@ -256,7 +256,7 @@ jobs: - shell: bash run: sbt '++ ${{ matrix.scala }}' '${{ matrix.ci }}' - - if: (matrix.scala == '2.13.14' || matrix.scala == '3.3.3') && matrix.ci == 'ciJVM' && matrix.java == 'temurin@17' + - if: (matrix.scala == '2.13.15' || matrix.scala == '3.3.3') && matrix.ci == 'ciJVM' && matrix.java == 'temurin@17' shell: bash run: sbt '++ ${{ matrix.scala }}' docs/mdoc @@ -271,7 +271,7 @@ jobs: run: example/test-js.sh ${{ matrix.scala }} - name: Test GraalVM Native Image - if: matrix.scala == '2.13.14' && matrix.java == 'graalvm@17' && matrix.os == 'ubuntu-latest' + if: matrix.scala == '2.13.15' && matrix.java == 'graalvm@17' && matrix.os == 'ubuntu-latest' shell: bash run: sbt '++ ${{ matrix.scala }}' graalVMExample/nativeImage graalVMExample/nativeImageRun @@ -281,7 +281,7 @@ jobs: run: example/test-native.sh ${{ matrix.scala }} - name: Scalafix tests - if: matrix.scala == '2.13.14' && matrix.ci == 'ciJVM' && matrix.os == 'ubuntu-latest' + if: matrix.scala == '2.13.15' && matrix.ci == 'ciJVM' && matrix.os == 'ubuntu-latest' shell: bash run: | cd scalafix @@ -452,52 +452,52 @@ jobs: tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.14, ciJVM) + - name: Download target directories (2.13.15, ciJVM) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.14-ciJVM + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.15-ciJVM - - name: Inflate target directories (2.13.14, ciJVM) + - name: Inflate target directories (2.13.15, ciJVM) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.14, ciNative) + - name: Download target directories (2.13.15, ciNative) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.14-ciNative + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.15-ciNative - - name: Inflate target directories (2.13.14, ciNative) + - name: Inflate target directories (2.13.15, ciNative) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.14, ciJS) + - name: Download target directories (2.13.15, ciJS) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.14-ciJS + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.15-ciJS - - name: Inflate target directories (2.13.14, ciJS) + - name: Inflate target directories (2.13.15, ciJS) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.14, ciFirefox) + - name: Download target directories (2.13.15, ciFirefox) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.14-ciFirefox + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.15-ciFirefox - - name: Inflate target directories (2.13.14, ciFirefox) + - name: Inflate target directories (2.13.15, ciFirefox) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.14, ciChrome) + - name: Download target directories (2.13.15, ciChrome) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.14-ciChrome + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.15-ciChrome - - name: Inflate target directories (2.13.14, ciChrome) + - name: Inflate target directories (2.13.15, ciChrome) run: | tar xf targets.tar rm targets.tar diff --git a/build.sbt b/build.sbt index 8bda350aa1..371d5cbcab 100644 --- a/build.sbt +++ b/build.sbt @@ -113,7 +113,7 @@ val Windows = "windows-latest" val MacOS = "macos-14" val Scala212 = "2.12.20" -val Scala213 = "2.13.14" +val Scala213 = "2.13.15" val Scala3 = "3.3.3" ThisBuild / crossScalaVersions := Seq(Scala3, Scala212, Scala213) diff --git a/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala b/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala index b917818531..0529109025 100644 --- a/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala @@ -986,7 +986,7 @@ class ResourceSpec extends BaseSpec with ScalaCheck with Discipline { case Left(()) => acquiredRight.get.ifM(loserReleased.get.map(_ must beRight[Unit]), IO.pure(ok)) case Right(()) => - acquiredLeft.get.ifM(loserReleased.get.map(_ must beLeft[Unit]).void, IO.pure(ok)) + acquiredLeft.get.ifM(loserReleased.get.map(_ must beLeft[Unit]), IO.pure(ok)) } } From ba17f7bdf4bc424138569baf17e424c4904eed48 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 08:07:59 +0000 Subject: [PATCH 422/429] Update scala3-library, ... to 3.3.4 in series/3.x --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 371d5cbcab..941d29e285 100644 --- a/build.sbt +++ b/build.sbt @@ -114,7 +114,7 @@ val MacOS = "macos-14" val Scala212 = "2.12.20" val Scala213 = "2.13.15" -val Scala3 = "3.3.3" +val Scala3 = "3.3.4" ThisBuild / crossScalaVersions := Seq(Scala3, Scala212, Scala213) ThisBuild / githubWorkflowScalaVersions := crossScalaVersions.value From d73ed805341b6ebd797298225686a408a7e61a3d Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 08:08:20 +0000 Subject: [PATCH 423/429] Regenerate GitHub Actions workflow Executed command: sbt githubWorkflowGenerate --- .github/workflows/ci.yml | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fda1ed4d66..3e27eb1ca3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-14] - scala: [3.3.3, 2.12.20, 2.13.15] + scala: [3.3.4, 2.12.20, 2.13.15] java: - temurin@8 - temurin@11 @@ -37,9 +37,9 @@ jobs: - graalvm@17 ci: [ciJVM, ciNative, ciJS, ciFirefox, ciChrome] exclude: - - scala: 3.3.3 + - scala: 3.3.4 java: temurin@11 - - scala: 3.3.3 + - scala: 3.3.4 java: temurin@21 - scala: 2.12.20 java: temurin@11 @@ -50,10 +50,10 @@ jobs: - scala: 2.12.20 java: graalvm@17 - os: windows-latest - scala: 3.3.3 + scala: 3.3.4 ci: ciJVM - os: macos-14 - scala: 3.3.3 + scala: 3.3.4 ci: ciJVM - os: windows-latest scala: 2.12.20 @@ -64,9 +64,9 @@ jobs: - os: macos-14 java: temurin@8 - ci: ciFirefox - scala: 3.3.3 + scala: 3.3.4 - ci: ciChrome - scala: 3.3.3 + scala: 3.3.4 - ci: ciFirefox scala: 2.12.20 - ci: ciChrome @@ -239,24 +239,24 @@ jobs: run: sbt githubWorkflowCheck - name: Check that scalafix has been run on JVM - if: matrix.ci == 'ciJVM' && matrix.scala != '3.3.3' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' + if: matrix.ci == 'ciJVM' && matrix.scala != '3.3.4' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' shell: bash run: sbt '++ ${{ matrix.scala }}' 'rootJVM/scalafixAll --check' - name: Check that scalafix has been run on JS - if: matrix.ci == 'ciJS' && matrix.scala != '3.3.3' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' + if: matrix.ci == 'ciJS' && matrix.scala != '3.3.4' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' shell: bash run: sbt '++ ${{ matrix.scala }}' 'rootJS/scalafixAll --check' - name: Check that scalafix has been run on Native - if: matrix.ci == 'ciNative' && matrix.scala != '3.3.3' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' + if: matrix.ci == 'ciNative' && matrix.scala != '3.3.4' && matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' shell: bash run: sbt '++ ${{ matrix.scala }}' 'rootNative/scalafixAll --check' - shell: bash run: sbt '++ ${{ matrix.scala }}' '${{ matrix.ci }}' - - if: (matrix.scala == '2.13.15' || matrix.scala == '3.3.3') && matrix.ci == 'ciJVM' && matrix.java == 'temurin@17' + - if: (matrix.scala == '2.13.15' || matrix.scala == '3.3.4') && matrix.ci == 'ciJVM' && matrix.java == 'temurin@17' shell: bash run: sbt '++ ${{ matrix.scala }}' docs/mdoc @@ -392,32 +392,32 @@ jobs: if: matrix.java == 'graalvm@17' && steps.setup-java-graalvm-17.outputs.cache-hit == 'false' run: sbt +update - - name: Download target directories (3.3.3, ciJVM) + - name: Download target directories (3.3.4, ciJVM) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.3-ciJVM + name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.4-ciJVM - - name: Inflate target directories (3.3.3, ciJVM) + - name: Inflate target directories (3.3.4, ciJVM) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (3.3.3, ciNative) + - name: Download target directories (3.3.4, ciNative) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.3-ciNative + name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.4-ciNative - - name: Inflate target directories (3.3.3, ciNative) + - name: Inflate target directories (3.3.4, ciNative) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (3.3.3, ciJS) + - name: Download target directories (3.3.4, ciJS) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.3-ciJS + name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.4-ciJS - - name: Inflate target directories (3.3.3, ciJS) + - name: Inflate target directories (3.3.4, ciJS) run: | tar xf targets.tar rm targets.tar From 2c7ef318d203f59048ad84f39938bcde17648e7e Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 30 Sep 2024 19:44:42 +0000 Subject: [PATCH 424/429] Fix Native compile --- core/native/src/main/scala/cats/effect/Signal.scala | 2 +- core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala | 2 +- .../native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala | 2 +- .../src/test/scala/cats/effect/FileDescriptorPollerSpec.scala | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/native/src/main/scala/cats/effect/Signal.scala b/core/native/src/main/scala/cats/effect/Signal.scala index 6d678211df..9a0d9bc2c4 100644 --- a/core/native/src/main/scala/cats/effect/Signal.scala +++ b/core/native/src/main/scala/cats/effect/Signal.scala @@ -34,7 +34,7 @@ import java.io.IOException private object Signal { private[this] def mkPipe() = if (isLinux || isMac) { - val fd = stackalloc[CInt](2) + val fd = stackalloc[CInt](2.toULong) if (pipe(fd) != 0) throw new IOException(fromCString(strerror(errno))) diff --git a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala index 9874edb58e..4e8dfd28d0 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/EpollSystem.scala @@ -185,7 +185,7 @@ object EpollSystem extends PollingSystem { private[EpollSystem] def poll(timeout: Long): Boolean = { - val events = stackalloc[epoll_event](MaxEvents.toLong) + val events = stackalloc[epoll_event](MaxEvents.toULong) var polled = false @tailrec diff --git a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala index 3a26a4eb6d..0e7aae2395 100644 --- a/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala +++ b/core/native/src/main/scala/cats/effect/unsafe/KqueueSystem.scala @@ -170,7 +170,7 @@ object KqueueSystem extends PollingSystem { private[KqueueSystem] def poll(timeout: Long): Boolean = { - val eventlist = stackalloc[kevent64_s](MaxEvents.toLong) + val eventlist = stackalloc[kevent64_s](MaxEvents.toULong) var polled = false @tailrec diff --git a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala index 543d71b0a6..879f122fdc 100644 --- a/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala +++ b/tests/native/src/test/scala/cats/effect/FileDescriptorPollerSpec.scala @@ -69,7 +69,7 @@ class FileDescriptorPollerSpec extends BaseSpec { Resource .make { IO { - val fd = stackalloc[CInt](2) + val fd = stackalloc[CInt](2.toULong) if (unistd.pipe(fd) != 0) throw new IOException(fromCString(strerror(errno))) (fd(0), fd(1)) From ca8f69bc76ee8249d3dde018e91ee4a10e431127 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Fri, 11 Oct 2024 04:11:34 +0000 Subject: [PATCH 425/429] Update sbt-typelevel to 0.7.4 in series/3.x --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index b71a01054f..2b6b2e9c8d 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.3") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.4") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") From 71538417f2972562fa0e4734b80898772b6ba167 Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Fri, 11 Oct 2024 04:12:43 +0000 Subject: [PATCH 426/429] Run prePR with sbt-typelevel Executed command: sbt tlPrePrBotHook --- .github/workflows/ci.yml | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e27eb1ca3..2c347cda7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,9 +126,7 @@ jobs: timeout-minutes: 60 steps: - name: Install sbt - if: contains(runner.os, 'macos') - shell: bash - run: brew install sbt + uses: sbt/setup-sbt@v1 - name: Ignore line ending differences in git if: contains(runner.os, 'windows') @@ -199,7 +197,7 @@ jobs: - name: Setup Java (graalvm@17) id: setup-java-graalvm-17 if: matrix.java == 'graalvm@17' - uses: graalvm/setup-graalvm@v1 + uses: actions/setup-java@v4 with: distribution: graalvm java-version: 17 @@ -315,8 +313,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Install sbt - if: contains(runner.os, 'macos') - run: brew install sbt + uses: sbt/setup-sbt@v1 - name: Ignore line ending differences in git if: contains(runner.os, 'windows') @@ -382,7 +379,7 @@ jobs: - name: Setup Java (graalvm@17) id: setup-java-graalvm-17 if: matrix.java == 'graalvm@17' - uses: graalvm/setup-graalvm@v1 + uses: actions/setup-java@v4 with: distribution: graalvm java-version: 17 @@ -544,8 +541,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Install sbt - if: contains(runner.os, 'macos') - run: brew install sbt + uses: sbt/setup-sbt@v1 - name: Ignore line ending differences in git if: contains(runner.os, 'windows') @@ -611,7 +607,7 @@ jobs: - name: Setup Java (graalvm@17) id: setup-java-graalvm-17 if: matrix.java == 'graalvm@17' - uses: graalvm/setup-graalvm@v1 + uses: actions/setup-java@v4 with: distribution: graalvm java-version: 17 From 7ff04061db3664ba88ebd265256e88901380d67d Mon Sep 17 00:00:00 2001 From: "typelevel-steward[bot]" <106827141+typelevel-steward[bot]@users.noreply.github.com> Date: Sun, 20 Oct 2024 04:09:37 +0000 Subject: [PATCH 427/429] Update sbt to 1.10.3 in series/3.x --- project/build.properties | 2 +- scalafix/project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project/build.properties b/project/build.properties index 0b699c3052..bc7390601f 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.2 +sbt.version=1.10.3 diff --git a/scalafix/project/build.properties b/scalafix/project/build.properties index 0b699c3052..bc7390601f 100644 --- a/scalafix/project/build.properties +++ b/scalafix/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.2 +sbt.version=1.10.3 From c0eb2a74560201ec4fc8bc4ecd0545c08499ce2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Zu=CC=88hlke?= Date: Sun, 20 Oct 2024 22:43:56 +0200 Subject: [PATCH 428/429] update GraalVM from 17 to 21 GraalVM is no longer available under the free license: https://github.com/graalvm/setup-graalvm#notes-on-oracle-graalvm-for-jdk-17 --- .github/workflows/ci.yml | 48 ++++++++++++++++++++-------------------- build.sbt | 2 +- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c347cda7d..01758f1e63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: - temurin@11 - temurin@17 - temurin@21 - - graalvm@17 + - graalvm@21 ci: [ciJVM, ciNative, ciJS, ciFirefox, ciChrome] exclude: - scala: 3.3.4 @@ -48,7 +48,7 @@ jobs: - scala: 2.12.20 java: temurin@21 - scala: 2.12.20 - java: graalvm@17 + java: graalvm@21 - os: windows-latest scala: 3.3.4 ci: ciJVM @@ -78,7 +78,7 @@ jobs: - ci: ciJS java: temurin@21 - ci: ciJS - java: graalvm@17 + java: graalvm@21 - os: windows-latest ci: ciJS - os: macos-14 @@ -90,7 +90,7 @@ jobs: - ci: ciFirefox java: temurin@21 - ci: ciFirefox - java: graalvm@17 + java: graalvm@21 - os: windows-latest ci: ciFirefox - os: macos-14 @@ -102,7 +102,7 @@ jobs: - ci: ciChrome java: temurin@21 - ci: ciChrome - java: graalvm@17 + java: graalvm@21 - os: windows-latest ci: ciChrome - os: macos-14 @@ -114,14 +114,14 @@ jobs: - ci: ciNative java: temurin@21 - ci: ciNative - java: graalvm@17 + java: graalvm@21 - os: windows-latest ci: ciNative - os: macos-14 ci: ciNative scala: 2.12.20 - os: windows-latest - java: graalvm@17 + java: graalvm@21 runs-on: ${{ matrix.os }} timeout-minutes: 60 steps: @@ -194,17 +194,17 @@ jobs: shell: bash run: sbt +update - - name: Setup Java (graalvm@17) - id: setup-java-graalvm-17 - if: matrix.java == 'graalvm@17' + - name: Setup Java (graalvm@21) + id: setup-java-graalvm-21 + if: matrix.java == 'graalvm@21' uses: actions/setup-java@v4 with: distribution: graalvm - java-version: 17 + java-version: 21 cache: sbt - name: sbt update - if: matrix.java == 'graalvm@17' && steps.setup-java-graalvm-17.outputs.cache-hit == 'false' + if: matrix.java == 'graalvm@21' && steps.setup-java-graalvm-21.outputs.cache-hit == 'false' shell: bash run: sbt +update @@ -220,7 +220,7 @@ jobs: run: npm install - name: Install GraalVM Native Image - if: matrix.java == 'graalvm@17' + if: matrix.java == 'graalvm@21' shell: bash run: gu install native-image @@ -269,7 +269,7 @@ jobs: run: example/test-js.sh ${{ matrix.scala }} - name: Test GraalVM Native Image - if: matrix.scala == '2.13.15' && matrix.java == 'graalvm@17' && matrix.os == 'ubuntu-latest' + if: matrix.scala == '2.13.15' && matrix.java == 'graalvm@21' && matrix.os == 'ubuntu-latest' shell: bash run: sbt '++ ${{ matrix.scala }}' graalVMExample/nativeImage graalVMExample/nativeImageRun @@ -376,17 +376,17 @@ jobs: if: matrix.java == 'temurin@21' && steps.setup-java-temurin-21.outputs.cache-hit == 'false' run: sbt +update - - name: Setup Java (graalvm@17) - id: setup-java-graalvm-17 - if: matrix.java == 'graalvm@17' + - name: Setup Java (graalvm@21) + id: setup-java-graalvm-21 + if: matrix.java == 'graalvm@21' uses: actions/setup-java@v4 with: distribution: graalvm - java-version: 17 + java-version: 21 cache: sbt - name: sbt update - if: matrix.java == 'graalvm@17' && steps.setup-java-graalvm-17.outputs.cache-hit == 'false' + if: matrix.java == 'graalvm@21' && steps.setup-java-graalvm-21.outputs.cache-hit == 'false' run: sbt +update - name: Download target directories (3.3.4, ciJVM) @@ -604,17 +604,17 @@ jobs: if: matrix.java == 'temurin@21' && steps.setup-java-temurin-21.outputs.cache-hit == 'false' run: sbt +update - - name: Setup Java (graalvm@17) - id: setup-java-graalvm-17 - if: matrix.java == 'graalvm@17' + - name: Setup Java (graalvm@21) + id: setup-java-graalvm-21 + if: matrix.java == 'graalvm@21' uses: actions/setup-java@v4 with: distribution: graalvm - java-version: 17 + java-version: 21 cache: sbt - name: sbt update - if: matrix.java == 'graalvm@17' && steps.setup-java-graalvm-17.outputs.cache-hit == 'false' + if: matrix.java == 'graalvm@21' && steps.setup-java-graalvm-21.outputs.cache-hit == 'false' run: sbt +update - name: Submit Dependencies diff --git a/build.sbt b/build.sbt index 941d29e285..adfd43fa46 100644 --- a/build.sbt +++ b/build.sbt @@ -139,7 +139,7 @@ val LatestJava = JavaSpec.temurin("17") val LoomJava = JavaSpec.temurin("21") val ScalaJSJava = OldGuardJava val ScalaNativeJava = OldGuardJava -val GraalVM = JavaSpec.graalvm("17") +val GraalVM = JavaSpec.graalvm("21") ThisBuild / githubWorkflowJavaVersions := Seq( OldGuardJava, From cf66797cadf32a5a6e03147be6e9433851c3f076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Zu=CC=88hlke?= Date: Sun, 20 Oct 2024 22:57:23 +0200 Subject: [PATCH 429/429] remove install native image step --- .github/workflows/ci.yml | 5 ----- build.sbt | 5 ----- 2 files changed, 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01758f1e63..6828206fd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -219,11 +219,6 @@ jobs: shell: bash run: npm install - - name: Install GraalVM Native Image - if: matrix.java == 'graalvm@21' - shell: bash - run: gu install native-image - - name: Configure Windows Pagefile if: matrix.os == 'windows-latest' uses: al-cheb/configure-pagefile-action@d298bdee6b133626425040e3788f1055a8b4cf7a diff --git a/build.sbt b/build.sbt index adfd43fa46..3fae6d4166 100644 --- a/build.sbt +++ b/build.sbt @@ -161,11 +161,6 @@ ThisBuild / githubWorkflowBuildPreamble ++= Seq( name = Some("Install jsdom and source-map-support"), cond = Some("matrix.ci == 'ciJS'") ), - WorkflowStep.Run( - List("gu install native-image"), - name = Some("Install GraalVM Native Image"), - cond = Some(s"matrix.java == '${GraalVM.render}'") - ), WorkflowStep.Use( UseRef.Public( "al-cheb",