From 42d566ecb1324fceb257565abdf7984361ae4855 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Sun, 9 Feb 2025 20:29:10 -0800 Subject: [PATCH 01/34] Optimize Promise --- .jvmopts | 2 + .vscode/settings.json | 2 +- .../src/main/scala/zio/BenchmarkUtil.scala | 9 +- .../main/scala/zio/PromiseBenchmarks.scala | 59 +++++- .../src/test/scala/zio/FiberRuntimeSpec.scala | 21 ++ .../zio/ZIOCompanionVersionSpecific.scala | 26 +-- .../zio/ZIOCompanionVersionSpecific.scala | 26 +-- core/shared/src/main/scala/zio/Promise.scala | 191 +++++++----------- 8 files changed, 174 insertions(+), 162 deletions(-) diff --git a/.jvmopts b/.jvmopts index 89c8775d9d2d..d8db65d6a4a8 100644 --- a/.jvmopts +++ b/.jvmopts @@ -1 +1,3 @@ -Dcats.effect.stackTracingMode=full +-Xmx8g +-Xms8g \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index b4bdcc7fa826..052d27309198 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,5 @@ ], "files.watcherExclude": { "**/target": true - } +} } \ No newline at end of file diff --git a/benchmarks/src/main/scala/zio/BenchmarkUtil.scala b/benchmarks/src/main/scala/zio/BenchmarkUtil.scala index fb5d72171165..45b224363268 100644 --- a/benchmarks/src/main/scala/zio/BenchmarkUtil.scala +++ b/benchmarks/src/main/scala/zio/BenchmarkUtil.scala @@ -39,9 +39,12 @@ object BenchmarkUtil extends Runtime[Any] { self => Unsafe.unsafe(implicit unsafe => rt.unsafe.run(zio).getOrThrowFiberFailure()) } + override val unsafe = super.unsafe + private object NoFiberRootsRuntime extends Runtime[Any] { - val environment = Runtime.default.environment - val fiberRefs = Runtime.default.fiberRefs - val runtimeFlags = RuntimeFlags(RuntimeFlag.CooperativeYielding, RuntimeFlag.Interruption) + override val unsafe = super.unsafe + val environment = Runtime.default.environment + val fiberRefs = Runtime.default.fiberRefs + val runtimeFlags = RuntimeFlags(RuntimeFlag.CooperativeYielding, RuntimeFlag.Interruption) } } diff --git a/benchmarks/src/main/scala/zio/PromiseBenchmarks.scala b/benchmarks/src/main/scala/zio/PromiseBenchmarks.scala index 16ab08e815c3..cfb2547b4813 100644 --- a/benchmarks/src/main/scala/zio/PromiseBenchmarks.scala +++ b/benchmarks/src/main/scala/zio/PromiseBenchmarks.scala @@ -1,8 +1,11 @@ package zio import cats.effect.kernel.Deferred +import cats.syntax.traverse._ +import cats.instances.list._ import cats.effect.unsafe.implicits.global import cats.effect.{IO => CIO} +import cats.syntax.foldable._ import org.openjdk.jmh.annotations.{Scope => JScope, _} import zio.BenchmarkUtil._ @@ -16,18 +19,19 @@ import java.util.concurrent.TimeUnit @Fork(value = 3) class PromiseBenchmarks { - val size = 100000 - - val ints: List[Int] = List.range(0, size) + val n = 100000 + val waiters: Int = 16 @Benchmark def zioPromiseAwaitDone(): Unit = { - val io = ZIO.foreachDiscard(ints) { _ => - Promise.make[Nothing, Unit].flatMap { promise => - promise.succeed(()) *> promise.await - } - } + val io = + Promise + .make[Nothing, Unit] + .flatMap { promise => + promise.succeed(()) *> promise.await + } + .repeatN(n) unsafeRun(io) } @@ -35,11 +39,46 @@ class PromiseBenchmarks { @Benchmark def catsPromiseAwaitDone(): Unit = { - val io = catsForeachDiscard(List.range(1, size)) { _ => + val io = Deferred[CIO, Unit].flatMap { promise => promise.complete(()).flatMap(_ => promise.get) + }.replicateA_(n) + + io.unsafeRunSync() + } + + @Benchmark + def zioPromiseMultiAwaitDone(): Unit = { + def createWaiters(promise: Promise[Nothing, Unit]): ZIO[Any, Nothing, Seq[Fiber[Nothing, Unit]]] = + ZIO.foreach(Range(0, waiters))(_ => promise.await.forkDaemon) + + val io = Promise + .make[Nothing, Unit] + .flatMap { promise => + for { + fibers <- createWaiters(promise) + _ <- promise.done(Exit.unit) + _ <- ZIO.foreachDiscard(fibers)(_.join) + } yield () } - } + .repeatN(1023) + + unsafeRun(io) + } + + @Benchmark + def catsPromiseMultiAwaitDone(): Unit = { + def createWaiters(promise: Deferred[CIO, Unit]): CIO[List[cats.effect.Fiber[CIO, Throwable, Unit]]] = + List.range(0, waiters).traverse(_ => promise.get.start) + + val io = + Deferred[CIO, Unit].flatMap { promise => + for { + fibers <- createWaiters(promise) + _ <- promise.complete(()) + _ <- fibers.traverse_(_.join) + } yield () + }.replicateA_(1023) io.unsafeRunSync() } diff --git a/core-tests/shared/src/test/scala/zio/FiberRuntimeSpec.scala b/core-tests/shared/src/test/scala/zio/FiberRuntimeSpec.scala index be5d6972efcc..ba6823d451d8 100644 --- a/core-tests/shared/src/test/scala/zio/FiberRuntimeSpec.scala +++ b/core-tests/shared/src/test/scala/zio/FiberRuntimeSpec.scala @@ -62,6 +62,27 @@ object FiberRuntimeSpec extends ZIOBaseSpec { } } } + ), + suite("async")( + test("async callback after interruption is ignored") { + ZIO.suspendSucceed { + val cb = Ref.unsafe.make[Option[ZIO[Any, Nothing, Unit] => Unit]](None) + val effect = + (ZIO.async[Any, Nothing, Unit] { (k: (ZIO[Any, Nothing, Unit] => Unit)) => + cb.unsafe.set(Some(k)) + } *> ZIO.never) + + for { + fiber <- effect.fork + _ <- fiber.interrupt + callback <- cb.get.some + _ <- ZIO.succeed(callback(ZIO.unit)) + first <- fiber.poll + _ <- ZIO.succeed(callback(ZIO.unit)) + second <- fiber.poll + } yield assertTrue(first == second) && assertTrue(first == None) + } + } ) ) diff --git a/core/shared/src/main/scala-2/zio/ZIOCompanionVersionSpecific.scala b/core/shared/src/main/scala-2/zio/ZIOCompanionVersionSpecific.scala index adcbec9e4fbe..0a0b20ca4879 100644 --- a/core/shared/src/main/scala-2/zio/ZIOCompanionVersionSpecific.scala +++ b/core/shared/src/main/scala-2/zio/ZIOCompanionVersionSpecific.scala @@ -4,6 +4,7 @@ import zio.ZIO.Async import zio.stacktracer.TracingImplicits.disableAutoTrace import java.io.IOException +import java.util.concurrent.atomic.AtomicReference private[zio] trait ZIOCompanionVersionSpecific { @@ -44,22 +45,15 @@ private[zio] trait ZIOCompanionVersionSpecific { blockingOn: => FiberId = FiberId.None )(implicit trace: Trace): ZIO[R, E, A] = ZIO.suspendSucceed { - val cancelerRef = new java.util.concurrent.atomic.AtomicReference[URIO[R, Any]](ZIO.unit) - - ZIO - .Async[R, E, A]( - trace, - { k => - val result = register(k(_)) - - result match { - case Left(canceler) => cancelerRef.set(canceler); null.asInstanceOf[ZIO[R, E, A]] - case Right(done) => done - } - }, - () => blockingOn - ) - .onInterrupt(cancelerRef.get()) + val state = new AtomicReference[URIO[R, Any]](Exit.unit) with ((ZIO[R, E, A] => Unit) => ZIO[R, E, A]) { + def apply(k: ZIO[R, E, A] => Unit): ZIO[R, E, A] = + register(k(_)) match { + case Left(canceler) => set(canceler); null.asInstanceOf[ZIO[R, E, A]] + case Right(done) => done + } + } + + ZIO.Async[R, E, A](trace, state, () => blockingOn).onInterrupt(state.get()) } /** diff --git a/core/shared/src/main/scala-3/zio/ZIOCompanionVersionSpecific.scala b/core/shared/src/main/scala-3/zio/ZIOCompanionVersionSpecific.scala index c9f766ea9953..4783107ec0b6 100644 --- a/core/shared/src/main/scala-3/zio/ZIOCompanionVersionSpecific.scala +++ b/core/shared/src/main/scala-3/zio/ZIOCompanionVersionSpecific.scala @@ -51,22 +51,16 @@ private[zio] transparent trait ZIOCompanionVersionSpecific { blockingOn: => FiberId = FiberId.None )(implicit trace: Trace): ZIO[R, E, A] = ZIO.suspendSucceed { - val cancelerRef = new java.util.concurrent.atomic.AtomicReference[URIO[R, Any]](ZIO.unit) - - ZIO - .Async[R, E, A]( - trace, - { k => - val result = register(using Unsafe)(k(_)) - - result match { - case Left(canceler) => cancelerRef.set(canceler); null.asInstanceOf[ZIO[R, E, A]] - case Right(done) => done - } - }, - () => blockingOn - ) - .onInterrupt(cancelerRef.get()) + val state = new AtomicReference[URIO[R, Any]](Exit.unit) with ((ZIO[R, E, A] => Unit) => ZIO[R, E, A]) { + def apply(k: ZIO[R, E, A] => Unit): ZIO[R, E, A] = { + register(using Unsafe)(k(_)) match { + case Left(canceler) => set(canceler); null.asInstanceOf[ZIO[R, E, A]] + case Right(done) => done + } + } + } + + ZIO.Async[R, E, A](trace, state, () => blockingOn).onInterrupt(state.get()) } /** diff --git a/core/shared/src/main/scala/zio/Promise.scala b/core/shared/src/main/scala/zio/Promise.scala index 73b21ec656f1..84d66a9b90c6 100644 --- a/core/shared/src/main/scala/zio/Promise.scala +++ b/core/shared/src/main/scala/zio/Promise.scala @@ -18,6 +18,7 @@ package zio import zio.Promise.internal._ import zio.stacktracer.TracingImplicits.disableAutoTrace +import scala.collection.immutable.LongMap import java.util.concurrent.atomic.AtomicReference @@ -38,10 +39,7 @@ import java.util.concurrent.atomic.AtomicReference * } yield value * }}} */ -final class Promise[E, A] private ( - private val state: AtomicReference[Promise.internal.State[E, A]], - blockingOn: FiberId -) extends Serializable { +final class Promise[E, A] private (blockingOn: FiberId) extends Serializable { /** * Retrieves the value of the promise, suspending the fiber running the action @@ -50,32 +48,19 @@ final class Promise[E, A] private ( def await(implicit trace: Trace): IO[E, A] = ZIO.suspendSucceed { state.get match { - case Done(value) => - value - case _ => - ZIO.asyncInterrupt[Any, E, A]( + case Done(value) => value + case pending => + ZIO.async[Any, E, A]( // intentionally never remove the callback, interrupted fibers won't be resumed k => { - var result = null.asInstanceOf[Either[UIO[Any], IO[E, A]]] - var retry = true - - while (retry) { - val oldState = state.get - - val newState = oldState match { - case Pending(joiners) => - result = Left(interruptJoiner(k)) - - Pending(k :: joiners) - case s @ Done(value) => - result = Right(value) - - s + @annotation.tailrec + def loop(current: State[E, A]): Unit = + current match { + case pending: Pending[?, ?] => + if (state.compareAndSet(pending, pending.add(k))) () + else loop(state.get) + case Done(value) => k(value) } - - retry = !state.compareAndSet(oldState, newState) - } - - result + loop(pending) }, blockingOn ) @@ -176,24 +161,6 @@ final class Promise[E, A] private ( def succeed(a: A)(implicit trace: Trace): UIO[Boolean] = ZIO.succeed(unsafe.succeed(a)(trace, Unsafe.unsafe)) - private def interruptJoiner(joiner: IO[E, A] => Any)(implicit trace: Trace): UIO[Any] = ZIO.succeed { - var retry = true - - while (retry) { - val oldState = state.get - - val newState = oldState match { - case Pending(joiners) => - Pending(joiners.filter(j => !j.eq(joiner))) - - case _ => - oldState - } - - retry = !state.compareAndSet(oldState, newState) - } - } - private[zio] trait UnsafeAPI extends Serializable { def completeWith(io: IO[E, A])(implicit unsafe: Unsafe): Boolean def die(e: Throwable)(implicit trace: Trace, unsafe: Unsafe): Boolean @@ -207,104 +174,96 @@ final class Promise[E, A] private ( def succeed(a: A)(implicit trace: Trace, unsafe: Unsafe): Boolean } - private[zio] val unsafe: UnsafeAPI = - new UnsafeAPI { - def completeWith(io: IO[E, A])(implicit unsafe: Unsafe): Boolean = { - var action: () => Boolean = null.asInstanceOf[() => Boolean] - var retry = true - - while (retry) { - val oldState = state.get - - val newState = oldState match { - case Pending(joiners) => - action = () => { joiners.foreach(_(io)); true } - - Done(io) - - case _ => - action = Promise.ConstFalse - - oldState - } - - retry = !state.compareAndSet(oldState, newState) + @deprecated("Kept for binary compatibility only. Do not use", "2.1.15") + private[zio] def state: AtomicReference[Promise.internal.State[E, A]] = + unsafe.asInstanceOf[AtomicReference[Promise.internal.State[E, A]]] + private[zio] val unsafe: UnsafeAPI = new AtomicReference(Promise.internal.State.empty[E, A]) with UnsafeAPI { state => + def completeWith(io: IO[E, A])(implicit unsafe: Unsafe): Boolean = { + @annotation.tailrec + def loop(): Boolean = + state.get match { + case pending: Pending[?, ?] => + if (state.compareAndSet(pending, Done(io))) { + pending.complete(io) + true + } else { + loop() + } + case _: Done[?, ?] => false } + loop() + } - action() - } + def die(e: Throwable)(implicit trace: Trace, unsafe: Unsafe): Boolean = + completeWith(ZIO.die(e)) - def die(e: Throwable)(implicit trace: Trace, unsafe: Unsafe): Boolean = - completeWith(ZIO.die(e)) + def done(io: IO[E, A])(implicit unsafe: Unsafe): Unit = completeWith(io) - def done(io: IO[E, A])(implicit unsafe: Unsafe): Unit = { - var retry: Boolean = true - var joiners: List[IO[E, A] => Any] = null + def fail(e: E)(implicit trace: Trace, unsafe: Unsafe): Boolean = + completeWith(ZIO.fail(e)) - while (retry) { - val oldState = state.get + def failCause(e: Cause[E])(implicit trace: Trace, unsafe: Unsafe): Boolean = + completeWith(ZIO.failCause(e)) - val newState = oldState match { - case Pending(js) => - joiners = js - Done(io) - case _ => oldState - } + def interruptAs(fiberId: FiberId)(implicit trace: Trace, unsafe: Unsafe): Boolean = + completeWith(ZIO.interruptAs(fiberId)) - retry = !state.compareAndSet(oldState, newState) - } + def isDone(implicit unsafe: Unsafe): Boolean = + state.get().isInstanceOf[Done[?, ?]] - if (joiners ne null) joiners.foreach(_(io)) + def poll(implicit unsafe: Unsafe): Option[IO[E, A]] = + state.get() match { + case _: Pending[?, ?] => None + case Done(value) => Some(value) } - def fail(e: E)(implicit trace: Trace, unsafe: Unsafe): Boolean = - completeWith(ZIO.fail(e)) - - def failCause(e: Cause[E])(implicit trace: Trace, unsafe: Unsafe): Boolean = - completeWith(ZIO.failCause(e)) - - def interruptAs(fiberId: FiberId)(implicit trace: Trace, unsafe: Unsafe): Boolean = - completeWith(ZIO.interruptAs(fiberId)) - - def isDone(implicit unsafe: Unsafe): Boolean = - state.get().isInstanceOf[Done[?, ?]] - - def poll(implicit unsafe: Unsafe): Option[IO[E, A]] = - state.get() match { - case _: Pending[?, ?] => None - case Done(io) => Some(io) - } - - def refailCause(e: Cause[E])(implicit trace: Trace, unsafe: Unsafe): Boolean = - completeWith(Exit.failCause(e)) + def refailCause(e: Cause[E])(implicit trace: Trace, unsafe: Unsafe): Boolean = + completeWith(Exit.failCause(e)) - def succeed(a: A)(implicit trace: Trace, unsafe: Unsafe): Boolean = - completeWith(Exit.succeed(a)) - } + def succeed(a: A)(implicit trace: Trace, unsafe: Unsafe): Boolean = + completeWith(Exit.succeed(a)) + } } object Promise { private val ConstFalse: () => Boolean = () => false private[zio] object internal { - sealed abstract class State[E, A] extends Serializable with Product - final case class Pending[E, A](joiners: List[IO[E, A] => Any]) extends State[E, A] - final case class Done[E, A](value: IO[E, A]) extends State[E, A] + sealed abstract class State[E, A] + final case class Done[E, A](val value: IO[E, A]) extends State[E, A] + sealed abstract class Pending[E, A] extends State[E, A] { self => + @annotation.tailrec + final def complete(io: IO[E, A]): Unit = + self match { + case Chain(j, js) => + j(io) + js.complete(io) + case _: Empty.type => () + } + def add(joiner: IO[E, A] => Any): Pending[E, A] = new Chain(joiner, self) + } + + final case class Chain[E, A](j: IO[E, A] => Any, js: Pending[E, A]) extends Pending[E, A] + case object Empty extends Pending[Nothing, Nothing] + + object State { + def empty[E, A]: State[E, A] = Empty.asInstanceOf[State[E, A]] + } } /** * Makes a new promise to be completed by the fiber creating the promise. */ - def make[E, A](implicit trace: Trace): UIO[Promise[E, A]] = ZIO.fiberIdWith(makeAs(_)) + def make[E, A](implicit trace: Trace): UIO[Promise[E, A]] = + ZIO.fiberIdWith(id => Exit.succeed(unsafe.make(id)(Unsafe))) /** * Makes a new promise to be completed by the fiber with the specified id. */ def makeAs[E, A](fiberId: => FiberId)(implicit trace: Trace): UIO[Promise[E, A]] = - ZIO.succeed(unsafe.make(fiberId)(Unsafe.unsafe)) + ZIO.succeed(unsafe.make(fiberId)(Unsafe)) object unsafe { - def make[E, A](fiberId: FiberId)(implicit unsafe: Unsafe): Promise[E, A] = - new Promise[E, A](new AtomicReference[State[E, A]](new internal.Pending[E, A](Nil)), fiberId) + def make[E, A](fiberId: FiberId)(implicit unsafe: Unsafe): Promise[E, A] = new Promise[E, A](fiberId) } } From a95ae72c151e6a1844721b2e90d52f81fb22e05c Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Sun, 9 Feb 2025 20:44:44 -0800 Subject: [PATCH 02/34] missing import --- .../src/main/scala-3/zio/ZIOCompanionVersionSpecific.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/core/shared/src/main/scala-3/zio/ZIOCompanionVersionSpecific.scala b/core/shared/src/main/scala-3/zio/ZIOCompanionVersionSpecific.scala index 4783107ec0b6..26572af6ca7f 100644 --- a/core/shared/src/main/scala-3/zio/ZIOCompanionVersionSpecific.scala +++ b/core/shared/src/main/scala-3/zio/ZIOCompanionVersionSpecific.scala @@ -4,6 +4,7 @@ import zio.ZIO.Async import zio.stacktracer.TracingImplicits.disableAutoTrace import java.io.IOException +import java.util.concurrent.atomic.AtomicReference import scala.annotation.targetName private[zio] transparent trait ZIOCompanionVersionSpecific { From a2b54e98b45db74d76fd9987f89d80cc19793e71 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Tue, 4 Mar 2025 18:33:05 -0800 Subject: [PATCH 03/34] fix merge/address feedback --- .../main/scala/zio/PromiseBenchmarks.scala | 4 +-- core/shared/src/main/scala/zio/Promise.scala | 30 ++++--------------- 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/benchmarks/src/main/scala/zio/PromiseBenchmarks.scala b/benchmarks/src/main/scala/zio/PromiseBenchmarks.scala index cfb2547b4813..1caa514ef435 100644 --- a/benchmarks/src/main/scala/zio/PromiseBenchmarks.scala +++ b/benchmarks/src/main/scala/zio/PromiseBenchmarks.scala @@ -29,7 +29,7 @@ class PromiseBenchmarks { Promise .make[Nothing, Unit] .flatMap { promise => - promise.succeed(()) *> promise.await + promise.done(Exit.unit) *> promise.await } .repeatN(n) @@ -58,7 +58,7 @@ class PromiseBenchmarks { for { fibers <- createWaiters(promise) _ <- promise.done(Exit.unit) - _ <- ZIO.foreachDiscard(fibers)(_.join) + _ <- ZIO.foreachDiscard(fibers)(_.await) } yield () } .repeatN(1023) diff --git a/core/shared/src/main/scala/zio/Promise.scala b/core/shared/src/main/scala/zio/Promise.scala index bacbbf60d946..cd38ba23d730 100644 --- a/core/shared/src/main/scala/zio/Promise.scala +++ b/core/shared/src/main/scala/zio/Promise.scala @@ -170,24 +170,6 @@ final class Promise[E, A] private (blockingOn: FiberId) extends Serializable { private[zio] def succeedUnit(implicit ev0: A =:= Unit, trace: Trace): UIO[Boolean] = ZIO.succeed(unsafe.succeedUnit(ev0, trace, Unsafe)) - private def interruptJoiner(joiner: IO[E, A] => Any)(implicit trace: Trace): UIO[Any] = ZIO.succeed { - var retry = true - - while (retry) { - val oldState = state.get - - val newState = oldState match { - case Pending(joiners) => - Pending(joiners.filter(j => !j.eq(joiner))) - - case _ => - oldState - } - - retry = !state.compareAndSet(oldState, newState) - } - } - private[zio] trait UnsafeAPI extends Serializable { def completeWith(io: IO[E, A])(implicit unsafe: Unsafe): Boolean def die(e: Throwable)(implicit trace: Trace, unsafe: Unsafe): Boolean @@ -217,7 +199,7 @@ final class Promise[E, A] private (blockingOn: FiberId) extends Serializable { } else { loop() } - case _: Done[?, ?] => false + case _ => false } loop() } @@ -241,8 +223,8 @@ final class Promise[E, A] private (blockingOn: FiberId) extends Serializable { def poll(implicit unsafe: Unsafe): Option[IO[E, A]] = state.get() match { - case _: Pending[?, ?] => None - case Done(value) => Some(value) + case Done(value) => Some(value) + case _ => None } def refailCause(e: Cause[E])(implicit trace: Trace, unsafe: Unsafe): Boolean = @@ -251,8 +233,8 @@ final class Promise[E, A] private (blockingOn: FiberId) extends Serializable { def succeed(a: A)(implicit trace: Trace, unsafe: Unsafe): Boolean = completeWith(Exit.succeed(a)) - override def succeedUnit(implicit ev0: A =:= Unit, trace: Trace, unsafe: Unsafe): Boolean = - completeWith(Exit.unit.asInstanceOf[IO[E, A]]) + override def succeedUnit(implicit ev0: A =:= Unit, trace: Trace, unsafe: Unsafe): Boolean = + completeWith(Exit.unit.asInstanceOf[IO[E, A]]) } } @@ -269,7 +251,7 @@ object Promise { case Chain(j, js) => j(io) js.complete(io) - case _: Empty.type => () + case _ => () } def add(joiner: IO[E, A] => Any): Pending[E, A] = new Chain(joiner, self) } From 6859183b271cd4f3e410008e4f60a13c8df23de4 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Tue, 4 Mar 2025 19:54:49 -0800 Subject: [PATCH 04/34] Invoke waiters in order --- .../src/test/scala/zio/PromiseSpec.scala | 10 ++++++- core/shared/src/main/scala/zio/Promise.scala | 26 +++++-------------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/core-tests/shared/src/test/scala/zio/PromiseSpec.scala b/core-tests/shared/src/test/scala/zio/PromiseSpec.scala index 04cf82c1f682..8b55ea9c55f3 100644 --- a/core-tests/shared/src/test/scala/zio/PromiseSpec.scala +++ b/core-tests/shared/src/test/scala/zio/PromiseSpec.scala @@ -120,6 +120,14 @@ object PromiseSpec extends ZIOBaseSpec { _ <- p.fail("failure") d <- p.isDone } yield assert(d)(isTrue) - } @@ zioTag(errors) + } @@ zioTag(errors), + test("waiter stack safety") { + for { + p <- Promise.make[Nothing, Unit] + fibers <- ZIO.foreach(1 to 100_000)(_ => p.await.forkDaemon) + _ <- p.complete(Exit.unit) + _ <- ZIO.foreach(fibers)(_.await) + } yield assertCompletes + }, ) } diff --git a/core/shared/src/main/scala/zio/Promise.scala b/core/shared/src/main/scala/zio/Promise.scala index cd38ba23d730..006056ea2d7d 100644 --- a/core/shared/src/main/scala/zio/Promise.scala +++ b/core/shared/src/main/scala/zio/Promise.scala @@ -16,7 +16,6 @@ package zio -import zio.Promise.internal._ import zio.stacktracer.TracingImplicits.disableAutoTrace import scala.collection.immutable.LongMap @@ -40,6 +39,7 @@ import java.util.concurrent.atomic.AtomicReference * }}} */ final class Promise[E, A] private (blockingOn: FiberId) extends Serializable { + import Promise.internal._ /** * Retrieves the value of the promise, suspending the fiber running the action @@ -239,27 +239,15 @@ final class Promise[E, A] private (blockingOn: FiberId) extends Serializable { } object Promise { - private val ConstFalse: () => Boolean = () => false - - private[zio] object internal { - sealed abstract class State[E, A] + private[Promise] object internal { + sealed abstract class State[E, A] extends Serializable final case class Done[E, A](val value: IO[E, A]) extends State[E, A] - sealed abstract class Pending[E, A] extends State[E, A] { self => - @annotation.tailrec - final def complete(io: IO[E, A]): Unit = - self match { - case Chain(j, js) => - j(io) - js.complete(io) - case _ => () - } - def add(joiner: IO[E, A] => Any): Pending[E, A] = new Chain(joiner, self) + final class Pending[E, A](waiters: LongMap[IO[E, A] => Any], next: Long) extends State[E, A] { self => + def complete(io: IO[E, A]): Unit = waiters.valuesIterator.foreach(_(io)) + def add(joiner: IO[E, A] => Any): Pending[E, A] = new Pending[E, A](waiters.updated(next, joiner), next + 1) } - - final case class Chain[E, A](j: IO[E, A] => Any, js: Pending[E, A]) extends Pending[E, A] - case object Empty extends Pending[Nothing, Nothing] - object State { + private val Empty = new Pending[Nothing, Nothing](LongMap.empty, 1) def empty[E, A]: State[E, A] = Empty.asInstanceOf[State[E, A]] } } From 4dc3766599c4ebfe0d5dda6b529149440a1e3fa0 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Tue, 4 Mar 2025 19:56:48 -0800 Subject: [PATCH 05/34] format --- core-tests/shared/src/test/scala/zio/PromiseSpec.scala | 2 +- core/shared/src/main/scala/zio/Promise.scala | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core-tests/shared/src/test/scala/zio/PromiseSpec.scala b/core-tests/shared/src/test/scala/zio/PromiseSpec.scala index 8b55ea9c55f3..7e25d7546cf7 100644 --- a/core-tests/shared/src/test/scala/zio/PromiseSpec.scala +++ b/core-tests/shared/src/test/scala/zio/PromiseSpec.scala @@ -128,6 +128,6 @@ object PromiseSpec extends ZIOBaseSpec { _ <- p.complete(Exit.unit) _ <- ZIO.foreach(fibers)(_.await) } yield assertCompletes - }, + } ) } diff --git a/core/shared/src/main/scala/zio/Promise.scala b/core/shared/src/main/scala/zio/Promise.scala index 006056ea2d7d..a232a938c6a1 100644 --- a/core/shared/src/main/scala/zio/Promise.scala +++ b/core/shared/src/main/scala/zio/Promise.scala @@ -240,14 +240,14 @@ final class Promise[E, A] private (blockingOn: FiberId) extends Serializable { } object Promise { private[Promise] object internal { - sealed abstract class State[E, A] extends Serializable + sealed abstract class State[E, A] extends Serializable final case class Done[E, A](val value: IO[E, A]) extends State[E, A] final class Pending[E, A](waiters: LongMap[IO[E, A] => Any], next: Long) extends State[E, A] { self => - def complete(io: IO[E, A]): Unit = waiters.valuesIterator.foreach(_(io)) + def complete(io: IO[E, A]): Unit = waiters.valuesIterator.foreach(_(io)) def add(joiner: IO[E, A] => Any): Pending[E, A] = new Pending[E, A](waiters.updated(next, joiner), next + 1) } object State { - private val Empty = new Pending[Nothing, Nothing](LongMap.empty, 1) + private val Empty = new Pending[Nothing, Nothing](LongMap.empty, 1) def empty[E, A]: State[E, A] = Empty.asInstanceOf[State[E, A]] } } From 0842795e2b884cea33cb6cb572bf7be271905988 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Tue, 4 Mar 2025 20:00:54 -0800 Subject: [PATCH 06/34] fix version --- core/shared/src/main/scala/zio/Promise.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shared/src/main/scala/zio/Promise.scala b/core/shared/src/main/scala/zio/Promise.scala index a232a938c6a1..1d10bfd59765 100644 --- a/core/shared/src/main/scala/zio/Promise.scala +++ b/core/shared/src/main/scala/zio/Promise.scala @@ -184,7 +184,7 @@ final class Promise[E, A] private (blockingOn: FiberId) extends Serializable { def succeedUnit(implicit ev0: A =:= Unit, trace: Trace, unsafe: Unsafe): Boolean } - @deprecated("Kept for binary compatibility only. Do not use", "2.1.15") + @deprecated("Kept for binary compatibility only. Do not use", "2.1.16") private[zio] def state: AtomicReference[Promise.internal.State[E, A]] = unsafe.asInstanceOf[AtomicReference[Promise.internal.State[E, A]]] private[zio] val unsafe: UnsafeAPI = new AtomicReference(Promise.internal.State.empty[E, A]) with UnsafeAPI { state => From f3f7a945cb78de59da4152e41e5c221a0e558a99 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Tue, 4 Mar 2025 20:10:12 -0800 Subject: [PATCH 07/34] fix PromiseSpec on 212 --- core-tests/shared/src/test/scala/zio/PromiseSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core-tests/shared/src/test/scala/zio/PromiseSpec.scala b/core-tests/shared/src/test/scala/zio/PromiseSpec.scala index 7e25d7546cf7..ed3e1a8949d9 100644 --- a/core-tests/shared/src/test/scala/zio/PromiseSpec.scala +++ b/core-tests/shared/src/test/scala/zio/PromiseSpec.scala @@ -124,7 +124,7 @@ object PromiseSpec extends ZIOBaseSpec { test("waiter stack safety") { for { p <- Promise.make[Nothing, Unit] - fibers <- ZIO.foreach(1 to 100_000)(_ => p.await.forkDaemon) + fibers <- ZIO.foreach(1 to 100000)(_ => p.await.forkDaemon) _ <- p.complete(Exit.unit) _ <- ZIO.foreach(fibers)(_.await) } yield assertCompletes From 78f163152a32a8ad0668b5de5eb2eae849d83730 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Tue, 4 Mar 2025 20:36:55 -0800 Subject: [PATCH 08/34] fix PromiseBenchmarks on 212 --- benchmarks/src/main/scala/zio/PromiseBenchmarks.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/src/main/scala/zio/PromiseBenchmarks.scala b/benchmarks/src/main/scala/zio/PromiseBenchmarks.scala index 1caa514ef435..43c92a50ee3a 100644 --- a/benchmarks/src/main/scala/zio/PromiseBenchmarks.scala +++ b/benchmarks/src/main/scala/zio/PromiseBenchmarks.scala @@ -50,7 +50,7 @@ class PromiseBenchmarks { @Benchmark def zioPromiseMultiAwaitDone(): Unit = { def createWaiters(promise: Promise[Nothing, Unit]): ZIO[Any, Nothing, Seq[Fiber[Nothing, Unit]]] = - ZIO.foreach(Range(0, waiters))(_ => promise.await.forkDaemon) + ZIO.foreach(Vector.range(0, waiters))(_ => promise.await.forkDaemon) val io = Promise .make[Nothing, Unit] From e56d50923897104562153d72a0861043dada40ec Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Wed, 5 Mar 2025 20:07:44 -0800 Subject: [PATCH 09/34] fix jvmopts --- .jvmopts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.jvmopts b/.jvmopts index d8db65d6a4a8..c49340da9834 100644 --- a/.jvmopts +++ b/.jvmopts @@ -1,3 +1,3 @@ -Dcats.effect.stackTracingMode=full -Xmx8g --Xms8g \ No newline at end of file +-Xms2g \ No newline at end of file From f62ab661de86959cc55127c0580b3155204a125d Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Wed, 5 Mar 2025 21:11:07 -0800 Subject: [PATCH 10/34] optimize Single/Zero waiter --- core/shared/src/main/scala/zio/Promise.scala | 9 ++++++++- project/MimaSettings.scala | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/core/shared/src/main/scala/zio/Promise.scala b/core/shared/src/main/scala/zio/Promise.scala index 1d10bfd59765..d7e3ded8c44b 100644 --- a/core/shared/src/main/scala/zio/Promise.scala +++ b/core/shared/src/main/scala/zio/Promise.scala @@ -21,6 +21,8 @@ import scala.collection.immutable.LongMap import java.util.concurrent.atomic.AtomicReference +import scala.annotation.switch + /** * A promise represents an asynchronous variable, of [[zio.ZIO]] type, that can * be set exactly once, with the ability for an arbitrary number of fibers to @@ -243,7 +245,12 @@ object Promise { sealed abstract class State[E, A] extends Serializable final case class Done[E, A](val value: IO[E, A]) extends State[E, A] final class Pending[E, A](waiters: LongMap[IO[E, A] => Any], next: Long) extends State[E, A] { self => - def complete(io: IO[E, A]): Unit = waiters.valuesIterator.foreach(_(io)) + def complete(io: IO[E, A]): Unit = + (next: @switch) match { + case 1 => () + case 2 => waiters(2L)(io) + case _ => waiters.valuesIterator.foreach(_(io)) + } def add(joiner: IO[E, A] => Any): Pending[E, A] = new Pending[E, A](waiters.updated(next, joiner), next + 1) } object State { diff --git a/project/MimaSettings.scala b/project/MimaSettings.scala index 4900181b8287..07d3d7e781b0 100644 --- a/project/MimaSettings.scala +++ b/project/MimaSettings.scala @@ -32,7 +32,8 @@ object MimaSettings { exclude[NewMixinForwarderProblem]("zio.Exit.mapBoth"), exclude[NewMixinForwarderProblem]("zio.Exit.mapError"), exclude[NewMixinForwarderProblem]("zio.Exit.mapErrorCause"), - exclude[NewMixinForwarderProblem]("zio.Exit.unit") + exclude[NewMixinForwarderProblem]("zio.Exit.unit"), + exclude[Problem]("zio.Promise#internal*"), ), mimaFailOnProblem := failOnProblem ) From 201d77437c2e78b9c2523e17383a9e6cac268862 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Wed, 5 Mar 2025 21:25:29 -0800 Subject: [PATCH 11/34] mima --- project/MimaSettings.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/project/MimaSettings.scala b/project/MimaSettings.scala index 07d3d7e781b0..8e4a43c33018 100644 --- a/project/MimaSettings.scala +++ b/project/MimaSettings.scala @@ -34,6 +34,7 @@ object MimaSettings { exclude[NewMixinForwarderProblem]("zio.Exit.mapErrorCause"), exclude[NewMixinForwarderProblem]("zio.Exit.unit"), exclude[Problem]("zio.Promise#internal*"), + exclude[Problem]("zio.Promise$internal*"), ), mimaFailOnProblem := failOnProblem ) From 1f98962396bdb2ffca31511d3a06820a99b76a83 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Wed, 5 Mar 2025 21:45:06 -0800 Subject: [PATCH 12/34] format --- project/MimaSettings.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/MimaSettings.scala b/project/MimaSettings.scala index 8e4a43c33018..09381a66efb9 100644 --- a/project/MimaSettings.scala +++ b/project/MimaSettings.scala @@ -34,7 +34,7 @@ object MimaSettings { exclude[NewMixinForwarderProblem]("zio.Exit.mapErrorCause"), exclude[NewMixinForwarderProblem]("zio.Exit.unit"), exclude[Problem]("zio.Promise#internal*"), - exclude[Problem]("zio.Promise$internal*"), + exclude[Problem]("zio.Promise$internal*") ), mimaFailOnProblem := failOnProblem ) From 511d5734ec78dae9fc44458a1683b1c7e9791787 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Thu, 6 Mar 2025 13:47:14 -0800 Subject: [PATCH 13/34] attempt to improve test --- .../src/test/scala/zio/FiberRuntimeSpec.scala | 42 ++++++++++++------- core/shared/src/main/scala/zio/Promise.scala | 2 +- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/core-tests/shared/src/test/scala/zio/FiberRuntimeSpec.scala b/core-tests/shared/src/test/scala/zio/FiberRuntimeSpec.scala index ba6823d451d8..65763f4cfc98 100644 --- a/core-tests/shared/src/test/scala/zio/FiberRuntimeSpec.scala +++ b/core-tests/shared/src/test/scala/zio/FiberRuntimeSpec.scala @@ -66,23 +66,35 @@ object FiberRuntimeSpec extends ZIOBaseSpec { suite("async")( test("async callback after interruption is ignored") { ZIO.suspendSucceed { - val cb = Ref.unsafe.make[Option[ZIO[Any, Nothing, Unit] => Unit]](None) - val effect = - (ZIO.async[Any, Nothing, Unit] { (k: (ZIO[Any, Nothing, Unit] => Unit)) => - cb.unsafe.set(Some(k)) - } *> ZIO.never) - + val executed = Ref.unsafe.make(0) + val cb = Ref.unsafe.make[Option[ZIO[Any, Nothing, Unit] => Unit]](None) + val latch = Promise.unsafe.make[Nothing, Unit](FiberId.None) + val async = ZIO.async[Any, Nothing, Unit] { k => + cb.unsafe.set(Some(k)) + latch.unsafe.done(Exit.unit) + } + val increment = executed.update(_ + 1) for { - fiber <- effect.fork - _ <- fiber.interrupt - callback <- cb.get.some - _ <- ZIO.succeed(callback(ZIO.unit)) - first <- fiber.poll - _ <- ZIO.succeed(callback(ZIO.unit)) - second <- fiber.poll - } yield assertTrue(first == second) && assertTrue(first == None) + fiber <- async.fork + _ <- latch.await + exit <- fiber.interrupt + callback <- cb.get.some + state1 <- fiber.poll + _ <- ZIO.succeed(callback(increment)) + state2 <- fiber.poll + executedBefore <- executed.get + _ <- ZIO.succeed(callback(increment)) + state3 <- fiber.poll + executedAfter <- executed.get + } yield assertTrue( + state1 == state2, + state1 == state3, + executedBefore == 0, + executedAfter == 0, + state1.exists(_.isInterrupted) + ) } - } + } @@ TestAspect.nonFlaky(10) ) ) diff --git a/core/shared/src/main/scala/zio/Promise.scala b/core/shared/src/main/scala/zio/Promise.scala index d7e3ded8c44b..0b789ece9bee 100644 --- a/core/shared/src/main/scala/zio/Promise.scala +++ b/core/shared/src/main/scala/zio/Promise.scala @@ -245,7 +245,7 @@ object Promise { sealed abstract class State[E, A] extends Serializable final case class Done[E, A](val value: IO[E, A]) extends State[E, A] final class Pending[E, A](waiters: LongMap[IO[E, A] => Any], next: Long) extends State[E, A] { self => - def complete(io: IO[E, A]): Unit = + def complete(io: IO[E, A]): Unit = (next: @switch) match { case 1 => () case 2 => waiters(2L)(io) From ee30a743b9c3247955ec01feeb2a510b71029fc5 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Thu, 6 Mar 2025 20:11:34 -0800 Subject: [PATCH 14/34] update test --- .../shared/src/test/scala/zio/FiberRuntimeSpec.scala | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core-tests/shared/src/test/scala/zio/FiberRuntimeSpec.scala b/core-tests/shared/src/test/scala/zio/FiberRuntimeSpec.scala index 65763f4cfc98..b80fd89649c3 100644 --- a/core-tests/shared/src/test/scala/zio/FiberRuntimeSpec.scala +++ b/core-tests/shared/src/test/scala/zio/FiberRuntimeSpec.scala @@ -87,11 +87,12 @@ object FiberRuntimeSpec extends ZIOBaseSpec { state3 <- fiber.poll executedAfter <- executed.get } yield assertTrue( - state1 == state2, - state1 == state3, + state1 == Some(exit), + state2 == Some(exit), + state3 == Some(exit), executedBefore == 0, executedAfter == 0, - state1.exists(_.isInterrupted) + exit.isInterrupted ) } } @@ TestAspect.nonFlaky(10) From 349202dca34fd51445b7b90652424a59c3413943 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Sun, 9 Mar 2025 13:29:23 -0700 Subject: [PATCH 15/34] fix: complete --- core/shared/src/main/scala/zio/Promise.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shared/src/main/scala/zio/Promise.scala b/core/shared/src/main/scala/zio/Promise.scala index 0b789ece9bee..658e99750d40 100644 --- a/core/shared/src/main/scala/zio/Promise.scala +++ b/core/shared/src/main/scala/zio/Promise.scala @@ -248,7 +248,7 @@ object Promise { def complete(io: IO[E, A]): Unit = (next: @switch) match { case 1 => () - case 2 => waiters(2L)(io) + case 2 => waiters(1L)(io) case _ => waiters.valuesIterator.foreach(_(io)) } def add(joiner: IO[E, A] => Any): Pending[E, A] = new Pending[E, A](waiters.updated(next, joiner), next + 1) From 071bfbbbbc53e24c8f62c6d5e016626644968dd4 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Sun, 9 Mar 2025 17:27:27 -0700 Subject: [PATCH 16/34] PromiseBench: 16 -> 8 waiters --- benchmarks/src/main/scala/zio/PromiseBenchmarks.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/src/main/scala/zio/PromiseBenchmarks.scala b/benchmarks/src/main/scala/zio/PromiseBenchmarks.scala index 43c92a50ee3a..7f7ea224431f 100644 --- a/benchmarks/src/main/scala/zio/PromiseBenchmarks.scala +++ b/benchmarks/src/main/scala/zio/PromiseBenchmarks.scala @@ -20,7 +20,7 @@ import java.util.concurrent.TimeUnit class PromiseBenchmarks { val n = 100000 - val waiters: Int = 16 + val waiters: Int = 8 @Benchmark def zioPromiseAwaitDone(): Unit = { From fad1bf8bedc578accc6bdcf2e189f7e3538115d1 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Sun, 9 Mar 2025 21:26:36 -0700 Subject: [PATCH 17/34] bring back linked list --- core/shared/src/main/scala/zio/Promise.scala | 41 ++++++++++++++------ 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/core/shared/src/main/scala/zio/Promise.scala b/core/shared/src/main/scala/zio/Promise.scala index 658e99750d40..f8e42bac14e9 100644 --- a/core/shared/src/main/scala/zio/Promise.scala +++ b/core/shared/src/main/scala/zio/Promise.scala @@ -242,19 +242,38 @@ final class Promise[E, A] private (blockingOn: FiberId) extends Serializable { } object Promise { private[Promise] object internal { - sealed abstract class State[E, A] extends Serializable - final case class Done[E, A](val value: IO[E, A]) extends State[E, A] - final class Pending[E, A](waiters: LongMap[IO[E, A] => Any], next: Long) extends State[E, A] { self => - def complete(io: IO[E, A]): Unit = - (next: @switch) match { - case 1 => () - case 2 => waiters(1L)(io) - case _ => waiters.valuesIterator.foreach(_(io)) - } - def add(joiner: IO[E, A] => Any): Pending[E, A] = new Pending[E, A](waiters.updated(next, joiner), next + 1) + sealed abstract class State[E, A] extends Serializable + final case class Done[E, A](value: IO[E, A]) extends State[E, A] + sealed abstract class Pending[E, A] extends State[E, A] { self => + def complete(io: IO[E, A]): Unit = { + val size = self.size + val arr = new Array[IO[E, A] => Any](size) + @annotation.tailrec + def fill(pending: Pending[E, A], i: Int): Unit = + pending match { + case link: Link[?, ?] => + arr(i) = link.waiter + fill(link.ws, i - 1) + case _ => () // Empty + } + fill(self, size - 1) + arr.foreach(_(io)) + } + def add(waiter: IO[E, A] => Any): Pending[E, A] + def size: Int + } + private case object Empty extends Pending[Nothing, Nothing] { self => + override def complete(io: IO[Nothing, Nothing]): Unit = () + override def add(waiter: IO[Nothing, Nothing] => Any): Pending[Nothing, Nothing] = new Link[Nothing, Nothing](waiter, self, 1) { + override def complete(io: IO[Nothing, Nothing]): Unit = waiter(io) + } + def size = 0 + } + private sealed class Link[E, A](val waiter: IO[E, A] => Any, val ws: Pending[E, A], val size: Int) extends Pending[E, A] { self => + def add(waiter: IO[E, A] => Any): Pending[E, A] = new Link(waiter, self, size + 1) } + object State { - private val Empty = new Pending[Nothing, Nothing](LongMap.empty, 1) def empty[E, A]: State[E, A] = Empty.asInstanceOf[State[E, A]] } } From aa485075e5563aa8864088125714818afeb0d5b6 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Sun, 9 Mar 2025 21:28:54 -0700 Subject: [PATCH 18/34] formatting --- core/shared/src/main/scala/zio/Promise.scala | 27 +++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/core/shared/src/main/scala/zio/Promise.scala b/core/shared/src/main/scala/zio/Promise.scala index f8e42bac14e9..08443d386ffd 100644 --- a/core/shared/src/main/scala/zio/Promise.scala +++ b/core/shared/src/main/scala/zio/Promise.scala @@ -245,9 +245,23 @@ object Promise { sealed abstract class State[E, A] extends Serializable final case class Done[E, A](value: IO[E, A]) extends State[E, A] sealed abstract class Pending[E, A] extends State[E, A] { self => + def complete(io: IO[E, A]): Unit + def add(waiter: IO[E, A] => Any): Pending[E, A] + def size: Int + } + private case object Empty extends Pending[Nothing, Nothing] { self => + override def complete(io: IO[Nothing, Nothing]): Unit = () + override def add(waiter: IO[Nothing, Nothing] => Any): Pending[Nothing, Nothing] = + new Link[Nothing, Nothing](waiter, self, 1) { + override def complete(io: IO[Nothing, Nothing]): Unit = waiter(io) + } + def size = 0 + } + private sealed class Link[E, A](val waiter: IO[E, A] => Any, val ws: Pending[E, A], val size: Int) + extends Pending[E, A] { self => def complete(io: IO[E, A]): Unit = { val size = self.size - val arr = new Array[IO[E, A] => Any](size) + val arr = new Array[IO[E, A] => Any](size) @annotation.tailrec def fill(pending: Pending[E, A], i: Int): Unit = pending match { @@ -259,17 +273,6 @@ object Promise { fill(self, size - 1) arr.foreach(_(io)) } - def add(waiter: IO[E, A] => Any): Pending[E, A] - def size: Int - } - private case object Empty extends Pending[Nothing, Nothing] { self => - override def complete(io: IO[Nothing, Nothing]): Unit = () - override def add(waiter: IO[Nothing, Nothing] => Any): Pending[Nothing, Nothing] = new Link[Nothing, Nothing](waiter, self, 1) { - override def complete(io: IO[Nothing, Nothing]): Unit = waiter(io) - } - def size = 0 - } - private sealed class Link[E, A](val waiter: IO[E, A] => Any, val ws: Pending[E, A], val size: Int) extends Pending[E, A] { self => def add(waiter: IO[E, A] => Any): Pending[E, A] = new Link(waiter, self, size + 1) } From 9265c1b1c81a2147f1a13bebced51062510f5d6b Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Sun, 9 Mar 2025 21:30:41 -0700 Subject: [PATCH 19/34] remove unused imports --- core/shared/src/main/scala/zio/Promise.scala | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/shared/src/main/scala/zio/Promise.scala b/core/shared/src/main/scala/zio/Promise.scala index 08443d386ffd..8d9f3a4b8350 100644 --- a/core/shared/src/main/scala/zio/Promise.scala +++ b/core/shared/src/main/scala/zio/Promise.scala @@ -17,12 +17,9 @@ package zio import zio.stacktracer.TracingImplicits.disableAutoTrace -import scala.collection.immutable.LongMap import java.util.concurrent.atomic.AtomicReference -import scala.annotation.switch - /** * A promise represents an asynchronous variable, of [[zio.ZIO]] type, that can * be set exactly once, with the ability for an arbitrary number of fibers to From a4fe187bf041cf273996291ee0e5f15fd4ad0c86 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Sun, 9 Mar 2025 21:46:42 -0700 Subject: [PATCH 20/34] Use Unsafe directly --- core/shared/src/main/scala/zio/Promise.scala | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/core/shared/src/main/scala/zio/Promise.scala b/core/shared/src/main/scala/zio/Promise.scala index 8d9f3a4b8350..f1fb80d28cda 100644 --- a/core/shared/src/main/scala/zio/Promise.scala +++ b/core/shared/src/main/scala/zio/Promise.scala @@ -71,14 +71,14 @@ final class Promise[E, A] private (blockingOn: FiberId) extends Serializable { * fibers waiting on the value of the promise. */ def die(e: Throwable)(implicit trace: Trace): UIO[Boolean] = - ZIO.succeed(unsafe.die(e)(trace, Unsafe.unsafe)) + ZIO.succeed(unsafe.die(e)(trace, Unsafe)) /** * Exits the promise with the specified exit, which will be propagated to all * fibers waiting on the value of the promise. */ def done(e: Exit[E, A])(implicit trace: Trace): UIO[Boolean] = - ZIO.succeed(unsafe.completeWith(e)(Unsafe.unsafe)) + ZIO.succeed(unsafe.completeWith(e)(Unsafe)) /** * Completes the promise with the result of the specified effect. If the @@ -102,21 +102,21 @@ final class Promise[E, A] private (blockingOn: FiberId) extends Serializable { * promise with the result of an effect see [[Promise.complete]]. */ def completeWith(io: IO[E, A])(implicit trace: Trace): UIO[Boolean] = - ZIO.succeed(unsafe.completeWith(io)(Unsafe.unsafe)) + ZIO.succeed(unsafe.completeWith(io)(Unsafe)) /** * Fails the promise with the specified error, which will be propagated to all * fibers waiting on the value of the promise. */ def fail(e: E)(implicit trace: Trace): UIO[Boolean] = - ZIO.succeed(unsafe.fail(e)(trace, Unsafe.unsafe)) + ZIO.succeed(unsafe.fail(e)(trace, Unsafe)) /** * Fails the promise with the specified cause, which will be propagated to all * fibers waiting on the value of the promise. */ def failCause(e: Cause[E])(implicit trace: Trace): UIO[Boolean] = - ZIO.succeed(unsafe.failCause(e)(trace, Unsafe.unsafe)) + ZIO.succeed(unsafe.failCause(e)(trace, Unsafe)) /** * Completes the promise with interruption. This will interrupt all fibers @@ -130,21 +130,21 @@ final class Promise[E, A] private (blockingOn: FiberId) extends Serializable { * waiting on the value of the promise as by the specified fiber. */ def interruptAs(fiberId: FiberId)(implicit trace: Trace): UIO[Boolean] = - ZIO.succeed(unsafe.interruptAs(fiberId)(trace, Unsafe.unsafe)) + ZIO.succeed(unsafe.interruptAs(fiberId)(trace, Unsafe)) /** * Checks for completion of this Promise. Produces true if this promise has * already been completed with a value or an error and false otherwise. */ def isDone(implicit trace: Trace): UIO[Boolean] = - ZIO.succeed(unsafe.isDone(Unsafe.unsafe)) + ZIO.succeed(unsafe.isDone(Unsafe)) /** * Checks for completion of this Promise. Returns the result effect if this * promise has already been completed or a `None` otherwise. */ def poll(implicit trace: Trace): UIO[Option[IO[E, A]]] = - ZIO.succeed(unsafe.poll(Unsafe.unsafe)) + ZIO.succeed(unsafe.poll(Unsafe)) /** * Fails the promise with the specified cause, which will be propagated to all @@ -152,13 +152,13 @@ final class Promise[E, A] private (blockingOn: FiberId) extends Serializable { * to the cause. */ def refailCause(e: Cause[E])(implicit trace: Trace): UIO[Boolean] = - ZIO.succeed(unsafe.refailCause(e)(trace, Unsafe.unsafe)) + ZIO.succeed(unsafe.refailCause(e)(trace, Unsafe)) /** * Completes the promise with the specified value. */ def succeed(a: A)(implicit trace: Trace): UIO[Boolean] = - ZIO.succeed(unsafe.succeed(a)(trace, Unsafe.unsafe)) + ZIO.succeed(unsafe.succeed(a)(trace, Unsafe)) /** * Internally, you can use this method instead of calling From e6cdde377a38dc858ccad2e8fe7465e5e7b7cf6c Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Sun, 9 Mar 2025 21:47:22 -0700 Subject: [PATCH 21/34] fix scala3 --- core/shared/src/main/scala/zio/Promise.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shared/src/main/scala/zio/Promise.scala b/core/shared/src/main/scala/zio/Promise.scala index f1fb80d28cda..5c6c56f6489a 100644 --- a/core/shared/src/main/scala/zio/Promise.scala +++ b/core/shared/src/main/scala/zio/Promise.scala @@ -250,7 +250,7 @@ object Promise { override def complete(io: IO[Nothing, Nothing]): Unit = () override def add(waiter: IO[Nothing, Nothing] => Any): Pending[Nothing, Nothing] = new Link[Nothing, Nothing](waiter, self, 1) { - override def complete(io: IO[Nothing, Nothing]): Unit = waiter(io) + override def complete(io: IO[Nothing, Nothing]): Unit = this.waiter(io) } def size = 0 } From 4689adf610daaec7f8c58ca3c94ebe14cdf16682 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Sun, 30 Mar 2025 16:25:44 -0700 Subject: [PATCH 22/34] fix formatting settings.json Co-authored-by: Jules Ivanic --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 052d27309198..b4bdcc7fa826 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,5 @@ ], "files.watcherExclude": { "**/target": true -} + } } \ No newline at end of file From 63bfeb96e37b9a52c71613faf6e479ef113146c9 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Thu, 13 Mar 2025 17:01:02 -0700 Subject: [PATCH 23/34] update benches --- .../main/scala/zio/PromiseBenchmarks.scala | 62 +++++++++++++++---- 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/benchmarks/src/main/scala/zio/PromiseBenchmarks.scala b/benchmarks/src/main/scala/zio/PromiseBenchmarks.scala index 7f7ea224431f..41a0ae690d58 100644 --- a/benchmarks/src/main/scala/zio/PromiseBenchmarks.scala +++ b/benchmarks/src/main/scala/zio/PromiseBenchmarks.scala @@ -14,16 +14,22 @@ import java.util.concurrent.TimeUnit @State(JScope.Thread) @BenchmarkMode(Array(Mode.Throughput)) @OutputTimeUnit(TimeUnit.SECONDS) -@Measurement(iterations = 5, timeUnit = TimeUnit.SECONDS, time = 3) -@Warmup(iterations = 5, timeUnit = TimeUnit.SECONDS, time = 3) +@Measurement(iterations = 5, timeUnit = TimeUnit.SECONDS, time = 10) +@Warmup(iterations = 5, timeUnit = TimeUnit.SECONDS, time = 10) @Fork(value = 3) class PromiseBenchmarks { val n = 100000 val waiters: Int = 8 + def createWaitersZIO(promise: Promise[Nothing, Unit]): ZIO[Any, Nothing, Seq[Fiber[Nothing, Unit]]] = + ZIO.foreach(Vector.range(0, waiters))(_ => promise.await.forkDaemon) + + def createWaitersCats(promise: Deferred[CIO, Unit]) = + List.range(0, waiters).traverse(_ => promise.get.start) + @Benchmark - def zioPromiseAwaitDone(): Unit = { + def zioPromiseDoneAwait(): Unit = { val io = Promise @@ -37,7 +43,7 @@ class PromiseBenchmarks { } @Benchmark - def catsPromiseAwaitDone(): Unit = { + def catsPromiseDoneAwait(): Unit = { val io = Deferred[CIO, Unit].flatMap { promise => @@ -49,14 +55,11 @@ class PromiseBenchmarks { @Benchmark def zioPromiseMultiAwaitDone(): Unit = { - def createWaiters(promise: Promise[Nothing, Unit]): ZIO[Any, Nothing, Seq[Fiber[Nothing, Unit]]] = - ZIO.foreach(Vector.range(0, waiters))(_ => promise.await.forkDaemon) - val io = Promise .make[Nothing, Unit] .flatMap { promise => for { - fibers <- createWaiters(promise) + fibers <- createWaitersZIO(promise) _ <- promise.done(Exit.unit) _ <- ZIO.foreachDiscard(fibers)(_.await) } yield () @@ -68,13 +71,10 @@ class PromiseBenchmarks { @Benchmark def catsPromiseMultiAwaitDone(): Unit = { - def createWaiters(promise: Deferred[CIO, Unit]): CIO[List[cats.effect.Fiber[CIO, Throwable, Unit]]] = - List.range(0, waiters).traverse(_ => promise.get.start) - val io = Deferred[CIO, Unit].flatMap { promise => for { - fibers <- createWaiters(promise) + fibers <- createWaitersCats(promise) _ <- promise.complete(()) _ <- fibers.traverse_(_.join) } yield () @@ -82,4 +82,42 @@ class PromiseBenchmarks { io.unsafeRunSync() } + + @Benchmark + def zioPromiseMultiAwaitMultiDone(): Unit = { + def createCompleters(promise: Promise[Nothing, Unit], latch: Promise[Nothing, Unit]) = + ZIO.foreach(Vector.range(0, waiters))(_ => (latch.await *> promise.done(Exit.unit)).forkDaemon) + + val io = { + for { + latch <- Promise.make[Nothing, Unit] + promise <- Promise.make[Nothing, Unit] + waiters <- createWaitersZIO(promise) + fibers <- createCompleters(promise, latch) + _ <- latch.done(Exit.unit) + result <- promise.await + } yield result + }.repeatN(1023) + + unsafeRun(io) + } + + @Benchmark + def catsPromiseMultiAwaitMultiDone(): Unit = { + def createCompleters(promise: Deferred[CIO, Unit], latch: Deferred[CIO, Unit]) = + List.range(0, waiters).traverse(_ => (latch.get *> promise.complete(())).start) + + val io = { + for { + latch <- Deferred[CIO, Unit] + promise <- Deferred[CIO, Unit] + waiters <- createWaitersCats(promise) + fibers <- createCompleters(promise, latch) + _ <- latch.complete(()) + result <- promise.get + } yield result + }.replicateA_(1023) + + io.unsafeRunSync() + } } From 4f05b1ee8791e499b2a93c1c55a72c193bb1b937 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Sun, 30 Mar 2025 19:01:17 -0700 Subject: [PATCH 24/34] Revert "fix formatting settings.json" This reverts commit 4689adf610daaec7f8c58ca3c94ebe14cdf16682. --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index b4bdcc7fa826..052d27309198 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,5 @@ ], "files.watcherExclude": { "**/target": true - } +} } \ No newline at end of file From e70a5a362327018c08dda56b9d5b8bf0043ca36b Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Sun, 30 Mar 2025 19:04:44 -0700 Subject: [PATCH 25/34] fix scala 3 formatting --- .../src/main/scala-3/zio/ZIOCompanionVersionSpecific.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/shared/src/main/scala-3/zio/ZIOCompanionVersionSpecific.scala b/core/shared/src/main/scala-3/zio/ZIOCompanionVersionSpecific.scala index 26572af6ca7f..070c8198dce9 100644 --- a/core/shared/src/main/scala-3/zio/ZIOCompanionVersionSpecific.scala +++ b/core/shared/src/main/scala-3/zio/ZIOCompanionVersionSpecific.scala @@ -53,12 +53,11 @@ private[zio] transparent trait ZIOCompanionVersionSpecific { )(implicit trace: Trace): ZIO[R, E, A] = ZIO.suspendSucceed { val state = new AtomicReference[URIO[R, Any]](Exit.unit) with ((ZIO[R, E, A] => Unit) => ZIO[R, E, A]) { - def apply(k: ZIO[R, E, A] => Unit): ZIO[R, E, A] = { + def apply(k: ZIO[R, E, A] => Unit): ZIO[R, E, A] = register(using Unsafe)(k(_)) match { case Left(canceler) => set(canceler); null.asInstanceOf[ZIO[R, E, A]] case Right(done) => done } - } } ZIO.Async[R, E, A](trace, state, () => blockingOn).onInterrupt(state.get()) From a34e5d81de9d4058b138631929579746a4cc13de Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Sat, 5 Apr 2025 12:36:19 -0700 Subject: [PATCH 26/34] updates --- .../src/test/scala/zio/PromiseSpec.scala | 65 ++++++++++++++++- core/shared/src/main/scala/zio/Promise.scala | 73 ++++++++++++++----- 2 files changed, 117 insertions(+), 21 deletions(-) diff --git a/core-tests/shared/src/test/scala/zio/PromiseSpec.scala b/core-tests/shared/src/test/scala/zio/PromiseSpec.scala index ed3e1a8949d9..6cd344b57cbb 100644 --- a/core-tests/shared/src/test/scala/zio/PromiseSpec.scala +++ b/core-tests/shared/src/test/scala/zio/PromiseSpec.scala @@ -7,6 +7,8 @@ object PromiseSpec extends ZIOBaseSpec { import ZIOTag._ + private def empty[E, A]: Promise.internal.Pending[E, A] = Promise.internal.State.empty[E, A].asInstanceOf[Promise.internal.Pending[E, A]] + def spec: Spec[Any, TestFailure[Any]] = suite("PromiseSpec")( test("complete a promise using succeed") { for { @@ -128,6 +130,67 @@ object PromiseSpec extends ZIOBaseSpec { _ <- p.complete(Exit.unit) _ <- ZIO.foreach(fibers)(_.await) } yield assertCompletes - } + }, + suite("State")( + suite("add")( + test("stack safety") { + (0 to 100000).foldLeft(empty[Nothing, Unit])((acc, _) => acc.add(_ => ())) + assertCompletes + } + ), + suite("complete")( + test("one") { + var increment = 0 + val state = empty[Nothing, Unit].add(_ => increment += 1) + state.complete(ZIO.unit) + assert(increment)(equalTo(1)) + }, + test("multiple") { + val n = 10 + var increment = 0 + val state = (0 until n).foldLeft(empty[Nothing, Unit])((acc, _) => acc.add(_ => increment += 1)) + state.complete(ZIO.unit) + assert(increment)(equalTo(n)) + } + ), + suite("remove")( + test("one") { + var increment = 0 + val cb = (_: IO[Nothing, Unit]) => increment += 1 + val state = empty[Nothing, Unit].add(cb) + val removed = state.remove(cb) + removed.complete(ZIO.unit) + assert(removed)(equalTo(empty[Nothing, Unit])) && + assert(increment)(equalTo(0)) + }, + test("multiple") { + val n = 10 + var fired = 0 + val cb = (_: IO[Nothing, Unit]) => () + val toRemove = (_: IO[Nothing, Unit]) => fired += 1 + val state = + (0 until n).foldLeft(empty[Nothing, Unit])((acc, i) => if (i < 5) acc.add(cb) else acc.add(toRemove)) + val removed = state.remove(toRemove) + removed.complete(ZIO.unit) + assert(removed.size)(equalTo(5)) && + assert(fired)(equalTo(0)) + } + ), + suite("complete")( + test("one") { + var completed = 0 + val state = empty[Nothing, Unit].add(_ => completed += 1) + state.complete(ZIO.unit) + assert(completed)(equalTo(1)) + }, + test("multiple") { + val n = 10 + var completed = Seq.empty[Int] + val state = (0 until n).foldLeft(empty[Nothing, Unit])((acc, i) => acc.add(_ => completed = completed :+ i)) + state.complete(ZIO.unit) + assert(completed)(equalTo(0 until n)) + } + ) + ) ) } diff --git a/core/shared/src/main/scala/zio/Promise.scala b/core/shared/src/main/scala/zio/Promise.scala index 5c6c56f6489a..2521939dace3 100644 --- a/core/shared/src/main/scala/zio/Promise.scala +++ b/core/shared/src/main/scala/zio/Promise.scala @@ -49,7 +49,7 @@ final class Promise[E, A] private (blockingOn: FiberId) extends Serializable { state.get match { case Done(value) => value case pending => - ZIO.async[Any, E, A]( // intentionally never remove the callback, interrupted fibers won't be resumed + ZIO.asyncInterrupt[Any, E, A]( k => { @annotation.tailrec def loop(current: State[E, A]): Unit = @@ -60,6 +60,11 @@ final class Promise[E, A] private (blockingOn: FiberId) extends Serializable { case Done(value) => k(value) } loop(pending) + + Left(ZIO.succeed(state.updateAndGet { + case pending: Pending[?, ?] => pending.remove(k) + case completed => completed + })) }, blockingOn ) @@ -238,39 +243,67 @@ final class Promise[E, A] private (blockingOn: FiberId) extends Serializable { } object Promise { - private[Promise] object internal { + private[zio] object internal { sealed abstract class State[E, A] extends Serializable final case class Done[E, A](value: IO[E, A]) extends State[E, A] sealed abstract class Pending[E, A] extends State[E, A] { self => def complete(io: IO[E, A]): Unit def add(waiter: IO[E, A] => Any): Pending[E, A] + def remove(waiter: IO[E, A] => Any): Pending[E, A] def size: Int } private case object Empty extends Pending[Nothing, Nothing] { self => override def complete(io: IO[Nothing, Nothing]): Unit = () - override def add(waiter: IO[Nothing, Nothing] => Any): Pending[Nothing, Nothing] = - new Link[Nothing, Nothing](waiter, self, 1) { - override def complete(io: IO[Nothing, Nothing]): Unit = this.waiter(io) + def size = 0 + def add(waiter: IO[Nothing, Nothing] => Any): Pending[Nothing, Nothing] = new Link[Nothing, Nothing](waiter, self) { + override def size = 1 + } + def remove(waiter: IO[Nothing, Nothing] => Any): Pending[Nothing, Nothing] = self + } + private sealed abstract class Link[E, A](val waiter: IO[E, A] => Any, val ws: Pending[E, A]) extends Pending[E, A] { self => + final def add(waiter: IO[E, A] => Any): Pending[E, A] = new Link(waiter, self) { + override val size = self.size + 1 + override val ws = self + } + final def complete(io: IO[E, A]): Unit = + if (size == 1) waiter(io) + else Link.materialize(self, size).foreach(_(io)) + + final def remove(waiter: IO[E, A] => Any): Pending[E, A] = + if (size == 1 && (waiter eq self.waiter)) ws + else { + val arr = Link.materialize(self, size) + + @annotation.tailrec + def tabulate(i: Int, acc: Pending[E, A]): Pending[E, A] = + if (i >= 0) { + if (arr(i) ne waiter) + tabulate(i - 1, acc.add(arr(i))) + else + tabulate(i - 1, acc) + } else acc + + tabulate(size - 1, Empty.asInstanceOf[Pending[E, A]]) } - def size = 0 } - private sealed class Link[E, A](val waiter: IO[E, A] => Any, val ws: Pending[E, A], val size: Int) - extends Pending[E, A] { self => - def complete(io: IO[E, A]): Unit = { - val size = self.size - val arr = new Array[IO[E, A] => Any](size) + + private object Link { + def materialize[E, A](pending: Pending[E, A], size: Int): Array[IO[E, A] => Any] = { + val array = new Array[IO[E, A] => Any](size) + @annotation.tailrec def fill(pending: Pending[E, A], i: Int): Unit = - pending match { - case link: Link[?, ?] => - arr(i) = link.waiter - fill(link.ws, i - 1) - case _ => () // Empty - } - fill(self, size - 1) - arr.foreach(_(io)) + if (i >= 0) + pending match { + case link: Link[?, ?] => + array(i) = link.waiter + fill(link.ws, i - 1) + case _ => () // Empty + } + + fill(pending, size - 1) + array } - def add(waiter: IO[E, A] => Any): Pending[E, A] = new Link(waiter, self, size + 1) } object State { From c85456279a48465a95448773f8ec8471d972e3d4 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Sat, 5 Apr 2025 12:36:42 -0700 Subject: [PATCH 27/34] fmt --- .../shared/src/test/scala/zio/PromiseSpec.scala | 15 ++++++++------- core/shared/src/main/scala/zio/Promise.scala | 12 +++++++----- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/core-tests/shared/src/test/scala/zio/PromiseSpec.scala b/core-tests/shared/src/test/scala/zio/PromiseSpec.scala index 6cd344b57cbb..e62a093563e0 100644 --- a/core-tests/shared/src/test/scala/zio/PromiseSpec.scala +++ b/core-tests/shared/src/test/scala/zio/PromiseSpec.scala @@ -7,7 +7,8 @@ object PromiseSpec extends ZIOBaseSpec { import ZIOTag._ - private def empty[E, A]: Promise.internal.Pending[E, A] = Promise.internal.State.empty[E, A].asInstanceOf[Promise.internal.Pending[E, A]] + private def empty[E, A]: Promise.internal.Pending[E, A] = + Promise.internal.State.empty[E, A].asInstanceOf[Promise.internal.Pending[E, A]] def spec: Spec[Any, TestFailure[Any]] = suite("PromiseSpec")( test("complete a promise using succeed") { @@ -164,9 +165,9 @@ object PromiseSpec extends ZIOBaseSpec { assert(increment)(equalTo(0)) }, test("multiple") { - val n = 10 - var fired = 0 - val cb = (_: IO[Nothing, Unit]) => () + val n = 10 + var fired = 0 + val cb = (_: IO[Nothing, Unit]) => () val toRemove = (_: IO[Nothing, Unit]) => fired += 1 val state = (0 until n).foldLeft(empty[Nothing, Unit])((acc, i) => if (i < 5) acc.add(cb) else acc.add(toRemove)) @@ -179,14 +180,14 @@ object PromiseSpec extends ZIOBaseSpec { suite("complete")( test("one") { var completed = 0 - val state = empty[Nothing, Unit].add(_ => completed += 1) + val state = empty[Nothing, Unit].add(_ => completed += 1) state.complete(ZIO.unit) assert(completed)(equalTo(1)) }, test("multiple") { - val n = 10 + val n = 10 var completed = Seq.empty[Int] - val state = (0 until n).foldLeft(empty[Nothing, Unit])((acc, i) => acc.add(_ => completed = completed :+ i)) + val state = (0 until n).foldLeft(empty[Nothing, Unit])((acc, i) => acc.add(_ => completed = completed :+ i)) state.complete(ZIO.unit) assert(completed)(equalTo(0 until n)) } diff --git a/core/shared/src/main/scala/zio/Promise.scala b/core/shared/src/main/scala/zio/Promise.scala index 2521939dace3..3f6e662421a9 100644 --- a/core/shared/src/main/scala/zio/Promise.scala +++ b/core/shared/src/main/scala/zio/Promise.scala @@ -255,12 +255,14 @@ object Promise { private case object Empty extends Pending[Nothing, Nothing] { self => override def complete(io: IO[Nothing, Nothing]): Unit = () def size = 0 - def add(waiter: IO[Nothing, Nothing] => Any): Pending[Nothing, Nothing] = new Link[Nothing, Nothing](waiter, self) { - override def size = 1 - } - def remove(waiter: IO[Nothing, Nothing] => Any): Pending[Nothing, Nothing] = self + def add(waiter: IO[Nothing, Nothing] => Any): Pending[Nothing, Nothing] = + new Link[Nothing, Nothing](waiter, self) { + override def size = 1 + } + def remove(waiter: IO[Nothing, Nothing] => Any): Pending[Nothing, Nothing] = self } - private sealed abstract class Link[E, A](val waiter: IO[E, A] => Any, val ws: Pending[E, A]) extends Pending[E, A] { self => + private sealed abstract class Link[E, A](val waiter: IO[E, A] => Any, val ws: Pending[E, A]) extends Pending[E, A] { + self => final def add(waiter: IO[E, A] => Any): Pending[E, A] = new Link(waiter, self) { override val size = self.size + 1 override val ws = self From 73dc4130b16c133359092fe4e44e5083895d233d Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Sat, 5 Apr 2025 12:40:56 -0700 Subject: [PATCH 28/34] cleanup --- core/shared/src/main/scala/zio/Promise.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/shared/src/main/scala/zio/Promise.scala b/core/shared/src/main/scala/zio/Promise.scala index 3f6e662421a9..0dcd77996585 100644 --- a/core/shared/src/main/scala/zio/Promise.scala +++ b/core/shared/src/main/scala/zio/Promise.scala @@ -261,11 +261,11 @@ object Promise { } def remove(waiter: IO[Nothing, Nothing] => Any): Pending[Nothing, Nothing] = self } - private sealed abstract class Link[E, A](val waiter: IO[E, A] => Any, val ws: Pending[E, A]) extends Pending[E, A] { + private sealed abstract class Link[E, A](final val waiter: IO[E, A] => Any, final val ws: Pending[E, A]) + extends Pending[E, A] { self => final def add(waiter: IO[E, A] => Any): Pending[E, A] = new Link(waiter, self) { override val size = self.size + 1 - override val ws = self } final def complete(io: IO[E, A]): Unit = if (size == 1) waiter(io) From 4ddbb3001300b99f76d896c8acd7968b3c74c817 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Sat, 5 Apr 2025 12:50:28 -0700 Subject: [PATCH 29/34] protected size --- core/shared/src/main/scala/zio/Promise.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shared/src/main/scala/zio/Promise.scala b/core/shared/src/main/scala/zio/Promise.scala index 0dcd77996585..c1f366f5123f 100644 --- a/core/shared/src/main/scala/zio/Promise.scala +++ b/core/shared/src/main/scala/zio/Promise.scala @@ -250,7 +250,7 @@ object Promise { def complete(io: IO[E, A]): Unit def add(waiter: IO[E, A] => Any): Pending[E, A] def remove(waiter: IO[E, A] => Any): Pending[E, A] - def size: Int + protected def size: Int } private case object Empty extends Pending[Nothing, Nothing] { self => override def complete(io: IO[Nothing, Nothing]): Unit = () From 7eb086e4aaa7a61a331f54186b9c6312554060a3 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Sat, 5 Apr 2025 13:20:55 -0700 Subject: [PATCH 30/34] Revert "protected size" This reverts commit 4ddbb3001300b99f76d896c8acd7968b3c74c817. --- core/shared/src/main/scala/zio/Promise.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shared/src/main/scala/zio/Promise.scala b/core/shared/src/main/scala/zio/Promise.scala index c1f366f5123f..0dcd77996585 100644 --- a/core/shared/src/main/scala/zio/Promise.scala +++ b/core/shared/src/main/scala/zio/Promise.scala @@ -250,7 +250,7 @@ object Promise { def complete(io: IO[E, A]): Unit def add(waiter: IO[E, A] => Any): Pending[E, A] def remove(waiter: IO[E, A] => Any): Pending[E, A] - protected def size: Int + def size: Int } private case object Empty extends Pending[Nothing, Nothing] { self => override def complete(io: IO[Nothing, Nothing]): Unit = () From c818f4c6080501be8e8dbd72d6fb70d042a5b0c0 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Sat, 26 Apr 2025 07:04:24 -0700 Subject: [PATCH 31/34] fix merge --- project/MimaSettings.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/MimaSettings.scala b/project/MimaSettings.scala index 01c8bbd1ee93..69fd86d52bc3 100644 --- a/project/MimaSettings.scala +++ b/project/MimaSettings.scala @@ -36,7 +36,7 @@ object MimaSettings { exclude[NewMixinForwarderProblem]("zio.Exit.mapErrorCause"), exclude[NewMixinForwarderProblem]("zio.Exit.unit"), exclude[Problem]("zio.Promise#internal*"), - exclude[Problem]("zio.Promise$internal*") + exclude[Problem]("zio.Promise$internal*"), exclude[Problem]("zio.Queue#Strategy*.shutdown") ), mimaFailOnProblem := failOnProblem From 9b906aa0f8da669126af6accd24c2ae7269b8d32 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Sat, 10 May 2025 05:48:05 -0700 Subject: [PATCH 32/34] fix merge --- core-tests/shared/src/test/scala/zio/FiberRuntimeSpec.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/core-tests/shared/src/test/scala/zio/FiberRuntimeSpec.scala b/core-tests/shared/src/test/scala/zio/FiberRuntimeSpec.scala index 7769cd472549..ebc2ffc1e235 100644 --- a/core-tests/shared/src/test/scala/zio/FiberRuntimeSpec.scala +++ b/core-tests/shared/src/test/scala/zio/FiberRuntimeSpec.scala @@ -99,6 +99,7 @@ object FiberRuntimeSpec extends ZIOBaseSpec { ) } } @@ TestAspect.nonFlaky(10) + ), suite("runtime metrics")( test("Failures are counted once for the fiber that caused them and exits are not") { val nullErrors = ZIO.foreachParDiscard(1 to 2)(_ => ZIO.attempt(throw new NullPointerException)) From 207bd2740469fd708f25a691dabf006ead5dd5f6 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Wed, 14 May 2025 12:04:51 -0600 Subject: [PATCH 33/34] address in person review --- .../src/test/scala/zio/PromiseSpec.scala | 13 ++-- core/shared/src/main/scala/zio/Promise.scala | 61 +++++++++++-------- 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/core-tests/shared/src/test/scala/zio/PromiseSpec.scala b/core-tests/shared/src/test/scala/zio/PromiseSpec.scala index e62a093563e0..747a9024e6e6 100644 --- a/core-tests/shared/src/test/scala/zio/PromiseSpec.scala +++ b/core-tests/shared/src/test/scala/zio/PromiseSpec.scala @@ -10,6 +10,8 @@ object PromiseSpec extends ZIOBaseSpec { private def empty[E, A]: Promise.internal.Pending[E, A] = Promise.internal.State.empty[E, A].asInstanceOf[Promise.internal.Pending[E, A]] + val n = 10000 + def spec: Spec[Any, TestFailure[Any]] = suite("PromiseSpec")( test("complete a promise using succeed") { for { @@ -127,7 +129,7 @@ object PromiseSpec extends ZIOBaseSpec { test("waiter stack safety") { for { p <- Promise.make[Nothing, Unit] - fibers <- ZIO.foreach(1 to 100000)(_ => p.await.forkDaemon) + fibers <- ZIO.foreach(1 to n)(_ => p.await.forkDaemon) _ <- p.complete(Exit.unit) _ <- ZIO.foreach(fibers)(_.await) } yield assertCompletes @@ -147,7 +149,6 @@ object PromiseSpec extends ZIOBaseSpec { assert(increment)(equalTo(1)) }, test("multiple") { - val n = 10 var increment = 0 val state = (0 until n).foldLeft(empty[Nothing, Unit])((acc, _) => acc.add(_ => increment += 1)) state.complete(ZIO.unit) @@ -165,7 +166,6 @@ object PromiseSpec extends ZIOBaseSpec { assert(increment)(equalTo(0)) }, test("multiple") { - val n = 10 var fired = 0 val cb = (_: IO[Nothing, Unit]) => () val toRemove = (_: IO[Nothing, Unit]) => fired += 1 @@ -185,11 +185,10 @@ object PromiseSpec extends ZIOBaseSpec { assert(completed)(equalTo(1)) }, test("multiple") { - val n = 10 - var completed = Seq.empty[Int] - val state = (0 until n).foldLeft(empty[Nothing, Unit])((acc, i) => acc.add(_ => completed = completed :+ i)) + var completed = List.empty[Int] + val state = (0 until n).foldLeft(empty[Nothing, Unit])((acc, i) => acc.add(_ => completed = i :: completed)) state.complete(ZIO.unit) - assert(completed)(equalTo(0 until n)) + assert(completed)(equalTo(List.range(0, n))) } ) ) diff --git a/core/shared/src/main/scala/zio/Promise.scala b/core/shared/src/main/scala/zio/Promise.scala index 0dcd77996585..7a922d86120c 100644 --- a/core/shared/src/main/scala/zio/Promise.scala +++ b/core/shared/src/main/scala/zio/Promise.scala @@ -269,41 +269,54 @@ object Promise { } final def complete(io: IO[E, A]): Unit = if (size == 1) waiter(io) - else Link.materialize(self, size).foreach(_(io)) + else { + var current: Pending[E, A] = self + while (current ne Empty) { + current match { + case link: Link[?, ?] => + link.waiter(io) + current = link.ws + case _ => // Empty + current = Empty.asInstanceOf[Pending[E, A]] + } + } + } final def remove(waiter: IO[E, A] => Any): Pending[E, A] = - if (size == 1 && (waiter eq self.waiter)) ws + if (size == 1) if (waiter eq self.waiter) ws else self else { val arr = Link.materialize(self, size) + var i = size - 1 + var acc: Pending[E, A] = Empty.asInstanceOf[Pending[E, A]] - @annotation.tailrec - def tabulate(i: Int, acc: Pending[E, A]): Pending[E, A] = - if (i >= 0) { - if (arr(i) ne waiter) - tabulate(i - 1, acc.add(arr(i))) - else - tabulate(i - 1, acc) - } else acc - - tabulate(size - 1, Empty.asInstanceOf[Pending[E, A]]) + while (i >= 0) { + if (arr(i) ne waiter) { + acc = acc.add(arr(i)) + } + i -= 1 + } + acc } } private object Link { + /** + * Materializes the pending state into an array of waiters in reverse order. + */ def materialize[E, A](pending: Pending[E, A], size: Int): Array[IO[E, A] => Any] = { val array = new Array[IO[E, A] => Any](size) - - @annotation.tailrec - def fill(pending: Pending[E, A], i: Int): Unit = - if (i >= 0) - pending match { - case link: Link[?, ?] => - array(i) = link.waiter - fill(link.ws, i - 1) - case _ => () // Empty - } - - fill(pending, size - 1) + var current = pending + var i = size - 1 + + while (i >= 0) { + current match { + case link: Link[?, ?] => + array(i) = link.waiter + current = link.ws + case _ => () // Empty + } + i -= 1 + } array } } From dd855c31eaf04293eca0defd4219009488d2716a Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Wed, 14 May 2025 12:05:34 -0600 Subject: [PATCH 34/34] format --- core/shared/src/main/scala/zio/Promise.scala | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/core/shared/src/main/scala/zio/Promise.scala b/core/shared/src/main/scala/zio/Promise.scala index 7a922d86120c..9e54ef47bf85 100644 --- a/core/shared/src/main/scala/zio/Promise.scala +++ b/core/shared/src/main/scala/zio/Promise.scala @@ -285,8 +285,8 @@ object Promise { final def remove(waiter: IO[E, A] => Any): Pending[E, A] = if (size == 1) if (waiter eq self.waiter) ws else self else { - val arr = Link.materialize(self, size) - var i = size - 1 + val arr = Link.materialize(self, size) + var i = size - 1 var acc: Pending[E, A] = Empty.asInstanceOf[Pending[E, A]] while (i >= 0) { @@ -300,13 +300,15 @@ object Promise { } private object Link { + /** - * Materializes the pending state into an array of waiters in reverse order. - */ + * Materializes the pending state into an array of waiters in reverse + * order. + */ def materialize[E, A](pending: Pending[E, A], size: Int): Array[IO[E, A] => Any] = { - val array = new Array[IO[E, A] => Any](size) + val array = new Array[IO[E, A] => Any](size) var current = pending - var i = size - 1 + var i = size - 1 while (i >= 0) { current match {