Skip to content

Commit 7af6add

Browse files
authored
Merge pull request #3432 from djspiewak/bug/timer-firing
Fixed issues in the timer handling state machine integration
2 parents 83f766a + 0dd30f9 commit 7af6add

3 files changed

Lines changed: 107 additions & 36 deletions

File tree

core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeBuilderPlatform.scala

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,20 @@ package cats.effect.unsafe
1818

1919
private[unsafe] abstract class IORuntimeBuilderPlatform { self: IORuntimeBuilder =>
2020

21+
// TODO unify this with the defaults in IORuntime.global and IOApp
2122
protected def platformSpecificBuild: IORuntime = {
2223
val (compute, computeShutdown) =
2324
customCompute.getOrElse(
2425
IORuntime.createWorkStealingComputeThreadPool(reportFailure = failureReporter))
26+
val xformedCompute = computeTransform(compute)
27+
28+
val (scheduler, schedulerShutdown) = xformedCompute match {
29+
case sched: Scheduler => customScheduler.getOrElse((sched, () => ()))
30+
case _ => customScheduler.getOrElse(IORuntime.createDefaultScheduler())
31+
}
32+
2533
val (blocking, blockingShutdown) =
2634
customBlocking.getOrElse(IORuntime.createDefaultBlockingExecutionContext())
27-
val (scheduler, schedulerShutdown) =
28-
customScheduler.getOrElse(IORuntime.createDefaultScheduler())
2935
val shutdown = () => {
3036
computeShutdown()
3137
blockingShutdown()

core/jvm/src/main/scala/cats/effect/unsafe/WorkerThread.scala

Lines changed: 69 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -327,21 +327,41 @@ private final class WorkerThread(
327327
}
328328
}
329329

330-
def parkUntilNextSleeper(): Unit = {
331-
if (!isInterrupted()) {
330+
// returns true if timed out, false if unparked
331+
def parkUntilNextSleeper(): Boolean = {
332+
while (!done.get()) {
332333
val now = System.nanoTime()
333334
val head = sleepersQueue.head()
334335
val nanos = head.triggerTime - now
336+
337+
// System.err.println(s"parking for $nanos nanos")
335338
LockSupport.parkNanos(pool, nanos)
336339

337-
if (parked.getAndSet(false)) {
338-
pool.doneSleeping()
340+
if (isInterrupted()) {
341+
pool.shutdown()
342+
} else {
343+
if (parked.get()) {
344+
// we were either awakened spuriously, or we timed out
345+
346+
if (head.triggerTime - System.nanoTime() <= 0) {
347+
// we timed out
348+
if (parked.getAndSet(false)) {
349+
pool.doneSleeping()
350+
}
351+
352+
return true
353+
}
354+
} else {
355+
// we were awakened
356+
return false
357+
}
339358
}
340359
}
360+
361+
false
341362
}
342363

343364
while (!done.get()) {
344-
345365
if (blocking) {
346366
// The worker thread was blocked before. It is no longer part of the
347367
// core pool and needs to be cached.
@@ -394,25 +414,6 @@ private final class WorkerThread(
394414

395415
val sleepers = sleepersQueue
396416

397-
if (sleepers.nonEmpty) {
398-
val now = System.nanoTime()
399-
400-
var cont = true
401-
while (cont) {
402-
val head = sleepers.head()
403-
404-
if (head.triggerTime - now <= 0) {
405-
if (head.get()) {
406-
head.callback(RightUnit)
407-
}
408-
sleepers.popHead()
409-
cont = sleepers.nonEmpty
410-
} else {
411-
cont = false
412-
}
413-
}
414-
}
415-
416417
((state & ExternalQueueTicksMask): @switch) match {
417418
case 0 =>
418419
if (pool.blockedThreadDetectionEnabled) {
@@ -538,13 +539,20 @@ private final class WorkerThread(
538539
// Park the thread.
539540
if (sleepers.isEmpty) {
540541
parkLoop()
542+
543+
// After the worker thread has been unparked, look for work in the
544+
// external queue.
545+
state = 3
541546
} else {
542-
parkUntilNextSleeper()
547+
if (parkUntilNextSleeper()) {
548+
// we made it to the end of our sleeping, so go straight to local queue stuff
549+
pool.transitionWorkerFromSearching(rnd)
550+
state = 4
551+
} else {
552+
// we were interrupted, look for more work in the external queue
553+
state = 3
554+
}
543555
}
544-
545-
// After the worker thread has been unparked, look for work in the
546-
// external queue.
547-
state = 3
548556
}
549557
}
550558

@@ -584,13 +592,20 @@ private final class WorkerThread(
584592
// Park the thread.
585593
if (sleepers.isEmpty) {
586594
parkLoop()
595+
596+
// After the worker thread has been unparked, look for work in the
597+
// external queue.
598+
state = 3
587599
} else {
588-
parkUntilNextSleeper()
600+
if (parkUntilNextSleeper()) {
601+
// we made it to the end of our sleeping, so go straight to local queue stuff
602+
pool.transitionWorkerFromSearching(rnd)
603+
state = 4
604+
} else {
605+
// we were interrupted, look for more work in the external queue
606+
state = 3
607+
}
589608
}
590-
591-
// After the worker thread has been unparked, look for work in the
592-
// external queue.
593-
state = 3
594609
}
595610

596611
case 3 =>
@@ -647,6 +662,26 @@ private final class WorkerThread(
647662
}
648663

649664
case _ =>
665+
if (sleepers.nonEmpty) {
666+
val now = System.nanoTime()
667+
668+
var cont = true
669+
while (cont) {
670+
val head = sleepers.head()
671+
672+
if (head.triggerTime - now <= 0) {
673+
if (head.get()) {
674+
// System.err.println(s"dequeued with state = $state; sleepers = $sleepers")
675+
head.callback(RightUnit)
676+
}
677+
sleepers.popHead()
678+
cont = sleepers.nonEmpty
679+
} else {
680+
cont = false
681+
}
682+
}
683+
}
684+
650685
// Check the queue bypass reference before dequeueing from the local
651686
// queue.
652687
val fiber = if (cedeBypass eq null) {

tests/jvm/src/test/scala/cats/effect/IOPlatformSpecification.scala

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,36 @@ trait IOPlatformSpecification { self: BaseSpec with ScalaCheck =>
340340
runtime.shutdown()
341341
}
342342
}
343+
344+
// this test ensures that the parkUntilNextSleeper bit works
345+
"run a timer when parking thread" in {
346+
val (pool, shutdown) = IORuntime.createWorkStealingComputeThreadPool(threads = 1)
347+
348+
implicit val runtime: IORuntime = IORuntime.builder().setCompute(pool, shutdown).build()
349+
350+
try {
351+
// longer sleep all-but guarantees this timer is fired *after* the worker is parked
352+
val test = IO.sleep(500.millis) *> IO.pure(true)
353+
test.unsafeRunTimed(5.seconds) must beSome(true)
354+
} finally {
355+
runtime.shutdown()
356+
}
357+
}
358+
359+
// this test ensures that we always see the timer, even when it fires just as we're about to park
360+
"run a timer when detecting just prior to park" in {
361+
val (pool, shutdown) = IORuntime.createWorkStealingComputeThreadPool(threads = 1)
362+
363+
implicit val runtime: IORuntime = IORuntime.builder().setCompute(pool, shutdown).build()
364+
365+
try {
366+
// shorter sleep makes it more likely this timer fires *before* the worker is parked
367+
val test = IO.sleep(1.milli) *> IO.pure(true)
368+
test.unsafeRunTimed(1.second) must beSome(true)
369+
} finally {
370+
runtime.shutdown()
371+
}
372+
}
343373
}
344374
}
345375
}

0 commit comments

Comments
 (0)