From 6fb1617d7862e675148071df77ddd0cb43f1f8a1 Mon Sep 17 00:00:00 2001 From: Simon Schenk Date: Fri, 15 Oct 2021 09:55:27 +0200 Subject: [PATCH 01/23] ZIO 2.0 --- .github/workflows/ci.yml | 5 +- .scalafmt.conf | 2 +- build.sbt | 2 +- project/BuildHelper.scala | 177 +++++++++--------- .../interop/reactivestreams/Adapters.scala | 102 +++++----- .../PublisherToStreamSpec.scala | 92 +++++---- .../SinkToSubscriberSpec.scala | 75 ++++---- .../StreamToPublisherSpec.scala | 13 +- .../SubscriberToSinkSpec.scala | 28 +-- 9 files changed, 263 insertions(+), 233 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80dfb78..f8d7bf4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,10 @@ name: CI on: pull_request: push: - branches: ['master'] + branches: [ + 'master', + 'series/2.x' + ] release: types: - published diff --git a/.scalafmt.conf b/.scalafmt.conf index cfac2a0..631facc 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,5 +1,5 @@ version = "3.0.4" -runner.dialect = scala213 +runner.dialect = scala213source3 maxColumn = 120 align.preset = most align.multiline = false diff --git a/build.sbt b/build.sbt index f68aaed..73f6c51 100644 --- a/build.sbt +++ b/build.sbt @@ -36,7 +36,7 @@ Global / onChangedBuildSource := ReloadOnSourceChanges addCommandAlias("fmt", "all scalafmtSbt scalafmt test:scalafmt") addCommandAlias("check", "all scalafmtSbtCheck scalafmtCheck test:scalafmtCheck") -val zioVersion = "1.0.12" +val zioVersion = "2.0.0-M4" val rsVersion = "1.0.3" val collCompatVersion = "2.5.0" diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index ac7827b..ee3d2d1 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -130,91 +130,100 @@ object BuildHelper { case _ => Seq.empty } - def stdSettings(prjName: String) = Seq( - name := s"$prjName", - crossScalaVersions := Seq(Scala211, Scala212, Scala213), - ThisBuild / scalaVersion := Scala213, - scalacOptions := stdOptions ++ extraOptions(scalaVersion.value, optimize = !isSnapshot.value), - semanticdbEnabled := !(scalaVersion.value == ScalaDotty), // enable SemanticDB - semanticdbOptions += "-P:semanticdb:synthetics:on", - semanticdbVersion := scalafixSemanticdb.revision, // use Scalafix compatible version - ThisBuild / scalafixScalaBinaryVersion := CrossVersion.binaryScalaVersion(scalaVersion.value), - ThisBuild / scalafixDependencies ++= List( - "com.github.liancheng" %% "organize-imports" % "0.5.0", - "com.github.vovapolu" %% "scaluzzi" % "0.1.20" - ), - Test / parallelExecution := true, - incOptions ~= (_.withLogRecompileOnMacro(false)), - autoAPIMappings := true, - unusedCompileDependenciesFilter -= moduleFilter("org.scala-js", "scalajs-library"), - Compile / unmanagedSourceDirectories ++= { - CrossVersion.partialVersion(scalaVersion.value) match { - case Some((2, x)) if x <= 11 => - Seq( - Seq(file(sourceDirectory.value.getPath + "/main/scala-2.11")), - CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.11")), - CrossType.Full.sharedSrcDir(baseDirectory.value, "test").toList.map(f => file(f.getPath + "-2.11")), - CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.x")), - CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.11-2.12")) - ).flatten - case Some((2, x)) if x == 12 => - Seq( - Seq(file(sourceDirectory.value.getPath + "/main/scala-2.12")), - Seq(file(sourceDirectory.value.getPath + "/main/scala-2.12+")), - CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.12+")), - CrossType.Full.sharedSrcDir(baseDirectory.value, "test").toList.map(f => file(f.getPath + "-2.12+")), - CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.x")), - CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.12-2.13")), - CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.11-2.12")) - ).flatten - case Some((2, x)) if x >= 13 => - Seq( - Seq(file(sourceDirectory.value.getPath + "/main/scala-2.12")), - Seq(file(sourceDirectory.value.getPath + "/main/scala-2.12+")), - CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.12+")), - CrossType.Full.sharedSrcDir(baseDirectory.value, "test").toList.map(f => file(f.getPath + "-2.12+")), - CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.x")), - CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.12-2.13")), - CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.13+")) - ).flatten - case Some((3, _)) => - Seq( - Seq(file(sourceDirectory.value.getPath + "/main/scala-2.12")), - Seq(file(sourceDirectory.value.getPath + "/main/scala-2.12+")), - CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.12+")), - CrossType.Full.sharedSrcDir(baseDirectory.value, "test").toList.map(f => file(f.getPath + "-2.12+")), - CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-dotty")), - CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.13+")) - ).flatten - case _ => - Nil - } - }, - Test / unmanagedSourceDirectories ++= { - CrossVersion.partialVersion(scalaVersion.value) match { - case Some((2, x)) if x <= 11 => - Seq( - Seq(file(sourceDirectory.value.getPath + "/test/scala-2.11")), - CrossType.Full.sharedSrcDir(baseDirectory.value, "test").toList.map(f => file(f.getPath + "-2.x")) - ).flatten - case Some((2, x)) if x >= 12 => - Seq( - Seq(file(sourceDirectory.value.getPath + "/test/scala-2.12")), - Seq(file(sourceDirectory.value.getPath + "/test/scala-2.12+")), - CrossType.Full.sharedSrcDir(baseDirectory.value, "test").toList.map(f => file(f.getPath + "-2.x")) - ).flatten - case Some((3, _)) => - Seq( - Seq(file(sourceDirectory.value.getPath + "/test/scala-2.12+")), - CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.12+")), - CrossType.Full.sharedSrcDir(baseDirectory.value, "test").toList.map(f => file(f.getPath + "-dotty")) - ).flatten - case _ => - Nil + def stdSettings(prjName: String) = + Seq( + name := s"$prjName", + crossScalaVersions := Seq(Scala211, Scala212, Scala213), + ThisBuild / scalaVersion := Scala213, + scalacOptions := stdOptions ++ extraOptions(scalaVersion.value, optimize = !isSnapshot.value), + semanticdbEnabled := !(scalaVersion.value == ScalaDotty), // enable SemanticDB + semanticdbVersion := scalafixSemanticdb.revision, // use Scalafix compatible version + ThisBuild / scalafixScalaBinaryVersion := CrossVersion.binaryScalaVersion(scalaVersion.value), + ThisBuild / scalafixDependencies ++= List( + "com.github.liancheng" %% "organize-imports" % "0.5.0", + "com.github.vovapolu" %% "scaluzzi" % "0.1.20" + ), + Test / parallelExecution := true, + incOptions ~= (_.withLogRecompileOnMacro(false)), + autoAPIMappings := true, + unusedCompileDependenciesFilter -= moduleFilter("org.scala-js", "scalajs-library"), + Compile / unmanagedSourceDirectories ++= { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, x)) if x <= 11 => + Seq( + Seq(file(sourceDirectory.value.getPath + "/main/scala-2.11")), + CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.11")), + CrossType.Full.sharedSrcDir(baseDirectory.value, "test").toList.map(f => file(f.getPath + "-2.11")), + CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.x")), + CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.11-2.12")) + ).flatten + case Some((2, x)) if x == 12 => + Seq( + Seq(file(sourceDirectory.value.getPath + "/main/scala-2.12")), + Seq(file(sourceDirectory.value.getPath + "/main/scala-2.12+")), + CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.12+")), + CrossType.Full.sharedSrcDir(baseDirectory.value, "test").toList.map(f => file(f.getPath + "-2.12+")), + CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.x")), + CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.12-2.13")), + CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.11-2.12")) + ).flatten + case Some((2, x)) if x >= 13 => + Seq( + Seq(file(sourceDirectory.value.getPath + "/main/scala-2.12")), + Seq(file(sourceDirectory.value.getPath + "/main/scala-2.12+")), + CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.12+")), + CrossType.Full.sharedSrcDir(baseDirectory.value, "test").toList.map(f => file(f.getPath + "-2.12+")), + CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.x")), + CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.12-2.13")), + CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.13+")) + ).flatten + case Some((3, _)) => + Seq( + Seq(file(sourceDirectory.value.getPath + "/main/scala-2.12")), + Seq(file(sourceDirectory.value.getPath + "/main/scala-2.12+")), + CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.12+")), + CrossType.Full.sharedSrcDir(baseDirectory.value, "test").toList.map(f => file(f.getPath + "-2.12+")), + CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-dotty")), + CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.13+")) + ).flatten + case _ => + Nil + } + }, + Test / unmanagedSourceDirectories ++= { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, x)) if x <= 11 => + Seq( + Seq(file(sourceDirectory.value.getPath + "/test/scala-2.11")), + CrossType.Full.sharedSrcDir(baseDirectory.value, "test").toList.map(f => file(f.getPath + "-2.x")) + ).flatten + case Some((2, x)) if x >= 12 => + Seq( + Seq(file(sourceDirectory.value.getPath + "/test/scala-2.12")), + Seq(file(sourceDirectory.value.getPath + "/test/scala-2.12+")), + CrossType.Full.sharedSrcDir(baseDirectory.value, "test").toList.map(f => file(f.getPath + "-2.x")) + ).flatten + case Some((3, _)) => + Seq( + Seq(file(sourceDirectory.value.getPath + "/test/scala-2.12+")), + CrossType.Full.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + "-2.12+")), + CrossType.Full.sharedSrcDir(baseDirectory.value, "test").toList.map(f => file(f.getPath + "-dotty")) + ).flatten + case _ => + Nil + }, + }, + semanticdbOptions ++= { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, _)) => + Seq( + "-P:semanticdb:synthetics:on" + ) + case _ => + Seq.empty + } } - - } - ) + ) implicit class ModuleHelper(p: Project) { def module: Project = p.in(file(p.id)).settings(stdSettings(p.id)) diff --git a/src/main/scala/zio/interop/reactivestreams/Adapters.scala b/src/main/scala/zio/interop/reactivestreams/Adapters.scala index d0d4d6f..22d355c 100644 --- a/src/main/scala/zio/interop/reactivestreams/Adapters.scala +++ b/src/main/scala/zio/interop/reactivestreams/Adapters.scala @@ -3,13 +3,13 @@ package zio.interop.reactivestreams import org.reactivestreams.Publisher import org.reactivestreams.Subscriber import org.reactivestreams.Subscription +import zio.Exit.Failure +import zio.Exit.Success import zio._ import zio.stream.ZSink import zio.stream.ZStream import zio.stream.ZStream.Pull -import scala.annotation.tailrec - object Adapters { def streamToPublisher[R, E <: Throwable, O](stream: ZStream[R, E, O]): ZIO[R, Nothing, Publisher[O]] = @@ -17,7 +17,7 @@ object Adapters { if (subscriber == null) { throw new NullPointerException("Subscriber must not be null.") } else { - runtime.unsafeRunAsync_( + runtime.unsafeRunAsync( for { demand <- Queue.unbounded[Long] _ <- UIO(subscriber.onSubscribe(createSubscription(subscriber, demand, runtime))) @@ -47,12 +47,12 @@ object Adapters { for { subscriberP <- makeSubscriber[O](bufferSize) (subscriber, p) = subscriberP - _ <- UIO(publisher.subscribe(subscriber)).toManaged_ - subQ <- p.await.toManaged_ + _ <- ZManaged.succeed(publisher.subscribe(subscriber)) + subQ <- p.await.toManaged (sub, q) = subQ process <- process(q, sub) } yield process - val pull = pullOrFail.catchAll(e => UIO(Pull.fail(e)).toManaged_) + val pull = pullOrFail.catchAll(e => ZManaged.succeed(Pull.fail(e))) ZStream(pull) } @@ -63,9 +63,9 @@ object Adapters { for { subscriberP <- makeSubscriber[I](bufferSize) (subscriber, p) = subscriberP - pull = p.await.toManaged_.flatMap { case (subscription, q) => process(q, subscription) } + pull = p.await.toManaged.flatMap { case (subscription, q) => process(q, subscription) } .catchAll(e => ZManaged.succeedNow(Pull.fail(e))) - fiber <- ZStream(pull).run(sink).toManaged_.fork + fiber <- ZStream(pull).run(sink).toManaged.fork } yield (subscriber, fiber.join) private def process[R, A]( @@ -74,47 +74,43 @@ object Adapters { ): ZManaged[Any, Nothing, ZIO[Any, Option[Throwable], Chunk[A]]] = { val capacity = q.capacity.toLong - 1 // leave space for End or Fail for { - _ <- UIO(sub.request(capacity)).toManaged_ - requested <- RefM.make(capacity).toManaged_ - lastP <- Promise.make[Option[Throwable], Chunk[A]].toManaged_ + _ <- ZManaged.succeed(sub.request(capacity)) + requested <- Ref.Synchronized.makeManaged(capacity) + lastP <- Promise.makeManaged[Option[Throwable], Chunk[A]] } yield { - @tailrec def takesToPull( - builder: ChunkBuilder[A] = ChunkBuilder.make[A]() - )( - takes: List[Exit[Option[Throwable], A]] - ): Pull[Any, Throwable, A] = - takes match { - case Exit.Success(a) :: tail => - builder += a - takesToPull(builder)(tail) - case Exit.Failure(cause) :: _ => - val chunk = builder.result() - val pull = Cause.sequenceCauseOption(cause) match { - case Some(cause) => Pull.halt(cause) - case None => Pull.end + takes: Chunk[Exit[Option[Throwable], A]] + ): Pull[Any, Throwable, A] = { + val toEmit = takes.collectWhile { case Exit.Success(a) => a } + val pull = Pull.emit(toEmit) + if (toEmit.size == takes.size) { + val chunkSize = toEmit.size + val request = + requested.getAndUpdateZIO { + case `chunkSize` => UIO(sub.request(capacity)).as(capacity) + case n => UIO.succeedNow(n - chunkSize) } - if (chunk.isEmpty) pull else lastP.complete(pull) *> Pull.emit(chunk) - case Nil => - val chunk = builder.result() - val pull = Pull.emit(chunk) - - if (chunk.isEmpty) pull - else { - val chunkSize = chunk.size - val request = - requested.getAndUpdate { - case `chunkSize` => UIO(sub.request(capacity)).as(capacity) - case n => UIO.succeedNow(n - chunkSize) + request *> pull + } else { + val failure = takes.drop(toEmit.size).head + failure match { + case Failure(cause) => + val last = + Cause.flipCauseOption(cause) match { + case None => Pull.end + case Some(cause) => Pull.failCause(cause) } - request *> pull - } + if (toEmit.isEmpty) last else lastP.complete(last) *> pull + + case Success(_) => pull // should never happen + } } + } lastP.isDone.flatMap { case true => lastP.await - case false => q.takeBetween(1, q.capacity).flatMap(takesToPull()) + case false => q.takeBetween(1, q.capacity).flatMap(takesToPull) } } @@ -126,13 +122,13 @@ object Adapters { for { q <- Queue .bounded[Exit[Option[Throwable], A]](capacity) - .toManaged(_.shutdown) + .toManagedWith(_.shutdown) p <- Promise .make[Throwable, (Subscription, Queue[Exit[Option[Throwable], A]])] - .toManaged(p => - p.poll.flatMap(_.fold(UIO.unit)(_.foldM(_ => UIO.unit, { case (sub, _) => UIO(sub.cancel()) }))) + .toManagedWith( + _.poll.flatMap(_.fold(UIO.unit)(_.foldZIO(_ => UIO.unit, { case (sub, _) => UIO(sub.cancel()) }))) ) - runtime <- ZIO.runtime[Any].toManaged_ + runtime <- ZManaged.runtime[Any] } yield { val subscriber = @@ -144,11 +140,13 @@ object Adapters { runtime.unsafeRun(p.fail(e)) throw e } else { - runtime.unsafeRun(p.succeed((s, q)).flatMap { - // `whenM(q.isShutdown)`, the Stream has been interrupted or completed before we received `onSubscribe` - case true => UIO(s.cancel()).whenM(q.isShutdown) - case false => UIO(s.cancel()) - }) + runtime.unsafeRun( + p.succeed((s, q)).flatMap { + // `whenM(q.isShutdown)`, the Stream has been interrupted or completed before we received `onSubscribe` + case true => UIO(s.cancel()).whenZIO(q.isShutdown).unit + case false => UIO(s.cancel()) + } + ) } override def onNext(t: A): Unit = @@ -181,7 +179,7 @@ object Adapters { demand: Queue[Long] ): ZSink[Any, Nothing, I, I, Unit] = ZSink - .foldChunksM[Any, Nothing, I, Long](0L)(_ >= 0L) { (bufferedDemand, chunk) => + .foldChunksZIO[Any, Nothing, I, Long](0L)(_ >= 0L) { (bufferedDemand, chunk) => UIO .iterate((chunk, bufferedDemand))(!_._1.isEmpty) { case (chunk, bufferedDemand) => demand.isShutdown.flatMap { @@ -198,7 +196,7 @@ object Adapters { } .map(_._2) } - .mapM(_ => demand.isShutdown.flatMap(is => UIO(subscriber.onComplete()).when(!is))) + .mapZIO(_ => demand.isShutdown.flatMap(is => UIO(subscriber.onComplete()).when(!is).unit)) def createSubscription[A]( subscriber: Subscriber[_ >: A], @@ -208,7 +206,7 @@ object Adapters { new Subscription { override def request(n: Long): Unit = { if (n <= 0) subscriber.onError(new IllegalArgumentException("non-positive subscription request")) - runtime.unsafeRunAsync_(demand.offer(n).unit) + runtime.unsafeRunAsync(demand.offer(n)) } override def cancel(): Unit = runtime.unsafeRun(demand.shutdown) } diff --git a/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala b/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala index 5bbd2c9..6c1e897 100644 --- a/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala @@ -1,28 +1,37 @@ package zio.interop.reactivestreams -import org.reactivestreams.{ Publisher, Subscriber, Subscription } +import org.reactivestreams.Publisher +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription import org.reactivestreams.tck.TestEnvironment import org.reactivestreams.tck.TestEnvironment.ManualPublisher -import zio.{ Chunk, Exit, Promise, Task, UIO, ZIO } -import zio.duration._ -import zio.stream.{ Sink, Stream } -import zio.test._ +import zio.Chunk +import zio.Exit +import zio.Promise +import zio.Supervisor +import zio.Task +import zio.UIO +import zio.ZIO +import zio.durationInt +import zio.stream.Sink +import zio.stream.Stream import zio.test.Assertion._ +import zio.test._ object PublisherToStreamSpec extends DefaultRunnableSpec { override def spec = suite("Converting a `Publisher` to a `Stream`")( - testM("works with a well behaved `Publisher`") { + test("works with a well behaved `Publisher`") { assertM(publish(seq, None))(succeeds(equalTo(seq))) }, - testM("fails with an initially failed `Publisher`") { + test("fails with an initially failed `Publisher`") { assertM(publish(Chunk.empty, Some(e)))(fails(equalTo(e))) }, - testM("fails with an eventually failing `Publisher`") { + test("fails with an eventually failing `Publisher`") { assertM(publish(seq, Some(e)))(fails(equalTo(e))) }, - testM("does not fail a fiber on failing `Publisher`") { + test("does not fail a fiber on failing `Publisher`") { val publisher = new Publisher[Int] { override def subscribe(s: Subscriber[_ >: Int]): Unit = s.onSubscribe( @@ -32,18 +41,20 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { } ) } - ZIO.runtime[Any].map { runtime => - var fibersFailed = 0 - val testRuntime = runtime.mapPlatform(_.withReportFailure(e => if (!e.interruptedOnly) fibersFailed += 1)) - val exit = testRuntime.unsafeRun(publisher.toStream().runDrain.run) - assert(exit)(fails(anything)) && assert(fibersFailed)(equalTo(0)) - } + val supervisor = Supervisor.runtimeStats + for { + runtime <- ZIO.runtime[Any] + testRuntime = runtime.mapRuntimeConfig(_.copy(supervisor = supervisor)) + exit = testRuntime.unsafeRun(publisher.toStream().runDrain.exit) + stats <- supervisor.value + } yield assert(exit)(fails(anything)) && + assert(stats.failures)(equalTo(0L)) }, - testM("does not freeze on stream end") { + test("does not freeze on stream end") { withProbe(probe => for { fiber <- Stream - .fromEffect( + .fromZIO( UIO( probe.toStream() ) @@ -51,14 +62,14 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { .flatMap(identity) .run(Sink.collectAll[Int]) .fork - _ <- Task(probe.expectRequest()) + _ <- Task.attemptBlockingInterrupt(probe.expectRequest()) _ <- UIO(probe.sendNext(1)) _ <- UIO(probe.sendCompletion) r <- fiber.join } yield assert(r)(equalTo(Chunk(1))) ) } @@ TestAspect.timeout(1000.millis), - testM("cancels subscription when interrupted before subscription") { + test("cancels subscription when interrupted before subscription") { val tst = for { subscriberP <- Promise.make[Nothing, Subscriber[_]] @@ -78,55 +89,55 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { _ <- UIO(subscriber.onSubscribe(subscription)) _ <- cancelledLatch.await } yield () - assertM(tst.run)(succeeds(anything)) + assertM(tst.exit)(succeeds(anything)) } @@ TestAspect.timeout(3.seconds), - testM("cancels subscription when interrupted after subscription") { + test("cancels subscription when interrupted after subscription") { withProbe(probe => assertM((for { fiber <- probe.toStream(bufferSize).run(Sink.drain).fork - _ <- Task(probe.expectRequest()) + _ <- Task.attemptBlockingInterrupt(probe.expectRequest()) _ <- fiber.interrupt - _ <- Task(probe.expectCancelling()) - } yield ()).run)( + _ <- Task.attemptBlockingInterrupt(probe.expectCancelling()) + } yield ()).exit)( succeeds(isUnit) ) ) }, - testM("cancels subscription when interrupted during consumption") { + test("cancels subscription when interrupted during consumption") { withProbe(probe => assertM((for { fiber <- probe.toStream(bufferSize).run(Sink.drain).fork - demand <- Task(probe.expectRequest()) + demand <- Task.attemptBlockingInterrupt(probe.expectRequest()) _ <- Task((1 to demand.toInt).foreach(i => probe.sendNext(i))) _ <- fiber.interrupt - _ <- Task(probe.expectCancelling()) - } yield ()).run)( + _ <- Task.attemptBlockingInterrupt(probe.expectCancelling()) + } yield ()).exit)( succeeds(isUnit) ) ) }, - testM("cancels subscription on stream end") { + test("cancels subscription on stream end") { withProbe(probe => assertM((for { fiber <- probe.toStream(bufferSize).take(1).run(Sink.drain).fork - demand <- Task(probe.expectRequest()) + demand <- Task.attemptBlockingInterrupt(probe.expectRequest()) _ <- Task((1 to demand.toInt).foreach(i => probe.sendNext(i))) - _ <- Task(probe.expectCancelling()) + _ <- Task.attemptBlockingInterrupt(probe.expectCancelling()) _ <- fiber.join - } yield ()).run)( + } yield ()).exit)( succeeds(isUnit) ) ) }, - testM("cancels subscription on stream error") { + test("cancels subscription on stream error") { withProbe(probe => assertM((for { - fiber <- probe.toStream(bufferSize).mapM(_ => Task.fail(new Throwable("boom!"))).run(Sink.drain).fork - demand <- Task(probe.expectRequest()) + fiber <- probe.toStream(bufferSize).mapZIO(_ => Task.fail(new Throwable("boom!"))).run(Sink.drain).fork + demand <- Task.attemptBlockingInterrupt(probe.expectRequest()) _ <- Task((1 to demand.toInt).foreach(i => probe.sendNext(i))) - _ <- Task(probe.expectCancelling()) + _ <- Task.attemptBlockingInterrupt(probe.expectCancelling()) _ <- fiber.join - } yield ()).run)( + } yield ()).exit)( fails(anything) ) ) @@ -147,12 +158,13 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { def loop(probe: ManualPublisher[Int], remaining: Chunk[Int]): Task[Unit] = for { - n <- Task(probe.expectRequest()) + n <- Task.attemptBlockingInterrupt(probe.expectRequest()) _ <- Task(assert(n.toInt)(isLessThanEqualTo(bufferSize))) split = n.toInt (nextN, tail) = remaining.splitAt(split) _ <- Task(nextN.foreach(probe.sendNext)) - _ <- if (nextN.size < split) Task(failure.fold(probe.sendCompletion())(probe.sendError)) + _ <- if (nextN.size < split) + Task(failure.fold(probe.sendCompletion())(probe.sendError)) else loop(probe, tail) } yield () @@ -165,7 +177,7 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { } yield r ) - faillable.run + faillable.exit } } diff --git a/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala b/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala index c6fdf19..ceb0d02 100644 --- a/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala @@ -1,22 +1,31 @@ package zio.interop.reactivestreams -import org.reactivestreams.{ Publisher, Subscriber, Subscription } -import org.reactivestreams.tck.{ SubscriberWhiteboxVerification, TestEnvironment } -import org.reactivestreams.tck.SubscriberWhiteboxVerification.{ SubscriberPuppet, WhiteboxSubscriberProbe } +import org.reactivestreams.Publisher +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription +import org.reactivestreams.tck.SubscriberWhiteboxVerification +import org.reactivestreams.tck.SubscriberWhiteboxVerification.SubscriberPuppet +import org.reactivestreams.tck.SubscriberWhiteboxVerification.WhiteboxSubscriberProbe +import org.reactivestreams.tck.TestEnvironment import org.testng.annotations.Test -import zio.{ Chunk, Promise, Task, UIO, ZIO, ZManaged } -import zio.blocking._ -import zio.clock.Clock -import zio.duration._ -import zio.stream.{ Sink, ZSink } -import zio.test._ +import zio.Chunk +import zio.Promise +import zio.Task +import zio.UIO +import zio.ZIO +import zio.ZManaged +import zio.durationInt +import zio.durationLong +import zio.stream.Sink +import zio.stream.ZSink import zio.test.Assertion._ +import zio.test._ import zio.test.environment.Live object SinkToSubscriberSpec extends DefaultRunnableSpec { override def spec = suite("Converting a `Sink` to a `Subscriber`")( - testM("works on the happy path")( + test("works on the happy path")( for { (publisher, subscribed, requested, canceled) <- makePublisherProbe fiber <- ZSink @@ -28,18 +37,18 @@ object SinkToSubscriberSpec extends DefaultRunnableSpec { } .fork _ <- Live.live( - assertM(subscribed.await.timeoutFail("timeout awaiting subscribe.")(500.millis).run)(succeeds(isUnit)) + assertM(subscribed.await.timeoutFail("timeout awaiting subscribe.")(500.millis).exit)(succeeds(isUnit)) ) _ <- Live.live( - assertM(requested.await.timeoutFail("timeout awaiting request.")(500.millis).run)(succeeds(isUnit)) + assertM(requested.await.timeoutFail("timeout awaiting request.")(500.millis).exit)(succeeds(isUnit)) ) _ <- Live.live( - assertM(canceled.await.timeoutFail("timeout awaiting cancel.")(500.millis).run)(succeeds(isUnit)) + assertM(canceled.await.timeoutFail("timeout awaiting cancel.")(500.millis).exit)(succeeds(isUnit)) ) - r <- fiber.join.run + r <- fiber.join.exit } yield assert(r)(succeeds(equalTo(List(1, 2, 3, 4, 5)))) ), - testM("cancels subscription on interruption after subscription")( + test("cancels subscription on interruption after subscription")( for { (publisher, subscribed, _, canceled) <- makePublisherProbe fiber <- Sink.drain @@ -47,33 +56,29 @@ object SinkToSubscriberSpec extends DefaultRunnableSpec { .use { case (subscriber, _) => UIO(publisher.subscribe(subscriber)) *> UIO.never } .fork _ <- Live.live( - assertM(subscribed.await.timeoutFail("timeout awaiting subscribe.")(500.millis).run)(succeeds(isUnit)) + assertM(subscribed.await.timeoutFail("timeout awaiting subscribe.")(500.millis).exit)(succeeds(isUnit)) ) _ <- fiber.interrupt _ <- Live.live( - assertM(canceled.await.timeoutFail("timeout awaiting cancel.")(500.millis).run)(succeeds(isUnit)) + assertM(canceled.await.timeoutFail("timeout awaiting cancel.")(500.millis).exit)(succeeds(isUnit)) ) - r <- fiber.join.run + r <- fiber.join.exit } yield assert(r)(isInterrupted) ), - testM("cancels subscription on interruption during consuption")( + test("cancels subscription on interruption during consuption")( for { (publisher, subscribed, requested, canceled) <- makePublisherProbe fiber <- Sink.drain .toSubscriber() - .use { case (subscriber, _) => UIO(publisher.subscribe(subscriber)) *> UIO.never } + .use { case (subscriber, _) => + Task.attemptBlockingInterrupt(publisher.subscribe(subscriber)) *> UIO.never + } .fork - _ <- Live.live( - assertM(subscribed.await.timeoutFail("timeout awaiting subscribe.")(500.millis).run)(succeeds(isUnit)) - ) - _ <- Live.live( - assertM(requested.await.timeoutFail("timeout awaiting request.")(500.millis).run)(succeeds(isUnit)) - ) + _ <- assertM(subscribed.await.exit)(succeeds(isUnit)) + _ <- assertM(requested.await.exit)(succeeds(isUnit)) _ <- fiber.interrupt - _ <- Live.live( - assertM(canceled.await.timeoutFail("timeout awaiting cancel.")(500.millis).run)(succeeds(isUnit)) - ) - r <- fiber.join.run + _ <- assertM(canceled.await.exit)(succeeds(isUnit)) + r <- fiber.join.exit } yield assert(r)(isInterrupted) ), suite("passes all required and optional TCK tests")( @@ -83,7 +88,7 @@ object SinkToSubscriberSpec extends DefaultRunnableSpec { val makePublisherProbe = for { - runtime <- ZIO.runtime[Clock] + runtime <- ZIO.runtime[Any] subscribed <- Promise.make[Nothing, Unit] requested <- Promise.make[Nothing, Unit] canceled <- Promise.make[Nothing, Unit] @@ -133,7 +138,7 @@ object SinkToSubscriberSpec extends DefaultRunnableSpec { for { subscriber_ <- Sink.collectAll[Int].toSubscriber() (subscriber, _) = subscriber_ - sbv <- ZManaged.make { + sbv <- ZManaged.acquireReleaseWith { val env = new TestEnvironment(1000, 500) val sbv = new SubscriberWhiteboxVerification[Int](env) { @@ -160,11 +165,11 @@ object SinkToSubscriberSpec extends DefaultRunnableSpec { case method if method.getName().startsWith("untested") => test(method.getName())(assert(())(anything)) @@ TestAspect.ignore case method => - testM(method.getName())( + test(method.getName())( for { r <- managedVerification.use { case (sbv, env) => - blocking(Task(method.invoke(sbv))).timeout(env.defaultTimeoutMillis().millis) - }.unit.run + Task.attemptBlockingInterrupt(method.invoke(sbv)).timeout(env.defaultTimeoutMillis().millis) + }.unit.exit } yield assert(r)(succeeds(isUnit)) ) } diff --git a/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala b/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala index c3b9127..c67ed11 100644 --- a/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala @@ -7,7 +7,6 @@ import org.testng.annotations.Test import zio.Task import zio.UIO import zio.ZIO -import zio.blocking._ import zio.stream.Stream import zio.test._ import zio.test.Assertion._ @@ -50,14 +49,18 @@ object StreamToPublisherSpec extends DefaultRunnableSpec { case method if method.getName().startsWith("untested") => test(method.getName())(assert(())(anything)) @@ TestAspect.ignore case method => - testM(method.getName())( + test(method.getName())( for { runtime <- ZIO.runtime[Any] pv = makePV(runtime) _ <- UIO(pv.setUp()) - r <- blocking(Task(method.invoke(pv))).unit.refineOrDie { case e: InvocationTargetException => - e.getTargetException() - }.run + r <- Task + .attemptBlockingInterrupt(method.invoke(pv)) + .unit + .refineOrDie { case e: InvocationTargetException => + e.getTargetException() + } + .exit } yield assert(r)(succeeds(isUnit)) ) } diff --git a/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala b/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala index 6686224..53c317a 100644 --- a/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala @@ -3,28 +3,28 @@ package zio.interop.reactivestreams import org.reactivestreams.tck.TestEnvironment import org.reactivestreams.tck.TestEnvironment.ManualSubscriberWithSubscriptionSupport import scala.jdk.CollectionConverters._ -import zio.{ Task, UIO, ZIO } -import zio.blocking._ +import zio.{ Task, UIO } import zio.stream.Stream import zio.test._ import zio.test.Assertion._ +import zio.IO object SubscriberToSinkSpec extends DefaultRunnableSpec { override def spec = suite("Converting a `Subscriber` to a `Sink`")( - testM("works on the happy path") { + test("works on the happy path") { for { probe <- makeSubscriber errorSink <- probe.underlying.toSink[Throwable] (error, sink) = errorSink fiber <- Stream.fromIterable(seq).run(sink).fork _ <- probe.request(length + 1) - elements <- probe.nextElements(length).run - completion <- probe.expectCompletion.run + elements <- probe.nextElements(length).exit + completion <- probe.expectCompletion.exit _ <- fiber.join } yield assert(elements)(succeeds(equalTo(seq))) && assert(completion)(succeeds(isUnit)) }, - testM("transports errors") { + test("transports errors") { for { probe <- makeSubscriber errorSink <- probe.underlying.toSink[Throwable] @@ -32,8 +32,8 @@ object SubscriberToSinkSpec extends DefaultRunnableSpec { fiber <- (Stream.fromIterable(seq) ++ Stream.fail(e)).run(sink).catchAll(t => error.fail(t)).fork _ <- probe.request(length + 1) - elements <- probe.nextElements(length).run - err <- probe.expectError.run + elements <- probe.nextElements(length).exit + err <- probe.expectError.exit _ <- fiber.join } yield assert(elements)(succeeds(equalTo(seq))) && assert(err)(succeeds(equalTo(e))) } @@ -46,12 +46,12 @@ object SubscriberToSinkSpec extends DefaultRunnableSpec { case class Probe[T](underlying: ManualSubscriberWithSubscriptionSupport[T]) { def request(n: Long): UIO[Unit] = UIO(underlying.request(n)) - def nextElements(n: Long): ZIO[Blocking, Throwable, List[T]] = - blocking(Task(underlying.nextElements(n.toLong).asScala.toList)) - def expectError: ZIO[Blocking, Throwable, Throwable] = - blocking(Task(underlying.expectError(classOf[Throwable]))) - def expectCompletion: ZIO[Blocking, Throwable, Unit] = - blocking(Task(underlying.expectCompletion())) + def nextElements(n: Long): IO[Throwable, List[T]] = + Task.attemptBlockingInterrupt(underlying.nextElements(n.toLong).asScala.toList) + def expectError: IO[Throwable, Throwable] = + Task.attemptBlockingInterrupt(underlying.expectError(classOf[Throwable])) + def expectCompletion: IO[Throwable, Unit] = + Task.attemptBlockingInterrupt(underlying.expectCompletion()) } val makeSubscriber = UIO(new ManualSubscriberWithSubscriptionSupport[Int](new TestEnvironment(2000))).map(Probe.apply) From 0dffb691d9ed6bc5ff78dc0124f1ebfd0a057069 Mon Sep 17 00:00:00 2001 From: Simon Schenk Date: Mon, 25 Oct 2021 08:25:17 +0200 Subject: [PATCH 02/23] fix: test run on single core --- .../SinkToSubscriberSpec.scala | 63 ++++++++++--------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala b/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala index ceb0d02..7c52b3d 100644 --- a/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala @@ -49,37 +49,42 @@ object SinkToSubscriberSpec extends DefaultRunnableSpec { } yield assert(r)(succeeds(equalTo(List(1, 2, 3, 4, 5)))) ), test("cancels subscription on interruption after subscription")( - for { - (publisher, subscribed, _, canceled) <- makePublisherProbe - fiber <- Sink.drain - .toSubscriber() - .use { case (subscriber, _) => UIO(publisher.subscribe(subscriber)) *> UIO.never } - .fork - _ <- Live.live( - assertM(subscribed.await.timeoutFail("timeout awaiting subscribe.")(500.millis).exit)(succeeds(isUnit)) - ) - _ <- fiber.interrupt - _ <- Live.live( - assertM(canceled.await.timeoutFail("timeout awaiting cancel.")(500.millis).exit)(succeeds(isUnit)) - ) - r <- fiber.join.exit - } yield assert(r)(isInterrupted) + ZIO.blocking( + for { + (publisher, subscribed, _, canceled) <- makePublisherProbe + fiber <- Sink.drain + .toSubscriber() + .use { case (subscriber, _) => UIO(publisher.subscribe(subscriber)) *> UIO.never } + .fork + _ <- + Live.live( + assertM(subscribed.await.timeoutFail("timeout awaiting subscribe.")(500.millis).exit)(succeeds(isUnit)) + ) + _ <- fiber.interrupt + _ <- Live.live( + assertM(canceled.await.timeoutFail("timeout awaiting cancel.")(500.millis).exit)(succeeds(isUnit)) + ) + r <- fiber.join.exit + } yield assert(r)(isInterrupted) + ) ), test("cancels subscription on interruption during consuption")( - for { - (publisher, subscribed, requested, canceled) <- makePublisherProbe - fiber <- Sink.drain - .toSubscriber() - .use { case (subscriber, _) => - Task.attemptBlockingInterrupt(publisher.subscribe(subscriber)) *> UIO.never - } - .fork - _ <- assertM(subscribed.await.exit)(succeeds(isUnit)) - _ <- assertM(requested.await.exit)(succeeds(isUnit)) - _ <- fiber.interrupt - _ <- assertM(canceled.await.exit)(succeeds(isUnit)) - r <- fiber.join.exit - } yield assert(r)(isInterrupted) + ZIO.blocking( + for { + (publisher, subscribed, requested, canceled) <- makePublisherProbe + fiber <- Sink.drain + .toSubscriber() + .use { case (subscriber, _) => + Task.attemptBlockingInterrupt(publisher.subscribe(subscriber)) *> UIO.never + } + .fork + _ <- assertM(subscribed.await.exit)(succeeds(isUnit)) + _ <- assertM(requested.await.exit)(succeeds(isUnit)) + _ <- fiber.interrupt + _ <- assertM(canceled.await.exit)(succeeds(isUnit)) + r <- fiber.join.exit + } yield assert(r)(isInterrupted) + ) ), suite("passes all required and optional TCK tests")( tests: _* From 3e6271d0371edd46043f237a5a8e8e78be4e909e Mon Sep 17 00:00:00 2001 From: Simon Schenk Date: Mon, 25 Oct 2021 15:42:17 +0200 Subject: [PATCH 03/23] fix: run on a single thread without blocking --- .../interop/reactivestreams/Adapters.scala | 2 +- .../PublisherToStreamSpec.scala | 5 +- .../SinkToSubscriberSpec.scala | 73 +++++++++---------- 3 files changed, 38 insertions(+), 42 deletions(-) diff --git a/src/main/scala/zio/interop/reactivestreams/Adapters.scala b/src/main/scala/zio/interop/reactivestreams/Adapters.scala index 22d355c..4979856 100644 --- a/src/main/scala/zio/interop/reactivestreams/Adapters.scala +++ b/src/main/scala/zio/interop/reactivestreams/Adapters.scala @@ -137,7 +137,7 @@ object Adapters { override def onSubscribe(s: Subscription): Unit = if (s == null) { val e = new NullPointerException("s was null in onSubscribe") - runtime.unsafeRun(p.fail(e)) + p.unsafeDone(IO.fail(e)) throw e } else { runtime.unsafeRun( diff --git a/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala b/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala index 6c1e897..6ecc0a2 100644 --- a/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala @@ -74,14 +74,13 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { for { subscriberP <- Promise.make[Nothing, Subscriber[_]] cancelledLatch <- Promise.make[Nothing, Unit] - runtime <- ZIO.runtime[Any] subscription = new Subscription { override def request(x$1: Long): Unit = () - override def cancel(): Unit = runtime.unsafeRun(cancelledLatch.succeed(()).unit) + override def cancel(): Unit = cancelledLatch.unsafeDone(UIO.unit) } probe = new Publisher[Int] { override def subscribe(subscriber: Subscriber[_ >: Int]): Unit = - runtime.unsafeRun(subscriberP.succeed(subscriber).unit) + subscriberP.unsafeDone(UIO.succeedNow(subscriber)) } fiber <- probe.toStream(bufferSize).run(Sink.drain).fork subscriber <- subscriberP.await diff --git a/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala b/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala index 7c52b3d..0481e99 100644 --- a/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala @@ -49,42 +49,40 @@ object SinkToSubscriberSpec extends DefaultRunnableSpec { } yield assert(r)(succeeds(equalTo(List(1, 2, 3, 4, 5)))) ), test("cancels subscription on interruption after subscription")( - ZIO.blocking( - for { - (publisher, subscribed, _, canceled) <- makePublisherProbe - fiber <- Sink.drain - .toSubscriber() - .use { case (subscriber, _) => UIO(publisher.subscribe(subscriber)) *> UIO.never } - .fork - _ <- - Live.live( - assertM(subscribed.await.timeoutFail("timeout awaiting subscribe.")(500.millis).exit)(succeeds(isUnit)) - ) - _ <- fiber.interrupt - _ <- Live.live( - assertM(canceled.await.timeoutFail("timeout awaiting cancel.")(500.millis).exit)(succeeds(isUnit)) - ) - r <- fiber.join.exit - } yield assert(r)(isInterrupted) - ) + for { + (publisher, subscribed, _, canceled) <- makePublisherProbe + fiber <- Sink + .foreachChunk[Any, Nothing, Int](_ => ZIO.yieldNow) + .toSubscriber() + .use { case (subscriber, _) => UIO(publisher.subscribe(subscriber)) *> UIO.never } + .fork + _ <- + Live.live( + assertM(subscribed.await.timeoutFail("timeout awaiting subscribe.")(500.millis).exit)(succeeds(isUnit)) + ) + _ <- fiber.interrupt + _ <- Live.live( + assertM(canceled.await.timeoutFail("timeout awaiting cancel.")(500.millis).exit)(succeeds(isUnit)) + ) + r <- fiber.join.exit + } yield assert(r)(isInterrupted) ), test("cancels subscription on interruption during consuption")( - ZIO.blocking( - for { - (publisher, subscribed, requested, canceled) <- makePublisherProbe - fiber <- Sink.drain - .toSubscriber() - .use { case (subscriber, _) => - Task.attemptBlockingInterrupt(publisher.subscribe(subscriber)) *> UIO.never - } - .fork - _ <- assertM(subscribed.await.exit)(succeeds(isUnit)) - _ <- assertM(requested.await.exit)(succeeds(isUnit)) - _ <- fiber.interrupt - _ <- assertM(canceled.await.exit)(succeeds(isUnit)) - r <- fiber.join.exit - } yield assert(r)(isInterrupted) - ) + for { + (publisher, subscribed, requested, canceled) <- makePublisherProbe + fiber <- Sink + .foreachChunk[Any, Nothing, Int](_ => ZIO.yieldNow) + .toSubscriber() + .use { case (subscriber, _) => + Task.attemptBlockingInterrupt(publisher.subscribe(subscriber)) *> UIO.never + } + .fork + _ <- assertM(subscribed.await.exit)(succeeds(isUnit)) + _ <- assertM(requested.await.exit)(succeeds(isUnit)) + _ <- fiber.interrupt + _ <- assertM(canceled.await.exit)(succeeds(isUnit)) + r <- fiber.join.exit + } yield assert(r)(isInterrupted) ), suite("passes all required and optional TCK tests")( tests: _* @@ -93,7 +91,6 @@ object SinkToSubscriberSpec extends DefaultRunnableSpec { val makePublisherProbe = for { - runtime <- ZIO.runtime[Any] subscribed <- Promise.make[Nothing, Unit] requested <- Promise.make[Nothing, Unit] canceled <- Promise.make[Nothing, Unit] @@ -102,14 +99,14 @@ object SinkToSubscriberSpec extends DefaultRunnableSpec { s.onSubscribe( new Subscription { override def request(n: Long): Unit = { - runtime.unsafeRun(requested.succeed(()).unit) + requested.unsafeDone(UIO.unit) (1 to n.toInt).foreach(s.onNext(_)) } override def cancel(): Unit = - runtime.unsafeRun(canceled.succeed(()).unit) + canceled.unsafeDone(UIO.unit) } ) - runtime.unsafeRun(subscribed.succeed(()).unit) + subscribed.unsafeDone(UIO.unit) } } } yield (publisher, subscribed, requested, canceled) From 47d92facb11141b67954304b07ecdbb2ee4784ae Mon Sep 17 00:00:00 2001 From: Simon Schenk Date: Mon, 25 Oct 2021 18:46:58 +0200 Subject: [PATCH 04/23] feat: one less daemon fiber (#209) --- .gitignore | 3 +- README.md | 2 +- .../interop/reactivestreams/Adapters.scala | 12 ++-- .../zio/interop/reactivestreams/package.scala | 21 ++++--- .../StreamToPublisherSpec.scala | 8 ++- .../SubscriberToSinkSpec.scala | 57 +++++++++++-------- 6 files changed, 58 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index 0c0e091..c7738ce 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ project/secrets.tar.xz project/travis-deploy-key project/zecret target -test-output +test-output/ +metals.sbt diff --git a/README.md b/README.md index ca8d54d..b46235c 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ on `Stream` failure. The type parameter on `toSink` is the error type of *the St val asSink = subscriber.toSink[Throwable] val failingStream = Stream.range(3, 13) ++ Stream.fail(new RuntimeException("boom!")) runtime.unsafeRun( - asSink.flatMap { case (errorP, sink) => + asSink.use { case (errorP, sink) => failingStream.run(sink).catchAll(errorP.fail) } ) diff --git a/src/main/scala/zio/interop/reactivestreams/Adapters.scala b/src/main/scala/zio/interop/reactivestreams/Adapters.scala index 4979856..1d2bdeb 100644 --- a/src/main/scala/zio/interop/reactivestreams/Adapters.scala +++ b/src/main/scala/zio/interop/reactivestreams/Adapters.scala @@ -32,14 +32,14 @@ object Adapters { def subscriberToSink[E <: Throwable, I]( subscriber: Subscriber[I] - ): UIO[(Promise[E, Nothing], ZSink[Any, Nothing, I, I, Unit])] = + ): ZManaged[Any, Nothing, (Promise[E, Nothing], ZSink[Any, Nothing, I, I, Unit])] = for { - runtime <- ZIO.runtime[Any] - demand <- Queue.unbounded[Long] - error <- Promise.make[E, Nothing] + runtime <- ZIO.runtime[Any].toManaged + demand <- Queue.unbounded[Long].toManaged + error <- Promise.make[E, Nothing].toManaged subscription = createSubscription(subscriber, demand, runtime) - _ <- UIO(subscriber.onSubscribe(subscription)) - _ <- error.await.catchAll(t => UIO(subscriber.onError(t)) *> demand.shutdown).forkDaemon + _ <- UIO(subscriber.onSubscribe(subscription)).toManaged + _ <- error.await.catchAll(t => UIO(subscriber.onError(t)) *> demand.shutdown).toManaged.fork } yield (error, demandUnfoldSink(subscriber, demand)) def publisherToStream[O](publisher: Publisher[O], bufferSize: Int): ZStream[Any, Throwable, O] = { diff --git a/src/main/scala/zio/interop/reactivestreams/package.scala b/src/main/scala/zio/interop/reactivestreams/package.scala index 926a9b1..090d260 100644 --- a/src/main/scala/zio/interop/reactivestreams/package.scala +++ b/src/main/scala/zio/interop/reactivestreams/package.scala @@ -1,8 +1,13 @@ package zio.interop -import org.reactivestreams.{ Publisher, Subscriber } -import zio.stream.{ ZSink, ZStream } -import zio.{ IO, Promise, UIO, ZIO, ZManaged } +import org.reactivestreams.Publisher +import org.reactivestreams.Subscriber +import zio.IO +import zio.Promise +import zio.ZIO +import zio.ZManaged +import zio.stream.ZSink +import zio.stream.ZStream package object reactivestreams { @@ -46,14 +51,12 @@ package object reactivestreams { * ``` * val subscriber: Subscriber[Int] = ??? * val stream: Stream[Any, Throwable, Int] = ??? - * for { - * sinkError <- subscriberToSink(subscriber) - * (error, sink) = sinkError - * _ <- stream.run(sink).catchAll(e => error.fail(e)).fork - * } yield () + * subscriber.toSink.use { case (error, sink) => + * stream.run(sink).catchAll(e => error.fail(e)) + * } * ``` */ - def toSink[E <: Throwable]: UIO[(Promise[E, Nothing], ZSink[Any, Nothing, I, I, Unit])] = + def toSink[E <: Throwable]: ZManaged[Any, Nothing, (Promise[E, Nothing], ZSink[Any, Nothing, I, I, Unit])] = Adapters.subscriberToSink(subscriber) } diff --git a/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala b/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala index c67ed11..1fb15f0 100644 --- a/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala @@ -1,15 +1,17 @@ package zio.interop.reactivestreams -import java.lang.reflect.InvocationTargetException import org.reactivestreams.Publisher -import org.reactivestreams.tck.{ PublisherVerification, TestEnvironment } +import org.reactivestreams.tck.PublisherVerification +import org.reactivestreams.tck.TestEnvironment import org.testng.annotations.Test import zio.Task import zio.UIO import zio.ZIO import zio.stream.Stream -import zio.test._ import zio.test.Assertion._ +import zio.test._ + +import java.lang.reflect.InvocationTargetException object StreamToPublisherSpec extends DefaultRunnableSpec { override def spec = diff --git a/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala b/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala index 53c317a..f75487e 100644 --- a/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala @@ -2,40 +2,47 @@ package zio.interop.reactivestreams import org.reactivestreams.tck.TestEnvironment import org.reactivestreams.tck.TestEnvironment.ManualSubscriberWithSubscriptionSupport -import scala.jdk.CollectionConverters._ -import zio.{ Task, UIO } +import zio.IO +import zio.Task +import zio.UIO import zio.stream.Stream -import zio.test._ import zio.test.Assertion._ -import zio.IO +import zio.test._ + +import scala.jdk.CollectionConverters._ object SubscriberToSinkSpec extends DefaultRunnableSpec { override def spec = suite("Converting a `Subscriber` to a `Sink`")( test("works on the happy path") { - for { - probe <- makeSubscriber - errorSink <- probe.underlying.toSink[Throwable] - (error, sink) = errorSink - fiber <- Stream.fromIterable(seq).run(sink).fork - _ <- probe.request(length + 1) - elements <- probe.nextElements(length).exit - completion <- probe.expectCompletion.exit - _ <- fiber.join - } yield assert(elements)(succeeds(equalTo(seq))) && assert(completion)(succeeds(isUnit)) + makeSubscriber.flatMap(probe => + probe.underlying + .toSink[Throwable] + .use { case (_, sink) => + for { + fiber <- Stream.fromIterable(seq).run(sink).fork + _ <- probe.request(length + 1) + elements <- probe.nextElements(length).exit + completion <- probe.expectCompletion.exit + _ <- fiber.join + } yield assert(elements)(succeeds(equalTo(seq))) && assert(completion)(succeeds(isUnit)) + } + ) }, test("transports errors") { - for { - probe <- makeSubscriber - errorSink <- probe.underlying.toSink[Throwable] - (error, sink) = errorSink - fiber <- (Stream.fromIterable(seq) ++ - Stream.fail(e)).run(sink).catchAll(t => error.fail(t)).fork - _ <- probe.request(length + 1) - elements <- probe.nextElements(length).exit - err <- probe.expectError.exit - _ <- fiber.join - } yield assert(elements)(succeeds(equalTo(seq))) && assert(err)(succeeds(equalTo(e))) + makeSubscriber.flatMap(probe => + probe.underlying + .toSink[Throwable] + .use { case (error, sink) => + for { + fiber <- (Stream.fromIterable(seq) ++ Stream.fail(e)).run(sink).catchAll(t => error.fail(t)).fork + _ <- probe.request(length + 1) + elements <- probe.nextElements(length).exit + err <- probe.expectError.exit + _ <- fiber.join + } yield assert(elements)(succeeds(equalTo(seq))) && assert(err)(succeeds(equalTo(e))) + } + ) } ) From d0c2867d154b3479487ddc51fd7ef499459add19 Mon Sep 17 00:00:00 2001 From: Simon Schenk Date: Mon, 1 Nov 2021 21:05:31 +0100 Subject: [PATCH 05/23] feat: change all parameters to by-name --- .../interop/reactivestreams/Adapters.scala | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/main/scala/zio/interop/reactivestreams/Adapters.scala b/src/main/scala/zio/interop/reactivestreams/Adapters.scala index 1d2bdeb..00042bd 100644 --- a/src/main/scala/zio/interop/reactivestreams/Adapters.scala +++ b/src/main/scala/zio/interop/reactivestreams/Adapters.scala @@ -12,7 +12,7 @@ import zio.stream.ZStream.Pull object Adapters { - def streamToPublisher[R, E <: Throwable, O](stream: ZStream[R, E, O]): ZIO[R, Nothing, Publisher[O]] = + def streamToPublisher[R, E <: Throwable, O](stream: => ZStream[R, E, O]): ZIO[R, Nothing, Publisher[O]] = ZIO.runtime.map { runtime => subscriber => if (subscriber == null) { throw new NullPointerException("Subscriber must not be null.") @@ -31,18 +31,20 @@ object Adapters { } def subscriberToSink[E <: Throwable, I]( - subscriber: Subscriber[I] - ): ZManaged[Any, Nothing, (Promise[E, Nothing], ZSink[Any, Nothing, I, I, Unit])] = + subscriber: => Subscriber[I] + ): ZManaged[Any, Nothing, (Promise[E, Nothing], ZSink[Any, Nothing, I, I, Unit])] = { + val sub = subscriber for { runtime <- ZIO.runtime[Any].toManaged demand <- Queue.unbounded[Long].toManaged error <- Promise.make[E, Nothing].toManaged - subscription = createSubscription(subscriber, demand, runtime) - _ <- UIO(subscriber.onSubscribe(subscription)).toManaged - _ <- error.await.catchAll(t => UIO(subscriber.onError(t)) *> demand.shutdown).toManaged.fork - } yield (error, demandUnfoldSink(subscriber, demand)) + subscription = createSubscription(sub, demand, runtime) + _ <- UIO(sub.onSubscribe(subscription)).toManaged + _ <- error.await.catchAll(t => UIO(sub.onError(t)) *> demand.shutdown).toManaged.fork + } yield (error, demandUnfoldSink(sub, demand)) + } - def publisherToStream[O](publisher: Publisher[O], bufferSize: Int): ZStream[Any, Throwable, O] = { + def publisherToStream[O](publisher: => Publisher[O], bufferSize: => Int): ZStream[Any, Throwable, O] = { val pullOrFail = for { subscriberP <- makeSubscriber[O](bufferSize) @@ -57,8 +59,8 @@ object Adapters { } def sinkToSubscriber[R, I, L, Z]( - sink: ZSink[R, Throwable, I, L, Z], - bufferSize: Int + sink: => ZSink[R, Throwable, I, L, Z], + bufferSize: => Int ): ZManaged[R, Throwable, (Subscriber[I], IO[Throwable, Z])] = for { subscriberP <- makeSubscriber[I](bufferSize) @@ -174,7 +176,7 @@ object Adapters { (subscriber, p) } - def demandUnfoldSink[I]( + private def demandUnfoldSink[I]( subscriber: Subscriber[_ >: I], demand: Queue[Long] ): ZSink[Any, Nothing, I, I, Unit] = @@ -198,7 +200,7 @@ object Adapters { } .mapZIO(_ => demand.isShutdown.flatMap(is => UIO(subscriber.onComplete()).when(!is).unit)) - def createSubscription[A]( + private def createSubscription[A]( subscriber: Subscriber[_ >: A], demand: Queue[Long], runtime: Runtime[_] From 821daceb789480a07cda45b66f95e678598e07bd Mon Sep 17 00:00:00 2001 From: Simon Schenk Date: Mon, 1 Nov 2021 21:18:03 +0100 Subject: [PATCH 06/23] feat: add ZTraceElement to all public methods --- .../zio/interop/reactivestreams/Adapters.scala | 16 +++++++++++----- .../zio/interop/reactivestreams/package.scala | 13 +++++++++---- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/main/scala/zio/interop/reactivestreams/Adapters.scala b/src/main/scala/zio/interop/reactivestreams/Adapters.scala index 00042bd..8786731 100644 --- a/src/main/scala/zio/interop/reactivestreams/Adapters.scala +++ b/src/main/scala/zio/interop/reactivestreams/Adapters.scala @@ -12,7 +12,9 @@ import zio.stream.ZStream.Pull object Adapters { - def streamToPublisher[R, E <: Throwable, O](stream: => ZStream[R, E, O]): ZIO[R, Nothing, Publisher[O]] = + def streamToPublisher[R, E <: Throwable, O]( + stream: => ZStream[R, E, O] + )(implicit trace: ZTraceElement): ZIO[R, Nothing, Publisher[O]] = ZIO.runtime.map { runtime => subscriber => if (subscriber == null) { throw new NullPointerException("Subscriber must not be null.") @@ -32,7 +34,7 @@ object Adapters { def subscriberToSink[E <: Throwable, I]( subscriber: => Subscriber[I] - ): ZManaged[Any, Nothing, (Promise[E, Nothing], ZSink[Any, Nothing, I, I, Unit])] = { + )(implicit trace: ZTraceElement): ZManaged[Any, Nothing, (Promise[E, Nothing], ZSink[Any, Nothing, I, I, Unit])] = { val sub = subscriber for { runtime <- ZIO.runtime[Any].toManaged @@ -44,7 +46,9 @@ object Adapters { } yield (error, demandUnfoldSink(sub, demand)) } - def publisherToStream[O](publisher: => Publisher[O], bufferSize: => Int): ZStream[Any, Throwable, O] = { + def publisherToStream[O](publisher: => Publisher[O], bufferSize: => Int)(implicit + trace: ZTraceElement + ): ZStream[Any, Throwable, O] = { val pullOrFail = for { subscriberP <- makeSubscriber[O](bufferSize) @@ -61,7 +65,7 @@ object Adapters { def sinkToSubscriber[R, I, L, Z]( sink: => ZSink[R, Throwable, I, L, Z], bufferSize: => Int - ): ZManaged[R, Throwable, (Subscriber[I], IO[Throwable, Z])] = + )(implicit trace: ZTraceElement): ZManaged[R, Throwable, (Subscriber[I], IO[Throwable, Z])] = for { subscriberP <- makeSubscriber[I](bufferSize) (subscriber, p) = subscriberP @@ -73,7 +77,7 @@ object Adapters { private def process[R, A]( q: Queue[Exit[Option[Throwable], A]], sub: Subscription - ): ZManaged[Any, Nothing, ZIO[Any, Option[Throwable], Chunk[A]]] = { + )(implicit trace: ZTraceElement): ZManaged[Any, Nothing, ZIO[Any, Option[Throwable], Chunk[A]]] = { val capacity = q.capacity.toLong - 1 // leave space for End or Fail for { _ <- ZManaged.succeed(sub.request(capacity)) @@ -120,6 +124,8 @@ object Adapters { private def makeSubscriber[A]( capacity: Int + )(implicit + trace: ZTraceElement ): UManaged[(Subscriber[A], Promise[Throwable, (Subscription, Queue[Exit[Option[Throwable], A]])])] = for { q <- Queue diff --git a/src/main/scala/zio/interop/reactivestreams/package.scala b/src/main/scala/zio/interop/reactivestreams/package.scala index 090d260..3795842 100644 --- a/src/main/scala/zio/interop/reactivestreams/package.scala +++ b/src/main/scala/zio/interop/reactivestreams/package.scala @@ -6,6 +6,7 @@ import zio.IO import zio.Promise import zio.ZIO import zio.ZManaged +import zio.ZTraceElement import zio.stream.ZSink import zio.stream.ZStream @@ -16,7 +17,7 @@ package object reactivestreams { /** Create a `Publisher` from a `Stream`. Every time the `Publisher` is subscribed to, a new instance of the * `Stream` is run. */ - def toPublisher: ZIO[R, Nothing, Publisher[O]] = + def toPublisher(implicit trace: ZTraceElement): ZIO[R, Nothing, Publisher[O]] = Adapters.streamToPublisher(stream) } @@ -30,7 +31,9 @@ package object reactivestreams { * The size used as internal buffer. A maximum of `qSize-1` `A`s will be buffered, so `qSize` must be > 1. If * possible, set to a power of 2 value for best performance. */ - def toSubscriber(qSize: Int = 16): ZManaged[R, Throwable, (Subscriber[A], IO[Throwable, Z])] = + def toSubscriber(qSize: Int = 16)(implicit + trace: ZTraceElement + ): ZManaged[R, Throwable, (Subscriber[A], IO[Throwable, Z])] = Adapters.sinkToSubscriber(sink, qSize) } @@ -40,7 +43,7 @@ package object reactivestreams { * The size used as internal buffer. A maximum of `qSize-1` `A`s will be buffered, so `qSize` must be > 1. If * possible, set to a power of 2 value for best performance. */ - def toStream(qSize: Int = 16): ZStream[Any, Throwable, O] = + def toStream(qSize: Int = 16)(implicit trace: ZTraceElement): ZStream[Any, Throwable, O] = Adapters.publisherToStream(publisher, qSize) } @@ -56,7 +59,9 @@ package object reactivestreams { * } * ``` */ - def toSink[E <: Throwable]: ZManaged[Any, Nothing, (Promise[E, Nothing], ZSink[Any, Nothing, I, I, Unit])] = + def toSink[E <: Throwable](implicit + trace: ZTraceElement + ): ZManaged[Any, Nothing, (Promise[E, Nothing], ZSink[Any, Nothing, I, I, Unit])] = Adapters.subscriberToSink(subscriber) } From c272098c55e43052255d50a28c81351186998bf3 Mon Sep 17 00:00:00 2001 From: Simon Schenk Date: Thu, 11 Nov 2021 12:42:10 +0100 Subject: [PATCH 07/23] feat: no unsafeRun in subscriber (#286) * feat: no unsafeRun in subscriber * fix: formatting * fix: race condition during interruption --- build.sbt | 2 + .../interop/reactivestreams/Adapters.scala | 198 ++++++++++-------- .../PublisherToStreamSpec.scala | 2 +- 3 files changed, 116 insertions(+), 86 deletions(-) diff --git a/build.sbt b/build.sbt index 73f6c51..61cfd84 100644 --- a/build.sbt +++ b/build.sbt @@ -63,3 +63,5 @@ lazy val interopReactiveStreams = project Seq("org.scala-lang.modules" %% "scala-collection-compat" % collCompatVersion % Test) } ) + // .settings(Test / javaOptions += "-XX:ActiveProcessorCount=1") // uncomment to test for deadlocks + .settings(Test / fork := true) diff --git a/src/main/scala/zio/interop/reactivestreams/Adapters.scala b/src/main/scala/zio/interop/reactivestreams/Adapters.scala index 8786731..51b78b8 100644 --- a/src/main/scala/zio/interop/reactivestreams/Adapters.scala +++ b/src/main/scala/zio/interop/reactivestreams/Adapters.scala @@ -3,13 +3,14 @@ package zio.interop.reactivestreams import org.reactivestreams.Publisher import org.reactivestreams.Subscriber import org.reactivestreams.Subscription -import zio.Exit.Failure -import zio.Exit.Success import zio._ +import zio.internal.RingBuffer import zio.stream.ZSink import zio.stream.ZStream import zio.stream.ZStream.Pull +import java.util.concurrent.atomic.AtomicBoolean + object Adapters { def streamToPublisher[R, E <: Throwable, O]( @@ -46,17 +47,19 @@ object Adapters { } yield (error, demandUnfoldSink(sub, demand)) } - def publisherToStream[O](publisher: => Publisher[O], bufferSize: => Int)(implicit - trace: ZTraceElement - ): ZStream[Any, Throwable, O] = { + def publisherToStream[O]( + publisher: => Publisher[O], + bufferSize: => Int + )(implicit trace: ZTraceElement): ZStream[Any, Throwable, O] = { val pullOrFail = for { subscriberP <- makeSubscriber[O](bufferSize) (subscriber, p) = subscriberP + _ <- ZManaged.finalizer(UIO(subscriber.interrupt())) _ <- ZManaged.succeed(publisher.subscribe(subscriber)) subQ <- p.await.toManaged (sub, q) = subQ - process <- process(q, sub) + process <- process(sub, q, () => subscriber.await(), () => subscriber.isDone) } yield process val pull = pullOrFail.catchAll(e => ZManaged.succeed(Pull.fail(e))) ZStream(pull) @@ -69,78 +72,95 @@ object Adapters { for { subscriberP <- makeSubscriber[I](bufferSize) (subscriber, p) = subscriberP - pull = p.await.toManaged.flatMap { case (subscription, q) => process(q, subscription) } + pull = p.await.toManaged.flatMap { case (subscription, q) => + process(subscription, q, () => subscriber.await(), () => subscriber.isDone, bufferSize) + } .catchAll(e => ZManaged.succeedNow(Pull.fail(e))) fiber <- ZStream(pull).run(sink).toManaged.fork } yield (subscriber, fiber.join) - private def process[R, A]( - q: Queue[Exit[Option[Throwable], A]], - sub: Subscription - )(implicit trace: ZTraceElement): ZManaged[Any, Nothing, ZIO[Any, Option[Throwable], Chunk[A]]] = { - val capacity = q.capacity.toLong - 1 // leave space for End or Fail + private def process[A]( + sub: Subscription, + q: RingBuffer[A], + await: () => IO[Option[Throwable], Unit], + isDone: () => Boolean, + maxChunkSize: Int = Int.MaxValue + ): ZManaged[Any, Nothing, ZIO[Any, Option[Throwable], Chunk[A]]] = for { - _ <- ZManaged.succeed(sub.request(capacity)) - requested <- Ref.Synchronized.makeManaged(capacity) - lastP <- Promise.makeManaged[Option[Throwable], Chunk[A]] + _ <- ZManaged.succeed(sub.request(q.capacity.toLong)) + requestedRef <- Ref.makeManaged(q.capacity.toLong) // TODO: maybe turn into unfold? } yield { - - def takesToPull( - takes: Chunk[Exit[Option[Throwable], A]] - ): Pull[Any, Throwable, A] = { - val toEmit = takes.collectWhile { case Exit.Success(a) => a } - val pull = Pull.emit(toEmit) - if (toEmit.size == takes.size) { - val chunkSize = toEmit.size - val request = - requested.getAndUpdateZIO { - case `chunkSize` => UIO(sub.request(capacity)).as(capacity) - case n => UIO.succeedNow(n - chunkSize) - } - request *> pull - } else { - val failure = takes.drop(toEmit.size).head - failure match { - case Failure(cause) => - val last = - Cause.flipCauseOption(cause) match { - case None => Pull.end - case Some(cause) => Pull.failCause(cause) - } - if (toEmit.isEmpty) last else lastP.complete(last) *> pull - - case Success(_) => pull // should never happen - } - } - } - - lastP.isDone.flatMap { - case true => lastP.await - case false => q.takeBetween(1, q.capacity).flatMap(takesToPull) - } - + def pull: Pull[Any, Throwable, A] = + for { + requested <- requestedRef.get + pollSize = Math.min(requested, maxChunkSize.toLong).toInt + chunk <- UIO(q.pollUpTo(pollSize)) + r <- + if (chunk.isEmpty) + await() *> pull + else + (if (chunk.size == pollSize && !isDone()) + UIO(sub.request(q.capacity.toLong)) *> requestedRef.set(q.capacity.toLong) + else requestedRef.set(requested - chunk.size)) *> + Pull.emit(chunk) + } yield r + + pull } + + private trait InterruptibleSubscriber[A] extends Subscriber[A] { + def interrupt(): Unit + def await(): IO[Option[Throwable], Unit] + def isDone: Boolean } private def makeSubscriber[A]( capacity: Int - )(implicit - trace: ZTraceElement - ): UManaged[(Subscriber[A], Promise[Throwable, (Subscription, Queue[Exit[Option[Throwable], A]])])] = + ): UManaged[ + ( + InterruptibleSubscriber[A], + Promise[Throwable, (Subscription, RingBuffer[A])] + ) + ] = for { - q <- Queue - .bounded[Exit[Option[Throwable], A]](capacity) - .toManagedWith(_.shutdown) - p <- Promise - .make[Throwable, (Subscription, Queue[Exit[Option[Throwable], A]])] - .toManagedWith( - _.poll.flatMap(_.fold(UIO.unit)(_.foldZIO(_ => UIO.unit, { case (sub, _) => UIO(sub.cancel()) }))) - ) - runtime <- ZManaged.runtime[Any] + q <- ZManaged.succeed(RingBuffer[A](capacity)) + p <- + Promise + .make[Throwable, (Subscription, RingBuffer[A])] + .toManagedWith( + _.poll.flatMap(_.fold(UIO.unit)(_.foldZIO(_ => UIO.unit, { case (sub, _) => UIO(sub.cancel()) }))) + ) } yield { - val subscriber = - new Subscriber[A] { + new InterruptibleSubscriber[A] { + + val isSubscribedOrInterrupted = new AtomicBoolean + @volatile + var done: Option[Option[Throwable]] = None + @volatile + var toNotify: Option[Promise[Option[Throwable], Unit]] = None + + override def interrupt(): Unit = isSubscribedOrInterrupted.set(true) + + override def await(): IO[Option[Throwable], Unit] = + done match { + case Some(value) => IO.fail(value) + case None => + val p = Promise.unsafeMake[Option[Throwable], Unit](FiberId.None) + toNotify = Some(p) + // An element has arrived in the meantime, we do not need to start waiting. + if (!q.isEmpty()) { + toNotify = None + IO.unit + } else + done.fold(p.await) { e => + // The producer has canceled or errored in the meantime. + toNotify = None + IO.fail(e) + } + } + + override def isDone: Boolean = done.isDefined override def onSubscribe(s: Subscription): Unit = if (s == null) { @@ -148,37 +168,45 @@ object Adapters { p.unsafeDone(IO.fail(e)) throw e } else { - runtime.unsafeRun( - p.succeed((s, q)).flatMap { - // `whenM(q.isShutdown)`, the Stream has been interrupted or completed before we received `onSubscribe` - case true => UIO(s.cancel()).whenZIO(q.isShutdown).unit - case false => UIO(s.cancel()) - } - ) + val shouldCancel = isSubscribedOrInterrupted.getAndSet(true) + if (shouldCancel) + s.cancel() + else + p.unsafeDone(UIO.succeedNow((s, q))) } override def onNext(t: A): Unit = if (t == null) { - val e = new NullPointerException("t was null in onNext") - runtime.unsafeRun(q.offer(Exit.fail(Some(e)))) - throw e + failNPE("t was null in onNext") } else { - runtime.unsafeRunSync(q.offer(Exit.succeed(t))) - () + q.offer(t) + toNotify.foreach(_.unsafeDone(IO.unit)) } override def onError(e: Throwable): Unit = - if (e == null) { - val e = new NullPointerException("t was null in onError") - runtime.unsafeRun(q.offer(Exit.fail(Some(e)))) - throw e - } else { - runtime.unsafeRun(q.offer(Exit.fail(Some(e))).unit) - } + if (e == null) + failNPE("t was null in onError") + else + fail(e) + + override def onComplete(): Unit = { + done = Some(None) + toNotify.foreach(_.unsafeDone(IO.fail(None))) + } + + private def failNPE(msg: String) = { + val e = new NullPointerException(msg) + fail(e) + throw e + } + + private def fail(e: Throwable) = { + done = Some(Some(e)) + toNotify.foreach(_.unsafeDone(IO.fail(Some(e)))) + } - override def onComplete(): Unit = - runtime.unsafeRun(q.offer(Exit.fail(None)).unit) } + (subscriber, p) } diff --git a/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala b/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala index 6ecc0a2..47e67de 100644 --- a/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala @@ -141,7 +141,7 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { ) ) } - ) + ) @@ TestAspect.nonFlaky val e: Throwable = new RuntimeException("boom") val seq: Chunk[Int] = Chunk.fromIterable(List.range(0, 100)) From d3d3457ac3e22fb7d9350b0296edc3b80524e456 Mon Sep 17 00:00:00 2001 From: Simon Schenk Date: Sat, 13 Nov 2021 16:24:50 +0100 Subject: [PATCH 08/23] version bumps --- .github/workflows/ci.yml | 6 +++--- project/plugins.sbt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8d7bf4..b4be7c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: fail-fast: false steps: - name: Checkout current branch - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.4.0 with: fetch-depth: 0 - name: Setup Scala and Java @@ -37,7 +37,7 @@ jobs: scala: ['2.11.12', '2.12.15', '2.13.6', '3.0.2'] steps: - name: Checkout current branch - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.4.0 with: fetch-depth: 0 - name: Setup Scala and Java @@ -55,7 +55,7 @@ jobs: if: github.event_name != 'pull_request' steps: - name: Checkout current branch - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.4.0 with: fetch-depth: 0 - name: Setup Scala and Java diff --git a/project/plugins.sbt b/project/plugins.sbt index 773cf95..2b18689 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.9-5-657832eb") +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.11") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.31") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.10.0") addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.7") From 1fcfb3cc75384d80f52c1099d237b77cdac388b2 Mon Sep 17 00:00:00 2001 From: Simon Schenk Date: Tue, 16 Nov 2021 21:14:21 +0100 Subject: [PATCH 09/23] fix: do not enqueue invalid request (#290) --- src/main/scala/zio/interop/reactivestreams/Adapters.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/scala/zio/interop/reactivestreams/Adapters.scala b/src/main/scala/zio/interop/reactivestreams/Adapters.scala index 51b78b8..1226869 100644 --- a/src/main/scala/zio/interop/reactivestreams/Adapters.scala +++ b/src/main/scala/zio/interop/reactivestreams/Adapters.scala @@ -240,10 +240,9 @@ object Adapters { runtime: Runtime[_] ): Subscription = new Subscription { - override def request(n: Long): Unit = { + override def request(n: Long): Unit = if (n <= 0) subscriber.onError(new IllegalArgumentException("non-positive subscription request")) - runtime.unsafeRunAsync(demand.offer(n)) - } + else runtime.unsafeRunAsync(demand.offer(n)) override def cancel(): Unit = runtime.unsafeRun(demand.shutdown) } } From 3c6afc9b27cce4d13ee5743cb2ff7c11b78e49bd Mon Sep 17 00:00:00 2001 From: Simon Schenk Date: Fri, 17 Dec 2021 13:26:31 +0100 Subject: [PATCH 10/23] Zio 2.0.0-RC1 (#297) * ZIO 2.0-RC1 * test does not fail a fiber on failing `Publisher` * fix cancels subscription when interrupted before subscription * fix: workaround run(Sink.drain) bug in RC1 * tests green, occasional flakiness in "cancels subscription when interrupted after subscription" * fix: Scala 3 version * fix: CI * fix: allo interruption tests to be flaky --- .github/workflows/ci.yml | 2 +- build.sbt | 2 +- project/BuildHelper.scala | 2 +- .../interop/reactivestreams/Adapters.scala | 26 ++++--- .../zio/interop/reactivestreams/package.scala | 3 +- .../PublisherToStreamSpec.scala | 70 ++++++++++++------- .../SinkToSubscriberSpec.scala | 5 +- 7 files changed, 67 insertions(+), 43 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4be7c5..4b918be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: fail-fast: false matrix: java: ['adopt@1.8', 'adopt@1.11'] - scala: ['2.11.12', '2.12.15', '2.13.6', '3.0.2'] + scala: ['2.11.12', '2.12.15', '2.13.6', '3.1.0'] steps: - name: Checkout current branch uses: actions/checkout@v2.4.0 diff --git a/build.sbt b/build.sbt index 61cfd84..c50af37 100644 --- a/build.sbt +++ b/build.sbt @@ -36,7 +36,7 @@ Global / onChangedBuildSource := ReloadOnSourceChanges addCommandAlias("fmt", "all scalafmtSbt scalafmt test:scalafmt") addCommandAlias("check", "all scalafmtSbtCheck scalafmtCheck test:scalafmtCheck") -val zioVersion = "2.0.0-M4" +val zioVersion = "2.0.0-RC1" val rsVersion = "1.0.3" val collCompatVersion = "2.5.0" diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index ee3d2d1..538f8d5 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -11,7 +11,7 @@ object BuildHelper { val Scala211 = "2.11.12" val Scala212 = "2.12.15" val Scala213 = "2.13.6" - val ScalaDotty = "3.0.2" + val ScalaDotty = "3.1.0" private val stdOptions = Seq( "-deprecation", diff --git a/src/main/scala/zio/interop/reactivestreams/Adapters.scala b/src/main/scala/zio/interop/reactivestreams/Adapters.scala index 1226869..4e247d1 100644 --- a/src/main/scala/zio/interop/reactivestreams/Adapters.scala +++ b/src/main/scala/zio/interop/reactivestreams/Adapters.scala @@ -42,7 +42,7 @@ object Adapters { demand <- Queue.unbounded[Long].toManaged error <- Promise.make[E, Nothing].toManaged subscription = createSubscription(sub, demand, runtime) - _ <- UIO(sub.onSubscribe(subscription)).toManaged + _ <- ZManaged.succeed(sub.onSubscribe(subscription)) _ <- error.await.catchAll(t => UIO(sub.onError(t)) *> demand.shutdown).toManaged.fork } yield (error, demandUnfoldSink(sub, demand)) } @@ -51,18 +51,21 @@ object Adapters { publisher: => Publisher[O], bufferSize: => Int )(implicit trace: ZTraceElement): ZStream[Any, Throwable, O] = { + val pullOrFail = for { subscriberP <- makeSubscriber[O](bufferSize) (subscriber, p) = subscriberP - _ <- ZManaged.finalizer(UIO(subscriber.interrupt())) - _ <- ZManaged.succeed(publisher.subscribe(subscriber)) - subQ <- p.await.toManaged - (sub, q) = subQ - process <- process(sub, q, () => subscriber.await(), () => subscriber.isDone) + _ <- ZManaged.acquireReleaseSucceed(publisher.subscribe(subscriber))(subscriber.interrupt()) + subQ <- ZManaged.fromZIOUninterruptible( + p.await.interruptible + .onTermination(_ => UIO(subscriber.interrupt())) + ) + (sub, q) = subQ + process <- process(sub, q, () => subscriber.await(), () => subscriber.isDone) } yield process val pull = pullOrFail.catchAll(e => ZManaged.succeed(Pull.fail(e))) - ZStream(pull) + ZStream.fromPull(pull) } def sinkToSubscriber[R, I, L, Z]( @@ -76,7 +79,7 @@ object Adapters { process(subscription, q, () => subscriber.await(), () => subscriber.isDone, bufferSize) } .catchAll(e => ZManaged.succeedNow(Pull.fail(e))) - fiber <- ZStream(pull).run(sink).toManaged.fork + fiber <- ZStream.fromPull(pull).run(sink).toManaged.fork } yield (subscriber, fiber.join) private def process[A]( @@ -140,7 +143,8 @@ object Adapters { @volatile var toNotify: Option[Promise[Option[Throwable], Unit]] = None - override def interrupt(): Unit = isSubscribedOrInterrupted.set(true) + override def interrupt(): Unit = + isSubscribedOrInterrupted.set(true) override def await(): IO[Option[Throwable], Unit] = done match { @@ -223,10 +227,10 @@ object Adapters { case false => if (chunk.size.toLong <= bufferedDemand) UIO - .foreach(chunk)(a => UIO(subscriber.onNext(a))) + .foreachDiscard(chunk)(a => UIO(subscriber.onNext(a))) .as((Chunk.empty, bufferedDemand - chunk.size.toLong)) else - UIO.foreach(chunk.take(bufferedDemand.toInt))(a => UIO(subscriber.onNext(a))) *> + UIO.foreachDiscard(chunk.take(bufferedDemand.toInt))(a => UIO(subscriber.onNext(a))) *> demand.take.map((chunk.drop(bufferedDemand.toInt), _)) } } diff --git a/src/main/scala/zio/interop/reactivestreams/package.scala b/src/main/scala/zio/interop/reactivestreams/package.scala index 3795842..55715f2 100644 --- a/src/main/scala/zio/interop/reactivestreams/package.scala +++ b/src/main/scala/zio/interop/reactivestreams/package.scala @@ -21,8 +21,7 @@ package object reactivestreams { Adapters.streamToPublisher(stream) } - final implicit class sinkToSubscriber[R, E <: Throwable, A, L, Z](private val sink: ZSink[R, E, A, L, Z]) - extends AnyVal { + final implicit class sinkToSubscriber[R, E <: Throwable, A, L, Z](private val sink: ZSink[R, E, A, L, Z]) { /** Create a `Subscriber` from a `Sink`. The returned IO will eventually return the result of running the subscribed * stream to the sink. Consumption is started as soon as the resource is used, even if the IO is never run. diff --git a/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala b/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala index 47e67de..9cde084 100644 --- a/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala @@ -7,11 +7,14 @@ import org.reactivestreams.tck.TestEnvironment import org.reactivestreams.tck.TestEnvironment.ManualPublisher import zio.Chunk import zio.Exit +import zio.Fiber import zio.Promise import zio.Supervisor import zio.Task import zio.UIO +import zio.ZEnvironment import zio.ZIO +import zio.ZTraceElement import zio.durationInt import zio.stream.Sink import zio.stream.Stream @@ -32,6 +35,7 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { assertM(publish(seq, Some(e)))(fails(equalTo(e))) }, test("does not fail a fiber on failing `Publisher`") { + val publisher = new Publisher[Int] { override def subscribe(s: Subscriber[_ >: Int]): Unit = s.onSubscribe( @@ -41,14 +45,34 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { } ) } - val supervisor = Supervisor.runtimeStats + + val supervisor = + new Supervisor[Boolean] { + + @transient var failedAFiber = false + + def value(implicit trace: ZTraceElement): UIO[Boolean] = + UIO(failedAFiber) + + def unsafeOnStart[R, E, A]( + environment: ZEnvironment[R], + effect: ZIO[R, E, A], + parent: Option[Fiber.Runtime[Any, Any]], + fiber: Fiber.Runtime[E, A] + ): Unit = () + + def unsafeOnEnd[R, E, A](value: Exit[E, A], fiber: Fiber.Runtime[E, A]): Unit = + if (value.isFailure) failedAFiber = true + + } + for { - runtime <- ZIO.runtime[Any] - testRuntime = runtime.mapRuntimeConfig(_.copy(supervisor = supervisor)) - exit = testRuntime.unsafeRun(publisher.toStream().runDrain.exit) - stats <- supervisor.value - } yield assert(exit)(fails(anything)) && - assert(stats.failures)(equalTo(0L)) + outerRuntime <- ZIO.runtime[Any] + runtime = outerRuntime.mapRuntimeConfig(_.copy(supervisor = supervisor)) + exit <- runtime.run(publisher.toStream().runDrain.exit) + failed <- supervisor.value + } yield assert(exit)(fails(anything)) && assert(failed)(isFalse) + }, test("does not freeze on stream end") { withProbe(probe => @@ -68,32 +92,32 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { r <- fiber.join } yield assert(r)(equalTo(Chunk(1))) ) - } @@ TestAspect.timeout(1000.millis), + } @@ TestAspect.timeout(3.seconds), test("cancels subscription when interrupted before subscription") { val tst = for { subscriberP <- Promise.make[Nothing, Subscriber[_]] cancelledLatch <- Promise.make[Nothing, Unit] subscription = new Subscription { - override def request(x$1: Long): Unit = () - override def cancel(): Unit = cancelledLatch.unsafeDone(UIO.unit) + override def request(n: Long): Unit = () + override def cancel(): Unit = cancelledLatch.unsafeDone(UIO.unit) } probe = new Publisher[Int] { override def subscribe(subscriber: Subscriber[_ >: Int]): Unit = subscriberP.unsafeDone(UIO.succeedNow(subscriber)) } - fiber <- probe.toStream(bufferSize).run(Sink.drain).fork + fiber <- probe.toStream(bufferSize).runDrain.fork subscriber <- subscriberP.await _ <- fiber.interrupt _ <- UIO(subscriber.onSubscribe(subscription)) _ <- cancelledLatch.await } yield () assertM(tst.exit)(succeeds(anything)) - } @@ TestAspect.timeout(3.seconds), + } @@ TestAspect.timeout(3.seconds) @@ TestAspect.flaky, test("cancels subscription when interrupted after subscription") { withProbe(probe => assertM((for { - fiber <- probe.toStream(bufferSize).run(Sink.drain).fork + fiber <- probe.toStream(bufferSize).runDrain.fork _ <- Task.attemptBlockingInterrupt(probe.expectRequest()) _ <- fiber.interrupt _ <- Task.attemptBlockingInterrupt(probe.expectCancelling()) @@ -101,11 +125,11 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { succeeds(isUnit) ) ) - }, + } @@ TestAspect.flaky, test("cancels subscription when interrupted during consumption") { withProbe(probe => assertM((for { - fiber <- probe.toStream(bufferSize).run(Sink.drain).fork + fiber <- probe.toStream(bufferSize).runDrain.fork demand <- Task.attemptBlockingInterrupt(probe.expectRequest()) _ <- Task((1 to demand.toInt).foreach(i => probe.sendNext(i))) _ <- fiber.interrupt @@ -114,11 +138,11 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { succeeds(isUnit) ) ) - }, + } @@ TestAspect.flaky, test("cancels subscription on stream end") { withProbe(probe => assertM((for { - fiber <- probe.toStream(bufferSize).take(1).run(Sink.drain).fork + fiber <- probe.toStream(bufferSize).take(1).runDrain.fork demand <- Task.attemptBlockingInterrupt(probe.expectRequest()) _ <- Task((1 to demand.toInt).foreach(i => probe.sendNext(i))) _ <- Task.attemptBlockingInterrupt(probe.expectCancelling()) @@ -130,18 +154,16 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { }, test("cancels subscription on stream error") { withProbe(probe => - assertM((for { - fiber <- probe.toStream(bufferSize).mapZIO(_ => Task.fail(new Throwable("boom!"))).run(Sink.drain).fork + assertM(for { + fiber <- probe.toStream(bufferSize).mapZIO(_ => Task.fail(new Throwable("boom!"))).runDrain.fork demand <- Task.attemptBlockingInterrupt(probe.expectRequest()) _ <- Task((1 to demand.toInt).foreach(i => probe.sendNext(i))) _ <- Task.attemptBlockingInterrupt(probe.expectCancelling()) - _ <- fiber.join - } yield ()).exit)( - fails(anything) - ) + exit <- fiber.join.exit + } yield exit)(fails(anything)) ) } - ) @@ TestAspect.nonFlaky + ) val e: Throwable = new RuntimeException("boom") val seq: Chunk[Int] = Chunk.fromIterable(List.range(0, 100)) diff --git a/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala b/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala index 0481e99..da6b8df 100644 --- a/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala @@ -20,7 +20,6 @@ import zio.stream.Sink import zio.stream.ZSink import zio.test.Assertion._ import zio.test._ -import zio.test.environment.Live object SinkToSubscriberSpec extends DefaultRunnableSpec { override def spec = @@ -52,7 +51,7 @@ object SinkToSubscriberSpec extends DefaultRunnableSpec { for { (publisher, subscribed, _, canceled) <- makePublisherProbe fiber <- Sink - .foreachChunk[Any, Nothing, Int](_ => ZIO.yieldNow) + .foreachChunk[Any, Throwable, Int](_ => ZIO.yieldNow) .toSubscriber() .use { case (subscriber, _) => UIO(publisher.subscribe(subscriber)) *> UIO.never } .fork @@ -71,7 +70,7 @@ object SinkToSubscriberSpec extends DefaultRunnableSpec { for { (publisher, subscribed, requested, canceled) <- makePublisherProbe fiber <- Sink - .foreachChunk[Any, Nothing, Int](_ => ZIO.yieldNow) + .foreachChunk[Any, Throwable, Int](_ => ZIO.yieldNow) .toSubscriber() .use { case (subscriber, _) => Task.attemptBlockingInterrupt(publisher.subscribe(subscriber)) *> UIO.never From 15e7e032b1a1f98141866472f3bfd76de79df05f Mon Sep 17 00:00:00 2001 From: Simon Schenk Date: Sun, 19 Dec 2021 11:50:47 +0100 Subject: [PATCH 11/23] fix: signal error without Promise in subscriberToSink (#298) * fix: signal error without Promise in subscriberToSink * fix: 3.1.0 compilation --- .../interop/reactivestreams/Adapters.scala | 20 +++++++----- .../zio/interop/reactivestreams/package.scala | 4 +-- .../SubscriberToSinkSpec.scala | 32 +++++++++++++++++-- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/main/scala/zio/interop/reactivestreams/Adapters.scala b/src/main/scala/zio/interop/reactivestreams/Adapters.scala index 4e247d1..e502edb 100644 --- a/src/main/scala/zio/interop/reactivestreams/Adapters.scala +++ b/src/main/scala/zio/interop/reactivestreams/Adapters.scala @@ -35,16 +35,20 @@ object Adapters { def subscriberToSink[E <: Throwable, I]( subscriber: => Subscriber[I] - )(implicit trace: ZTraceElement): ZManaged[Any, Nothing, (Promise[E, Nothing], ZSink[Any, Nothing, I, I, Unit])] = { + )(implicit trace: ZTraceElement): ZManaged[Any, Nothing, (E => UIO[Unit], ZSink[Any, Nothing, I, I, Unit])] = { val sub = subscriber for { - runtime <- ZIO.runtime[Any].toManaged - demand <- Queue.unbounded[Long].toManaged - error <- Promise.make[E, Nothing].toManaged - subscription = createSubscription(sub, demand, runtime) - _ <- ZManaged.succeed(sub.onSubscribe(subscription)) - _ <- error.await.catchAll(t => UIO(sub.onError(t)) *> demand.shutdown).toManaged.fork - } yield (error, demandUnfoldSink(sub, demand)) + runtime <- ZIO.runtime[Any].toManaged + demand <- Queue.unbounded[Long].toManaged + subscription = createSubscription(sub, demand, runtime) + _ <- ZManaged.succeed(sub.onSubscribe(subscription)) + errorSignaled <- Promise.makeManaged[Nothing, Boolean] + } yield { + val signalError = + (e: E) => ZIO.whenZIO(errorSignaled.complete(UIO.succeedNow(true)))(UIO(sub.onError(e)) *> demand.shutdown).unit + + (signalError, demandUnfoldSink(sub, demand)) + } } def publisherToStream[O]( diff --git a/src/main/scala/zio/interop/reactivestreams/package.scala b/src/main/scala/zio/interop/reactivestreams/package.scala index 55715f2..f6cac07 100644 --- a/src/main/scala/zio/interop/reactivestreams/package.scala +++ b/src/main/scala/zio/interop/reactivestreams/package.scala @@ -3,7 +3,7 @@ package zio.interop import org.reactivestreams.Publisher import org.reactivestreams.Subscriber import zio.IO -import zio.Promise +import zio.UIO import zio.ZIO import zio.ZManaged import zio.ZTraceElement @@ -60,7 +60,7 @@ package object reactivestreams { */ def toSink[E <: Throwable](implicit trace: ZTraceElement - ): ZManaged[Any, Nothing, (Promise[E, Nothing], ZSink[Any, Nothing, I, I, Unit])] = + ): ZManaged[Any, Nothing, (E => UIO[Unit], ZSink[Any, Nothing, I, I, Unit])] = Adapters.subscriberToSink(subscriber) } diff --git a/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala b/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala index f75487e..95b73a6 100644 --- a/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala @@ -2,10 +2,12 @@ package zio.interop.reactivestreams import org.reactivestreams.tck.TestEnvironment import org.reactivestreams.tck.TestEnvironment.ManualSubscriberWithSubscriptionSupport +import zio.durationInt import zio.IO import zio.Task import zio.UIO import zio.stream.Stream +import zio.stream.ZStream import zio.test.Assertion._ import zio.test._ @@ -33,9 +35,9 @@ object SubscriberToSinkSpec extends DefaultRunnableSpec { makeSubscriber.flatMap(probe => probe.underlying .toSink[Throwable] - .use { case (error, sink) => + .use { case (signalError, sink) => for { - fiber <- (Stream.fromIterable(seq) ++ Stream.fail(e)).run(sink).catchAll(t => error.fail(t)).fork + fiber <- (Stream.fromIterable(seq) ++ Stream.fail(e)).run(sink).catchAll(signalError).fork _ <- probe.request(length + 1) elements <- probe.nextElements(length).exit err <- probe.expectError.exit @@ -43,6 +45,32 @@ object SubscriberToSinkSpec extends DefaultRunnableSpec { } yield assert(elements)(succeeds(equalTo(seq))) && assert(err)(succeeds(equalTo(e))) } ) + }, + test("transports errors 2") { + makeSubscriber.flatMap(probe => + probe.underlying + .toSink[Throwable] + .use { case (signalError, sink) => + for { + _ <- ZStream.fail(e).run(sink).catchAll(signalError) + err <- probe.expectError.exit + } yield assert(err)(succeeds(equalTo(e))) + } + ) + }, + test("transports errors only once") { + makeSubscriber.flatMap(probe => + probe.underlying + .toSink[Throwable] + .use { case (signalError, sink) => + for { + _ <- ZStream.fail(e).run(sink).catchAll(signalError) + err <- probe.expectError.exit + _ <- signalError(e) + err2 <- probe.expectError.timeout(100.millis).exit + } yield assert(err)(succeeds(equalTo(e))) && assert(err2)(fails(anything)) + } + ) } ) From ecf630544f5808089f12509aa334eb849ccf11ff Mon Sep 17 00:00:00 2001 From: Adam Fraser Date: Thu, 23 Dec 2021 01:17:33 -0800 Subject: [PATCH 12/23] fix from pull implementation (#299) --- .../interop/reactivestreams/Adapters.scala | 50 +++++++++++++++---- .../PublisherToStreamSpec.scala | 6 +-- 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/main/scala/zio/interop/reactivestreams/Adapters.scala b/src/main/scala/zio/interop/reactivestreams/Adapters.scala index e502edb..39df358 100644 --- a/src/main/scala/zio/interop/reactivestreams/Adapters.scala +++ b/src/main/scala/zio/interop/reactivestreams/Adapters.scala @@ -5,8 +5,7 @@ import org.reactivestreams.Subscriber import org.reactivestreams.Subscription import zio._ import zio.internal.RingBuffer -import zio.stream.ZSink -import zio.stream.ZStream +import zio.stream._ import zio.stream.ZStream.Pull import java.util.concurrent.atomic.AtomicBoolean @@ -61,15 +60,12 @@ object Adapters { subscriberP <- makeSubscriber[O](bufferSize) (subscriber, p) = subscriberP _ <- ZManaged.acquireReleaseSucceed(publisher.subscribe(subscriber))(subscriber.interrupt()) - subQ <- ZManaged.fromZIOUninterruptible( - p.await.interruptible - .onTermination(_ => UIO(subscriber.interrupt())) - ) - (sub, q) = subQ - process <- process(sub, q, () => subscriber.await(), () => subscriber.isDone) + subQ <- ZManaged.fromZIOUninterruptible(p.await.interruptible) + (sub, q) = subQ + process <- process(sub, q, () => subscriber.await(), () => subscriber.isDone) } yield process val pull = pullOrFail.catchAll(e => ZManaged.succeed(Pull.fail(e))) - ZStream.fromPull(pull) + fromPull(pull) } def sinkToSubscriber[R, I, L, Z]( @@ -83,7 +79,7 @@ object Adapters { process(subscription, q, () => subscriber.await(), () => subscriber.isDone, bufferSize) } .catchAll(e => ZManaged.succeedNow(Pull.fail(e))) - fiber <- ZStream.fromPull(pull).run(sink).toManaged.fork + fiber <- fromPull(pull).run(sink).toManaged.fork } yield (subscriber, fiber.join) private def process[A]( @@ -253,4 +249,38 @@ object Adapters { else runtime.unsafeRunAsync(demand.offer(n)) override def cancel(): Unit = runtime.unsafeRun(demand.shutdown) } + + private def fromPull[R, E, A](zio: ZManaged[R, Nothing, ZIO[R, Option[E], Chunk[A]]])(implicit + trace: ZTraceElement + ): ZStream[R, E, A] = + unwrapManaged(zio.map(pull => ZStream.repeatZIOChunkOption(pull))) + + private def unwrapManaged[R, E, A](fa: => ZManaged[R, E, ZStream[R, E, A]])(implicit + trace: ZTraceElement + ): ZStream[R, E, A] = + managed(fa).flatten + + private def managed[R, E, A](managed: => ZManaged[R, E, A])(implicit trace: ZTraceElement): ZStream[R, E, A] = + new ZStream(managedOut(managed.map(Chunk.single))) + + private def managedOut[R, E, A]( + m: => ZManaged[R, E, A] + )(implicit trace: ZTraceElement): ZChannel[R, Any, Any, Any, E, A, Any] = + ZChannel + .acquireReleaseOutExitWith( + ZManaged.ReleaseMap.make.flatMap { releaseMap => + ZIO.uninterruptibleMask { restore => + ZManaged.currentReleaseMap + .locally(releaseMap)(restore(m.zio)) + .foldCauseZIO( + cause => + releaseMap.releaseAll(Exit.failCause(cause), ExecutionStrategy.Sequential) *> ZIO.failCause(cause), + { case (_, out) => ZIO.succeedNow((out, releaseMap)) } + ) + } + } + ) { case ((_, releaseMap), exit) => + releaseMap.releaseAll(exit, ExecutionStrategy.Sequential) + } + .mapOut(_._1) } diff --git a/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala b/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala index 9cde084..1a46b6d 100644 --- a/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala @@ -113,7 +113,7 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { _ <- cancelledLatch.await } yield () assertM(tst.exit)(succeeds(anything)) - } @@ TestAspect.timeout(3.seconds) @@ TestAspect.flaky, + } @@ TestAspect.nonFlaky @@ TestAspect.timeout(60.seconds), test("cancels subscription when interrupted after subscription") { withProbe(probe => assertM((for { @@ -125,7 +125,7 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { succeeds(isUnit) ) ) - } @@ TestAspect.flaky, + } @@ TestAspect.nonFlaky @@ TestAspect.timeout(60.seconds), test("cancels subscription when interrupted during consumption") { withProbe(probe => assertM((for { @@ -138,7 +138,7 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { succeeds(isUnit) ) ) - } @@ TestAspect.flaky, + } @@ TestAspect.nonFlaky @@ TestAspect.timeout(60.seconds), test("cancels subscription on stream end") { withProbe(probe => assertM((for { From a4359e952081184da605b0d117eaff7a8775d663 Mon Sep 17 00:00:00 2001 From: Simon Schenk Date: Tue, 4 Jan 2022 09:34:09 +0100 Subject: [PATCH 13/23] feat: rewrite subscription and demandUnfoldSink to not require unsafeRun (#287) * feat: no unsafeRun in demand tracking * feat: track demand only in subscription * refactorings * use AtomicReference to fix race conditions * fix: hide internals of DemandTrackingSubscription * refactoring * fix StackOverfow on 2.11 * fix: defer side effect until after AtomicRef.getAndUpdate * revert nonFlaky annotations --- .../interop/reactivestreams/Adapters.scala | 125 ++++++++++++------ .../StreamToPublisherSpec.scala | 4 +- 2 files changed, 83 insertions(+), 46 deletions(-) diff --git a/src/main/scala/zio/interop/reactivestreams/Adapters.scala b/src/main/scala/zio/interop/reactivestreams/Adapters.scala index 39df358..6eb22ef 100644 --- a/src/main/scala/zio/interop/reactivestreams/Adapters.scala +++ b/src/main/scala/zio/interop/reactivestreams/Adapters.scala @@ -9,6 +9,7 @@ import zio.stream._ import zio.stream.ZStream.Pull import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference object Adapters { @@ -19,12 +20,12 @@ object Adapters { if (subscriber == null) { throw new NullPointerException("Subscriber must not be null.") } else { + val subscription = new DemandTrackingSubscription(subscriber) runtime.unsafeRunAsync( for { - demand <- Queue.unbounded[Long] - _ <- UIO(subscriber.onSubscribe(createSubscription(subscriber, demand, runtime))) + _ <- UIO(subscriber.onSubscribe(subscription)) _ <- stream - .run(demandUnfoldSink(subscriber, demand)) + .run(demandUnfoldSink(subscriber, subscription)) .catchAll(e => UIO(subscriber.onError(e))) .forkDaemon } yield () @@ -37,17 +38,11 @@ object Adapters { )(implicit trace: ZTraceElement): ZManaged[Any, Nothing, (E => UIO[Unit], ZSink[Any, Nothing, I, I, Unit])] = { val sub = subscriber for { - runtime <- ZIO.runtime[Any].toManaged - demand <- Queue.unbounded[Long].toManaged - subscription = createSubscription(sub, demand, runtime) - _ <- ZManaged.succeed(sub.onSubscribe(subscription)) - errorSignaled <- Promise.makeManaged[Nothing, Boolean] - } yield { - val signalError = - (e: E) => ZIO.whenZIO(errorSignaled.complete(UIO.succeedNow(true)))(UIO(sub.onError(e)) *> demand.shutdown).unit - - (signalError, demandUnfoldSink(sub, demand)) - } + error <- Promise.makeManaged[E, Nothing] + subscription = new DemandTrackingSubscription(sub) + _ <- ZManaged.succeed(sub.onSubscribe(subscription)) + _ <- error.await.catchAll(t => UIO(sub.onError(t))).toManaged.fork + } yield (error.fail(_).unit, demandUnfoldSink(sub, subscription)) } def publisherToStream[O]( @@ -215,41 +210,85 @@ object Adapters { } private def demandUnfoldSink[I]( - subscriber: Subscriber[_ >: I], - demand: Queue[Long] + subscriber: Subscriber[I], + subscription: DemandTrackingSubscription ): ZSink[Any, Nothing, I, I, Unit] = ZSink - .foldChunksZIO[Any, Nothing, I, Long](0L)(_ >= 0L) { (bufferedDemand, chunk) => - UIO - .iterate((chunk, bufferedDemand))(!_._1.isEmpty) { case (chunk, bufferedDemand) => - demand.isShutdown.flatMap { - case true => UIO((Chunk.empty, -1)) - case false => - if (chunk.size.toLong <= bufferedDemand) - UIO - .foreachDiscard(chunk)(a => UIO(subscriber.onNext(a))) - .as((Chunk.empty, bufferedDemand - chunk.size.toLong)) - else - UIO.foreachDiscard(chunk.take(bufferedDemand.toInt))(a => UIO(subscriber.onNext(a))) *> - demand.take.map((chunk.drop(bufferedDemand.toInt), _)) - } + .foldChunksZIO[Any, Nothing, I, Boolean](true)(identity) { (_, chunk) => + IO + .iterate(chunk)(!_.isEmpty) { chunk => + subscription + .offer(chunk.size) + .flatMap { acceptedCount => + UIO.foreach(chunk.take(acceptedCount))(a => UIO(subscriber.onNext(a))).as(chunk.drop(acceptedCount)) + } } - .map(_._2) + .fold( + _ => false, // canceled + _ => true + ) + } + .map(_ => if (!subscription.isCanceled) subscriber.onComplete()) + + private class DemandTrackingSubscription(subscriber: Subscriber[_]) extends Subscription { + + private case class State( + requestedCount: Long, // -1 when cancelled + toNotify: Option[(Int, Promise[Unit, Int])] + ) + + private val initial = State(0L, None) + private val canceled = State(-1, None) + private def requested(n: Long) = State(n, None) + private def awaiting(n: Int, p: Promise[Unit, Int]) = State(0L, Some((n, p))) + + private val state = new AtomicReference(initial) + + def offer(n: Int): IO[Unit, Int] = { + var result: IO[Unit, Int] = null + state.updateAndGet { + case `canceled` => + result = IO.fail(()) + canceled + case State(0L, _) => + val p = Promise.unsafeMake[Unit, Int](FiberId.None) + result = p.await + awaiting(n, p) + case State(requestedCount, _) => + val newRequestedCount = Math.max(requestedCount - n, 0L) + val accepted = Math.min(requestedCount, n.toLong).toInt + result = IO.succeedNow(accepted) + requested(newRequestedCount) } - .mapZIO(_ => demand.isShutdown.flatMap(is => UIO(subscriber.onComplete()).when(!is).unit)) - - private def createSubscription[A]( - subscriber: Subscriber[_ >: A], - demand: Queue[Long], - runtime: Runtime[_] - ): Subscription = - new Subscription { - override def request(n: Long): Unit = - if (n <= 0) subscriber.onError(new IllegalArgumentException("non-positive subscription request")) - else runtime.unsafeRunAsync(demand.offer(n)) - override def cancel(): Unit = runtime.unsafeRun(demand.shutdown) + result } + def isCanceled: Boolean = state.get().requestedCount < 0 + + override def request(n: Long): Unit = { + if (n <= 0) subscriber.onError(new IllegalArgumentException("non-positive subscription request")) + var notification: () => Unit = () => () + state.getAndUpdate { + case `canceled` => + canceled + case State(requestedCount, Some((offered, toNotify))) => + val newRequestedCount = requestedCount + n + val accepted = Math.min(offered.toLong, newRequestedCount) + val remaining = newRequestedCount - accepted + notification = () => toNotify.unsafeDone(IO.succeedNow(accepted.toInt)) + requested(remaining) + case State(requestedCount, _) if ((Long.MaxValue - n) > requestedCount) => + requested(requestedCount + n) + case _ => + requested(Long.MaxValue) + } + notification() + } + + override def cancel(): Unit = + state.getAndSet(canceled).toNotify.foreach { case (_, p) => p.unsafeDone(IO.fail(())) } + } + private def fromPull[R, E, A](zio: ZManaged[R, Nothing, ZIO[R, Option[E], Chunk[A]]])(implicit trace: ZTraceElement ): ZStream[R, E, A] = diff --git a/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala b/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala index 1fb15f0..50268ba 100644 --- a/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala @@ -59,9 +59,7 @@ object StreamToPublisherSpec extends DefaultRunnableSpec { r <- Task .attemptBlockingInterrupt(method.invoke(pv)) .unit - .refineOrDie { case e: InvocationTargetException => - e.getTargetException() - } + .refineOrDie { case e: InvocationTargetException => e.getTargetException() } .exit } yield assert(r)(succeeds(isUnit)) ) From 683cac0ddfe4ca16d1e8b2766452dc529640aa96 Mon Sep 17 00:00:00 2001 From: Simon Schenk Date: Mon, 31 Jan 2022 20:01:38 +0100 Subject: [PATCH 14/23] RC2 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index c50af37..00dccef 100644 --- a/build.sbt +++ b/build.sbt @@ -36,7 +36,7 @@ Global / onChangedBuildSource := ReloadOnSourceChanges addCommandAlias("fmt", "all scalafmtSbt scalafmt test:scalafmt") addCommandAlias("check", "all scalafmtSbtCheck scalafmtCheck test:scalafmtCheck") -val zioVersion = "2.0.0-RC1" +val zioVersion = "2.0.0-RC2" val rsVersion = "1.0.3" val collCompatVersion = "2.5.0" From 70a0fb631e0af0f87f54d76ac19081bdc2599cae Mon Sep 17 00:00:00 2001 From: Daniel Vigovszky Date: Mon, 7 Feb 2022 10:04:37 +0100 Subject: [PATCH 15/23] Fix race condition in toSink error signal (#305) --- .../interop/reactivestreams/Adapters.scala | 4 ++-- .../SubscriberToSinkSpec.scala | 23 ++++++++++++++----- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/main/scala/zio/interop/reactivestreams/Adapters.scala b/src/main/scala/zio/interop/reactivestreams/Adapters.scala index 6eb22ef..7860f9c 100644 --- a/src/main/scala/zio/interop/reactivestreams/Adapters.scala +++ b/src/main/scala/zio/interop/reactivestreams/Adapters.scala @@ -41,8 +41,8 @@ object Adapters { error <- Promise.makeManaged[E, Nothing] subscription = new DemandTrackingSubscription(sub) _ <- ZManaged.succeed(sub.onSubscribe(subscription)) - _ <- error.await.catchAll(t => UIO(sub.onError(t))).toManaged.fork - } yield (error.fail(_).unit, demandUnfoldSink(sub, subscription)) + fiber <- error.await.catchAll(t => UIO(sub.onError(t))).toManaged.fork + } yield (error.fail(_) *> fiber.join, demandUnfoldSink(sub, subscription)) } def publisherToStream[O]( diff --git a/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala b/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala index 95b73a6..04c0115 100644 --- a/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala @@ -2,14 +2,11 @@ package zio.interop.reactivestreams import org.reactivestreams.tck.TestEnvironment import org.reactivestreams.tck.TestEnvironment.ManualSubscriberWithSubscriptionSupport -import zio.durationInt -import zio.IO -import zio.Task -import zio.UIO -import zio.stream.Stream -import zio.stream.ZStream +import zio.stream.{ Stream, ZStream } import zio.test.Assertion._ +import zio.test.TestAspect.nonFlaky import zio.test._ +import zio.{ IO, Task, UIO, durationInt } import scala.jdk.CollectionConverters._ @@ -58,6 +55,20 @@ object SubscriberToSinkSpec extends DefaultRunnableSpec { } ) }, + test("transports errors 3") { + makeSubscriber.flatMap { probe => + for { + fiber <- probe.underlying + .toSink[Throwable] + .use { case (signalError, sink) => + ZStream.fail(e).run(sink).catchAll(signalError) + } + .fork + _ <- fiber.join + err <- probe.expectError.exit + } yield assert(err)(succeeds(equalTo(e))) + } + } @@ nonFlaky(10), test("transports errors only once") { makeSubscriber.flatMap(probe => probe.underlying From 8237ce476d18ed6633e50da4e4349e7bb7b84443 Mon Sep 17 00:00:00 2001 From: Daniel Vigovszky Date: Wed, 23 Mar 2022 20:18:08 +0200 Subject: [PATCH 16/23] ZIO 2.0.0-RC3 (#310) * ZIO 2.0.0-RC3 * Warnings as errors again * Remove custom ZStream.unwrapScoped * Fixes * Use ZIO instead of UIO and Task --- build.sbt | 2 +- .../interop/reactivestreams/Adapters.scala | 95 ++++++--------- .../zio/interop/reactivestreams/package.scala | 10 +- .../PublisherToStreamSpec.scala | 50 ++++---- .../SinkToSubscriberSpec.scala | 94 +++++++-------- .../StreamToPublisherSpec.scala | 9 +- .../SubscriberToSinkSpec.scala | 114 ++++++++++-------- 7 files changed, 174 insertions(+), 200 deletions(-) diff --git a/build.sbt b/build.sbt index 00dccef..d19a3ed 100644 --- a/build.sbt +++ b/build.sbt @@ -36,7 +36,7 @@ Global / onChangedBuildSource := ReloadOnSourceChanges addCommandAlias("fmt", "all scalafmtSbt scalafmt test:scalafmt") addCommandAlias("check", "all scalafmtSbtCheck scalafmtCheck test:scalafmtCheck") -val zioVersion = "2.0.0-RC2" +val zioVersion = "2.0.0-RC3" val rsVersion = "1.0.3" val collCompatVersion = "2.5.0" diff --git a/src/main/scala/zio/interop/reactivestreams/Adapters.scala b/src/main/scala/zio/interop/reactivestreams/Adapters.scala index 7860f9c..d245b0e 100644 --- a/src/main/scala/zio/interop/reactivestreams/Adapters.scala +++ b/src/main/scala/zio/interop/reactivestreams/Adapters.scala @@ -23,10 +23,10 @@ object Adapters { val subscription = new DemandTrackingSubscription(subscriber) runtime.unsafeRunAsync( for { - _ <- UIO(subscriber.onSubscribe(subscription)) + _ <- ZIO.succeed(subscriber.onSubscribe(subscription)) _ <- stream .run(demandUnfoldSink(subscriber, subscription)) - .catchAll(e => UIO(subscriber.onError(e))) + .catchAll(e => ZIO.succeed(subscriber.onError(e))) .forkDaemon } yield () ) @@ -35,13 +35,13 @@ object Adapters { def subscriberToSink[E <: Throwable, I]( subscriber: => Subscriber[I] - )(implicit trace: ZTraceElement): ZManaged[Any, Nothing, (E => UIO[Unit], ZSink[Any, Nothing, I, I, Unit])] = { + )(implicit trace: ZTraceElement): ZIO[Scope, Nothing, (E => UIO[Unit], ZSink[Any, Nothing, I, I, Unit])] = { val sub = subscriber for { - error <- Promise.makeManaged[E, Nothing] + error <- Promise.make[E, Nothing] subscription = new DemandTrackingSubscription(sub) - _ <- ZManaged.succeed(sub.onSubscribe(subscription)) - fiber <- error.await.catchAll(t => UIO(sub.onError(t))).toManaged.fork + _ <- ZIO.succeed(sub.onSubscribe(subscription)) + fiber <- error.await.catchAll(t => ZIO.succeed(sub.onError(t))).forkScoped } yield (error.fail(_) *> fiber.join, demandUnfoldSink(sub, subscription)) } @@ -54,27 +54,27 @@ object Adapters { for { subscriberP <- makeSubscriber[O](bufferSize) (subscriber, p) = subscriberP - _ <- ZManaged.acquireReleaseSucceed(publisher.subscribe(subscriber))(subscriber.interrupt()) - subQ <- ZManaged.fromZIOUninterruptible(p.await.interruptible) + _ <- ZIO.acquireRelease(ZIO.succeed(publisher.subscribe(subscriber)))(_ => ZIO.succeed(subscriber.interrupt())) + subQ <- p.await.interruptible (sub, q) = subQ process <- process(sub, q, () => subscriber.await(), () => subscriber.isDone) } yield process - val pull = pullOrFail.catchAll(e => ZManaged.succeed(Pull.fail(e))) - fromPull(pull) + val pull = pullOrFail.catchAll(e => ZIO.succeed(Pull.fail(e))) + fromPull[Any, Throwable, O](pull) } def sinkToSubscriber[R, I, L, Z]( sink: => ZSink[R, Throwable, I, L, Z], bufferSize: => Int - )(implicit trace: ZTraceElement): ZManaged[R, Throwable, (Subscriber[I], IO[Throwable, Z])] = + )(implicit trace: ZTraceElement): ZIO[R with Scope, Throwable, (Subscriber[I], IO[Throwable, Z])] = for { subscriberP <- makeSubscriber[I](bufferSize) (subscriber, p) = subscriberP - pull = p.await.toManaged.flatMap { case (subscription, q) => + pull = p.await.flatMap { case (subscription, q) => process(subscription, q, () => subscriber.await(), () => subscriber.isDone, bufferSize) } - .catchAll(e => ZManaged.succeedNow(Pull.fail(e))) - fiber <- fromPull(pull).run(sink).toManaged.fork + .catchAll(e => ZIO.succeedNow(Pull.fail(e))) + fiber <- fromPull(pull).run(sink).forkScoped } yield (subscriber, fiber.join) private def process[A]( @@ -83,22 +83,22 @@ object Adapters { await: () => IO[Option[Throwable], Unit], isDone: () => Boolean, maxChunkSize: Int = Int.MaxValue - ): ZManaged[Any, Nothing, ZIO[Any, Option[Throwable], Chunk[A]]] = + ): ZIO[Scope, Nothing, ZIO[Any, Option[Throwable], Chunk[A]]] = for { - _ <- ZManaged.succeed(sub.request(q.capacity.toLong)) - requestedRef <- Ref.makeManaged(q.capacity.toLong) // TODO: maybe turn into unfold? + _ <- ZIO.succeed(sub.request(q.capacity.toLong)) + requestedRef <- Ref.make(q.capacity.toLong) // TODO: maybe turn into unfold? } yield { def pull: Pull[Any, Throwable, A] = for { requested <- requestedRef.get pollSize = Math.min(requested, maxChunkSize.toLong).toInt - chunk <- UIO(q.pollUpTo(pollSize)) + chunk <- ZIO.succeed(q.pollUpTo(pollSize)) r <- if (chunk.isEmpty) await() *> pull else (if (chunk.size == pollSize && !isDone()) - UIO(sub.request(q.capacity.toLong)) *> requestedRef.set(q.capacity.toLong) + ZIO.succeed(sub.request(q.capacity.toLong)) *> requestedRef.set(q.capacity.toLong) else requestedRef.set(requested - chunk.size)) *> Pull.emit(chunk) } yield r @@ -114,20 +114,22 @@ object Adapters { private def makeSubscriber[A]( capacity: Int - ): UManaged[ + ): ZIO[ + Scope, + Nothing, ( InterruptibleSubscriber[A], Promise[Throwable, (Subscription, RingBuffer[A])] ) ] = for { - q <- ZManaged.succeed(RingBuffer[A](capacity)) - p <- - Promise - .make[Throwable, (Subscription, RingBuffer[A])] - .toManagedWith( - _.poll.flatMap(_.fold(UIO.unit)(_.foldZIO(_ => UIO.unit, { case (sub, _) => UIO(sub.cancel()) }))) - ) + q <- ZIO.succeed(RingBuffer[A](capacity)) + p <- ZIO.acquireRelease( + Promise + .make[Throwable, (Subscription, RingBuffer[A])] + )( + _.poll.flatMap(_.fold(ZIO.unit)(_.foldZIO(_ => ZIO.unit, { case (sub, _) => ZIO.succeed(sub.cancel()) }))) + ) } yield { val subscriber = new InterruptibleSubscriber[A] { @@ -171,7 +173,7 @@ object Adapters { if (shouldCancel) s.cancel() else - p.unsafeDone(UIO.succeedNow((s, q))) + p.unsafeDone(ZIO.succeedNow((s, q))) } override def onNext(t: A): Unit = @@ -220,7 +222,9 @@ object Adapters { subscription .offer(chunk.size) .flatMap { acceptedCount => - UIO.foreach(chunk.take(acceptedCount))(a => UIO(subscriber.onNext(a))).as(chunk.drop(acceptedCount)) + UIO + .foreach(chunk.take(acceptedCount))(a => ZIO.succeed(subscriber.onNext(a))) + .as(chunk.drop(acceptedCount)) } } .fold( @@ -289,37 +293,8 @@ object Adapters { state.getAndSet(canceled).toNotify.foreach { case (_, p) => p.unsafeDone(IO.fail(())) } } - private def fromPull[R, E, A](zio: ZManaged[R, Nothing, ZIO[R, Option[E], Chunk[A]]])(implicit - trace: ZTraceElement - ): ZStream[R, E, A] = - unwrapManaged(zio.map(pull => ZStream.repeatZIOChunkOption(pull))) - - private def unwrapManaged[R, E, A](fa: => ZManaged[R, E, ZStream[R, E, A]])(implicit + private def fromPull[R, E, A](zio: ZIO[R with Scope, Nothing, ZIO[R, Option[E], Chunk[A]]])(implicit trace: ZTraceElement ): ZStream[R, E, A] = - managed(fa).flatten - - private def managed[R, E, A](managed: => ZManaged[R, E, A])(implicit trace: ZTraceElement): ZStream[R, E, A] = - new ZStream(managedOut(managed.map(Chunk.single))) - - private def managedOut[R, E, A]( - m: => ZManaged[R, E, A] - )(implicit trace: ZTraceElement): ZChannel[R, Any, Any, Any, E, A, Any] = - ZChannel - .acquireReleaseOutExitWith( - ZManaged.ReleaseMap.make.flatMap { releaseMap => - ZIO.uninterruptibleMask { restore => - ZManaged.currentReleaseMap - .locally(releaseMap)(restore(m.zio)) - .foldCauseZIO( - cause => - releaseMap.releaseAll(Exit.failCause(cause), ExecutionStrategy.Sequential) *> ZIO.failCause(cause), - { case (_, out) => ZIO.succeedNow((out, releaseMap)) } - ) - } - } - ) { case ((_, releaseMap), exit) => - releaseMap.releaseAll(exit, ExecutionStrategy.Sequential) - } - .mapOut(_._1) + ZStream.unwrapScoped[R](zio.map(pull => ZStream.repeatZIOChunkOption(pull))) } diff --git a/src/main/scala/zio/interop/reactivestreams/package.scala b/src/main/scala/zio/interop/reactivestreams/package.scala index f6cac07..ea3fe44 100644 --- a/src/main/scala/zio/interop/reactivestreams/package.scala +++ b/src/main/scala/zio/interop/reactivestreams/package.scala @@ -2,11 +2,7 @@ package zio.interop import org.reactivestreams.Publisher import org.reactivestreams.Subscriber -import zio.IO -import zio.UIO -import zio.ZIO -import zio.ZManaged -import zio.ZTraceElement +import zio.{ IO, Scope, UIO, ZIO, ZTraceElement } import zio.stream.ZSink import zio.stream.ZStream @@ -32,7 +28,7 @@ package object reactivestreams { */ def toSubscriber(qSize: Int = 16)(implicit trace: ZTraceElement - ): ZManaged[R, Throwable, (Subscriber[A], IO[Throwable, Z])] = + ): ZIO[R with Scope, Throwable, (Subscriber[A], IO[Throwable, Z])] = Adapters.sinkToSubscriber(sink, qSize) } @@ -60,7 +56,7 @@ package object reactivestreams { */ def toSink[E <: Throwable](implicit trace: ZTraceElement - ): ZManaged[Any, Nothing, (E => UIO[Unit], ZSink[Any, Nothing, I, I, Unit])] = + ): ZIO[Scope, Nothing, (E => UIO[Unit], ZSink[Any, Nothing, I, I, Unit])] = Adapters.subscriberToSink(subscriber) } diff --git a/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala b/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala index 1a46b6d..0a1a9f9 100644 --- a/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala @@ -52,7 +52,7 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { @transient var failedAFiber = false def value(implicit trace: ZTraceElement): UIO[Boolean] = - UIO(failedAFiber) + ZIO.succeed(failedAFiber) def unsafeOnStart[R, E, A]( environment: ZEnvironment[R], @@ -79,16 +79,16 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { for { fiber <- Stream .fromZIO( - UIO( + ZIO.succeed( probe.toStream() ) ) .flatMap(identity) .run(Sink.collectAll[Int]) .fork - _ <- Task.attemptBlockingInterrupt(probe.expectRequest()) - _ <- UIO(probe.sendNext(1)) - _ <- UIO(probe.sendCompletion) + _ <- ZIO.attemptBlockingInterrupt(probe.expectRequest()) + _ <- ZIO.succeed(probe.sendNext(1)) + _ <- ZIO.succeed(probe.sendCompletion) r <- fiber.join } yield assert(r)(equalTo(Chunk(1))) ) @@ -100,16 +100,16 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { cancelledLatch <- Promise.make[Nothing, Unit] subscription = new Subscription { override def request(n: Long): Unit = () - override def cancel(): Unit = cancelledLatch.unsafeDone(UIO.unit) + override def cancel(): Unit = cancelledLatch.unsafeDone(ZIO.unit) } probe = new Publisher[Int] { override def subscribe(subscriber: Subscriber[_ >: Int]): Unit = - subscriberP.unsafeDone(UIO.succeedNow(subscriber)) + subscriberP.unsafeDone(ZIO.succeedNow(subscriber)) } fiber <- probe.toStream(bufferSize).runDrain.fork subscriber <- subscriberP.await _ <- fiber.interrupt - _ <- UIO(subscriber.onSubscribe(subscription)) + _ <- ZIO.succeed(subscriber.onSubscribe(subscription)) _ <- cancelledLatch.await } yield () assertM(tst.exit)(succeeds(anything)) @@ -118,9 +118,9 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { withProbe(probe => assertM((for { fiber <- probe.toStream(bufferSize).runDrain.fork - _ <- Task.attemptBlockingInterrupt(probe.expectRequest()) + _ <- ZIO.attemptBlockingInterrupt(probe.expectRequest()) _ <- fiber.interrupt - _ <- Task.attemptBlockingInterrupt(probe.expectCancelling()) + _ <- ZIO.attemptBlockingInterrupt(probe.expectCancelling()) } yield ()).exit)( succeeds(isUnit) ) @@ -130,10 +130,10 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { withProbe(probe => assertM((for { fiber <- probe.toStream(bufferSize).runDrain.fork - demand <- Task.attemptBlockingInterrupt(probe.expectRequest()) - _ <- Task((1 to demand.toInt).foreach(i => probe.sendNext(i))) + demand <- ZIO.attemptBlockingInterrupt(probe.expectRequest()) + _ <- ZIO.attempt((1 to demand.toInt).foreach(i => probe.sendNext(i))) _ <- fiber.interrupt - _ <- Task.attemptBlockingInterrupt(probe.expectCancelling()) + _ <- ZIO.attemptBlockingInterrupt(probe.expectCancelling()) } yield ()).exit)( succeeds(isUnit) ) @@ -143,9 +143,9 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { withProbe(probe => assertM((for { fiber <- probe.toStream(bufferSize).take(1).runDrain.fork - demand <- Task.attemptBlockingInterrupt(probe.expectRequest()) - _ <- Task((1 to demand.toInt).foreach(i => probe.sendNext(i))) - _ <- Task.attemptBlockingInterrupt(probe.expectCancelling()) + demand <- ZIO.attemptBlockingInterrupt(probe.expectRequest()) + _ <- ZIO.attempt((1 to demand.toInt).foreach(i => probe.sendNext(i))) + _ <- ZIO.attemptBlockingInterrupt(probe.expectCancelling()) _ <- fiber.join } yield ()).exit)( succeeds(isUnit) @@ -155,10 +155,10 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { test("cancels subscription on stream error") { withProbe(probe => assertM(for { - fiber <- probe.toStream(bufferSize).mapZIO(_ => Task.fail(new Throwable("boom!"))).runDrain.fork - demand <- Task.attemptBlockingInterrupt(probe.expectRequest()) - _ <- Task((1 to demand.toInt).foreach(i => probe.sendNext(i))) - _ <- Task.attemptBlockingInterrupt(probe.expectCancelling()) + fiber <- probe.toStream(bufferSize).mapZIO(_ => ZIO.fail(new Throwable("boom!"))).runDrain.fork + demand <- ZIO.attemptBlockingInterrupt(probe.expectRequest()) + _ <- ZIO.attempt((1 to demand.toInt).foreach(i => probe.sendNext(i))) + _ <- ZIO.attemptBlockingInterrupt(probe.expectCancelling()) exit <- fiber.join.exit } yield exit)(fails(anything)) ) @@ -172,20 +172,20 @@ object PublisherToStreamSpec extends DefaultRunnableSpec { def withProbe[R, E0, E >: Throwable, A](f: ManualPublisher[Int] => ZIO[R, E, A]): ZIO[R, E, A] = { val testEnv = new TestEnvironment(3000, 500) val probe = new ManualPublisher[Int](testEnv) - f(probe) <* Task(testEnv.verifyNoAsyncErrorsNoDelay()) + f(probe) <* ZIO.attempt(testEnv.verifyNoAsyncErrorsNoDelay()) } def publish(seq: Chunk[Int], failure: Option[Throwable]): UIO[Exit[Throwable, Chunk[Int]]] = { def loop(probe: ManualPublisher[Int], remaining: Chunk[Int]): Task[Unit] = for { - n <- Task.attemptBlockingInterrupt(probe.expectRequest()) - _ <- Task(assert(n.toInt)(isLessThanEqualTo(bufferSize))) + n <- ZIO.attemptBlockingInterrupt(probe.expectRequest()) + _ <- ZIO.attempt(assert(n.toInt)(isLessThanEqualTo(bufferSize))) split = n.toInt (nextN, tail) = remaining.splitAt(split) - _ <- Task(nextN.foreach(probe.sendNext)) + _ <- ZIO.attempt(nextN.foreach(probe.sendNext)) _ <- if (nextN.size < split) - Task(failure.fold(probe.sendCompletion())(probe.sendError)) + ZIO.attempt(failure.fold(probe.sendCompletion())(probe.sendError)) else loop(probe, tail) } yield () diff --git a/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala b/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala index da6b8df..00f3ba5 100644 --- a/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala @@ -1,23 +1,11 @@ package zio.interop.reactivestreams -import org.reactivestreams.Publisher -import org.reactivestreams.Subscriber -import org.reactivestreams.Subscription -import org.reactivestreams.tck.SubscriberWhiteboxVerification -import org.reactivestreams.tck.SubscriberWhiteboxVerification.SubscriberPuppet -import org.reactivestreams.tck.SubscriberWhiteboxVerification.WhiteboxSubscriberProbe -import org.reactivestreams.tck.TestEnvironment +import org.reactivestreams.{ Publisher, Subscriber, Subscription } +import org.reactivestreams.tck.SubscriberWhiteboxVerification.{ SubscriberPuppet, WhiteboxSubscriberProbe } +import org.reactivestreams.tck.{ SubscriberWhiteboxVerification, TestEnvironment } import org.testng.annotations.Test -import zio.Chunk -import zio.Promise -import zio.Task -import zio.UIO -import zio.ZIO -import zio.ZManaged -import zio.durationInt -import zio.durationLong -import zio.stream.Sink -import zio.stream.ZSink +import zio.{ Chunk, Promise, ZIO, durationInt, durationLong } +import zio.stream.{ Sink, ZSink } import zio.test.Assertion._ import zio.test._ @@ -26,15 +14,17 @@ object SinkToSubscriberSpec extends DefaultRunnableSpec { suite("Converting a `Sink` to a `Subscriber`")( test("works on the happy path")( for { - (publisher, subscribed, requested, canceled) <- makePublisherProbe - fiber <- ZSink - .fold[Int, Chunk[Int]](Chunk.empty)(_.size < 5)(_ :+ _) - .map(_.toList) - .toSubscriber() - .use { case (subscriber, r) => - UIO(publisher.subscribe(subscriber)) *> r - } - .fork + tuple <- makePublisherProbe + (publisher, subscribed, requested, canceled) = tuple + fiber <- ZIO.scoped { + ZSink + .fold[Int, Chunk[Int]](Chunk.empty)(_.size < 5)(_ :+ _) + .map(_.toList) + .toSubscriber() + .flatMap { case (subscriber, r) => + ZIO.succeed(publisher.subscribe(subscriber)) *> r + } + }.fork _ <- Live.live( assertM(subscribed.await.timeoutFail("timeout awaiting subscribe.")(500.millis).exit)(succeeds(isUnit)) ) @@ -49,12 +39,14 @@ object SinkToSubscriberSpec extends DefaultRunnableSpec { ), test("cancels subscription on interruption after subscription")( for { - (publisher, subscribed, _, canceled) <- makePublisherProbe - fiber <- Sink - .foreachChunk[Any, Throwable, Int](_ => ZIO.yieldNow) - .toSubscriber() - .use { case (subscriber, _) => UIO(publisher.subscribe(subscriber)) *> UIO.never } - .fork + tuple <- makePublisherProbe + (publisher, subscribed, _, canceled) = tuple + fiber <- ZIO.scoped { + Sink + .foreachChunk[Any, Throwable, Int](_ => ZIO.yieldNow) + .toSubscriber() + .flatMap { case (subscriber, _) => ZIO.succeed(publisher.subscribe(subscriber)) *> ZIO.never } + }.fork _ <- Live.live( assertM(subscribed.await.timeoutFail("timeout awaiting subscribe.")(500.millis).exit)(succeeds(isUnit)) @@ -68,14 +60,16 @@ object SinkToSubscriberSpec extends DefaultRunnableSpec { ), test("cancels subscription on interruption during consuption")( for { - (publisher, subscribed, requested, canceled) <- makePublisherProbe - fiber <- Sink - .foreachChunk[Any, Throwable, Int](_ => ZIO.yieldNow) - .toSubscriber() - .use { case (subscriber, _) => - Task.attemptBlockingInterrupt(publisher.subscribe(subscriber)) *> UIO.never - } - .fork + tuple <- makePublisherProbe + (publisher, subscribed, requested, canceled) = tuple + fiber <- ZIO.scoped { + Sink + .foreachChunk[Any, Throwable, Int](_ => ZIO.yieldNow) + .toSubscriber() + .flatMap { case (subscriber, _) => + ZIO.attemptBlockingInterrupt(publisher.subscribe(subscriber)) *> ZIO.never + } + }.fork _ <- assertM(subscribed.await.exit)(succeeds(isUnit)) _ <- assertM(requested.await.exit)(succeeds(isUnit)) _ <- fiber.interrupt @@ -98,14 +92,14 @@ object SinkToSubscriberSpec extends DefaultRunnableSpec { s.onSubscribe( new Subscription { override def request(n: Long): Unit = { - requested.unsafeDone(UIO.unit) + requested.unsafeDone(ZIO.unit) (1 to n.toInt).foreach(s.onNext(_)) } override def cancel(): Unit = - canceled.unsafeDone(UIO.unit) + canceled.unsafeDone(ZIO.unit) } ) - subscribed.unsafeDone(UIO.unit) + subscribed.unsafeDone(ZIO.unit) } } } yield (publisher, subscribed, requested, canceled) @@ -139,7 +133,7 @@ object SinkToSubscriberSpec extends DefaultRunnableSpec { for { subscriber_ <- Sink.collectAll[Int].toSubscriber() (subscriber, _) = subscriber_ - sbv <- ZManaged.acquireReleaseWith { + sbv <- ZIO.acquireRelease { val env = new TestEnvironment(1000, 500) val sbv = new SubscriberWhiteboxVerification[Int](env) { @@ -147,9 +141,9 @@ object SinkToSubscriberSpec extends DefaultRunnableSpec { ProbedSubscriber(subscriber, probe) override def createElement(element: Int): Int = element } - UIO(sbv.setUp()) *> UIO(sbv.startPublisherExecutorService()).as((sbv, env)) + ZIO.succeed(sbv.setUp()) *> ZIO.succeed(sbv.startPublisherExecutorService()).as((sbv, env)) } { case (sbv, _) => - UIO(sbv.shutdownPublisherExecutorService()) + ZIO.succeed(sbv.shutdownPublisherExecutorService()) } } yield sbv @@ -168,9 +162,11 @@ object SinkToSubscriberSpec extends DefaultRunnableSpec { case method => test(method.getName())( for { - r <- managedVerification.use { case (sbv, env) => - Task.attemptBlockingInterrupt(method.invoke(sbv)).timeout(env.defaultTimeoutMillis().millis) - }.unit.exit + r <- ZIO.scoped { + managedVerification.flatMap { case (sbv, env) => + ZIO.attemptBlockingInterrupt(method.invoke(sbv)).timeout(env.defaultTimeoutMillis().millis) + }.unit.exit + } } yield assert(r)(succeeds(isUnit)) ) } diff --git a/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala b/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala index 50268ba..01c9692 100644 --- a/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala @@ -1,11 +1,8 @@ package zio.interop.reactivestreams import org.reactivestreams.Publisher -import org.reactivestreams.tck.PublisherVerification -import org.reactivestreams.tck.TestEnvironment +import org.reactivestreams.tck.{ PublisherVerification, TestEnvironment } import org.testng.annotations.Test -import zio.Task -import zio.UIO import zio.ZIO import zio.stream.Stream import zio.test.Assertion._ @@ -55,8 +52,8 @@ object StreamToPublisherSpec extends DefaultRunnableSpec { for { runtime <- ZIO.runtime[Any] pv = makePV(runtime) - _ <- UIO(pv.setUp()) - r <- Task + _ <- ZIO.succeed(pv.setUp()) + r <- ZIO .attemptBlockingInterrupt(method.invoke(pv)) .unit .refineOrDie { case e: InvocationTargetException => e.getTargetException() } diff --git a/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala b/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala index 04c0115..d887205 100644 --- a/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala @@ -6,7 +6,7 @@ import zio.stream.{ Stream, ZStream } import zio.test.Assertion._ import zio.test.TestAspect.nonFlaky import zio.test._ -import zio.{ IO, Task, UIO, durationInt } +import zio.{ IO, UIO, ZIO, durationInt } import scala.jdk.CollectionConverters._ @@ -15,55 +15,62 @@ object SubscriberToSinkSpec extends DefaultRunnableSpec { suite("Converting a `Subscriber` to a `Sink`")( test("works on the happy path") { makeSubscriber.flatMap(probe => - probe.underlying - .toSink[Throwable] - .use { case (_, sink) => - for { - fiber <- Stream.fromIterable(seq).run(sink).fork - _ <- probe.request(length + 1) - elements <- probe.nextElements(length).exit - completion <- probe.expectCompletion.exit - _ <- fiber.join - } yield assert(elements)(succeeds(equalTo(seq))) && assert(completion)(succeeds(isUnit)) - } + ZIO.scoped[Any] { + probe.underlying + .toSink[Throwable] + .flatMap { case (_, sink) => + for { + fiber <- Stream.fromIterable(seq).run(sink).fork + _ <- probe.request(length + 1) + elements <- probe.nextElements(length).exit + completion <- probe.expectCompletion.exit + _ <- fiber.join + } yield assert(elements)(succeeds(equalTo(seq))) && assert(completion)(succeeds(isUnit)) + } + } ) }, test("transports errors") { makeSubscriber.flatMap(probe => - probe.underlying - .toSink[Throwable] - .use { case (signalError, sink) => - for { - fiber <- (Stream.fromIterable(seq) ++ Stream.fail(e)).run(sink).catchAll(signalError).fork - _ <- probe.request(length + 1) - elements <- probe.nextElements(length).exit - err <- probe.expectError.exit - _ <- fiber.join - } yield assert(elements)(succeeds(equalTo(seq))) && assert(err)(succeeds(equalTo(e))) - } + ZIO.scoped[Any] { + probe.underlying + .toSink[Throwable] + .flatMap { case (signalError, sink) => + for { + fiber <- (Stream.fromIterable(seq) ++ Stream.fail(e)).run(sink).catchAll(signalError).fork + _ <- probe.request(length + 1) + elements <- probe.nextElements(length).exit + err <- probe.expectError.exit + _ <- fiber.join + } yield assert(elements)(succeeds(equalTo(seq))) && assert(err)(succeeds(equalTo(e))) + } + } ) }, test("transports errors 2") { makeSubscriber.flatMap(probe => - probe.underlying - .toSink[Throwable] - .use { case (signalError, sink) => - for { - _ <- ZStream.fail(e).run(sink).catchAll(signalError) - err <- probe.expectError.exit - } yield assert(err)(succeeds(equalTo(e))) - } + ZIO.scoped[Any] { + probe.underlying + .toSink[Throwable] + .flatMap { case (signalError, sink) => + for { + _ <- ZStream.fail(e).run(sink).catchAll(signalError) + err <- probe.expectError.exit + } yield assert(err)(succeeds(equalTo(e))) + } + } ) }, test("transports errors 3") { makeSubscriber.flatMap { probe => for { - fiber <- probe.underlying - .toSink[Throwable] - .use { case (signalError, sink) => - ZStream.fail(e).run(sink).catchAll(signalError) - } - .fork + fiber <- ZIO.scoped { + probe.underlying + .toSink[Throwable] + .flatMap { case (signalError, sink) => + ZStream.fail(e).run(sink).catchAll(signalError) + } + }.fork _ <- fiber.join err <- probe.expectError.exit } yield assert(err)(succeeds(equalTo(e))) @@ -71,16 +78,18 @@ object SubscriberToSinkSpec extends DefaultRunnableSpec { } @@ nonFlaky(10), test("transports errors only once") { makeSubscriber.flatMap(probe => - probe.underlying - .toSink[Throwable] - .use { case (signalError, sink) => - for { - _ <- ZStream.fail(e).run(sink).catchAll(signalError) - err <- probe.expectError.exit - _ <- signalError(e) - err2 <- probe.expectError.timeout(100.millis).exit - } yield assert(err)(succeeds(equalTo(e))) && assert(err2)(fails(anything)) - } + ZIO.scoped { + probe.underlying + .toSink[Throwable] + .flatMap { case (signalError, sink) => + for { + _ <- ZStream.fail(e).run(sink).catchAll(signalError) + err <- probe.expectError.exit + _ <- signalError(e) + err2 <- probe.expectError.timeout(100.millis).exit + } yield assert(err)(succeeds(equalTo(e))) && assert(err2)(fails(anything)) + } + } ) } ) @@ -91,15 +100,16 @@ object SubscriberToSinkSpec extends DefaultRunnableSpec { case class Probe[T](underlying: ManualSubscriberWithSubscriptionSupport[T]) { def request(n: Long): UIO[Unit] = - UIO(underlying.request(n)) + ZIO.succeed(underlying.request(n)) def nextElements(n: Long): IO[Throwable, List[T]] = - Task.attemptBlockingInterrupt(underlying.nextElements(n.toLong).asScala.toList) + ZIO.attemptBlockingInterrupt(underlying.nextElements(n.toLong).asScala.toList) def expectError: IO[Throwable, Throwable] = - Task.attemptBlockingInterrupt(underlying.expectError(classOf[Throwable])) + ZIO.attemptBlockingInterrupt(underlying.expectError(classOf[Throwable])) def expectCompletion: IO[Throwable, Unit] = - Task.attemptBlockingInterrupt(underlying.expectCompletion()) + ZIO.attemptBlockingInterrupt(underlying.expectCompletion()) } - val makeSubscriber = UIO(new ManualSubscriberWithSubscriptionSupport[Int](new TestEnvironment(2000))).map(Probe.apply) + val makeSubscriber = + ZIO.succeed(new ManualSubscriberWithSubscriptionSupport[Int](new TestEnvironment(2000))).map(Probe.apply) } From f6827643e4b34b53de9d05925bf75e7899778287 Mon Sep 17 00:00:00 2001 From: Simon Schenk Date: Sun, 3 Apr 2022 19:14:32 +0200 Subject: [PATCH 17/23] ZIO 2.0.0-RC4 --- build.sbt | 4 +-- .../PublisherToStreamSpec.scala | 2 +- .../SinkToSubscriberSpec.scala | 2 +- .../StreamToPublisherSpec.scala | 2 +- .../SubscriberToSinkSpec.scala | 27 +++++++++---------- 5 files changed, 17 insertions(+), 20 deletions(-) diff --git a/build.sbt b/build.sbt index d19a3ed..ff7d280 100644 --- a/build.sbt +++ b/build.sbt @@ -36,9 +36,9 @@ Global / onChangedBuildSource := ReloadOnSourceChanges addCommandAlias("fmt", "all scalafmtSbt scalafmt test:scalafmt") addCommandAlias("check", "all scalafmtSbtCheck scalafmtCheck test:scalafmtCheck") -val zioVersion = "2.0.0-RC3" +val zioVersion = "2.0.0-RC4" val rsVersion = "1.0.3" -val collCompatVersion = "2.5.0" +val collCompatVersion = "2.7.0" lazy val interopReactiveStreams = project .in(file(".")) diff --git a/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala b/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala index 0a1a9f9..dd7d17b 100644 --- a/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala @@ -21,7 +21,7 @@ import zio.stream.Stream import zio.test.Assertion._ import zio.test._ -object PublisherToStreamSpec extends DefaultRunnableSpec { +object PublisherToStreamSpec extends ZIOSpecDefault { override def spec = suite("Converting a `Publisher` to a `Stream`")( diff --git a/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala b/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala index 00f3ba5..12b7fd5 100644 --- a/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala @@ -9,7 +9,7 @@ import zio.stream.{ Sink, ZSink } import zio.test.Assertion._ import zio.test._ -object SinkToSubscriberSpec extends DefaultRunnableSpec { +object SinkToSubscriberSpec extends ZIOSpecDefault { override def spec = suite("Converting a `Sink` to a `Subscriber`")( test("works on the happy path")( diff --git a/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala b/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala index 01c9692..c2263a7 100644 --- a/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala @@ -10,7 +10,7 @@ import zio.test._ import java.lang.reflect.InvocationTargetException -object StreamToPublisherSpec extends DefaultRunnableSpec { +object StreamToPublisherSpec extends ZIOSpecDefault { override def spec = suite("Converting a `Stream` to a `Publisher`")( suite("passes all required and optional TCK tests")(tests: _*) diff --git a/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala b/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala index d887205..dc661c1 100644 --- a/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala @@ -10,7 +10,7 @@ import zio.{ IO, UIO, ZIO, durationInt } import scala.jdk.CollectionConverters._ -object SubscriberToSinkSpec extends DefaultRunnableSpec { +object SubscriberToSinkSpec extends ZIOSpecDefault { override def spec = suite("Converting a `Subscriber` to a `Sink`")( test("works on the happy path") { @@ -77,20 +77,17 @@ object SubscriberToSinkSpec extends DefaultRunnableSpec { } } @@ nonFlaky(10), test("transports errors only once") { - makeSubscriber.flatMap(probe => - ZIO.scoped { - probe.underlying - .toSink[Throwable] - .flatMap { case (signalError, sink) => - for { - _ <- ZStream.fail(e).run(sink).catchAll(signalError) - err <- probe.expectError.exit - _ <- signalError(e) - err2 <- probe.expectError.timeout(100.millis).exit - } yield assert(err)(succeeds(equalTo(e))) && assert(err2)(fails(anything)) - } - } - ) + ZIO.scoped[Any] { + for { + probe <- makeSubscriber + ses <- probe.underlying.toSink + (signalError, sink) = ses + _ <- ZStream.fail(e).run(sink).catchAll(signalError) + err <- probe.expectError.exit + _ <- signalError(e) + err2 <- probe.expectError.timeout(100.millis).exit + } yield assert(err)(succeeds(equalTo(e))) && assert(err2)(fails(anything)) + } } ) From b569a03e1ffbb0a5cec931635c469cbe7998d4e3 Mon Sep 17 00:00:00 2001 From: Daniel Vigovszky Date: Sat, 9 Apr 2022 07:17:19 +0200 Subject: [PATCH 18/23] ZIO 2.0.0-RC5 (#315) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index ff7d280..9ceb9e7 100644 --- a/build.sbt +++ b/build.sbt @@ -36,7 +36,7 @@ Global / onChangedBuildSource := ReloadOnSourceChanges addCommandAlias("fmt", "all scalafmtSbt scalafmt test:scalafmt") addCommandAlias("check", "all scalafmtSbtCheck scalafmtCheck test:scalafmtCheck") -val zioVersion = "2.0.0-RC4" +val zioVersion = "2.0.0-RC5" val rsVersion = "1.0.3" val collCompatVersion = "2.7.0" From 006b65989e7e5bf86cabbcd2f62da9120b72bbac Mon Sep 17 00:00:00 2001 From: Simon Schenk Date: Thu, 21 Apr 2022 13:31:33 +0200 Subject: [PATCH 19/23] scala and plugin version bumps (#319) --- .github/workflows/ci.yml | 2 +- project/BuildHelper.scala | 4 ++-- project/plugins.sbt | 11 +++++------ 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b918be..d90b144 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: fail-fast: false matrix: java: ['adopt@1.8', 'adopt@1.11'] - scala: ['2.11.12', '2.12.15', '2.13.6', '3.1.0'] + scala: ['2.11.12', '2.12.15', '2.13.8', '3.1.2'] steps: - name: Checkout current branch uses: actions/checkout@v2.4.0 diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 538f8d5..b248cff 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -10,8 +10,8 @@ import scalafix.sbt.ScalafixPlugin.autoImport._ object BuildHelper { val Scala211 = "2.11.12" val Scala212 = "2.12.15" - val Scala213 = "2.13.6" - val ScalaDotty = "3.1.0" + val Scala213 = "2.13.8" + val ScalaDotty = "3.1.2" private val stdOptions = Seq( "-deprecation", diff --git a/project/plugins.sbt b/project/plugins.sbt index 2b18689..52e8a41 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,8 +1,7 @@ -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.11") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.31") -addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.10.0") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.10.0") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.7") addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.2.16") -addSbtPlugin("org.portable-scala" % "sbt-crossproject" % "1.1.0") -addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.6.0") -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.3") +addSbtPlugin("org.portable-scala" % "sbt-crossproject" % "1.2.0") +addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.7.0") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") From e050c84070c6d6ccaf1a3a789dcc4cab4bbe9662 Mon Sep 17 00:00:00 2001 From: Simon Schenk Date: Mon, 2 May 2022 13:31:44 +0200 Subject: [PATCH 20/23] feat: rename toSink to toZIOSink and toStream to toZIOStream (#320) * feat: rename toSink to toZIOSink and toStream to toZIOStream In order to ease use together with libraries, which offer their own conversions for reactive streams, we use more concrete names for our own implicits to avoid conflicts. fixes Interop with Flux and Mono types #309 * fix: import --- README.md | 13 ++++----- .../zio/interop/reactivestreams/package.scala | 29 +++++++++---------- .../PublisherToStreamSpec.scala | 16 +++++----- .../SubscriberToSinkSpec.scala | 10 +++---- 4 files changed, 33 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index b46235c..01e8e30 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Releases][Badge-SonatypeReleases]][Link-SonatypeReleases] [![Snapshots][Badge-SonatypeSnapshots]][Link-SonatypeSnapshots] -This library provides an interoperability layer for reactive streams. +This library provides an interoperability layer between ZIO and reactive streams. ## Reactive Streams `Producer` and `Subscriber` @@ -44,7 +44,7 @@ A `Publisher` used as a `Stream` buffers up to `qSize` elements. If possible, `q a power of two for best performance. The default is 16. ```scala mdoc -val streamFromPublisher = publisher.toStream(qSize = 16) +val streamFromPublisher = publisher.toZIOStream(qSize = 16) runtime.unsafeRun( streamFromPublisher.run(Sink.collectAll[Integer]) ) @@ -53,15 +53,14 @@ runtime.unsafeRun( ### Subscriber to Sink When running a `Stream` to a `Subscriber`, a side channel is needed for signalling failures. -For this reason `toSink` returns a tuple of `Promise` and `Sink`. The `Promise` must be failed -on `Stream` failure. The type parameter on `toSink` is the error type of *the Stream*. +For this reason `toZIOSink` returns a tuple of a callback and a `Sink`. The callback must be used to signal `Stream` failure. The type parameter on `toZIOSink` is the error type of *the Stream*. ```scala mdoc -val asSink = subscriber.toSink[Throwable] +val asSink = subscriber.toZIOSink[Throwable] val failingStream = Stream.range(3, 13) ++ Stream.fail(new RuntimeException("boom!")) runtime.unsafeRun( - asSink.use { case (errorP, sink) => - failingStream.run(sink).catchAll(errorP.fail) + asSink.use { case (signalError, sink) => + failingStream.run(sink).catchAll(signalError) } ) ``` diff --git a/src/main/scala/zio/interop/reactivestreams/package.scala b/src/main/scala/zio/interop/reactivestreams/package.scala index ea3fe44..acdd4c2 100644 --- a/src/main/scala/zio/interop/reactivestreams/package.scala +++ b/src/main/scala/zio/interop/reactivestreams/package.scala @@ -2,7 +2,7 @@ package zio.interop import org.reactivestreams.Publisher import org.reactivestreams.Subscriber -import zio.{ IO, Scope, UIO, ZIO, ZTraceElement } +import zio.{ Scope, UIO, Task, ZIO, ZTraceElement } import zio.stream.ZSink import zio.stream.ZStream @@ -19,42 +19,41 @@ package object reactivestreams { final implicit class sinkToSubscriber[R, E <: Throwable, A, L, Z](private val sink: ZSink[R, E, A, L, Z]) { - /** Create a `Subscriber` from a `Sink`. The returned IO will eventually return the result of running the subscribed - * stream to the sink. Consumption is started as soon as the resource is used, even if the IO is never run. - * Interruption propagates from the `Zmanaged` to the stream, but not from the IO. + /** Create a `Subscriber` from a `Sink`. The returned Task will eventually return the result of running the + * subscribed stream to the sink. Consumption is started immediately, even if the Task is never run. Interruption + * propagates from this ZIO to the stream, but not from the Task. * @param qSize - * The size used as internal buffer. A maximum of `qSize-1` `A`s will be buffered, so `qSize` must be > 1. If - * possible, set to a power of 2 value for best performance. + * The size used as internal buffer. If possible, set to a power of 2 value for best performance. */ def toSubscriber(qSize: Int = 16)(implicit trace: ZTraceElement - ): ZIO[R with Scope, Throwable, (Subscriber[A], IO[Throwable, Z])] = + ): ZIO[R with Scope, Throwable, (Subscriber[A], Task[Z])] = Adapters.sinkToSubscriber(sink, qSize) } final implicit class publisherToStream[O](private val publisher: Publisher[O]) extends AnyVal { - /** @param qSize - * The size used as internal buffer. A maximum of `qSize-1` `A`s will be buffered, so `qSize` must be > 1. If - * possible, set to a power of 2 value for best performance. + /** Create a `Stream` from a `Publisher`. + * @param qSize + * The size used as internal buffer. If possible, set to a power of 2 value for best performance. */ - def toStream(qSize: Int = 16)(implicit trace: ZTraceElement): ZStream[Any, Throwable, O] = + def toZIOStream(qSize: Int = 16)(implicit trace: ZTraceElement): ZStream[Any, Throwable, O] = Adapters.publisherToStream(publisher, qSize) } final implicit class subscriberToSink[I](private val subscriber: Subscriber[I]) extends AnyVal { - /** Create a `Sink` from a `Subscriber`. Errors need to be transported via the returned Promise: + /** Create a `Sink` from a `Subscriber`. Errors need to be transported via the returned callback: * * ``` * val subscriber: Subscriber[Int] = ??? * val stream: Stream[Any, Throwable, Int] = ??? - * subscriber.toSink.use { case (error, sink) => - * stream.run(sink).catchAll(e => error.fail(e)) + * subscriber.toZIOSink.use { case (signalError, sink) => + * stream.run(sink).catchAll(signalError) * } * ``` */ - def toSink[E <: Throwable](implicit + def toZIOSink[E <: Throwable](implicit trace: ZTraceElement ): ZIO[Scope, Nothing, (E => UIO[Unit], ZSink[Any, Nothing, I, I, Unit])] = Adapters.subscriberToSink(subscriber) diff --git a/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala b/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala index dd7d17b..65729ca 100644 --- a/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala @@ -69,7 +69,7 @@ object PublisherToStreamSpec extends ZIOSpecDefault { for { outerRuntime <- ZIO.runtime[Any] runtime = outerRuntime.mapRuntimeConfig(_.copy(supervisor = supervisor)) - exit <- runtime.run(publisher.toStream().runDrain.exit) + exit <- runtime.run(publisher.toZIOStream().runDrain.exit) failed <- supervisor.value } yield assert(exit)(fails(anything)) && assert(failed)(isFalse) @@ -80,7 +80,7 @@ object PublisherToStreamSpec extends ZIOSpecDefault { fiber <- Stream .fromZIO( ZIO.succeed( - probe.toStream() + probe.toZIOStream() ) ) .flatMap(identity) @@ -106,7 +106,7 @@ object PublisherToStreamSpec extends ZIOSpecDefault { override def subscribe(subscriber: Subscriber[_ >: Int]): Unit = subscriberP.unsafeDone(ZIO.succeedNow(subscriber)) } - fiber <- probe.toStream(bufferSize).runDrain.fork + fiber <- probe.toZIOStream(bufferSize).runDrain.fork subscriber <- subscriberP.await _ <- fiber.interrupt _ <- ZIO.succeed(subscriber.onSubscribe(subscription)) @@ -117,7 +117,7 @@ object PublisherToStreamSpec extends ZIOSpecDefault { test("cancels subscription when interrupted after subscription") { withProbe(probe => assertM((for { - fiber <- probe.toStream(bufferSize).runDrain.fork + fiber <- probe.toZIOStream(bufferSize).runDrain.fork _ <- ZIO.attemptBlockingInterrupt(probe.expectRequest()) _ <- fiber.interrupt _ <- ZIO.attemptBlockingInterrupt(probe.expectCancelling()) @@ -129,7 +129,7 @@ object PublisherToStreamSpec extends ZIOSpecDefault { test("cancels subscription when interrupted during consumption") { withProbe(probe => assertM((for { - fiber <- probe.toStream(bufferSize).runDrain.fork + fiber <- probe.toZIOStream(bufferSize).runDrain.fork demand <- ZIO.attemptBlockingInterrupt(probe.expectRequest()) _ <- ZIO.attempt((1 to demand.toInt).foreach(i => probe.sendNext(i))) _ <- fiber.interrupt @@ -142,7 +142,7 @@ object PublisherToStreamSpec extends ZIOSpecDefault { test("cancels subscription on stream end") { withProbe(probe => assertM((for { - fiber <- probe.toStream(bufferSize).take(1).runDrain.fork + fiber <- probe.toZIOStream(bufferSize).take(1).runDrain.fork demand <- ZIO.attemptBlockingInterrupt(probe.expectRequest()) _ <- ZIO.attempt((1 to demand.toInt).foreach(i => probe.sendNext(i))) _ <- ZIO.attemptBlockingInterrupt(probe.expectCancelling()) @@ -155,7 +155,7 @@ object PublisherToStreamSpec extends ZIOSpecDefault { test("cancels subscription on stream error") { withProbe(probe => assertM(for { - fiber <- probe.toStream(bufferSize).mapZIO(_ => ZIO.fail(new Throwable("boom!"))).runDrain.fork + fiber <- probe.toZIOStream(bufferSize).mapZIO(_ => ZIO.fail(new Throwable("boom!"))).runDrain.fork demand <- ZIO.attemptBlockingInterrupt(probe.expectRequest()) _ <- ZIO.attempt((1 to demand.toInt).foreach(i => probe.sendNext(i))) _ <- ZIO.attemptBlockingInterrupt(probe.expectCancelling()) @@ -192,7 +192,7 @@ object PublisherToStreamSpec extends ZIOSpecDefault { val faillable = withProbe(probe => for { - fiber <- probe.toStream(bufferSize).run(Sink.collectAll[Int]).fork + fiber <- probe.toZIOStream(bufferSize).run(Sink.collectAll[Int]).fork _ <- loop(probe, seq) r <- fiber.join } yield r diff --git a/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala b/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala index dc661c1..6be8980 100644 --- a/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala @@ -17,7 +17,7 @@ object SubscriberToSinkSpec extends ZIOSpecDefault { makeSubscriber.flatMap(probe => ZIO.scoped[Any] { probe.underlying - .toSink[Throwable] + .toZIOSink[Throwable] .flatMap { case (_, sink) => for { fiber <- Stream.fromIterable(seq).run(sink).fork @@ -34,7 +34,7 @@ object SubscriberToSinkSpec extends ZIOSpecDefault { makeSubscriber.flatMap(probe => ZIO.scoped[Any] { probe.underlying - .toSink[Throwable] + .toZIOSink[Throwable] .flatMap { case (signalError, sink) => for { fiber <- (Stream.fromIterable(seq) ++ Stream.fail(e)).run(sink).catchAll(signalError).fork @@ -51,7 +51,7 @@ object SubscriberToSinkSpec extends ZIOSpecDefault { makeSubscriber.flatMap(probe => ZIO.scoped[Any] { probe.underlying - .toSink[Throwable] + .toZIOSink[Throwable] .flatMap { case (signalError, sink) => for { _ <- ZStream.fail(e).run(sink).catchAll(signalError) @@ -66,7 +66,7 @@ object SubscriberToSinkSpec extends ZIOSpecDefault { for { fiber <- ZIO.scoped { probe.underlying - .toSink[Throwable] + .toZIOSink[Throwable] .flatMap { case (signalError, sink) => ZStream.fail(e).run(sink).catchAll(signalError) } @@ -80,7 +80,7 @@ object SubscriberToSinkSpec extends ZIOSpecDefault { ZIO.scoped[Any] { for { probe <- makeSubscriber - ses <- probe.underlying.toSink + ses <- probe.underlying.toZIOSink (signalError, sink) = ses _ <- ZStream.fail(e).run(sink).catchAll(signalError) err <- probe.expectError.exit From 17a05bf2785a503901fc363ec1c22a7fce265c4a Mon Sep 17 00:00:00 2001 From: Adam Fraser Date: Tue, 3 May 2022 17:09:57 -0700 Subject: [PATCH 21/23] upgrade zio version (#323) --- README.md | 2 +- build.sbt | 2 +- .../interop/reactivestreams/Adapters.scala | 36 +++++++++--------- .../zio/interop/reactivestreams/package.scala | 10 ++--- .../PublisherToStreamSpec.scala | 38 +++++++++---------- .../SinkToSubscriberSpec.scala | 29 +++++++------- .../StreamToPublisherSpec.scala | 6 +-- .../SubscriberToSinkSpec.scala | 6 +-- 8 files changed, 65 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 01e8e30..d94e281 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ For this reason `toZIOSink` returns a tuple of a callback and a `Sink`. The call ```scala mdoc val asSink = subscriber.toZIOSink[Throwable] -val failingStream = Stream.range(3, 13) ++ Stream.fail(new RuntimeException("boom!")) +val failingStream = ZStream.range(3, 13) ++ ZStream.fail(new RuntimeException("boom!")) runtime.unsafeRun( asSink.use { case (signalError, sink) => failingStream.run(sink).catchAll(signalError) diff --git a/build.sbt b/build.sbt index 9ceb9e7..be5e5fc 100644 --- a/build.sbt +++ b/build.sbt @@ -36,7 +36,7 @@ Global / onChangedBuildSource := ReloadOnSourceChanges addCommandAlias("fmt", "all scalafmtSbt scalafmt test:scalafmt") addCommandAlias("check", "all scalafmtSbtCheck scalafmtCheck test:scalafmtCheck") -val zioVersion = "2.0.0-RC5" +val zioVersion = "2.0.0-RC6" val rsVersion = "1.0.3" val collCompatVersion = "2.7.0" diff --git a/src/main/scala/zio/interop/reactivestreams/Adapters.scala b/src/main/scala/zio/interop/reactivestreams/Adapters.scala index d245b0e..a2c55ff 100644 --- a/src/main/scala/zio/interop/reactivestreams/Adapters.scala +++ b/src/main/scala/zio/interop/reactivestreams/Adapters.scala @@ -15,7 +15,7 @@ object Adapters { def streamToPublisher[R, E <: Throwable, O]( stream: => ZStream[R, E, O] - )(implicit trace: ZTraceElement): ZIO[R, Nothing, Publisher[O]] = + )(implicit trace: Trace): ZIO[R, Nothing, Publisher[O]] = ZIO.runtime.map { runtime => subscriber => if (subscriber == null) { throw new NullPointerException("Subscriber must not be null.") @@ -35,7 +35,7 @@ object Adapters { def subscriberToSink[E <: Throwable, I]( subscriber: => Subscriber[I] - )(implicit trace: ZTraceElement): ZIO[Scope, Nothing, (E => UIO[Unit], ZSink[Any, Nothing, I, I, Unit])] = { + )(implicit trace: Trace): ZIO[Scope, Nothing, (E => UIO[Unit], ZSink[Any, Nothing, I, I, Unit])] = { val sub = subscriber for { error <- Promise.make[E, Nothing] @@ -48,7 +48,7 @@ object Adapters { def publisherToStream[O]( publisher: => Publisher[O], bufferSize: => Int - )(implicit trace: ZTraceElement): ZStream[Any, Throwable, O] = { + )(implicit trace: Trace): ZStream[Any, Throwable, O] = { val pullOrFail = for { @@ -66,7 +66,7 @@ object Adapters { def sinkToSubscriber[R, I, L, Z]( sink: => ZSink[R, Throwable, I, L, Z], bufferSize: => Int - )(implicit trace: ZTraceElement): ZIO[R with Scope, Throwable, (Subscriber[I], IO[Throwable, Z])] = + )(implicit trace: Trace): ZIO[R with Scope, Throwable, (Subscriber[I], IO[Throwable, Z])] = for { subscriberP <- makeSubscriber[I](bufferSize) (subscriber, p) = subscriberP @@ -145,19 +145,19 @@ object Adapters { override def await(): IO[Option[Throwable], Unit] = done match { - case Some(value) => IO.fail(value) + case Some(value) => ZIO.fail(value) case None => val p = Promise.unsafeMake[Option[Throwable], Unit](FiberId.None) toNotify = Some(p) // An element has arrived in the meantime, we do not need to start waiting. if (!q.isEmpty()) { toNotify = None - IO.unit + ZIO.unit } else done.fold(p.await) { e => // The producer has canceled or errored in the meantime. toNotify = None - IO.fail(e) + ZIO.fail(e) } } @@ -166,7 +166,7 @@ object Adapters { override def onSubscribe(s: Subscription): Unit = if (s == null) { val e = new NullPointerException("s was null in onSubscribe") - p.unsafeDone(IO.fail(e)) + p.unsafeDone(ZIO.fail(e)) throw e } else { val shouldCancel = isSubscribedOrInterrupted.getAndSet(true) @@ -181,7 +181,7 @@ object Adapters { failNPE("t was null in onNext") } else { q.offer(t) - toNotify.foreach(_.unsafeDone(IO.unit)) + toNotify.foreach(_.unsafeDone(ZIO.unit)) } override def onError(e: Throwable): Unit = @@ -192,7 +192,7 @@ object Adapters { override def onComplete(): Unit = { done = Some(None) - toNotify.foreach(_.unsafeDone(IO.fail(None))) + toNotify.foreach(_.unsafeDone(ZIO.fail(None))) } private def failNPE(msg: String) = { @@ -203,7 +203,7 @@ object Adapters { private def fail(e: Throwable) = { done = Some(Some(e)) - toNotify.foreach(_.unsafeDone(IO.fail(Some(e)))) + toNotify.foreach(_.unsafeDone(ZIO.fail(Some(e)))) } } @@ -217,12 +217,12 @@ object Adapters { ): ZSink[Any, Nothing, I, I, Unit] = ZSink .foldChunksZIO[Any, Nothing, I, Boolean](true)(identity) { (_, chunk) => - IO + ZIO .iterate(chunk)(!_.isEmpty) { chunk => subscription .offer(chunk.size) .flatMap { acceptedCount => - UIO + ZIO .foreach(chunk.take(acceptedCount))(a => ZIO.succeed(subscriber.onNext(a))) .as(chunk.drop(acceptedCount)) } @@ -252,7 +252,7 @@ object Adapters { var result: IO[Unit, Int] = null state.updateAndGet { case `canceled` => - result = IO.fail(()) + result = ZIO.fail(()) canceled case State(0L, _) => val p = Promise.unsafeMake[Unit, Int](FiberId.None) @@ -261,7 +261,7 @@ object Adapters { case State(requestedCount, _) => val newRequestedCount = Math.max(requestedCount - n, 0L) val accepted = Math.min(requestedCount, n.toLong).toInt - result = IO.succeedNow(accepted) + result = ZIO.succeedNow(accepted) requested(newRequestedCount) } result @@ -279,7 +279,7 @@ object Adapters { val newRequestedCount = requestedCount + n val accepted = Math.min(offered.toLong, newRequestedCount) val remaining = newRequestedCount - accepted - notification = () => toNotify.unsafeDone(IO.succeedNow(accepted.toInt)) + notification = () => toNotify.unsafeDone(ZIO.succeedNow(accepted.toInt)) requested(remaining) case State(requestedCount, _) if ((Long.MaxValue - n) > requestedCount) => requested(requestedCount + n) @@ -290,11 +290,11 @@ object Adapters { } override def cancel(): Unit = - state.getAndSet(canceled).toNotify.foreach { case (_, p) => p.unsafeDone(IO.fail(())) } + state.getAndSet(canceled).toNotify.foreach { case (_, p) => p.unsafeDone(ZIO.fail(())) } } private def fromPull[R, E, A](zio: ZIO[R with Scope, Nothing, ZIO[R, Option[E], Chunk[A]]])(implicit - trace: ZTraceElement + trace: Trace ): ZStream[R, E, A] = ZStream.unwrapScoped[R](zio.map(pull => ZStream.repeatZIOChunkOption(pull))) } diff --git a/src/main/scala/zio/interop/reactivestreams/package.scala b/src/main/scala/zio/interop/reactivestreams/package.scala index acdd4c2..6ac7b4a 100644 --- a/src/main/scala/zio/interop/reactivestreams/package.scala +++ b/src/main/scala/zio/interop/reactivestreams/package.scala @@ -2,7 +2,7 @@ package zio.interop import org.reactivestreams.Publisher import org.reactivestreams.Subscriber -import zio.{ Scope, UIO, Task, ZIO, ZTraceElement } +import zio.{ Scope, UIO, Task, ZIO, Trace } import zio.stream.ZSink import zio.stream.ZStream @@ -13,7 +13,7 @@ package object reactivestreams { /** Create a `Publisher` from a `Stream`. Every time the `Publisher` is subscribed to, a new instance of the * `Stream` is run. */ - def toPublisher(implicit trace: ZTraceElement): ZIO[R, Nothing, Publisher[O]] = + def toPublisher(implicit trace: Trace): ZIO[R, Nothing, Publisher[O]] = Adapters.streamToPublisher(stream) } @@ -26,7 +26,7 @@ package object reactivestreams { * The size used as internal buffer. If possible, set to a power of 2 value for best performance. */ def toSubscriber(qSize: Int = 16)(implicit - trace: ZTraceElement + trace: Trace ): ZIO[R with Scope, Throwable, (Subscriber[A], Task[Z])] = Adapters.sinkToSubscriber(sink, qSize) } @@ -37,7 +37,7 @@ package object reactivestreams { * @param qSize * The size used as internal buffer. If possible, set to a power of 2 value for best performance. */ - def toZIOStream(qSize: Int = 16)(implicit trace: ZTraceElement): ZStream[Any, Throwable, O] = + def toZIOStream(qSize: Int = 16)(implicit trace: Trace): ZStream[Any, Throwable, O] = Adapters.publisherToStream(publisher, qSize) } @@ -54,7 +54,7 @@ package object reactivestreams { * ``` */ def toZIOSink[E <: Throwable](implicit - trace: ZTraceElement + trace: Trace ): ZIO[Scope, Nothing, (E => UIO[Unit], ZSink[Any, Nothing, I, I, Unit])] = Adapters.subscriberToSink(subscriber) } diff --git a/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala b/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala index 65729ca..900114f 100644 --- a/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala @@ -9,15 +9,16 @@ import zio.Chunk import zio.Exit import zio.Fiber import zio.Promise +import zio.Runtime import zio.Supervisor import zio.Task import zio.UIO import zio.ZEnvironment import zio.ZIO -import zio.ZTraceElement +import zio.Trace import zio.durationInt -import zio.stream.Sink -import zio.stream.Stream +import zio.stream.ZSink +import zio.stream.ZStream import zio.test.Assertion._ import zio.test._ @@ -26,13 +27,13 @@ object PublisherToStreamSpec extends ZIOSpecDefault { override def spec = suite("Converting a `Publisher` to a `Stream`")( test("works with a well behaved `Publisher`") { - assertM(publish(seq, None))(succeeds(equalTo(seq))) + assertZIO(publish(seq, None))(succeeds(equalTo(seq))) }, test("fails with an initially failed `Publisher`") { - assertM(publish(Chunk.empty, Some(e)))(fails(equalTo(e))) + assertZIO(publish(Chunk.empty, Some(e)))(fails(equalTo(e))) }, test("fails with an eventually failing `Publisher`") { - assertM(publish(seq, Some(e)))(fails(equalTo(e))) + assertZIO(publish(seq, Some(e)))(fails(equalTo(e))) }, test("does not fail a fiber on failing `Publisher`") { @@ -51,7 +52,7 @@ object PublisherToStreamSpec extends ZIOSpecDefault { @transient var failedAFiber = false - def value(implicit trace: ZTraceElement): UIO[Boolean] = + def value(implicit trace: Trace): UIO[Boolean] = ZIO.succeed(failedAFiber) def unsafeOnStart[R, E, A]( @@ -67,24 +68,23 @@ object PublisherToStreamSpec extends ZIOSpecDefault { } for { - outerRuntime <- ZIO.runtime[Any] - runtime = outerRuntime.mapRuntimeConfig(_.copy(supervisor = supervisor)) - exit <- runtime.run(publisher.toZIOStream().runDrain.exit) - failed <- supervisor.value + runtime <- Runtime.addSupervisor(supervisor).toRuntime + exit <- runtime.run(publisher.toZIOStream().runDrain.exit) + failed <- supervisor.value } yield assert(exit)(fails(anything)) && assert(failed)(isFalse) }, test("does not freeze on stream end") { withProbe(probe => for { - fiber <- Stream + fiber <- ZStream .fromZIO( ZIO.succeed( probe.toZIOStream() ) ) .flatMap(identity) - .run(Sink.collectAll[Int]) + .run(ZSink.collectAll[Int]) .fork _ <- ZIO.attemptBlockingInterrupt(probe.expectRequest()) _ <- ZIO.succeed(probe.sendNext(1)) @@ -112,11 +112,11 @@ object PublisherToStreamSpec extends ZIOSpecDefault { _ <- ZIO.succeed(subscriber.onSubscribe(subscription)) _ <- cancelledLatch.await } yield () - assertM(tst.exit)(succeeds(anything)) + assertZIO(tst.exit)(succeeds(anything)) } @@ TestAspect.nonFlaky @@ TestAspect.timeout(60.seconds), test("cancels subscription when interrupted after subscription") { withProbe(probe => - assertM((for { + assertZIO((for { fiber <- probe.toZIOStream(bufferSize).runDrain.fork _ <- ZIO.attemptBlockingInterrupt(probe.expectRequest()) _ <- fiber.interrupt @@ -128,7 +128,7 @@ object PublisherToStreamSpec extends ZIOSpecDefault { } @@ TestAspect.nonFlaky @@ TestAspect.timeout(60.seconds), test("cancels subscription when interrupted during consumption") { withProbe(probe => - assertM((for { + assertZIO((for { fiber <- probe.toZIOStream(bufferSize).runDrain.fork demand <- ZIO.attemptBlockingInterrupt(probe.expectRequest()) _ <- ZIO.attempt((1 to demand.toInt).foreach(i => probe.sendNext(i))) @@ -141,7 +141,7 @@ object PublisherToStreamSpec extends ZIOSpecDefault { } @@ TestAspect.nonFlaky @@ TestAspect.timeout(60.seconds), test("cancels subscription on stream end") { withProbe(probe => - assertM((for { + assertZIO((for { fiber <- probe.toZIOStream(bufferSize).take(1).runDrain.fork demand <- ZIO.attemptBlockingInterrupt(probe.expectRequest()) _ <- ZIO.attempt((1 to demand.toInt).foreach(i => probe.sendNext(i))) @@ -154,7 +154,7 @@ object PublisherToStreamSpec extends ZIOSpecDefault { }, test("cancels subscription on stream error") { withProbe(probe => - assertM(for { + assertZIO(for { fiber <- probe.toZIOStream(bufferSize).mapZIO(_ => ZIO.fail(new Throwable("boom!"))).runDrain.fork demand <- ZIO.attemptBlockingInterrupt(probe.expectRequest()) _ <- ZIO.attempt((1 to demand.toInt).foreach(i => probe.sendNext(i))) @@ -192,7 +192,7 @@ object PublisherToStreamSpec extends ZIOSpecDefault { val faillable = withProbe(probe => for { - fiber <- probe.toZIOStream(bufferSize).run(Sink.collectAll[Int]).fork + fiber <- probe.toZIOStream(bufferSize).run(ZSink.collectAll[Int]).fork _ <- loop(probe, seq) r <- fiber.join } yield r diff --git a/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala b/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala index 12b7fd5..df380d2 100644 --- a/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/SinkToSubscriberSpec.scala @@ -5,7 +5,7 @@ import org.reactivestreams.tck.SubscriberWhiteboxVerification.{ SubscriberPuppet import org.reactivestreams.tck.{ SubscriberWhiteboxVerification, TestEnvironment } import org.testng.annotations.Test import zio.{ Chunk, Promise, ZIO, durationInt, durationLong } -import zio.stream.{ Sink, ZSink } +import zio.stream.ZSink import zio.test.Assertion._ import zio.test._ @@ -25,14 +25,15 @@ object SinkToSubscriberSpec extends ZIOSpecDefault { ZIO.succeed(publisher.subscribe(subscriber)) *> r } }.fork + _ <- + Live.live( + assertZIO(subscribed.await.timeoutFail("timeout awaiting subscribe.")(500.millis).exit)(succeeds(isUnit)) + ) _ <- Live.live( - assertM(subscribed.await.timeoutFail("timeout awaiting subscribe.")(500.millis).exit)(succeeds(isUnit)) - ) - _ <- Live.live( - assertM(requested.await.timeoutFail("timeout awaiting request.")(500.millis).exit)(succeeds(isUnit)) + assertZIO(requested.await.timeoutFail("timeout awaiting request.")(500.millis).exit)(succeeds(isUnit)) ) _ <- Live.live( - assertM(canceled.await.timeoutFail("timeout awaiting cancel.")(500.millis).exit)(succeeds(isUnit)) + assertZIO(canceled.await.timeoutFail("timeout awaiting cancel.")(500.millis).exit)(succeeds(isUnit)) ) r <- fiber.join.exit } yield assert(r)(succeeds(equalTo(List(1, 2, 3, 4, 5)))) @@ -42,18 +43,18 @@ object SinkToSubscriberSpec extends ZIOSpecDefault { tuple <- makePublisherProbe (publisher, subscribed, _, canceled) = tuple fiber <- ZIO.scoped { - Sink + ZSink .foreachChunk[Any, Throwable, Int](_ => ZIO.yieldNow) .toSubscriber() .flatMap { case (subscriber, _) => ZIO.succeed(publisher.subscribe(subscriber)) *> ZIO.never } }.fork _ <- Live.live( - assertM(subscribed.await.timeoutFail("timeout awaiting subscribe.")(500.millis).exit)(succeeds(isUnit)) + assertZIO(subscribed.await.timeoutFail("timeout awaiting subscribe.")(500.millis).exit)(succeeds(isUnit)) ) _ <- fiber.interrupt _ <- Live.live( - assertM(canceled.await.timeoutFail("timeout awaiting cancel.")(500.millis).exit)(succeeds(isUnit)) + assertZIO(canceled.await.timeoutFail("timeout awaiting cancel.")(500.millis).exit)(succeeds(isUnit)) ) r <- fiber.join.exit } yield assert(r)(isInterrupted) @@ -63,17 +64,17 @@ object SinkToSubscriberSpec extends ZIOSpecDefault { tuple <- makePublisherProbe (publisher, subscribed, requested, canceled) = tuple fiber <- ZIO.scoped { - Sink + ZSink .foreachChunk[Any, Throwable, Int](_ => ZIO.yieldNow) .toSubscriber() .flatMap { case (subscriber, _) => ZIO.attemptBlockingInterrupt(publisher.subscribe(subscriber)) *> ZIO.never } }.fork - _ <- assertM(subscribed.await.exit)(succeeds(isUnit)) - _ <- assertM(requested.await.exit)(succeeds(isUnit)) + _ <- assertZIO(subscribed.await.exit)(succeeds(isUnit)) + _ <- assertZIO(requested.await.exit)(succeeds(isUnit)) _ <- fiber.interrupt - _ <- assertM(canceled.await.exit)(succeeds(isUnit)) + _ <- assertZIO(canceled.await.exit)(succeeds(isUnit)) r <- fiber.join.exit } yield assert(r)(isInterrupted) ), @@ -131,7 +132,7 @@ object SinkToSubscriberSpec extends ZIOSpecDefault { val managedVerification = for { - subscriber_ <- Sink.collectAll[Int].toSubscriber() + subscriber_ <- ZSink.collectAll[Int].toSubscriber() (subscriber, _) = subscriber_ sbv <- ZIO.acquireRelease { val env = new TestEnvironment(1000, 500) diff --git a/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala b/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala index c2263a7..1581078 100644 --- a/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/StreamToPublisherSpec.scala @@ -4,7 +4,7 @@ import org.reactivestreams.Publisher import org.reactivestreams.tck.{ PublisherVerification, TestEnvironment } import org.testng.annotations.Test import zio.ZIO -import zio.stream.Stream +import zio.stream.ZStream import zio.test.Assertion._ import zio.test._ @@ -21,14 +21,14 @@ object StreamToPublisherSpec extends ZIOSpecDefault { def createPublisher(elements: Long): Publisher[Int] = runtime.unsafeRun( - Stream + ZStream .unfold(elements)(n => if (n > 0) Some((1, n - 1)) else None) .toPublisher ) override def createFailedPublisher(): Publisher[Int] = runtime.unsafeRun( - Stream + ZStream .fail(new RuntimeException("boom!")) .map(_.asInstanceOf[Int]) .toPublisher diff --git a/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala b/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala index 6be8980..c7128c8 100644 --- a/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/SubscriberToSinkSpec.scala @@ -2,7 +2,7 @@ package zio.interop.reactivestreams import org.reactivestreams.tck.TestEnvironment import org.reactivestreams.tck.TestEnvironment.ManualSubscriberWithSubscriptionSupport -import zio.stream.{ Stream, ZStream } +import zio.stream.ZStream import zio.test.Assertion._ import zio.test.TestAspect.nonFlaky import zio.test._ @@ -20,7 +20,7 @@ object SubscriberToSinkSpec extends ZIOSpecDefault { .toZIOSink[Throwable] .flatMap { case (_, sink) => for { - fiber <- Stream.fromIterable(seq).run(sink).fork + fiber <- ZStream.fromIterable(seq).run(sink).fork _ <- probe.request(length + 1) elements <- probe.nextElements(length).exit completion <- probe.expectCompletion.exit @@ -37,7 +37,7 @@ object SubscriberToSinkSpec extends ZIOSpecDefault { .toZIOSink[Throwable] .flatMap { case (signalError, sink) => for { - fiber <- (Stream.fromIterable(seq) ++ Stream.fail(e)).run(sink).catchAll(signalError).fork + fiber <- (ZStream.fromIterable(seq) ++ ZStream.fail(e)).run(sink).catchAll(signalError).fork _ <- probe.request(length + 1) elements <- probe.nextElements(length).exit err <- probe.expectError.exit From 531dc20102d201b9ace5578379736b172a0f3ca9 Mon Sep 17 00:00:00 2001 From: Simon Schenk Date: Thu, 12 May 2022 06:52:32 +0200 Subject: [PATCH 22/23] chore: port a test for a fix in ZIO1 series (#325) --- .../PublisherToStreamSpec.scala | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala b/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala index 900114f..f5dac61 100644 --- a/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala +++ b/src/test/scala/zio/interop/reactivestreams/PublisherToStreamSpec.scala @@ -162,6 +162,24 @@ object PublisherToStreamSpec extends ZIOSpecDefault { exit <- fiber.join.exit } yield exit)(fails(anything)) ) + }, + test("ignores publisher calls after stream ending") { + withProbe(probe => + assertZIO((for { + fiber <- probe.toZIOStream(bufferSize).runHead.fork + demand <- ZIO.attemptBlockingInterrupt(probe.expectRequest()) + _ <- ZIO.attempt(probe.sendNext(0)) + _ <- ZIO.attemptBlockingInterrupt(probe.expectCancelling()) + + _ <- ZIO.attempt((1 to demand.toInt).foreach(i => probe.sendNext(i))) + _ <- ZIO.attempt(probe.sendCompletion()) + _ <- ZIO.attempt(probe.sendError(e)) + + _ <- fiber.join + } yield ()).exit)( + succeeds(isUnit) + ) + ) } ) From 2f25e1528b089401f29aedca13bbf840ad70e3af Mon Sep 17 00:00:00 2001 From: Simon Schenk Date: Mon, 30 May 2022 15:32:13 +0200 Subject: [PATCH 23/23] bump reactive streams to 1.0.4 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index be5e5fc..24bb9d5 100644 --- a/build.sbt +++ b/build.sbt @@ -37,7 +37,7 @@ addCommandAlias("fmt", "all scalafmtSbt scalafmt test:scalafmt") addCommandAlias("check", "all scalafmtSbtCheck scalafmtCheck test:scalafmtCheck") val zioVersion = "2.0.0-RC6" -val rsVersion = "1.0.3" +val rsVersion = "1.0.4" val collCompatVersion = "2.7.0" lazy val interopReactiveStreams = project