Custom stream processing
While the processing vocabulary of Akka Streams is quite rich (see the Streams Cookbook for examples) it is sometimes necessary to define new transformation stages either because some functionality is missing from the stock operations, or for performance reasons. In this part we show how to build custom processing stages and graph junctions of various kinds.
注釈
A custom graph stage should not be the first tool you reach for, defining graphs using flows
and the graph DSL is in general easier and does to a larger extent protect you from mistakes that
might be easy to make with a custom GraphStage
Custom processing with GraphStage
The GraphStage
abstraction can be used to create arbitrary graph processing stages with any number of input
or output ports. It is a counterpart of the GraphDSL.create()
method which creates new stream processing
stages by composing others. Where GraphStage
differs is that it creates a stage that is itself not divisible into
smaller ones, and allows state to be maintained inside it in a safe way.
As a first motivating example, we will build a new Source
that will simply emit numbers from 1 until it is
cancelled. To start, we need to define the "interface" of our stage, which is called shape in Akka Streams terminology
(this is explained in more detail in the section Modularity, Composition and Hierarchy).
public class NumbersSource extends GraphStage<SourceShape<Integer>> {
// Define the (sole) output port of this stage
public final Outlet<Integer> out = Outlet.create("NumbersSource.out");
// Define the shape of this stage, which is SourceShape with the port we defined above
private final SourceShape<Integer> shape = SourceShape.of(out);
@Override
public SourceShape<Integer> shape() {
return shape;
}
// This is where the actual (possibly stateful) logic is created
@Override
public GraphStageLogic createLogic(Attributes inheritedAttributes) {
return new GraphStageLogic(shape()) {
// All state MUST be inside the GraphStageLogic,
// never inside the enclosing GraphStage.
// This state is safe to access and modify from all the
// callbacks that are provided by GraphStageLogic and the
// registered handlers.
private int counter = 1;
{
setHandler(out, new AbstractOutHandler() {
@Override
public void onPull() throws Exception {
push(out, counter);
counter += 1;
}
});
}
};
}
}
As you see, in itself the GraphStage
only defines the ports of this stage and a shape that contains the ports.
It also has a user implemented method called createLogic
. If you recall, stages are reusable in multiple
materializations, each resulting in a different executing entity. In the case of GraphStage
the actual running
logic is modeled as an instance of a GraphStageLogic
which will be created by the materializer by calling
the createLogic
method. In other words, all we need to do is to create a suitable logic that will emit the
numbers we want.
注釈
It is very important to keep the GraphStage object itself immutable and reusable. All mutable state needs to be confined to the GraphStageLogic that is created for every materialization.
In order to emit from a Source
in a backpressured stream one needs first to have demand from downstream.
To receive the necessary events one needs to register a subclass of AbstractOutHandler
with the output port
(Outlet
). This handler will receive events related to the lifecycle of the port. In our case we need to
override onPull()
which indicates that we are free to emit a single element. There is another callback,
onDownstreamFinish()
which is called if the downstream cancelled. Since the default behavior of that callback is
to stop the stage, we don't need to override it. In the onPull
callback we simply emit the next number.
Instances of the above GraphStage
are subclasses of Graph<SourceShape<Int>,Unit>
which means
that they are already usable in many situations, but do not provide the DSL methods we usually have for other
Source
s. In order to convert this Graph
to a proper Source
we need to wrap it using
Source.fromGraph
(see Modularity, Composition and Hierarchy for more details about graphs and DSLs). Now we can use the
source as any other built-in one:
// A GraphStage is a proper Graph, just like what GraphDSL.create would return
Graph<SourceShape<Integer>, NotUsed> sourceGraph = new NumbersSource();
// Create a Source from the Graph to access the DSL
Source<Integer, NotUsed> mySource = Source.fromGraph(sourceGraph);
// Returns 55
CompletionStage<Integer> result1 = mySource.take(10).runFold(0, (sum, next) -> sum + next, mat);
// The source is reusable. This returns 5050
CompletionStage<Integer> result2 = mySource.take(100).runFold(0, (sum, next) -> sum + next, mat);
Similarly, to create a custom Sink
one can register a subclass InHandler
with the stage Inlet
.
The onPush()
callback is used to signal the handler a new element has been pushed to the stage,
and can hence be grabbed and used. onPush()
can be overridden to provide custom behaviour.
Please note, most Sinks would need to request upstream elements as soon as they are created: this can be
done by calling pull(inlet)
in the preStart()
callback.
public class StdoutSink extends GraphStage<SinkShape<Integer>> {
public final Inlet<Integer> in = Inlet.create("StdoutSink.in");
private final SinkShape<Integer> shape = SinkShape.of(in);
@Override
public SinkShape<Integer> shape() {
return shape;
}
@Override
public GraphStageLogic createLogic(Attributes inheritedAttributes) {
return new GraphStageLogic(shape()) {
// This requests one element at the Sink startup.
@Override
public void preStart() {
pull(in);
}
{
setHandler(in, new AbstractInHandler() {
@Override
public void onPush() throws Exception {
Integer element = grab(in);
System.out.println(element);
pull(in);
}
});
}
};
}
}
Port states, AbstractInHandler and AbstractOutHandler
In order to interact with a port (Inlet
or Outlet
) of the stage we need to be able to receive events
and generate new events belonging to the port. From the GraphStageLogic
the following operations are available
on an output port:
push(out,elem)
pushes an element to the output port. Only possible after the port has been pulled by downstream.complete(out)
closes the output port normally.fail(out,exception)
closes the port with a failure signal.
The events corresponding to an output port can be received in an AbstractOutHandler
instance registered to the
output port using setHandler(out,handler)
. This handler has two callbacks:
onPull()
is called when the output port is ready to emit the next element,push(out, elem)
is now allowed to be called on this port.onDownstreamFinish()
is called once the downstream has cancelled and no longer allows messages to be pushed to it. No moreonPull()
will arrive after this event. If not overridden this will default to stopping the stage.
Also, there are two query methods available for output ports:
isAvailable(out)
returns true if the port can be pushed.isClosed(out)
returns true if the port is closed. At this point the port can not be pushed and will not be pulled anymore.
The relationship of the above operations, events and queries are summarized in the state machine below. Green shows the initial state while orange indicates the end state. If an operation is not listed for a state, then it is invalid to call it while the port is in that state. If an event is not listed for a state, then that event cannot happen in that state.
The following operations are available for input ports:
pull(in)
requests a new element from an input port. This is only possible after the port has been pushed by upstream.grab(in)
acquires the element that has been received during anonPush()
. It cannot be called again until the port is pushed again by the upstream.cancel(in)
closes the input port.
The events corresponding to an input port can be received in an AbstractInHandler
instance registered to the
input port using setHandler(in, handler)
. This handler has three callbacks:
onPush()
is called when the input port has now a new element. Now it is possible to acquire this element usinggrab(in)
and/or callpull(in)
on the port to request the next element. It is not mandatory to grab the element, but if it is pulled while the element has not been grabbed it will drop the buffered element.onUpstreamFinish()
is called once the upstream has completed and no longer can be pulled for new elements. No moreonPush()
will arrive after this event. If not overridden this will default to stopping the stage.onUpstreamFailure()
is called if the upstream failed with an exception and no longer can be pulled for new elements. No moreonPush()
will arrive after this event. If not overridden this will default to failing the stage.
Also, there are three query methods available for input ports:
isAvailable(in)
returns true if a data element can be grabbed from the porthasBeenPulled(in)
returns true if the port has been already pulled. Callingpull(in)
in this state is illegal.isClosed(in)
returns true if the port is closed. At this point the port can not be pulled and will not be pushed anymore.
The relationship of the above operations, events and queries are summarized in the state machine below. Green shows the initial state while orange indicates the end state. If an operation is not listed for a state, then it is invalid to call it while the port is in that state. If an event is not listed for a state, then that event cannot happen in that state.
Finally, there are two methods available for convenience to complete the stage and all of its ports:
completeStage()
is equivalent to closing all output ports and cancelling all input ports.failStage(exception)
is equivalent to failing all output ports and cancelling all input ports.
In some cases it is inconvenient and error prone to react on the regular state machine events with the signal based API described above. For those cases there is an API which allows for a more declarative sequencing of actions which will greatly simplify some use cases at the cost of some extra allocations. The difference between the two APIs could be described as that the first one is signal driven from the outside, while this API is more active and drives its surroundings.
The operations of this part of the :class:GraphStage
API are:
emit(out, elem)
andemitMultiple(out, Iterable(elem1, elem2))
replaces theOutHandler
with a handler that emits one or more elements when there is demand, and then reinstalls the current handlersread(in)(andThen)
andreadN(in, n)(andThen)
replaces theInHandler
with a handler that reads one or more elements as they are pushed and allows the handler to react once the requested number of elements has been read.abortEmitting()
andabortReading()
which will cancel an ongoing emit or read
Note that since the above methods are implemented by temporarily replacing the handlers of the stage you should never
call setHandler
while they are running emit
or read
as that interferes with how they are implemented.
The following methods are safe to call after invoking emit
and read
(and will lead to actually running the
operation when those are done): complete(out)
, completeStage()
, emit
, emitMultiple
, abortEmitting()
and abortReading()
An example of how this API simplifies a stage can be found below in the second version of the :class:Duplicator
.
Custom linear processing stages using GraphStage
Graph stages allows for custom linear processing stages through letting them
have one input and one output and using FlowShape
as their shape.
Such a stage can be illustrated as a box with two flows as it is seen in the illustration below. Demand flowing upstream leading to elements flowing downstream.
To illustrate these concepts we create a small GraphStage
that implements the map
transformation.
Map calls push(out)
from the onPush()
handler and it also calls pull()
from the onPull
handler resulting in the
conceptual wiring above, and fully expressed in code below:
public class Map<A, B> extends GraphStage<FlowShape<A, B>> {
private final Function<A, B> f;
public Map(Function<A, B> f) {
this.f = f;
}
public final Inlet<A> in = Inlet.create("Map.in");
public final Outlet<B> out = Outlet.create("Map.out");
private final FlowShape<A, B> shape = FlowShape.of(in, out);
@Override
public FlowShape<A,B> shape() {
return shape;
}
@Override
public GraphStageLogic createLogic(Attributes inheritedAttributes) {
return new GraphStageLogic(shape) {
{
setHandler(in, new AbstractInHandler() {
@Override
public void onPush() throws Exception {
push(out, f.apply(grab(in)));
}
});
setHandler(out, new AbstractOutHandler() {
@Override
public void onPull() throws Exception {
pull(in);
}
});
}
};
}
}
Map is a typical example of a one-to-one transformation of a stream where demand is passed along upstream elements passed on downstream.
To demonstrate a many-to-one stage we will implement
filter. The conceptual wiring of Filter
looks like this:
As we see above, if the given predicate matches the current element we are propagating it downwards, otherwise
we return the “ball” to our upstream so that we get the new element. This is achieved by modifying the map
example by adding a conditional in the onPush
handler and decide between a pull(in)
or push(out)
call
(and of course not having a mapping f
function).
public final class Filter<A> extends GraphStage<FlowShape<A, A>> {
private final Predicate<A> p;
public Filter(Predicate<A> p) {
this.p = p;
}
public final Inlet<A> in = Inlet.create("Filter.in");
public final Outlet<A> out = Outlet.create("Filter.out");
private final FlowShape<A, A> shape = FlowShape.of(in, out);
@Override
public FlowShape<A, A> shape() {
return shape;
}
public GraphStageLogic createLogic(Attributes inheritedAttributes) {
return new GraphStageLogic(shape) {
{
setHandler(in, new AbstractInHandler() {
@Override
public void onPush() {
A elem = grab(in);
if (p.test(elem)) {
push(out, elem);
} else {
pull(in);
}
}
});
setHandler(out, new AbstractOutHandler() {
@Override
public void onPull() throws Exception {
pull(in);
}
});
}
};
}
}
To complete the picture we define a one-to-many transformation as the next step. We chose a straightforward example stage that emits every upstream element twice downstream. The conceptual wiring of this stage looks like this:
This is a stage that has state: an option with the last element it has seen indicating if it has duplicated this last element already or not. We must also make sure to emit the extra element if the upstream completes.
public class Duplicator<A> extends GraphStage<FlowShape<A, A>> {
public final Inlet<A> in = Inlet.create("Duplicator.in");
public final Outlet<A> out = Outlet.create("Duplicator.out");
private final FlowShape<A, A> shape = FlowShape.of(in, out);
@Override
public FlowShape<A, A> shape() {
return shape;
}
public GraphStageLogic createLogic(Attributes inheritedAttributes) {
return new GraphStageLogic(shape) {
// Again: note that all mutable state
// MUST be inside the GraphStageLogic
Option<A> lastElem = Option.none();
{
setHandler(in, new AbstractInHandler() {
@Override
public void onPush() {
A elem = grab(in);
lastElem = Option.some(elem);
push(out, elem);
}
@Override
public void onUpstreamFinish() {
if (lastElem.isDefined()) {
emit(out, lastElem.get());
}
complete(out);
}
});
setHandler(out, new AbstractOutHandler() {
@Override
public void onPull() throws Exception {
if (lastElem.isDefined()) {
push(out, lastElem.get());
lastElem = Option.none();
} else {
pull(in);
}
}
});
}
};
}
}
In this case a pull from downstream might be consumed by the stage itself rather than passed along upstream as the stage might contain an element it wants to push. Note that we also need to handle the case where the upstream closes while the stage still has elements it wants to push downstream. This is done by overriding onUpstreamFinish in the AbstractInHandler and provide custom logic that should happen when the upstream has been finished.
This example can be simplified by replacing the usage of a mutable state with calls to
emitMultiple
which will replace the handlers, emit each of multiple elements and then
reinstate the original handlers:
public class Duplicator2<A> extends GraphStage<FlowShape<A, A>> {
public final Inlet<A> in = Inlet.create("Duplicator.in");
public final Outlet<A> out = Outlet.create("Duplicator.out");
private final FlowShape<A, A> shape = FlowShape.of(in, out);
@Override
public FlowShape<A, A> shape() {
return shape;
}
@Override
public GraphStageLogic createLogic(Attributes inheritedAttributes) {
return new GraphStageLogic(shape) {
{
setHandler(in, new AbstractInHandler() {
@Override
public void onPush() {
A elem = grab(in);
// this will temporarily suspend this handler until the two elems
// are emitted and then reinstates it
emitMultiple(out, Arrays.asList(elem, elem).iterator());
}
});
setHandler(out, new AbstractOutHandler() {
@Override
public void onPull() throws Exception {
pull(in);
}
});
}
};
}
}
Finally, to demonstrate all of the stages above, we put them together into a processing chain, which conceptually would correspond to the following structure:
In code this is only a few lines, using the via
use our custom stages in a stream:
CompletionStage<String> resultFuture = Source.from(Arrays.asList(1,2,3,4,5))
.via(new Filter<Integer>((n) -> n % 2 == 0))
.via(new Duplicator<Integer>())
.via(new Map<Integer, Integer>((n) -> n / 2))
.runWith(sink, mat);
If we attempt to draw the sequence of events, it shows that there is one "event token" in circulation in a potential chain of stages, just like our conceptual "railroad tracks" representation predicts.
Completion
Completion handling usually (but not exclusively) comes into the picture when processing stages need to emit
a few more elements after their upstream source has been completed. We have seen an example of this in our
first Duplicator
implementation where the last element needs to be doubled even after the upstream neighbor
stage has been completed. This can be done by overriding the onUpstreamFinish
method in AbstractInHandler
.
Stages by default automatically stop once all of their ports (input and output) have been closed externally or internally.
It is possible to opt out from this behavior by invoking setKeepGoing(true)
(which is not supported from the stage’s
constructor and usually done in preStart
). In this case the stage must be explicitly closed by calling completeStage()
or failStage(exception)
. This feature carries the risk of leaking streams and actors, therefore it should be used
with care.
Using timers
It is possible to use timers in GraphStages
by using TimerGraphStageLogic
as the base class for
the returned logic. Timers can be scheduled by calling one of scheduleOnce(key,delay)
, schedulePeriodically(key,period)
or
schedulePeriodicallyWithInitialDelay(key,delay,period)
and passing an object as a key for that timer (can be any object, for example
a String
). The onTimer(key)
method needs to be overridden and it will be called once the timer of key
fires. It is possible to cancel a timer using cancelTimer(key)
and check the status of a timer with
isTimerActive(key)
. Timers will be automatically cleaned up when the stage completes.
Timers can not be scheduled from the constructor of the logic, but it is possible to schedule them from the
preStart()
lifecycle hook.
In this sample the stage toggles between open and closed, where open means no elements are passed through. The stage starts out as closed but as soon as an element is pushed downstream the gate becomes open for a duration of time during which it will consume and drop upstream messages:
// each time an event is pushed through it will trigger a period of silence
public class TimedGate<A> extends GraphStage<FlowShape<A, A>> {
private final FiniteDuration silencePeriod;
public TimedGate(FiniteDuration silencePeriod) {
this.silencePeriod = silencePeriod;
}
public final Inlet<A> in = Inlet.create("TimedGate.in");
public final Outlet<A> out = Outlet.create("TimedGate.out");
private final FlowShape<A, A> shape = FlowShape.of(in, out);
@Override
public FlowShape<A, A> shape() {
return shape;
}
@Override
public GraphStageLogic createLogic(Attributes inheritedAttributes) {
return new TimerGraphStageLogic(shape) {
private boolean open = false;
{
setHandler(in, new AbstractInHandler() {
@Override
public void onPush() throws Exception {
A elem = grab(in);
if (open) pull(in);
else {
push(out, elem);
open = true;
scheduleOnce("key", silencePeriod);
}
}
});
setHandler(out, new AbstractOutHandler() {
@Override
public void onPull() throws Exception {
pull(in);
}
});
}
@Override
public void onTimer(Object key) {
if (key.equals("key")) {
open = false;
}
}
};
}
}
Using asynchronous side-channels
In order to receive asynchronous events that are not arriving as stream elements (for example a completion of a future
or a callback from a 3rd party API) one must acquire a AsyncCallback
by calling getAsyncCallback()
from the
stage logic. The method getAsyncCallback
takes as a parameter a callback that will be called once the asynchronous
event fires. It is important to not call the callback directly, instead, the external API must call the
invoke(event)
method on the returned AsyncCallback
. The execution engine will take care of calling the
provided callback in a thread-safe way. The callback can safely access the state of the GraphStageLogic
implementation.
Sharing the AsyncCallback from the constructor risks race conditions, therefore it is recommended to use the
preStart()
lifecycle hook instead.
This example shows an asynchronous side channel graph stage that starts dropping elements when a future completes:
// will close upstream in all materializations of the stage instance
// when the completion stage completes
public class KillSwitch<A> extends GraphStage<FlowShape<A, A>> {
private final CompletionStage<Done> switchF;
public KillSwitch(CompletionStage<Done> switchF) {
this.switchF = switchF;
}
public final Inlet<A> in = Inlet.create("KillSwitch.in");
public final Outlet<A> out = Outlet.create("KillSwitch.out");
private final FlowShape<A, A> shape = FlowShape.of(in, out);
@Override
public FlowShape<A, A> shape() {
return shape;
}
@Override
public GraphStageLogic createLogic(Attributes inheritedAttributes) {
return new GraphStageLogic(shape) {
{
setHandler(in, new AbstractInHandler() {
@Override
public void onPush() {
push(out, grab(in));
}
});
setHandler(out, new AbstractOutHandler() {
@Override
public void onPull() throws Exception {
pull(in);
}
});
}
@Override
public void preStart() {
AsyncCallback<Done> callback = createAsyncCallback(new Procedure<Done>() {
@Override
public void apply(Done param) throws Exception {
completeStage();
}
});
ExecutionContext ec = system.dispatcher();
switchF.thenAccept(callback::invoke);
}
};
}
}
Integration with actors
This section is a stub and will be extended in the next release This is an experimental feature*
It is possible to acquire an ActorRef that can be addressed from the outside of the stage, similarly how
AsyncCallback
allows injecting asynchronous events into a stage logic. This reference can be obtained
by calling getStageActorRef(receive)
passing in a function that takes a Pair
of the sender
ActorRef
and the received message. This reference can be used to watch other actors by calling its watch(ref)
or unwatch(ref)
methods. The reference can be also watched by external actors. The current limitations of this
ActorRef
are:
- they are not location transparent, they cannot be accessed via remoting.
- they cannot be returned as materialized values.
- they cannot be accessed from the constructor of the
GraphStageLogic
, but they can be accessed from thepreStart()
method.
Custom materialized values
Custom stages can return materialized values instead of Unit
by inheriting from GraphStageWithMaterializedValue
instead of the simpler GraphStage
. The difference is that in this case the method
createLogicAndMaterializedValue(inheritedAttributes)
needs to be overridden, and in addition to the
stage logic the materialized value must be provided
警告
There is no built-in synchronization of accessing this value from both of the thread where the logic runs and the thread that got hold of the materialized value. It is the responsibility of the programmer to add the necessary (non-blocking) synchronization and visibility guarantees to this shared object.
In this sample the materialized value is a future containing the first element to go through the stream:
public class FirstValue<A> extends GraphStageWithMaterializedValue<FlowShape<A, A>, CompletionStage<A>> {
public final Inlet<A> in = Inlet.create("FirstValue.in");
public final Outlet<A> out = Outlet.create("FirstValue.out");
private final FlowShape<A, A> shape = FlowShape.of(in, out);
@Override
public FlowShape<A, A> shape() {
return shape;
}
@Override
public Tuple2<GraphStageLogic, CompletionStage<A>> createLogicAndMaterializedValue(Attributes inheritedAttributes) {
Promise<A> promise = Futures.promise();
GraphStageLogic logic = new GraphStageLogic(shape) {
{
setHandler(in, new AbstractInHandler() {
@Override
public void onPush() {
A elem = grab(in);
promise.success(elem);
push(out, elem);
// replace handler with one just forwarding
setHandler(in, new AbstractInHandler() {
@Override
public void onPush() {
push(out, grab(in));
}
});
}
});
setHandler(out, new AbstractOutHandler() {
@Override
public void onPull() throws Exception {
pull(in);
}
});
}
};
return new Tuple2(logic, promise.future());
}
}
Using attributes to affect the behavior of a stage
This section is a stub and will be extended in the next release
Stages can access the Attributes
object created by the materializer. This contains all the applied (inherited)
attributes applying to the stage, ordered from least specific (outermost) towards the most specific (innermost)
attribute. It is the responsibility of the stage to decide how to reconcile this inheritance chain to a final effective
decision.
See Modularity, Composition and Hierarchy for an explanation on how attributes work.
Rate decoupled graph stages
Sometimes it is desirable to decouple the rate of the upstream and downstream of a stage, synchronizing only when needed.
This is achieved in the model by representing a GraphStage
as a boundary between two regions where the
demand sent upstream is decoupled from the demand that arrives from downstream. One immediate consequence of this
difference is that an onPush
call does not always lead to calling push
and an onPull
call does not always
lead to calling pull
.
One of the important use-case for this is to build buffer-like entities, that allow independent progress of upstream and downstream stages when the buffer is not full or empty, and slowing down the appropriate side if the buffer becomes empty or full.
The next diagram illustrates the event sequence for a buffer with capacity of two elements in a setting where the downstream demand is slow to start and the buffer will fill up with upstream elements before any demand is seen from downstream.
Another scenario would be where the demand from downstream starts coming in before any element is pushed into the buffer stage.
The first difference we can notice is that our Buffer
stage is automatically pulling its upstream on
initialization. The buffer has demand for up to two elements without any downstream demand.
The following code example demonstrates a buffer class corresponding to the message sequence chart above.
public class TwoBuffer<A> extends GraphStage<FlowShape<A, A>> {
public final Inlet<A> in = Inlet.create("TwoBuffer.in");
public final Outlet<A> out = Outlet.create("TwoBuffer.out");
private final FlowShape<A, A> shape = FlowShape.of(in, out);
@Override
public FlowShape<A, A> shape() {
return shape;
}
@Override
public GraphStageLogic createLogic(Attributes inheritedAttributes) {
return new GraphStageLogic(shape) {
private final int SIZE = 2;
private Queue<A> buffer = new ArrayDeque<>(SIZE);
private boolean downstreamWaiting = false;
private boolean isBufferFull() {
return buffer.size() == SIZE;
}
@Override
public void preStart() {
// a detached stage needs to start upstream demand
// itself as it is not triggered by downstream demand
pull(in);
}
{
setHandler(in, new AbstractInHandler() {
@Override
public void onPush() {
A elem = grab(in);
buffer.add(elem);
if (downstreamWaiting) {
downstreamWaiting = false;
A bufferedElem = buffer.poll();
push(out, bufferedElem);
}
if (!isBufferFull()) {
pull(in);
}
}
@Override
public void onUpstreamFinish() {
if (!buffer.isEmpty()) {
// emit the rest if possible
emitMultiple(out, buffer.iterator());
}
completeStage();
}
});
setHandler(out, new AbstractOutHandler() {
@Override
public void onPull() throws Exception {
if (buffer.isEmpty()) {
downstreamWaiting = true;
} else {
A elem = buffer.poll();
push(out, elem);
}
if (!isBufferFull() && !hasBeenPulled(in)) {
pull(in);
}
}
});
}
};
}
}
Thread safety of custom processing stages
- All of the above custom stages (linear or graph) provide a few simple guarantees that implementors can rely on.
- The callbacks exposed by all of these classes are never called concurrently.
- The state encapsulated by these classes can be safely modified from the provided callbacks, without any further synchronization.
In essence, the above guarantees are similar to what Actor
s provide, if one thinks of the state of a custom
stage as state of an actor, and the callbacks as the receive
block of the actor.
警告
It is not safe to access the state of any custom stage outside of the callbacks that it provides, just like it is unsafe to access the state of an actor from the outside. This means that Future callbacks should not close over internal state of custom stages because such access can be concurrent with the provided callbacks, leading to undefined behavior.
Contents