diff --git a/docs/src/main/paradox/stream/operators/Sink/asJavaStream.md b/docs/src/main/paradox/stream/operators/Sink/asJavaStream.md new file mode 100644 index 00000000000..e9fb8de6139 --- /dev/null +++ b/docs/src/main/paradox/stream/operators/Sink/asJavaStream.md @@ -0,0 +1,38 @@ +# Sink.asJavaStream + +Create a sink which materializes into Java 8 `Stream` that can be run to trigger demand through the sink. + +@ref[Sink operators](../index.md#sink-operators) + +## Signature + +@apidoc[Sink.asJavaStream](Sink$) { scala="#asJavaStream[T]():org.apache.pekko.stream.scaladsl.Sink[T,java.util.stream.Stream[T]]" java="#asJavaStream()" } + +## Description + +Create a sink which materializes into Java 8 `Stream` that can be run to trigger demand through the sink. +Elements emitted through the stream will be available for reading through the Java 8 `Stream`. + +The Java 8 `Stream` will be ended when the stream flowing into this `Sink` completes, and closing the Java +`Stream` will cancel the inflow of this `Sink`. If the Java `Stream` throws an exception, the Pekko stream is cancelled. + +Be aware that Java `Stream` blocks current thread while waiting on next element from downstream. + +## Example + +Here is an example of a @apidoc[Sink] that materializes into a @javadoc[java.util.stream.Stream](java.util.stream.Stream). + +Scala +: @@snip [StreamConvertersToJava.scala](/docs/src/test/scala/docs/stream/operators/converters/StreamConvertersToJava.scala) { #import #asJavaStreamOnSink } + +Java +: @@snip [StreamConvertersToJava.java](/docs/src/test/java/jdocs/stream/operators/converters/StreamConvertersToJava.java) { #import #asJavaStreamOnSink } + + +## Reactive Streams semantics + +@@@div { .callout } +**cancels** when the Java Stream is closed + +**backpressures** when no read is pending on the Java Stream +@@@ diff --git a/docs/src/main/paradox/stream/operators/index.md b/docs/src/main/paradox/stream/operators/index.md index 011a1e1cf76..304c55f4855 100644 --- a/docs/src/main/paradox/stream/operators/index.md +++ b/docs/src/main/paradox/stream/operators/index.md @@ -52,6 +52,7 @@ These built-in sinks are available from @scala[`org.apache.pekko.stream.scaladsl | |Operator|Description| |--|--|--| +|Sink|@ref[asJavaStream](Sink/asJavaStream.md)|Create a sink which materializes into Java 8 `Stream` that can be run to trigger demand through the sink.| |Sink|@ref[asPublisher](Sink/asPublisher.md)|Integration with Reactive Streams, materializes into a `org.reactivestreams.Publisher`.| |Sink|@ref[cancelled](Sink/cancelled.md)|Immediately cancel the stream| |Sink|@ref[collect](Sink/collect.md)|Collect all input elements using a Java @javadoc[Collector](java.util.stream.Collector).| @@ -407,6 +408,7 @@ For more background see the @ref[Error Handling in Streams](../stream-error.md) * [alsoToAll](Source-or-Flow/alsoToAll.md) * [asFlowWithContext](Flow/asFlowWithContext.md) * [asInputStream](StreamConverters/asInputStream.md) +* [asJavaStream](Sink/asJavaStream.md) * [asJavaStream](StreamConverters/asJavaStream.md) * [ask](Source-or-Flow/ask.md) * [ask](ActorFlow/ask.md) diff --git a/docs/src/test/java/jdocs/stream/operators/converters/StreamConvertersToJava.java b/docs/src/test/java/jdocs/stream/operators/converters/StreamConvertersToJava.java index 594e2583fbb..bb84c949c25 100644 --- a/docs/src/test/java/jdocs/stream/operators/converters/StreamConvertersToJava.java +++ b/docs/src/test/java/jdocs/stream/operators/converters/StreamConvertersToJava.java @@ -66,6 +66,17 @@ public void demonstrateConverterToJava8Stream() { assertEquals(5, jStream.count()); } + @Test + public void demonstrateConverterToJava8StreamOnSink() { + // #asJavaStreamOnSink + + Source source = Source.range(0, 9).filter(i -> i % 2 == 0); + Stream jStream = source.runWith(Sink.asJavaStream(), system); + + // #asJavaStreamOnSink + assertEquals(5, jStream.count()); + } + @Test public void demonstrateCreatingASourceFromJava8Stream() throws InterruptedException, ExecutionException, TimeoutException { diff --git a/docs/src/test/scala/docs/stream/operators/converters/StreamConvertersToJava.scala b/docs/src/test/scala/docs/stream/operators/converters/StreamConvertersToJava.scala index dfe5dc1791a..168b1526b22 100644 --- a/docs/src/test/scala/docs/stream/operators/converters/StreamConvertersToJava.scala +++ b/docs/src/test/scala/docs/stream/operators/converters/StreamConvertersToJava.scala @@ -43,6 +43,15 @@ class StreamConvertersToJava extends PekkoSpec with Futures { jStream.count should be(5) } + "demonstrate materialization to Java8 streams with methods on Sink" in { + // #asJavaStreamOnSink + val source: Source[Int, NotUsed] = Source(0 to 9).filter(_ % 2 == 0) + + val jStream: java.util.stream.Stream[Int] = source.runWith(Sink.asJavaStream[Int]()) + // #asJavaStreamOnSink + jStream.count should be(5) + } + "demonstrate conversion from Java8 streams" in { // #fromJavaStream def factory(): IntStream = IntStream.rangeClosed(0, 9) diff --git a/stream-tests/src/test/java/org/apache/pekko/stream/io/SinkAsJavaSourceTest.java b/stream-tests/src/test/java/org/apache/pekko/stream/io/SinkAsJavaSourceTest.java index 4e11037ff24..0f63e7e1fc5 100644 --- a/stream-tests/src/test/java/org/apache/pekko/stream/io/SinkAsJavaSourceTest.java +++ b/stream-tests/src/test/java/org/apache/pekko/stream/io/SinkAsJavaSourceTest.java @@ -43,4 +43,12 @@ public void mustBeAbleToUseAsJavaStream() throws Exception { java.util.stream.Stream javaStream = Source.from(list).runWith(streamSink, system); assertEquals(list, javaStream.toList()); } + + @Test + public void mustBeAbleToUseAsJavaStreamOnSink() throws Exception { + final List list = Arrays.asList(1, 2, 3); + java.util.stream.Stream javaStream = + Source.from(list).runWith(Sink.asJavaStream(), system); + assertEquals(list, javaStream.collect(Collectors.toList())); + } } diff --git a/stream-tests/src/test/scala/org/apache/pekko/stream/scaladsl/SinkAsJavaStreamSpec.scala b/stream-tests/src/test/scala/org/apache/pekko/stream/scaladsl/SinkAsJavaStreamSpec.scala index 1bab31323b4..c5597c7e091 100644 --- a/stream-tests/src/test/scala/org/apache/pekko/stream/scaladsl/SinkAsJavaStreamSpec.scala +++ b/stream-tests/src/test/scala/org/apache/pekko/stream/scaladsl/SinkAsJavaStreamSpec.scala @@ -32,6 +32,9 @@ class SinkAsJavaStreamSpec extends StreamSpec(UnboundedMailboxConfig) { "work in happy case" in { val javaSource = Source(1 to 100).runWith(StreamConverters.asJavaStream()) javaSource.count() should ===(100L) + // + Source(1 to 100).runWith(Sink.asJavaStream()) + .count() should ===(100L) } "fail if parent stream is failed" in { diff --git a/stream/src/main/scala/org/apache/pekko/stream/impl/fusing/GraphInterpreter.scala b/stream/src/main/scala/org/apache/pekko/stream/impl/fusing/GraphInterpreter.scala index 7c7db1bca02..b5ba68ae369 100644 --- a/stream/src/main/scala/org/apache/pekko/stream/impl/fusing/GraphInterpreter.scala +++ b/stream/src/main/scala/org/apache/pekko/stream/impl/fusing/GraphInterpreter.scala @@ -342,12 +342,25 @@ import pekko.stream.stage._ var i = 0 while (i < logics.length) { val logic = logics(i) - if (!isStageCompleted(logic) && !isStageFinalized(logic)) { + if ((logic ne null) && !isStageCompleted(logic) && !isStageFinalized(logic)) { markStageFinalized(logic) finalizeStage(logic) } + // Release reference to the stage logic so it can be garbage collected + // even if the GraphInterpreter is still alive due to other references + logics(i) = null i += 1 } + // Null connection handlers for all finalized stages to break handler-to-logic reference chains + var j = 0 + while (j < connections.length) { + val conn = connections(j) + if (conn ne null) { + conn.inHandler = null + conn.outHandler = null + } + j += 1 + } } // Debug name for a connections input part @@ -357,10 +370,16 @@ import pekko.stream.stage._ private def outOwnerName(connection: Connection): String = connection.outOwner.toString // Debug name for a connections input part - private def inLogicName(connection: Connection): String = logics(connection.inOwner.stageId).toString + private def inLogicName(connection: Connection): String = { + val logic = logics(connection.inOwner.stageId) + if (logic ne null) logic.toString else "" + } // Debug name for a connections output part - private def outLogicName(connection: Connection): String = logics(connection.outOwner.stageId).toString + private def outLogicName(connection: Connection): String = { + val logic = logics(connection.outOwner.stageId) + if (logic ne null) logic.toString else "" + } private def shutdownCounters: String = shutdownCounter.map(x => if (x >= KeepGoingFlag) s"${x & KeepGoingMask}(KeepGoing)" else x.toString).mkString(",") diff --git a/stream/src/main/scala/org/apache/pekko/stream/javadsl/Sink.scala b/stream/src/main/scala/org/apache/pekko/stream/javadsl/Sink.scala index fbf1becfbde..f576742da87 100644 --- a/stream/src/main/scala/org/apache/pekko/stream/javadsl/Sink.scala +++ b/stream/src/main/scala/org/apache/pekko/stream/javadsl/Sink.scala @@ -228,6 +228,23 @@ object Sink { def source[T](): Sink[T, Source[T, NotUsed]] = new Sink(scaladsl.Sink.source[T]) .mapMaterializedValue(src => src.asJava) + /** + * Creates a sink which materializes into Java 8 ``Stream`` that can be run to trigger demand through the sink. + * Elements emitted through the stream will be available for reading through the Java 8 ``Stream``. + * + * The Java 8 ``Stream`` will be ended when the stream flowing into this ``Sink`` completes, and closing the Java + * ``Stream`` will cancel the inflow of this ``Sink``. + * + * Java 8 ``Stream`` throws exception in case reactive stream failed. + * + * Be aware that Java ``Stream`` blocks current thread while waiting on next element from downstream. + * As it is interacting with blocking API the implementation runs on a separate dispatcher + * configured through the ``pekko.stream.blocking-io-dispatcher``. + * + * @since 2.0.0 + */ + def asJavaStream[T](): Sink[T, java.util.stream.Stream[T]] = new Sink(scaladsl.StreamConverters.asJavaStream()) + /** * A `Sink` that will invoke the given procedure for each received element. The sink is materialized * into a [[java.util.concurrent.CompletionStage]] which will be completed with `Success` when reaching the diff --git a/stream/src/main/scala/org/apache/pekko/stream/scaladsl/Sink.scala b/stream/src/main/scala/org/apache/pekko/stream/scaladsl/Sink.scala index 7828e4b503f..ae61ef44d3a 100644 --- a/stream/src/main/scala/org/apache/pekko/stream/scaladsl/Sink.scala +++ b/stream/src/main/scala/org/apache/pekko/stream/scaladsl/Sink.scala @@ -323,6 +323,23 @@ object Sink { private val _sourceSink = fromGraph(SourceSink) + /** + * Creates a sink which materializes into Java 8 ``Stream`` that can be run to trigger demand through the sink. + * Elements emitted through the stream will be available for reading through the Java 8 ``Stream``. + * + * The Java 8 ``Stream`` will be ended when the stream flowing into this ``Sink`` completes, and closing the Java + * ``Stream`` will cancel the inflow of this ``Sink``. + * + * If the Java 8 ``Stream`` throws exception the Pekko stream is cancelled. + * + * Be aware that Java ``Stream`` blocks current thread while waiting on next element from downstream. + * As it is interacting with blocking API the implementation runs on a separate dispatcher + * configured through the ``pekko.stream.blocking-io-dispatcher``. + * + * @since 2.0.0 + */ + def asJavaStream[T](): Sink[T, java.util.stream.Stream[T]] = StreamConverters.asJavaStream() + /** * A `Sink` that will consume the stream and discard the elements. */