From 6a21e49012bf5abfbbf1806c263bceacf0bb0cd5 Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Thu, 24 Nov 2022 12:22:41 -0700 Subject: [PATCH 01/95] 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 02/95] 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 03/95] 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 04/95] 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 05/95] 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 06/95] 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 07/95] 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 08/95] 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 09/95] 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 10/95] 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 11/95] 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 12/95] 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 13/95] 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 14/95] 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 15/95] 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 16/95] 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 17/95] 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 18/95] 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 19/95] 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 20/95] 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 21/95] 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 22/95] 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 23/95] 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 24/95] 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 25/95] 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 26/95] 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 27/95] 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 28/95] 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 29/95] 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 30/95] 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 31/95] 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 32/95] 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 33/95] 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 34/95] 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 35/95] 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 36/95] 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 37/95] 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 38/95] 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 39/95] 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 40/95] 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 41/95] 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 42/95] 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 43/95] 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 44/95] 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 45/95] 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 46/95] 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 47/95] 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 48/95] 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 49/95] 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 50/95] 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 51/95] `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 52/95] 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 53/95] 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 54/95] 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 55/95] 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 56/95] 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 57/95] 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 58/95] 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 59/95] 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 60/95] 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 61/95] 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 62/95] 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 63/95] 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 4661ba342fd6dc96c06c68c2bc7b3d5a870b4e69 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 23 May 2023 19:53:58 +0000 Subject: [PATCH 64/95] 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 65/95] 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 66/95] 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 67/95] 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 68/95] 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 69/95] 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 70/95] 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 71/95] `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 72/95] 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 73/95] 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 74/95] 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 75/95] 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 5f6a7b3075ba9edf871ed3e592fdd8cbe88beef1 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 31 May 2023 14:24:54 +0000 Subject: [PATCH 76/95] 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 704b8ec5deeb212d4bc52394b3207e2210ce4c25 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Wed, 31 May 2023 21:52:07 +0000 Subject: [PATCH 77/95] 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 2096fadfc2ce2c8885b8134f46f86870d29a3ff2 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 6 Jun 2023 02:34:00 +0000 Subject: [PATCH 78/95] 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 8d0157ac0c8cb0b9438d9254ec3745b755659a49 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 6 Jun 2023 02:42:32 +0000 Subject: [PATCH 79/95] 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 dfbe7c79464cccffbc4ad46740e8bb4c06c2962d Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Thu, 8 Jun 2023 16:45:19 +0000 Subject: [PATCH 80/95] `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 81/95] 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 6442d7a02bb7b39f267825b6e7df80ff0960e2f1 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Mon, 12 Jun 2023 05:44:37 +0000 Subject: [PATCH 82/95] 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 83/95] 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 84/95] 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 85/95] 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 86/95] 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 87/95] 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 88/95] 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 89/95] 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 90/95] 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 91/95] 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 92/95] 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 93/95] `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 94/95] 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 95/95] 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] =