Skip to content

Commit 3649a5d

Browse files
committed
Restructured failure case propagation
1 parent 2e276bb commit 3649a5d

2 files changed

Lines changed: 65 additions & 9 deletions

File tree

kernel/shared/src/main/scala/cats/effect/kernel/GenConcurrent.scala

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ trait GenConcurrent[F[_], E] extends GenSpawn[F, E] {
141141

142142
F.deferred[Option[E]] flatMap { preempt =>
143143
F.ref[Set[Fiber[F, ?, ?]]](Set()) flatMap { supervision =>
144+
// has to be done in parallel to avoid head of line issues
145+
val cancelAllF = supervision.get.flatMap(_.toList.parTraverse_(_.cancel))
146+
144147
MiniSemaphore[F](n) flatMap { sem =>
145148
val results = ta traverse { a =>
146149
preempt.tryGet flatMap {
@@ -154,10 +157,43 @@ trait GenConcurrent[F[_], E] extends GenSpawn[F, E] {
154157
F.deferred[Outcome[F, E, B]] flatMap { result =>
155158
val action = poll(sem.acquire) >> f(a)
156159
.guaranteeCase { oc =>
157-
result.complete(oc) *> oc.fold(
158-
preempt.complete(None).void,
159-
e => preempt.complete(Some(e)).void,
160-
_ => F.unit) *> sem.release
160+
val completion = oc match {
161+
case Outcome.Succeeded(_) =>
162+
preempt.tryGet flatMap {
163+
case Some(Some(e)) =>
164+
result.complete(Outcome.Errored(e))
165+
166+
case Some(None) =>
167+
result.complete(Outcome.Canceled())
168+
169+
case None =>
170+
result.complete(oc)
171+
}
172+
173+
case Outcome.Errored(e) =>
174+
preempt.complete(Some(e)) flatMap { won =>
175+
if (won)
176+
result.complete(oc) <* cancelAllF.start // avoid deadlock
177+
else
178+
preempt.get flatMap {
179+
case Some(e) => result.complete(Outcome.Errored(e))
180+
case None => result.complete(Outcome.Canceled())
181+
}
182+
}
183+
184+
case Outcome.Canceled() =>
185+
preempt.complete(None) flatMap { won =>
186+
if (won)
187+
result.complete(oc) <* cancelAllF.start // avoid deadlock
188+
else
189+
preempt.get flatMap {
190+
case Some(e) => result.complete(Outcome.Errored(e))
191+
case None => result.complete(Outcome.Canceled())
192+
}
193+
}
194+
}
195+
196+
completion *> sem.release
161197
}
162198
.void
163199
.voidError
@@ -177,11 +213,7 @@ trait GenConcurrent[F[_], E] extends GenSpawn[F, E] {
177213
}
178214
}
179215

180-
results.flatMap(_.sequence) guaranteeCase {
181-
case Outcome.Succeeded(_) => F.unit
182-
// has to be done in parallel to avoid head of line issues
183-
case _ => supervision.get.flatMap(_.toList.parTraverse_(_.cancel))
184-
}
216+
results.flatMap(_.sequence).onCancel(cancelAllF)
185217
}
186218
}
187219
}

tests/shared/src/test/scala/cats/effect/IOSpec.scala

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1762,6 +1762,30 @@ class IOSpec extends BaseSpec with Discipline with IOPlatformSpecification {
17621762

17631763
test.attempt.timeoutTo(500.millis, IO(false must beTrue)).as(ok)
17641764
}
1765+
1766+
"run finalizers in parallel" in ticked { implicit ticker =>
1767+
// this test also tests to ensure that we get the errored results rather than cancels
1768+
// note that the first two effects will have a Canceled outcome, while the third is Errored
1769+
// if we just go by first wins in sequence, then Canceled is the (incorrect) result
1770+
// first wins *in time* is the expected semantic here
1771+
val test = for {
1772+
latch1 <- IO.deferred[Unit]
1773+
latch2 <- IO.deferred[Unit]
1774+
1775+
_ <- List(1, 2, 3).parTraverseN(3) {
1776+
case 1 =>
1777+
IO.never.onCancel(latch1.complete(()) *> latch2.get)
1778+
1779+
case 2 =>
1780+
IO.never.onCancel(latch2.complete(()) *> latch1.get)
1781+
1782+
case 3 =>
1783+
IO.sleep(10.millis) *> IO.raiseError(new RuntimeException)
1784+
}
1785+
} yield ()
1786+
1787+
test.attempt.void must completeAs(())
1788+
}
17651789
}
17661790

17671791
"parTraverseN_" should {

0 commit comments

Comments
 (0)