Restart Flow/Source/Sink #19950
This commit is contained in:
parent
b43e6b519a
commit
c60d20af32
8 changed files with 1174 additions and 4 deletions
|
|
@ -101,4 +101,49 @@ Java
|
|||
: @@snip [IntegrationDocTest.java]($code$/java/jdocs/stream/IntegrationDocTest.java) { #email-addresses-mapAsync-supervision }
|
||||
|
||||
If we would not use `Resume` the default stopping strategy would complete the stream
|
||||
with failure on the first @scala[`Future`] @java[`CompletionStage`] that was completed @scala[with `Failure`]@java[exceptionally].
|
||||
with failure on the first @scala[`Future`] @java[`CompletionStage`] that was completed @scala[with `Failure`]@java[exceptionally].
|
||||
|
||||
## Delayed restarts with a backoff stage
|
||||
|
||||
Just as Akka provides the @ref:[backoff supervision pattern for actors](../general/supervision.md#backoff-supervisor), Akka streams
|
||||
also provides a `RestartSource`, `RestartSink` and `RestartFlow` for implementing the so-called *exponential backoff
|
||||
supervision strategy*, starting a stage again when it fails, each time with a growing time delay between restarts.
|
||||
|
||||
This pattern is useful when the stage fails or completes because some external resource is not available
|
||||
and we need to give it some time to start-up again. One of the prime examples when this is useful is
|
||||
when a WebSocket connection fails due to the HTTP server it's running on going down, perhaps because it is overloaded.
|
||||
By using an exponential backoff, we avoid going into a tight reconnect look, which both gives the HTTP server some time
|
||||
to recover, and it avoids using needless resources on the client side.
|
||||
|
||||
The following snippet shows how to create a backoff supervisor using @scala[`akka.stream.scaladsl.RestartSource`]
|
||||
@java[`akka.stream.javadsl.RestartSource`] which will supervise the given `Source`. The `Source` in this case is a
|
||||
stream of Server Sent Events, produced by akka-http. If the stream fails or completes at any point, the request will
|
||||
be made again, in increasing intervals of 3, 6, 12, 24 and finally 30 seconds (at which point it will remain capped due
|
||||
to the `maxBackoff` parameter):
|
||||
|
||||
Scala
|
||||
: @@snip [RestartDocSpec.scala]($code$/scala/docs/stream/RestartDocSpec.scala) { #restart-with-backoff-source }
|
||||
|
||||
Java
|
||||
: @@snip [RestartDocTest.java]($code$/java/jdocs/stream/RestartDocTest.java) { #restart-with-backoff-source }
|
||||
|
||||
Using a `randomFactor` to add a little bit of additional variance to the backoff intervals
|
||||
is highly recommended, in order to avoid multiple streams re-start at the exact same point in time,
|
||||
for example because they were stopped due to a shared resource such as the same server going down
|
||||
and re-starting after the same configured interval. By adding additional randomness to the
|
||||
re-start intervals the streams will start in slightly different points in time, thus avoiding
|
||||
large spikes of traffic hitting the recovering server or other resource that they all need to contact.
|
||||
|
||||
The above `RestartSource` will never terminate unless the `Sink` it's fed into cancels. It will often be handy to use
|
||||
it in combination with a @ref:[`KillSwitch`](stream-dynamic.md#kill-switch), so that you can terminate it when needed:
|
||||
|
||||
Scala
|
||||
: @@snip [RestartDocSpec.scala]($code$/scala/docs/stream/RestartDocSpec.scala) { #with-kill-switch }
|
||||
|
||||
Java
|
||||
: @@snip [RestartDocTest.java]($code$/java/jdocs/stream/RestartDocTest.java) { #with-kill-switch }
|
||||
|
||||
Sinks and flows can also be supervised, using @scala[`akka.stream.scaladsl.RestartSink` and `akka.stream.scaladsl.RestartFlow`]
|
||||
@java[`akka.stream.scaladsl.RestartSink` and `akka.stream.scaladsl.RestartFlow`]. The `RestartSink` is restarted when
|
||||
it cancels, while the `RestartFlow` is restarted when either the in port cancels, the out port completes, or the out
|
||||
port sends an error.
|
||||
86
akka-docs/src/test/java/jdocs/stream/RestartDocTest.java
Normal file
86
akka-docs/src/test/java/jdocs/stream/RestartDocTest.java
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* Copyright (C) 2015-2017 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
package jdocs.stream;
|
||||
|
||||
import akka.NotUsed;
|
||||
import akka.actor.ActorSystem;
|
||||
import akka.stream.KillSwitch;
|
||||
import akka.stream.KillSwitches;
|
||||
import akka.stream.Materializer;
|
||||
import akka.stream.javadsl.*;
|
||||
import scala.concurrent.duration.Duration;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class RestartDocTest {
|
||||
|
||||
static ActorSystem system;
|
||||
static Materializer materializer;
|
||||
|
||||
// Mocking akka-http
|
||||
public static class Http {
|
||||
public static Http get(ActorSystem system) {
|
||||
return new Http();
|
||||
}
|
||||
public CompletionStage<Http> singleRequest(String uri) {
|
||||
return new CompletableFuture<>();
|
||||
}
|
||||
public NotUsed entity() {
|
||||
return NotUsed.getInstance();
|
||||
}
|
||||
}
|
||||
public static class HttpRequest {
|
||||
public static String create(String uri) {
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
public static class ServerSentEvent {}
|
||||
public static class EventStreamUnmarshalling {
|
||||
public static EventStreamUnmarshalling fromEventStream() {
|
||||
return new EventStreamUnmarshalling();
|
||||
}
|
||||
public CompletionStage<Source<ServerSentEvent, NotUsed>> unmarshall(Http http, Materializer mat) {
|
||||
return new CompletableFuture<>();
|
||||
}
|
||||
}
|
||||
public void doSomethingElse() {
|
||||
|
||||
}
|
||||
|
||||
public void recoverWithBackoffSource() {
|
||||
//#restart-with-backoff-source
|
||||
Source<ServerSentEvent, NotUsed> eventStream = RestartSource.withBackoff(
|
||||
Duration.apply(3, TimeUnit.SECONDS), // min backoff
|
||||
Duration.apply(30, TimeUnit.SECONDS), // max backoff
|
||||
0.2, // adds 20% "noise" to vary the intervals slightly
|
||||
|
||||
() ->
|
||||
// Create a source from a future of a source
|
||||
Source.fromSourceCompletionStage(
|
||||
// Issue a GET request on the event stream
|
||||
Http.get(system).singleRequest(HttpRequest.create("http://example.com/eventstream"))
|
||||
.thenCompose(response ->
|
||||
// Unmarshall it to a stream of ServerSentEvents
|
||||
EventStreamUnmarshalling.fromEventStream()
|
||||
.unmarshall(response, materializer)
|
||||
)
|
||||
)
|
||||
);
|
||||
//#restart-with-backoff-source
|
||||
|
||||
//#with-kill-switch
|
||||
KillSwitch killSwitch = eventStream
|
||||
.viaMat(KillSwitches.single(), Keep.right())
|
||||
.toMat(Sink.foreach(event -> System.out.println("Got event: " + event)), Keep.left())
|
||||
.run(materializer);
|
||||
|
||||
doSomethingElse();
|
||||
|
||||
killSwitch.shutdown();
|
||||
//#with-kill-switch
|
||||
|
||||
}
|
||||
}
|
||||
67
akka-docs/src/test/scala/docs/stream/RestartDocSpec.scala
Normal file
67
akka-docs/src/test/scala/docs/stream/RestartDocSpec.scala
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* Copyright (C) 2015-2017 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
package docs.stream
|
||||
|
||||
import akka.NotUsed
|
||||
import akka.stream.{ ActorMaterializer, KillSwitches }
|
||||
import akka.stream.scaladsl._
|
||||
import akka.testkit.AkkaSpec
|
||||
import docs.CompileOnlySpec
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent._
|
||||
|
||||
class RestartDocSpec extends AkkaSpec with CompileOnlySpec {
|
||||
implicit val materializer = ActorMaterializer()
|
||||
import system.dispatcher
|
||||
|
||||
// Mock akka-http interfaces
|
||||
object Http {
|
||||
def apply() = this
|
||||
def singleRequest(req: HttpRequest) = Future.successful(())
|
||||
}
|
||||
case class HttpRequest(uri: String)
|
||||
case class Unmarshal(b: Any) {
|
||||
def to[T]: Future[T] = Promise[T]().future
|
||||
}
|
||||
case class ServerSentEvent()
|
||||
|
||||
def doSomethingElse(): Unit = ()
|
||||
|
||||
"Restart stages" should {
|
||||
|
||||
"demonstrate a restart with backoff source" in compileOnlySpec {
|
||||
|
||||
//#restart-with-backoff-source
|
||||
val restartSource = RestartSource.withBackoff(
|
||||
minBackoff = 3.seconds,
|
||||
maxBackoff = 30.seconds,
|
||||
randomFactor = 0.2 // adds 20% "noise" to vary the intervals slightly
|
||||
) { () =>
|
||||
// Create a source from a future of a source
|
||||
Source.fromFutureSource {
|
||||
// Make a single request with akka-http
|
||||
Http().singleRequest(HttpRequest(
|
||||
uri = "http://example.com/eventstream"
|
||||
))
|
||||
// Unmarshall it as a source of server sent events
|
||||
.flatMap(Unmarshal(_).to[Source[ServerSentEvent, NotUsed]])
|
||||
}
|
||||
}
|
||||
//#restart-with-backoff-source
|
||||
|
||||
//#with-kill-switch
|
||||
val killSwitch = restartSource
|
||||
.viaMat(KillSwitches.single)(Keep.right)
|
||||
.toMat(Sink.foreach(event => println(s"Got event: $event")))(Keep.left)
|
||||
.run()
|
||||
|
||||
doSomethingElse()
|
||||
|
||||
killSwitch.shutdown()
|
||||
//#with-kill-switch
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue