This is an automated email from the ASF dual-hosted git repository.
He-Pin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko.git
The following commit(s) were added to refs/heads/main by this push:
new 559c5b9cee docs: add code examples for migrating off deprecated
Source.queue overloads (#3605)
559c5b9cee is described below
commit 559c5b9ceef376fd77cef77e650927c2cf62a802
Author: PJ Fanning <[email protected]>
AuthorDate: Sat Oct 10 06:27:17 2026 +0100
docs: add code examples for migrating off deprecated Source.queue overloads
(#3605)
* docs: add code examples for migrating off deprecated Source.queue
overloads
Motivation:
2.0.0-M4 deprecated the Source.queue overloads that take an
OverflowStrategy, but the migration table only described replacements
without code, and the OverflowStrategy.backpressure alternatives were
hard to apply. The table also referenced OverflowStrategy.dropNew,
which was removed in 2.0, and described dropTail as equivalent to
BoundedSourceQueue, which it is not.
Modification:
Add compiled Scala and Java before/after examples to the Source.queue
operator page for the drop strategies, fail (BoundedSourceQueue.fail
with BufferOverflowException), backpressure with a single producer
(Source.actorRefWithBackpressure with ask) and backpressure with
multiple producers (MergeHub.source). Correct the dropNew, dropHead
and dropTail rows of the migration table. Link the section from the
1.x to 2.x migration guide.
Result:
Users of the deprecated overloads have tested code examples for each
OverflowStrategy replacement.
Tests:
- sbt "docs/testOnly docs.stream.operators.source.QueueMigrationDocSpec
jdocs.stream.operators.source.QueueMigrationDocTest" - 8 passed
References:
Fixes #3458
* docs: use a plain-text heading for the Source.queue migration anchor
Paradox only generates a heading anchor when the heading is a single
text node, so the heading containing inline code had no anchor and the
migration guide link failed validation.
---
.../paradox/migration/migration-guide-1.x-2.x.md | 9 +
.../main/paradox/stream/operators/Source/queue.md | 68 ++++++-
.../operators/source/QueueMigrationDocTest.java | 213 +++++++++++++++++++++
.../operators/source/QueueMigrationDocSpec.scala | 158 +++++++++++++++
4 files changed, 442 insertions(+), 6 deletions(-)
diff --git a/docs/src/main/paradox/migration/migration-guide-1.x-2.x.md
b/docs/src/main/paradox/migration/migration-guide-1.x-2.x.md
index 7bb7a1ee40..0b4d1f7a78 100644
--- a/docs/src/main/paradox/migration/migration-guide-1.x-2.x.md
+++ b/docs/src/main/paradox/migration/migration-guide-1.x-2.x.md
@@ -28,6 +28,15 @@ Not every deprecated API has been removed in Pekko 2.x but
many have been.
Java API users may find that they have more deprecations to deal with because
the Scala API is more stable and there
were a few mistakes in the Java API where Scala classes leaked into some of
the Java API methods.
+### Deprecations in Pekko 2.x
+
+* The `Source.queue` overloads that take an `OverflowStrategy` and materialize
a `SourceQueueWithComplete` are
+deprecated. Their asynchronous `offer` can hang indefinitely under
`OverflowStrategy.backpressure` when downstream
+stalls. Use `Source.queue(bufferSize)` (which materializes a
`BoundedSourceQueue` with synchronous feedback),
+`Source.actorRefWithBackpressure` or `MergeHub.source` instead. See
+@ref:[Migrating from the deprecated Source.queue
overloads](../stream/operators/Source/queue.md#migrating-from-the-deprecated-overflowstrategy-overloads)
+for a replacement per `OverflowStrategy`, with code examples.
+
## Additional Breaking Changes in Pekko 2.x
* In the Scala DSL for Flow and Source, the `watchTermination` function call
no longer needs an empty param
list before a second param list. Instead of `watchTermination(){ ... }`, you
now must use `watchTermination{ ... }`.
diff --git a/docs/src/main/paradox/stream/operators/Source/queue.md
b/docs/src/main/paradox/stream/operators/Source/queue.md
index 02d706ba80..9227001975 100644
--- a/docs/src/main/paradox/stream/operators/Source/queue.md
+++ b/docs/src/main/paradox/stream/operators/Source/queue.md
@@ -8,7 +8,7 @@ Materialize a `BoundedSourceQueue` or `SourceQueue` onto which
elements can be p
The `Source.queue` overloads that accept an @apidoc[OverflowStrategy] and
materialize a `SourceQueueWithComplete` are **deprecated**. Their asynchronous
`offer` @scala[`Future`]@java[`CompletionStage`] can hang indefinitely under
`OverflowStrategy.backpressure` when downstream stalls, which has caused
real-world deadlocks.
-Prefer `Source.queue[T](bufferSize)` (this page), which materializes a
@apidoc[BoundedSourceQueue] with synchronous feedback and drop-newest overflow.
For backpressure towards the producer, use
@ref:[`Source.actorRefWithBackpressure`](actorRefWithBackpressure.md) (single
imperative producer) or `MergeHub.source` (multiple producers). See the
[migration
table](#migrating-from-the-deprecated-sourcequeueint-overflowstrategy-overloads)
below for a per-strategy replacement.
+Prefer `Source.queue[T](bufferSize)` (this page), which materializes a
@apidoc[BoundedSourceQueue] with synchronous feedback and drop-newest overflow.
For backpressure towards the producer, use
@ref:[`Source.actorRefWithBackpressure`](actorRefWithBackpressure.md) (single
imperative producer) or `MergeHub.source` (multiple producers). See the
[migration table](#migrating-from-the-deprecated-overflowstrategy-overloads)
below for a per-strategy replacement.
@@@
@@ -45,7 +45,7 @@ Java
@@@ warning
-These two overloads are deprecated since 2.0.0. See the [migration
table](#migrating-from-the-deprecated-sourcequeueint-overflowstrategy-overloads)
below.
+These two overloads are deprecated since 2.0.0. See the [migration
table](#migrating-from-the-deprecated-overflowstrategy-overloads) below.
@@@
@@ -83,15 +83,71 @@ Java
@@@
-## Migrating from the deprecated `Source.queue(Int, OverflowStrategy)`
overloads
+## Migrating from the deprecated OverflowStrategy overloads
| Old call | Replacement |
|----------|-------------|
-| `Source.queue(n, OverflowStrategy.dropNew)` | `Source.queue[T](n)` —
`BoundedSourceQueue` already drops the newest element. |
-| `Source.queue(n, OverflowStrategy.dropHead)` | `Source.queue[T](n)` if
drop-new is acceptable. Otherwise build a custom @apidoc[GraphStage] with a
FIFO buffer that drops the head. |
-| `Source.queue(n, OverflowStrategy.dropTail)` | Same as above;
`BoundedSourceQueue` always drops the newest offer (i.e. the tail). |
+| `Source.queue(n, OverflowStrategy.dropNew)` (`dropNew` was removed in 2.0.0)
| `Source.queue[T](n)`, which has the same semantics: the offered element is
dropped when the buffer is full. |
+| `Source.queue(n, OverflowStrategy.dropHead)` | `Source.queue[T](n)` if
dropping the offered element instead of the oldest buffered one is acceptable.
Otherwise build a custom @apidoc[GraphStage] with a FIFO buffer that drops the
head. |
+| `Source.queue(n, OverflowStrategy.dropTail)` | `Source.queue[T](n)` if
dropping the offered element instead of the youngest buffered one is
acceptable. `BoundedSourceQueue` leaves the buffer unchanged and reports
`QueueOfferResult.Dropped` for the offered element. |
| `Source.queue(n, OverflowStrategy.dropBuffer)` | `Source.queue[T](n)`
combined with a @apidoc[GraphStage] that clears the buffer on overflow, or
rework the producer to tolerate drops. |
| `Source.queue(n, OverflowStrategy.fail)` | `Source.queue[T](n)` and, on
`QueueOfferResult.Dropped`, call `BoundedSourceQueue.fail` with a
`BufferOverflowException`. |
| `Source.queue(n, OverflowStrategy.backpressure)` |
@ref:[`Source.actorRefWithBackpressure`](actorRefWithBackpressure.md) (single
imperative producer) or `MergeHub.source` (multiple producers). |
`SourceQueueWithComplete.offer` returned a
@scala[`Future[QueueOfferResult]`]@java[`CompletionStage<QueueOfferResult>`];
`BoundedSourceQueue.offer` returns `QueueOfferResult` synchronously. Call sites
that previously chained `.map`/`.flatMap` on the offer future can usually be
rewritten as a direct `match`/`switch` on the result.
+
+### Dropping elements (`dropHead`, `dropTail`, `dropBuffer`)
+
+`BoundedSourceQueue` drops the element being offered when the buffer is full
and tells the caller synchronously, instead of completing a
@scala[`Future`]@java[`CompletionStage`].
+
+Before:
+
+Scala
+: @@snip
[QueueMigrationDocSpec.scala](/docs/src/test/scala/docs/stream/operators/source/QueueMigrationDocSpec.scala)
{ #drop-before }
+
+Java
+: @@snip
[QueueMigrationDocTest.java](/docs/src/test/java/jdocs/stream/operators/source/QueueMigrationDocTest.java)
{ #drop-before }
+
+After:
+
+Scala
+: @@snip
[QueueMigrationDocSpec.scala](/docs/src/test/scala/docs/stream/operators/source/QueueMigrationDocSpec.scala)
{ #drop-after }
+
+Java
+: @@snip
[QueueMigrationDocTest.java](/docs/src/test/java/jdocs/stream/operators/source/QueueMigrationDocTest.java)
{ #drop-after }
+
+### Failing the stream on overflow (`fail`)
+
+`BoundedSourceQueue` reports `QueueOfferResult.Dropped` instead of failing the
stream. To keep the old behavior, fail the queue yourself:
+
+Scala
+: @@snip
[QueueMigrationDocSpec.scala](/docs/src/test/scala/docs/stream/operators/source/QueueMigrationDocSpec.scala)
{ #fail-after }
+
+Java
+: @@snip
[QueueMigrationDocTest.java](/docs/src/test/java/jdocs/stream/operators/source/QueueMigrationDocTest.java)
{ #fail-after }
+
+### Backpressuring the producer (`backpressure`)
+
+A typical use of `OverflowStrategy.backpressure` offers the next element only
after the previous offer completed:
+
+Scala
+: @@snip
[QueueMigrationDocSpec.scala](/docs/src/test/scala/docs/stream/operators/source/QueueMigrationDocSpec.scala)
{ #backpressure-before }
+
+Java
+: @@snip
[QueueMigrationDocTest.java](/docs/src/test/java/jdocs/stream/operators/source/QueueMigrationDocTest.java)
{ #backpressure-before }
+
+For a single imperative producer,
@ref:[`Source.actorRefWithBackpressure`](actorRefWithBackpressure.md) combined
with `ask` gives the same "wait for the offer to be accepted" shape. The ack is
sent to the sender of each element, so the `ask`
@scala[`Future`]@java[`CompletionStage`] completes once the element was
emitted. Sending a new element before the previous one was acknowledged fails
the stream, just like exceeding `maxConcurrentOffers` failed the offer before.
With the typed actor [...]
+
+Scala
+: @@snip
[QueueMigrationDocSpec.scala](/docs/src/test/scala/docs/stream/operators/source/QueueMigrationDocSpec.scala)
{ #backpressure-single-producer }
+
+Java
+: @@snip
[QueueMigrationDocTest.java](/docs/src/test/java/jdocs/stream/operators/source/QueueMigrationDocTest.java)
{ #backpressure-single-producer }
+
+For multiple producers, or when `maxConcurrentOffers` was greater than 1, use
@ref:[`MergeHub.source`](../../stream-dynamic.md#using-the-mergehub). Each
producer becomes a stream connected to the materialized `Sink` and is
back-pressured on its own. If the elements already come from a stream (an
iterator, a `Future`, another `Source`), connecting that stream directly is
usually simpler than offering elements one at a time.
+
+Scala
+: @@snip
[QueueMigrationDocSpec.scala](/docs/src/test/scala/docs/stream/operators/source/QueueMigrationDocSpec.scala)
{ #backpressure-multiple-producers }
+
+Java
+: @@snip
[QueueMigrationDocTest.java](/docs/src/test/java/jdocs/stream/operators/source/QueueMigrationDocTest.java)
{ #backpressure-multiple-producers }
diff --git
a/docs/src/test/java/jdocs/stream/operators/source/QueueMigrationDocTest.java
b/docs/src/test/java/jdocs/stream/operators/source/QueueMigrationDocTest.java
new file mode 100644
index 0000000000..52e4fccf09
--- /dev/null
+++
b/docs/src/test/java/jdocs/stream/operators/source/QueueMigrationDocTest.java
@@ -0,0 +1,213 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package jdocs.stream.operators.source;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import jdocs.AbstractJavaTest;
+import org.apache.pekko.Done;
+import org.apache.pekko.NotUsed;
+import org.apache.pekko.actor.ActorRef;
+import org.apache.pekko.actor.ActorSystem;
+import org.apache.pekko.japi.Pair;
+import org.apache.pekko.pattern.Patterns;
+import org.apache.pekko.stream.BoundedSourceQueue;
+import org.apache.pekko.stream.BufferOverflowException;
+import org.apache.pekko.stream.CompletionStrategy;
+import org.apache.pekko.stream.OverflowStrategy;
+import org.apache.pekko.stream.QueueOfferResult;
+import org.apache.pekko.stream.javadsl.Keep;
+import org.apache.pekko.stream.javadsl.MergeHub;
+import org.apache.pekko.stream.javadsl.Sink;
+import org.apache.pekko.stream.javadsl.Source;
+import org.apache.pekko.stream.javadsl.SourceQueueWithComplete;
+import org.apache.pekko.stream.testkit.TestSubscriber;
+import org.apache.pekko.stream.testkit.javadsl.TestSink;
+import org.apache.pekko.testkit.javadsl.TestKit;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class QueueMigrationDocTest extends AbstractJavaTest {
+
+ static ActorSystem system;
+
+ @BeforeAll
+ public static void setup() {
+ system = ActorSystem.create("QueueMigrationDocTest");
+ }
+
+ @AfterAll
+ public static void tearDown() {
+ TestKit.shutdownActorSystem(system);
+ system = null;
+ }
+
+ @SuppressWarnings("deprecation")
+ private CompletionStage<QueueOfferResult> dropTailBefore() {
+ // #drop-before
+ SourceQueueWithComplete<Integer> queue =
+ Source.<Integer>queue(100, OverflowStrategy.dropTail())
+ .to(Sink.foreach(System.out::println))
+ .run(system);
+
+ CompletionStage<QueueOfferResult> result = queue.offer(1);
+ // #drop-before
+ return result;
+ }
+
+ @Test
+ public void replaceDropTail() throws Exception {
+ assertEquals(
+ QueueOfferResult.enqueued(),
+ dropTailBefore().toCompletableFuture().get(3, TimeUnit.SECONDS));
+
+ // #drop-after
+ BoundedSourceQueue<Integer> queue =
+
Source.<Integer>queue(100).to(Sink.foreach(System.out::println)).run(system);
+
+ // offer returns the result synchronously, there is no CompletionStage to
wait for
+ QueueOfferResult result = queue.offer(1);
+ if (result == QueueOfferResult.enqueued()) {
+ // element accepted
+ } else if (result == QueueOfferResult.dropped()) {
+ // buffer full, this element was dropped
+ } else if (result instanceof QueueOfferResult.Failure) {
+ // the stream failed
+ } else if (result == QueueOfferResult.closed()) {
+ // the stream was completed
+ }
+ // #drop-after
+ assertEquals(QueueOfferResult.enqueued(), result);
+ }
+
+ // #fail-after
+ static final int bufferSize = 2;
+
+ // fail the stream like OverflowStrategy.fail did when the buffer is full
+ static QueueOfferResult offerOrFail(BoundedSourceQueue<Integer> queue, int
elem) {
+ QueueOfferResult result = queue.offer(elem);
+ if (result == QueueOfferResult.dropped()) {
+ queue.fail(
+ new BufferOverflowException("Buffer overflow (max capacity was: " +
bufferSize + ")!"));
+ }
+ return result;
+ }
+
+ // #fail-after
+
+ @Test
+ public void replaceFail() {
+ // downstream never requests, so the buffer fills up
+ Pair<BoundedSourceQueue<Integer>, TestSubscriber.Probe<Integer>> pair =
+ Source.<Integer>queue(bufferSize).toMat(TestSink.create(system),
Keep.both()).run(system);
+ BoundedSourceQueue<Integer> queue = pair.first();
+ assertEquals(QueueOfferResult.enqueued(), offerOrFail(queue, 1));
+ assertEquals(QueueOfferResult.enqueued(), offerOrFail(queue, 2));
+ assertEquals(QueueOfferResult.dropped(), offerOrFail(queue, 3));
+ assertInstanceOf(BufferOverflowException.class,
pair.second().expectSubscriptionAndError());
+ }
+
+ @SuppressWarnings("deprecation")
+ private CompletionStage<Done> backpressureBefore() {
+ // #backpressure-before
+ SourceQueueWithComplete<Integer> queue =
+ Source.<Integer>queue(100, OverflowStrategy.backpressure())
+ .to(Sink.foreach(System.out::println))
+ .run(system);
+
+ // the next offer is only made once the previous one has completed
+ CompletionStage<QueueOfferResult> allOffered =
+ CompletableFuture.completedFuture(QueueOfferResult.enqueued());
+ for (int i = 1; i <= 10; i++) {
+ final int elem = i;
+ allOffered = allOffered.thenCompose(previous -> queue.offer(elem));
+ }
+ allOffered.thenRun(queue::complete);
+ // #backpressure-before
+ return queue.watchCompletion();
+ }
+
+ @Test
+ public void replaceBackpressureSingleProducer() throws Exception {
+ assertEquals(
+ Done.getInstance(), backpressureBefore().toCompletableFuture().get(3,
TimeUnit.SECONDS));
+
+ // #backpressure-single-producer
+ Pair<ActorRef, CompletionStage<List<Integer>>> pair =
+ Source.<Integer>actorRefWithBackpressure(
+ "ack",
+ // complete when we send "complete"
+ msg ->
+ "complete".equals(msg)
+ ? Optional.of(CompletionStrategy.draining())
+ : Optional.empty(),
+ // do not fail on any message
+ msg -> Optional.empty())
+ .toMat(Sink.seq(), Keep.both())
+ .run(system);
+ ActorRef ref = pair.first();
+
+ // Replaces queue.offer(elem): the CompletionStage completes with "ack"
once the element was
+ // emitted downstream. Unlike the deprecated queue it fails with an
AskTimeoutException instead
+ // of hanging forever if downstream stalls.
+ Duration timeout = Duration.ofSeconds(3);
+
+ // Like before, the next element must only be offered once the previous
one was acknowledged,
+ // otherwise the stream fails.
+ CompletionStage<Object> allOffered =
CompletableFuture.completedFuture("ack");
+ for (int i = 1; i <= 10; i++) {
+ final int elem = i;
+ allOffered = allOffered.thenCompose(previous -> Patterns.ask(ref, elem,
timeout));
+ }
+ allOffered.thenRun(() -> ref.tell("complete", ActorRef.noSender()));
+ // #backpressure-single-producer
+
+ assertEquals(
+ IntStream.rangeClosed(1, 10).boxed().collect(Collectors.toList()),
+ pair.second().toCompletableFuture().get(3, TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void replaceBackpressureMultipleProducers() throws Exception {
+ // #backpressure-multiple-producers
+ Pair<Sink<Integer, NotUsed>, CompletionStage<List<Integer>>> pair =
+ MergeHub.of(Integer.class, 16).take(20).toMat(Sink.seq(),
Keep.both()).run(system);
+ Sink<Integer, NotUsed> sink = pair.first();
+
+ // Each producer is a stream of its own and is back-pressured
independently,
+ // there is no offer CompletionStage to track per element.
+ Source.range(1, 10).runWith(sink, system);
+ Source.range(11, 20).runWith(sink, system);
+ // #backpressure-multiple-producers
+
+ List<Integer> result = pair.second().toCompletableFuture().get(3,
TimeUnit.SECONDS);
+ assertEquals(
+ IntStream.rangeClosed(1, 20).boxed().collect(Collectors.toSet()),
+ result.stream().collect(Collectors.toSet()));
+ }
+}
diff --git
a/docs/src/test/scala/docs/stream/operators/source/QueueMigrationDocSpec.scala
b/docs/src/test/scala/docs/stream/operators/source/QueueMigrationDocSpec.scala
new file mode 100644
index 0000000000..80167be97e
--- /dev/null
+++
b/docs/src/test/scala/docs/stream/operators/source/QueueMigrationDocSpec.scala
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package docs.stream.operators.source
+
+import scala.annotation.nowarn
+import scala.concurrent.Future
+import scala.concurrent.duration._
+
+import org.apache.pekko
+import pekko.Done
+import pekko.NotUsed
+import pekko.actor.ActorRef
+import pekko.pattern.ask
+import pekko.stream.BoundedSourceQueue
+import pekko.stream.BufferOverflowException
+import pekko.stream.CompletionStrategy
+import pekko.stream.OverflowStrategy
+import pekko.stream.QueueOfferResult
+import pekko.stream.scaladsl.Keep
+import pekko.stream.scaladsl.MergeHub
+import pekko.stream.scaladsl.Sink
+import pekko.stream.scaladsl.Source
+import pekko.stream.scaladsl.SourceQueueWithComplete
+import pekko.stream.testkit.scaladsl.TestSink
+import pekko.testkit.PekkoSpec
+import pekko.util.Timeout
+
+class QueueMigrationDocSpec extends PekkoSpec {
+ import system.dispatcher
+
+ "Source.queue migration" should {
+
+ "replace dropTail with Source.queue(bufferSize)" in {
+ @nowarn("msg=deprecated")
+ def before(): Future[QueueOfferResult] = {
+ // #drop-before
+ val queue: SourceQueueWithComplete[Int] =
+ Source.queue[Int](100,
OverflowStrategy.dropTail).to(Sink.foreach(println)).run()
+
+ val result: Future[QueueOfferResult] = queue.offer(1)
+ // #drop-before
+ result
+ }
+ before().futureValue shouldBe QueueOfferResult.Enqueued
+
+ // #drop-after
+ val queue: BoundedSourceQueue[Int] =
+ Source.queue[Int](100).to(Sink.foreach(println)).run()
+
+ // offer returns the result synchronously, there is no Future to wait for
+ queue.offer(1) match {
+ case QueueOfferResult.Enqueued => // element accepted
+ case QueueOfferResult.Dropped => // buffer full, this element was
dropped
+ case QueueOfferResult.Failure(ex) => // the stream failed
+ case QueueOfferResult.QueueClosed => // the stream was completed
+ }
+ // #drop-after
+ }
+
+ "replace fail with Source.queue(bufferSize) and BoundedSourceQueue.fail"
in {
+ // #fail-after
+ val bufferSize = 2
+
+ // fail the stream like OverflowStrategy.fail did when the buffer is full
+ def offerOrFail(queue: BoundedSourceQueue[Int], elem: Int):
QueueOfferResult = {
+ val result = queue.offer(elem)
+ if (result == QueueOfferResult.Dropped)
+ queue.fail(BufferOverflowException(s"Buffer overflow (max capacity
was: $bufferSize)!"))
+ result
+ }
+ // #fail-after
+
+ // downstream never requests, so the buffer fills up
+ val (queue, probe) =
Source.queue[Int](bufferSize).toMat(TestSink[Int]())(Keep.both).run()
+ offerOrFail(queue, 1) shouldBe QueueOfferResult.Enqueued
+ offerOrFail(queue, 2) shouldBe QueueOfferResult.Enqueued
+ offerOrFail(queue, 3) shouldBe QueueOfferResult.Dropped
+ probe.expectSubscriptionAndError() shouldBe a[BufferOverflowException]
+ }
+
+ "replace backpressure with Source.actorRefWithBackpressure for a single
producer" in {
+ @nowarn("msg=deprecated")
+ def before(): Future[Done] = {
+ // #backpressure-before
+ val queue: SourceQueueWithComplete[Int] =
+ Source.queue[Int](100,
OverflowStrategy.backpressure).to(Sink.foreach(println)).run()
+
+ // the next offer is only made once the previous one has completed
+ val allOffered: Future[QueueOfferResult] = (1 to 10).foldLeft(
+ Future.successful[QueueOfferResult](QueueOfferResult.Enqueued)) {
(previous, elem) =>
+ previous.flatMap(_ => queue.offer(elem))
+ }
+ allOffered.map(_ => queue.complete())
+ // #backpressure-before
+ queue.watchCompletion()
+ }
+ before().futureValue shouldBe Done
+
+ // #backpressure-single-producer
+ case object Ack
+ case object Complete
+
+ val (ref: ActorRef, done: Future[Seq[Int]]) =
+ Source
+ .actorRefWithBackpressure[Int](
+ ackMessage = Ack,
+ completionMatcher = { case Complete => CompletionStrategy.draining
},
+ failureMatcher = PartialFunction.empty)
+ .toMat(Sink.seq)(Keep.both)
+ .run()
+
+ // Replaces queue.offer(elem): the Future completes with Ack once the
element was emitted
+ // downstream. Unlike the deprecated queue it fails with an
AskTimeoutException instead of
+ // hanging forever if downstream stalls.
+ implicit val timeout: Timeout = 3.seconds
+ def offer(elem: Int): Future[Any] = ref ? elem
+
+ // Like before, the next element must only be offered once the previous
one was acknowledged,
+ // otherwise the stream fails.
+ val allOffered: Future[Any] = (1 to
10).foldLeft(Future.successful[Any](Ack)) { (previous, elem) =>
+ previous.flatMap(_ => offer(elem))
+ }
+ allOffered.foreach(_ => ref ! Complete)
+ // #backpressure-single-producer
+
+ done.futureValue shouldBe (1 to 10)
+ }
+
+ "replace backpressure with MergeHub.source for multiple producers" in {
+ // #backpressure-multiple-producers
+ val (sink: Sink[Int, NotUsed], done: Future[Seq[Int]]) =
+ MergeHub.source[Int](perProducerBufferSize =
16).take(20).toMat(Sink.seq)(Keep.both).run()
+
+ // Each producer is a stream of its own and is back-pressured
independently,
+ // there is no offer Future to track per element.
+ Source(1 to 10).runWith(sink)
+ Source(11 to 20).runWith(sink)
+ // #backpressure-multiple-producers
+
+ done.futureValue should contain theSameElementsAs (1 to 20)
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]