Skip to content

Commit 5f77316

Browse files
authoredJul 11, 2022
* Change Outcome conversion to always treat typed failure as Outcome.Erorred (typed failure excludes possibility of external interruption) (#549)
* Treat `Cause.Empty` + external interruption as `Outcome.Canceled`, since this combination manifests sometimes due to a bug in ZIO runtime * Fix Cause comparison in tests, fix Cogen[Cause] instance * Compare Cause by converting to Outcome first to ignore Cause tree details not important to cats-effect * Enable `genFail` and `genCancel` generators since with above fixes the laws pass with them now * Replace `genRace` and `genParallel` with cats-effect based impls to preserve Outcome when F.canceled is generated * Add test that `Cause.Fail` cannot be present after external interruption * Delegate `race` & `both` to default implementations, because `raceFirst` & `zipPar` semantics do not match them
1 parent d745d7e commit 5f77316

File tree

6 files changed

+223
-121
lines changed

6 files changed

+223
-121
lines changed
 

‎zio-interop-cats-tests/jvm/src/test/scala/zio/interop/CatsInteropSpec.scala

+53-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import cats.effect.{ Async, IO as CIO, LiftIO, Outcome }
44
import cats.effect.kernel.{ Concurrent, Resource }
55
import zio.interop.catz.*
66
import zio.test.*
7-
import zio.{ Promise, Task, ZIO }
7+
import zio.{ Cause, Exit, Promise, Task, ZIO }
88

99
object CatsInteropSpec extends CatsRunnableSpec {
1010
def spec = suite("Cats interop")(
@@ -130,6 +130,58 @@ object CatsInteropSpec extends CatsRunnableSpec {
130130
res <- counter.get
131131
} yield assertTrue(!res.contains("1")) && assertTrue(res == "AC")
132132
},
133+
testM(
134+
"onCancel is triggered when a fiber executing ZIO.parTraverse + ZIO.fail is interrupted and the inner typed" +
135+
" error is lost in final Cause (Fail & Interrupt nodes cannot both exist in Cause after external interruption)"
136+
) {
137+
val F = Concurrent[Task]
138+
139+
for {
140+
latch1 <- F.deferred[Unit]
141+
latch2 <- F.deferred[Unit]
142+
latch3 <- F.deferred[Unit]
143+
counter <- F.ref("")
144+
cause <- F.ref(Option.empty[Cause[Throwable]])
145+
outerScope <- ZIO.forkScope
146+
fiber <- F.guaranteeCase(
147+
F.onError(
148+
F.onCancel(
149+
ZIO
150+
.collectAllPar(
151+
List(
152+
F.onCancel(
153+
ZIO.never,
154+
latch2.complete(()).unit
155+
),
156+
(latch1.complete(()) *> latch3.get).uninterruptible,
157+
counter.update(_ + "A") *>
158+
latch1.get *>
159+
ZIO.fail(new RuntimeException("The_Error")).unit
160+
)
161+
)
162+
.overrideForkScope(outerScope)
163+
.onExit {
164+
case Exit.Success(_) => ZIO.unit
165+
case Exit.Failure(c) => cause.set(Some(c)).orDie
166+
},
167+
counter.update(_ + "B")
168+
)
169+
) { case _ => counter.update(_ + "1") }
170+
) {
171+
case Outcome.Errored(_) => counter.update(_ + "2")
172+
case Outcome.Canceled() => counter.update(_ + "C")
173+
case Outcome.Succeeded(_) => counter.update(_ + "3")
174+
}.fork
175+
_ <- latch2.get
176+
_ <- fiber.interrupt
177+
_ <- latch3.complete(())
178+
res <- counter.get
179+
cause <- cause.get
180+
} yield assertTrue(!res.contains("1")) &&
181+
assertTrue(res == "ABC") &&
182+
assertTrue(cause.isDefined) &&
183+
assertTrue(!cause.get.prettyPrint.contains("The_Error"))
184+
},
133185
test("F.canceled.toEffect results in CancellationException, not BoxedException") {
134186
val F = Concurrent[Task]
135187

‎zio-interop-cats-tests/shared/src/test/scala/zio/interop/CatsSpecBase.scala

+37-25
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import cats.effect.testkit.TestInstances
44
import cats.effect.kernel.Outcome
55
import cats.effect.IO as CIO
66
import cats.syntax.all.*
7-
import cats.{ Eq, Order }
7+
import cats.{ Eq, Id, Order }
88
import org.scalacheck.{ Arbitrary, Cogen, Gen, Prop }
99
import org.scalatest.funsuite.AnyFunSuite
1010
import org.scalatest.prop.Configuration
@@ -69,12 +69,15 @@ private[zio] trait CatsSpecBase
6969
ZEnv.Services.live ++ Has(testClock) ++ Has(testBlocking)
7070
}
7171

72-
def unsafeRun[E, A](io: IO[E, A])(implicit ticker: Ticker): Exit[E, Option[A]] =
72+
def unsafeRun[E, A](io: IO[E, A])(implicit ticker: Ticker): (Exit[E, Option[A]], Boolean) =
7373
try {
7474
var exit: Exit[E, Option[A]] = Exit.succeed(Option.empty[A])
75-
runtime.unsafeRunAsync[E, Option[A]](io.asSome)(exit = _)
75+
var interrupted: Boolean = true
76+
runtime.unsafeRunAsync[E, Option[A]] {
77+
signalOnNoExternalInterrupt(io)(ZIO.effectTotal { interrupted = false }).asSome
78+
}(exit = _)
7679
ticker.ctx.tickAll(FiniteDuration(1, TimeUnit.SECONDS))
77-
exit
80+
(exit, interrupted)
7881
} catch {
7982
case error: Throwable =>
8083
error.printStackTrace()
@@ -102,23 +105,15 @@ private[zio] trait CatsSpecBase
102105
Eq.allEqual
103106

104107
implicit val eqForCauseOfNothing: Eq[Cause[Nothing]] =
105-
eqForCauseOf[Nothing]
106-
107-
implicit def eqForCauseOf[E]: Eq[Cause[E]] =
108-
(x, y) => (x.interrupted && y.interrupted) || x == y
109-
110-
implicit def eqForExitOfNothing[A: Eq]: Eq[Exit[Nothing, A]] = {
111-
case (Exit.Success(x), Exit.Success(y)) => x eqv y
112-
case (Exit.Failure(x), Exit.Failure(y)) => x eqv y
113-
case _ => false
114-
}
108+
(x, y) => (x.interrupted && y.interrupted && x.failureOption.isEmpty && y.failureOption.isEmpty) || x == y
115109

116110
implicit def eqForUIO[A: Eq](implicit ticker: Ticker): Eq[UIO[A]] = { (uio1, uio2) =>
117-
val exit1 = unsafeRun(uio1)
118-
val exit2 = unsafeRun(uio2)
119-
// println(s"comparing $exit1 $exit2")
120-
(exit1 eqv exit2) || {
121-
println(s"$exit1 was not equal to $exit2")
111+
val (exit1, i1) = unsafeRun(uio1)
112+
val (exit2, i2) = unsafeRun(uio2)
113+
val out1 = toOutcomeCauseOtherFiber[Id, Nothing, Option[A]](i1)(identity, exit1)
114+
val out2 = toOutcomeCauseOtherFiber[Id, Nothing, Option[A]](i2)(identity, exit2)
115+
(out1 eqv out2) || {
116+
println(s"$out1 was not equal to $out2")
122117
false
123118
}
124119
}
@@ -136,7 +131,7 @@ private[zio] trait CatsSpecBase
136131
.toEffect[CIO]
137132

138133
implicit def orderForUIOofFiniteDuration(implicit ticker: Ticker): Order[UIO[FiniteDuration]] =
139-
Order.by(unsafeRun(_).toEither.toOption)
134+
Order.by(unsafeRun(_)._1.toEither.toOption)
140135

141136
implicit def orderForRIOofFiniteDuration[R: Arbitrary](implicit ticker: Ticker): Order[RIO[R, FiniteDuration]] =
142137
(x, y) =>
@@ -149,7 +144,7 @@ private[zio] trait CatsSpecBase
149144
ticker: Ticker
150145
): Order[ZIO[R, E, FiniteDuration]] = {
151146
implicit val orderForIOofFiniteDuration: Order[IO[E, FiniteDuration]] =
152-
Order.by(unsafeRun(_) match {
147+
Order.by(unsafeRun(_)._1 match {
153148
case Exit.Success(value) => Right(value)
154149
case Exit.Failure(cause) => Left(cause.failureOption)
155150
})
@@ -167,11 +162,11 @@ private[zio] trait CatsSpecBase
167162
Cogen[Outcome[Option, E, A]].contramap { (zio: ZIO[R, E, A]) =>
168163
Arbitrary.arbitrary[R].sample match {
169164
case Some(r) =>
170-
val result = unsafeRun(zio.provide(r))
165+
val (result, extInterrupted) = unsafeRun(zio.provide(r))
171166

172167
result match {
173168
case Exit.Failure(cause) =>
174-
if (cause.interrupted) Outcome.canceled[Option, E, A]
169+
if (cause.interrupted && extInterrupted) Outcome.canceled[Option, E, A]
175170
else Outcome.errored(cause.failureOption.get)
176171
case Exit.Success(value) => Outcome.succeeded(value)
177172
}
@@ -181,8 +176,8 @@ private[zio] trait CatsSpecBase
181176

182177
implicit def cogenOutcomeZIO[R, A](implicit
183178
cogen: Cogen[ZIO[R, Throwable, A]]
184-
): Cogen[Outcome[ZIO[R, Throwable, *], Throwable, A]] =
185-
cogenOutcome[RIO[R, *], Throwable, A]
179+
): Cogen[Outcome[ZIO[R, Throwable, _], Throwable, A]] =
180+
cogenOutcome[RIO[R, _], Throwable, A]
186181
}
187182

188183
private[interop] sealed trait CatsSpecBaseLowPriority { this: CatsSpecBase =>
@@ -213,4 +208,21 @@ private[interop] sealed trait CatsSpecBaseLowPriority { this: CatsSpecBase =>
213208

214209
implicit def eqForTaskManaged[A: Eq](implicit ticker: Ticker): Eq[TaskManaged[A]] =
215210
zManagedEq[Any, Throwable, A]
211+
212+
implicit def eqForCauseOf[E: Eq]: Eq[Cause[E]] = { (exit1, exit2) =>
213+
val out1 =
214+
toOutcomeOtherFiber0[Id, E, Either[E, Cause[Nothing]], Unit](true)(identity, Exit.Failure(exit1))(
215+
(e, _) => Left(e),
216+
Right(_)
217+
)
218+
val out2 =
219+
toOutcomeOtherFiber0[Id, E, Either[E, Cause[Nothing]], Unit](true)(identity, Exit.Failure(exit2))(
220+
(e, _) => Left(e),
221+
Right(_)
222+
)
223+
(out1 eqv out2) || {
224+
println(s"cause $out1 was not equal to cause $out2")
225+
false
226+
}
227+
}
216228
}

‎zio-interop-cats-tests/shared/src/test/scala/zio/interop/GenIOInteropCats.scala

+46-16
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@ import zio.*
66

77
trait GenIOInteropCats {
88

9-
// FIXME generating anything but success (even genFail)
10-
// surfaces multiple further unaddressed law failures
9+
// FIXME `genDie` and `genInternalInterrupt` surface multiple further unaddressed law failures
10+
// See `genDie` scaladoc
1111
def betterGenerators: Boolean = false
1212

13-
// FIXME cats conversion surfaces failures in the following laws:
14-
// `async left is uncancelable sequenced raiseError`
15-
// `async right is uncancelable sequenced pure`
16-
// `applicativeError onError raise`
17-
// `canceled sequences onCanceled in order`
13+
// FIXME cats conversion generator works most of the time
14+
// but generates rare law failures in
15+
// - `canceled sequences onCanceled in order`
16+
// - `uncancelable eliminates onCancel`
17+
// - `fiber join is guarantee case`
18+
// possibly coming from the `GenSpawnGenerators#genRacePair` generator + `F.canceled`.
19+
// Errors occur more often when combined with `genOfRace` or `genOfParallel`
1820
def catsConversionGenerator: Boolean = false
1921

2022
/**
@@ -35,9 +37,28 @@ trait GenIOInteropCats {
3537

3638
def genFail[E: Arbitrary, A]: Gen[IO[E, A]] = Arbitrary.arbitrary[E].map(IO.fail[E](_))
3739

40+
/**
41+
* We can't pass laws like `cats.effect.laws.GenSpawnLaws#fiberJoinIsGuaranteeCase`
42+
* with either `genDie` or `genInternalInterrupt` because
43+
* we are forced to rethrow an `Outcome.Errored` using
44+
* `raiseError` in `Outcome#embed` which converts the
45+
* specific state into a typed error.
46+
*
47+
* While we consider both states to be `Outcome.Errored`,
48+
* they aren't really 'equivalent' even if we massage them
49+
* into having the same `Outcome`, because `handleErrorWith`
50+
* can't recover from these states.
51+
*
52+
* Now, we could make ZIO Throwable instances recover from
53+
* all errors via [[zio.Cause#squashTraceWith]], but
54+
* this would make Throwable instances contradict the
55+
* generic MonadError instance.
56+
* (Which I believe is acceptable, if confusing, as long
57+
* as the generic instances are moved to a separate `generic`
58+
* object.)
59+
*/
3860
def genDie(implicit arbThrowable: Arbitrary[Throwable]): Gen[UIO[Nothing]] = arbThrowable.arbitrary.map(IO.die(_))
39-
40-
def genInternalInterrupt: Gen[UIO[Nothing]] = ZIO.interrupt
61+
def genInternalInterrupt: Gen[UIO[Nothing]] = ZIO.interrupt
4162

4263
def genCancel[E, A: Arbitrary](implicit F: GenConcurrent[IO[E, _], ?]): Gen[IO[E, A]] =
4364
Arbitrary.arbitrary[A].map(F.canceled.as(_))
@@ -60,18 +81,22 @@ trait GenIOInteropCats {
6081
else
6182
Gen.oneOf(
6283
genSuccess[E, A],
84+
genFail[E, A],
85+
genCancel[E, A],
6386
genNever
6487
)
6588

66-
def genUIO[A: Arbitrary]: Gen[UIO[A]] =
89+
def genUIO[A: Arbitrary](implicit F: GenConcurrent[UIO, ?]): Gen[UIO[A]] =
6790
Gen.oneOf(genSuccess[Nothing, A], genIdentityTrans(genSuccess[Nothing, A]))
6891

6992
/**
7093
* Given a generator for `IO[E, A]`, produces a sized generator for `IO[E, A]` which represents a transformation,
7194
* by using some random combination of the methods `map`, `flatMap`, `mapError`, and any other method that does not change
7295
* the success/failure of the value, but may change the value itself.
7396
*/
74-
def genLikeTrans[E: Arbitrary: Cogen, A: Arbitrary: Cogen](gen: Gen[IO[E, A]]): Gen[IO[E, A]] = {
97+
def genLikeTrans[E: Arbitrary: Cogen, A: Arbitrary: Cogen](
98+
gen: Gen[IO[E, A]]
99+
)(implicit F: GenConcurrent[IO[E, _], ?]): Gen[IO[E, A]] = {
75100
val functions: IO[E, A] => Gen[IO[E, A]] = io =>
76101
Gen.oneOf(
77102
genOfFlatMaps[E, A](io)(genSuccess[E, A]),
@@ -87,7 +112,8 @@ trait GenIOInteropCats {
87112
* Given a generator for `IO[E, A]`, produces a sized generator for `IO[E, A]` which represents a transformation,
88113
* by using methods that can have no effect on the resulting value (e.g. `map(identity)`, `io.race(never)`, `io.par(io2).map(_._1)`).
89114
*/
90-
def genIdentityTrans[E, A: Arbitrary](gen: Gen[IO[E, A]]): Gen[IO[E, A]] = {
115+
def genIdentityTrans[E, A: Arbitrary](gen: Gen[IO[E, A]])(implicit F: GenConcurrent[IO[E, _], ?]): Gen[IO[E, A]] = {
116+
implicitly[Arbitrary[A]]
91117
val functions: IO[E, A] => Gen[IO[E, A]] = io =>
92118
Gen.oneOf(
93119
genOfIdentityFlatMaps[E, A](io),
@@ -131,9 +157,13 @@ trait GenIOInteropCats {
131157
private def genOfIdentityFlatMaps[E, A](io: IO[E, A]): Gen[IO[E, A]] =
132158
Gen.const(io.flatMap(a => IO.succeed(a)))
133159

134-
private def genOfRace[E, A](io: IO[E, A]): Gen[IO[E, A]] =
135-
Gen.const(io.interruptible.raceFirst(ZIO.never.interruptible))
160+
private def genOfRace[E, A](io: IO[E, A])(implicit F: GenConcurrent[IO[E, _], ?]): Gen[IO[E, A]] =
161+
// Gen.const(io.interruptible.raceFirst(ZIO.never.interruptible))
162+
Gen.const(F.race(io, ZIO.never).map(_.merge)) // we must use cats version for Outcome preservation in F.canceled
136163

137-
private def genOfParallel[E, A](io: IO[E, A])(gen: Gen[IO[E, A]]): Gen[IO[E, A]] =
138-
gen.map(parIo => io.interruptible.zipPar(parIo.interruptible).map(_._1))
164+
private def genOfParallel[E, A](io: IO[E, A])(
165+
gen: Gen[IO[E, A]]
166+
)(implicit F: GenConcurrent[IO[E, _], ?]): Gen[IO[E, A]] =
167+
// gen.map(parIo => io.interruptible.zipPar(parIo.interruptible).map(_._1))
168+
gen.map(parIO => F.both(io, parIO).map(_._1)) // we must use cats version for Outcome preservation in F.canceled
139169
}

‎zio-interop-cats-tests/shared/src/test/scala/zio/interop/ZioSpecBase.scala

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package zio.interop
22

3+
import cats.effect.kernel.Outcome
34
import org.scalacheck.{ Arbitrary, Cogen, Gen }
45
import zio.*
56
import zio.clock.Clock
67

78
private[interop] trait ZioSpecBase extends CatsSpecBase with ZioSpecBaseLowPriority with GenIOInteropCats {
89

9-
implicit def arbitraryUIO[A: Arbitrary]: Arbitrary[UIO[A]] =
10+
implicit def arbitraryUIO[A: Arbitrary]: Arbitrary[UIO[A]] = {
11+
import zio.interop.catz.generic.concurrentInstanceCause
1012
Arbitrary(genUIO[A])
13+
}
1114

1215
implicit def arbitraryURIO[R: Cogen, A: Arbitrary]: Arbitrary[URIO[R, A]] =
1316
Arbitrary(Arbitrary.arbitrary[R => UIO[A]].map(ZIO.environment[R].flatMap))
@@ -39,8 +42,13 @@ private[interop] trait ZioSpecBase extends CatsSpecBase with ZioSpecBaseLowPrior
3942
Arbitrary(self)
4043
}
4144

42-
implicit def cogenCause[E]: Cogen[Cause[E]] =
43-
Cogen(_.hashCode.toLong)
45+
implicit def cogenCause[E: Cogen]: Cogen[Cause[E]] =
46+
Cogen[Outcome[Option, Either[E, Int], Unit]].contramap { cause =>
47+
toOutcomeOtherFiber0[Option, E, Either[E, Int], Unit](true)(Option(_), Exit.Failure(cause))(
48+
(e, _) => Left(e),
49+
c => Right(c.hashCode())
50+
)
51+
}
4452
}
4553

4654
private[interop] trait ZioSpecBaseLowPriority { self: ZioSpecBase =>

‎zio-interop-cats/shared/src/main/scala/zio/interop/cats.scala

+7-2
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,9 @@ private abstract class ZioConcurrent[R, E, E1]
335335
)
336336
} yield res
337337

338-
override final def both[A, B](fa: F[A], fb: F[B]): F[(A, B)] =
339-
fa.interruptible zipPar fb.interruptible
338+
// delegate race & both to default implementations, because `raceFirst` & `zipPar` semantics do not match them
339+
override final def race[A, B](fa: F[A], fb: F[B]): F[Either[A, B]] = super.race(fa, fb)
340+
override final def both[A, B](fa: F[A], fb: F[B]): F[(A, B)] = super.both(fa, fb)
340341

341342
override final def guarantee[A](fa: F[A], fin: F[Unit]): F[A] =
342343
fa.ensuring(fin.orDieWith(toThrowableOrFiberFailure))
@@ -580,17 +581,21 @@ private abstract class ZioMonadErrorExit[R, E, E1] extends ZioMonadError[R, E, E
580581
private trait ZioMonadErrorExitThrowable[R]
581582
extends ZioMonadErrorExit[R, Throwable, Throwable]
582583
with ZioMonadErrorE[R, Throwable] {
584+
583585
override final protected def toOutcomeThisFiber[A](exit: Exit[Throwable, A]): UIO[Outcome[F, Throwable, A]] =
584586
toOutcomeThrowableThisFiber(exit)
587+
585588
protected final def toOutcomeOtherFiber[A](interruptedHandle: zio.Ref[Boolean])(
586589
exit: Exit[Throwable, A]
587590
): UIO[Outcome[F, Throwable, A]] =
588591
interruptedHandle.get.map(toOutcomeThrowableOtherFiber(_)(ZIO.succeedNow, exit))
589592
}
590593

591594
private trait ZioMonadErrorExitCause[R, E] extends ZioMonadErrorExit[R, E, Cause[E]] with ZioMonadErrorCause[R, E] {
595+
592596
override protected def toOutcomeThisFiber[A](exit: Exit[E, A]): UIO[Outcome[F, Cause[E], A]] =
593597
toOutcomeCauseThisFiber(exit)
598+
594599
protected final def toOutcomeOtherFiber[A](interruptedHandle: zio.Ref[Boolean])(
595600
exit: Exit[E, A]
596601
): UIO[Outcome[F, Cause[E], A]] =

‎zio-interop-cats/shared/src/main/scala/zio/interop/package.scala

+69-74
Original file line numberDiff line numberDiff line change
@@ -76,107 +76,102 @@ package object interop {
7676
@inline private[interop] def toOutcomeCauseOtherFiber[F[_], E, A](
7777
actuallyInterrupted: Boolean
7878
)(pure: A => F[A], exit: Exit[E, A]): Outcome[F, Cause[E], A] =
79-
exit match {
80-
case Exit.Success(value) =>
81-
Outcome.Succeeded(pure(value))
82-
case Exit.Failure(cause) if cause.interrupted && actuallyInterrupted =>
83-
Outcome.Canceled()
84-
case Exit.Failure(cause) =>
85-
Outcome.Errored(cause)
86-
}
79+
toOutcomeOtherFiber0(actuallyInterrupted)(pure, exit)((_, c) => c, identity)
8780

8881
@inline private[interop] def toOutcomeThrowableOtherFiber[F[_], A](
8982
actuallyInterrupted: Boolean
9083
)(pure: A => F[A], exit: Exit[Throwable, A]): Outcome[F, Throwable, A] =
84+
toOutcomeOtherFiber0(actuallyInterrupted)(pure, exit)((e, _) => e, dieCauseToThrowable)
85+
86+
@inline private[interop] def toOutcomeOtherFiber0[F[_], E, E1, A](
87+
actuallyInterrupted: Boolean
88+
)(pure: A => F[A], exit: Exit[E, A])(
89+
convertFail: (E, Cause[E]) => E1,
90+
convertDie: Cause[Nothing] => E1
91+
): Outcome[F, E1, A] =
9192
exit match {
92-
case Exit.Success(value) =>
93+
case Exit.Success(value) =>
9394
Outcome.Succeeded(pure(value))
94-
case Exit.Failure(cause) if cause.interrupted && actuallyInterrupted =>
95-
Outcome.Canceled()
96-
case Exit.Failure(cause) =>
95+
case Exit.Failure(cause) =>
9796
cause.failureOrCause match {
98-
case Left(error) =>
99-
Outcome.Errored(error)
100-
case Right(cause) =>
101-
val compositeError = dieCauseToThrowable(cause)
102-
Outcome.Errored(compositeError)
97+
// if we have a typed failure then we're guaranteed to not be interrupting,
98+
// typed failure absence is guaranteed by this line https://github.com/zio/zio/blob/22921ee5ac0d2e03531f8b37dfc0d5793a467af8/core/shared/src/main/scala/zio/internal/FiberContext.scala#L415=
99+
case Left(error) =>
100+
Outcome.Errored(convertFail(error, cause))
101+
// deem empty cause to be interruption as well, due to occasional invalid ZIO states
102+
// in `ZIO.fail().uninterruptible` caused by this line https://github.com/zio/zio/blob/22921ee5ac0d2e03531f8b37dfc0d5793a467af8/core/shared/src/main/scala/zio/internal/FiberContext.scala#L415=
103+
case Right(cause) if (cause.interrupted || cause.isEmpty) && actuallyInterrupted =>
104+
Outcome.Canceled()
105+
case Right(cause) =>
106+
Outcome.Errored(convertDie(cause))
103107
}
104108
}
105109

106110
@inline private[interop] def toOutcomeCauseThisFiber[R, E, A](
107111
exit: Exit[E, A]
108112
): UIO[Outcome[ZIO[R, E, _], Cause[E], A]] =
109-
exit match {
110-
case Exit.Success(value) =>
111-
ZIO.succeedNow(Outcome.Succeeded(ZIO.succeedNow(value)))
112-
case Exit.Failure(cause) =>
113-
if (cause.interrupted)
114-
ZIO.descriptorWith { descriptor =>
115-
ZIO.succeedNow(
116-
if (descriptor.interrupters.nonEmpty)
117-
Outcome.Canceled()
118-
else
119-
Outcome.Errored(cause)
120-
)
121-
}
122-
else ZIO.succeedNow(Outcome.Errored(cause))
123-
}
113+
toOutcomeThisFiber0(exit)((_, c) => c, identity)
124114

125-
private[interop] def toOutcomeThrowableThisFiber[R, A](
115+
@inline private[interop] def toOutcomeThrowableThisFiber[R, A](
126116
exit: Exit[Throwable, A]
127117
): UIO[Outcome[ZIO[R, Throwable, _], Throwable, A]] =
128-
exit match {
129-
case Exit.Success(value) =>
130-
ZIO.succeedNow(Outcome.Succeeded(ZIO.succeedNow(value)))
131-
case Exit.Failure(cause) =>
132-
def outcomeErrored: Outcome[ZIO[R, Throwable, _], Throwable, A] =
133-
cause.failureOrCause match {
134-
case Left(error) =>
135-
Outcome.Errored(error)
136-
case Right(cause) =>
137-
val compositeError = dieCauseToThrowable(cause)
138-
Outcome.Errored(compositeError)
139-
}
140-
141-
if (cause.interrupted)
118+
toOutcomeThisFiber0(exit)((e, _) => e, dieCauseToThrowable)
119+
120+
@inline private def toOutcomeThisFiber0[R, E, E1, A](exit: Exit[E, A])(
121+
convertFail: (E, Cause[E]) => E1,
122+
convertDie: Cause[Nothing] => E1
123+
): UIO[Outcome[ZIO[R, E, _], E1, A]] = exit match {
124+
case Exit.Success(value) =>
125+
ZIO.succeedNow(Outcome.Succeeded(ZIO.succeedNow(value)))
126+
case Exit.Failure(cause) =>
127+
cause.failureOrCause match {
128+
// if we have a typed failure then we're guaranteed to not be interrupting,
129+
// typed failure absence is guaranteed by this line https://github.com/zio/zio/blob/22921ee5ac0d2e03531f8b37dfc0d5793a467af8/core/shared/src/main/scala/zio/internal/FiberContext.scala#L415=
130+
case Left(error) =>
131+
ZIO.succeedNow(Outcome.Errored(convertFail(error, cause)))
132+
// deem empty cause to be interruption as well, due to occasional invalid ZIO states
133+
// in `ZIO.fail().uninterruptible` caused by this line https://github.com/zio/zio/blob/22921ee5ac0d2e03531f8b37dfc0d5793a467af8/core/shared/src/main/scala/zio/internal/FiberContext.scala#L415=
134+
case Right(cause) if cause.interrupted || cause.isEmpty =>
142135
ZIO.descriptorWith { descriptor =>
143136
ZIO.succeedNow(
144137
if (descriptor.interrupters.nonEmpty)
145138
Outcome.Canceled()
146-
else
147-
outcomeErrored
139+
else {
140+
Outcome.Errored(convertDie(cause))
141+
}
148142
)
149143
}
150-
else ZIO.succeedNow(outcomeErrored)
151-
}
144+
case Right(cause) =>
145+
ZIO.succeedNow(Outcome.Errored(convertDie(cause)))
146+
}
147+
}
152148

153149
private[interop] def toExitCaseThisFiber(exit: Exit[Any, Any]): UIO[Resource.ExitCase] =
154150
exit match {
155151
case Exit.Success(_) =>
156152
ZIO.succeedNow(Resource.ExitCase.Succeeded)
157153
case Exit.Failure(cause) =>
158-
def exitCaseErrored: Resource.ExitCase.Errored =
159-
cause.failureOrCause match {
160-
case Left(error: Throwable) =>
161-
Resource.ExitCase.Errored(error)
162-
case Left(_) =>
163-
Resource.ExitCase.Errored(FiberFailure(cause))
164-
case Right(cause) =>
165-
val compositeError = dieCauseToThrowable(cause)
166-
Resource.ExitCase.Errored(compositeError)
167-
}
168-
169-
if (cause.interrupted)
170-
ZIO.descriptorWith { descriptor =>
171-
ZIO.succeedNow(
172-
if (descriptor.interrupters.nonEmpty)
173-
Resource.ExitCase.Canceled
174-
else
175-
exitCaseErrored
176-
)
177-
}
178-
else
179-
ZIO.succeedNow(exitCaseErrored)
154+
cause.failureOrCause match {
155+
// if we have a typed failure then we're guaranteed to not be interrupting,
156+
// typed failure absence is guaranteed by this line https://github.com/zio/zio/blob/22921ee5ac0d2e03531f8b37dfc0d5793a467af8/core/shared/src/main/scala/zio/internal/FiberContext.scala#L415=
157+
case Left(error: Throwable) =>
158+
ZIO.succeedNow(Resource.ExitCase.Errored(error))
159+
case Left(_) =>
160+
ZIO.succeedNow(Resource.ExitCase.Errored(FiberFailure(cause)))
161+
// deem empty cause to be interruption as well, due to occasional invalid ZIO states
162+
// in `ZIO.fail().uninterruptible` caused by this line https://github.com/zio/zio/blob/22921ee5ac0d2e03531f8b37dfc0d5793a467af8/core/shared/src/main/scala/zio/internal/FiberContext.scala#L415=
163+
case Right(cause) if cause.interrupted || cause.isEmpty =>
164+
ZIO.descriptorWith { descriptor =>
165+
ZIO.succeedNow {
166+
if (descriptor.interrupters.nonEmpty) {
167+
Resource.ExitCase.Canceled
168+
} else
169+
Resource.ExitCase.Errored(dieCauseToThrowable(cause))
170+
}
171+
}
172+
case Right(cause) =>
173+
ZIO.succeedNow(Resource.ExitCase.Errored(dieCauseToThrowable(cause)))
174+
}
180175
}
181176

182177
@inline private[interop] def toExit(exitCase: Resource.ExitCase): Exit[Throwable, Unit] =
@@ -204,7 +199,7 @@ package object interop {
204199
ZIO.descriptorWith(d => if (d.interrupters.isEmpty) notInterrupted else ZIO.unit)
205200
}
206201

207-
@inline private def dieCauseToThrowable(cause: Cause[Nothing]): Throwable =
202+
@inline private[interop] def dieCauseToThrowable(cause: Cause[Nothing]): Throwable =
208203
cause.defects match {
209204
case one :: Nil => one
210205
case _ => FiberFailure(cause)

0 commit comments

Comments
 (0)
Please sign in to comment.