Skip to content

Commit 5516608

Browse files
authored
Merge pull request #3510 from samspills/sam/3506/dispatcher-await
Dispatcher: check for outstanding actions before release
2 parents 5887416 + 090aad0 commit 5516608

2 files changed

Lines changed: 58 additions & 16 deletions

File tree

std/shared/src/main/scala/cats/effect/std/Dispatcher.scala

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -242,19 +242,14 @@ object Dispatcher {
242242
states
243243
})
244244
ec <- Resource.eval(F.executionContext)
245-
alive <- Resource.make(F.delay(new AtomicBoolean(true)))(ref => F.delay(ref.set(false)))
246245

247246
// supervisor for the main loop, which needs to always restart unless the Supervisor itself is canceled
248247
// critically, inner actions can be canceled without impacting the loop itself
249248
supervisor <- Supervisor[F](await, Some((_: Outcome[F, Throwable, _]) => true))
250249

251250
_ <- {
252-
def dispatcher(
253-
doneR: AtomicBoolean,
254-
latch: AtomicReference[() => Unit],
255-
state: Array[AtomicReference[List[Registration]]]): F[Unit] = {
256-
257-
val step = for {
251+
def step(state: Array[AtomicReference[List[Registration]]], await: F[Unit]): F[Unit] =
252+
for {
258253
regs <- F delay {
259254
val buffer = mutable.ListBuffer.empty[Registration]
260255
var i = 0
@@ -271,12 +266,7 @@ object Dispatcher {
271266

272267
_ <-
273268
if (regs.isEmpty) {
274-
F.async_[Unit] { cb =>
275-
if (!latch.compareAndSet(Noop, () => cb(Completed))) {
276-
// state was changed between when we last set the latch and now; complete the callback immediately
277-
cb(Completed)
278-
}
279-
}
269+
await
280270
} else {
281271
regs traverse_ {
282272
case r @ Registration(action, prepareCancel) =>
@@ -289,10 +279,23 @@ object Dispatcher {
289279
}
290280
} yield ()
291281

282+
def dispatcher(
283+
doneR: AtomicBoolean,
284+
latch: AtomicReference[() => Unit],
285+
state: Array[AtomicReference[List[Registration]]]): F[Unit] = {
286+
287+
val await =
288+
F.async_[Unit] { cb =>
289+
if (!latch.compareAndSet(Noop, () => cb(Completed))) {
290+
// state was changed between when we last set the latch and now; complete the callback immediately
291+
cb(Completed)
292+
}
293+
}
294+
292295
F.delay(latch.set(Noop)) *> // reset latch
293296
// if we're marked as done, yield immediately to give other fibers a chance to shut us down
294297
// we might loop on this a few times since we're marked as done before the supervisor is canceled
295-
F.delay(doneR.get()).ifM(F.cede, step)
298+
F.delay(doneR.get()).ifM(F.cede, step(state, await))
296299
}
297300

298301
0.until(workers).toList traverse_ { n =>
@@ -301,12 +304,15 @@ object Dispatcher {
301304
val worker = dispatcher(doneR, latch, states(n))
302305
val release = F.delay(latch.getAndSet(Open)())
303306
Resource.make(supervisor.supervise(worker)) { _ =>
304-
// published by release
305-
F.delay(doneR.lazySet(true)) *> release
307+
F.delay(doneR.set(true)) *> step(states(n), F.unit) *> release
306308
}
307309
}
308310
}
309311
}
312+
313+
// Alive is the innermost resource so that when releasing
314+
// the very first thing we do is set dispatcher to un-alive
315+
alive <- Resource.make(F.delay(new AtomicBoolean(true)))(ref => F.delay(ref.set(false)))
310316
} yield {
311317
new Dispatcher[F] {
312318
override def unsafeRunAndForget[A](fa: F[A]): Unit = {

tests/shared/src/test/scala/cats/effect/std/DispatcherSpec.scala

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,33 @@ class DispatcherSpec extends BaseSpec with DetectPlatform {
321321
} yield ok
322322
}
323323
}
324+
325+
"reject new tasks while shutting down" in real {
326+
(IO.ref(false), IO.ref(false)).flatMapN { (resultR, rogueResultR) =>
327+
dispatcher
328+
.allocated
329+
.flatMap {
330+
case (runner, release) =>
331+
IO(runner.unsafeRunAndForget(
332+
IO.sleep(1.second).uncancelable.guarantee(resultR.set(true)))) *>
333+
IO.sleep(100.millis) *>
334+
release.both(
335+
IO.sleep(500.nanos) *>
336+
IO(runner.unsafeRunAndForget(rogueResultR.set(true))).attempt
337+
)
338+
}
339+
.flatMap {
340+
case (_, rogueSubmitResult) =>
341+
for {
342+
result <- resultR.get
343+
rogueResult <- rogueResultR.get
344+
_ <- IO(result must beTrue)
345+
_ <- IO(rogueResult must beFalse)
346+
_ <- IO(rogueSubmitResult must beLeft)
347+
} yield ok
348+
}
349+
}
350+
}
324351
}
325352

326353
private def awaitTermination(dispatcher: Resource[IO, Dispatcher[IO]]) = {
@@ -353,6 +380,15 @@ class DispatcherSpec extends BaseSpec with DetectPlatform {
353380
}
354381
}
355382

383+
"issue #3506: await unsafeRunAndForget" in ticked { implicit ticker =>
384+
val result = for {
385+
resultR <- IO.ref(false)
386+
_ <- dispatcher.use { runner => IO(runner.unsafeRunAndForget(resultR.set(true))) }
387+
result <- resultR.get
388+
} yield result
389+
result must completeAs(true)
390+
}
391+
356392
"cancel active fibers when an error is produced" in real {
357393
case object TestException extends RuntimeException
358394

0 commit comments

Comments
 (0)