Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Contract Definition – Multicast/Unicast – Simplified Types #41

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
96 changes: 65 additions & 31 deletions README.md
Expand Up @@ -19,7 +19,7 @@ The latest preview release is available on Maven Central as

Handling streams of data—especially “live” data whose volume is not predetermined—requires special care in an asynchronous system. The most prominent issue is that resource consumption needs to be carefully controlled such that a fast data source does not overwhelm the stream destination. Asynchrony is needed in order to enable the parallel use of computing resources, on collaborating network hosts or multiple CPU cores within a single machine.

The main goal of Reactive Streams is to govern the exchange of stream data across an asynchronous boundarythink passing elements on to another thread or thread-poolwhile ensuring that the receiving side is not forced to buffer arbitrary amounts of data. In other words, backpressure is an integral part of this model in order to allow the queues which mediate between threads to be bounded. The benefits of asynchronous processing would be negated if the communication of backpressure were synchronous (see also the [Reactive Manifesto](http://reactivemanifesto.org/)), therefore care has been taken to mandate fully non-blocking and asynchronous behavior of all aspects of a Reactive Streams implementation.
The main goal of Reactive Streams is to govern the exchange of stream data across an asynchronous boundarythink passing elements on to another thread or thread-poolwhile ensuring that the receiving side is not forced to buffer arbitrary amounts of data. In other words, backpressure is an integral part of this model in order to allow the queues which mediate between threads to be bounded. The benefits of asynchronous processing would be negated if the communication of backpressure were synchronous (see also the [Reactive Manifesto](http://reactivemanifesto.org/)), therefore care has been taken to mandate fully non-blocking and asynchronous behavior of all aspects of a Reactive Streams implementation.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think "fully non-blocking and asynchronous behavior of all aspects of a Reactive Streams implementation" [emphasis mine] is a little too inclusive given there will be times that actions taken in an implementation are non-blocking but not asynchronous. Shouldn't this language be restricted to the specific components in which this behavior should be indisputable vs times when its allowed to be synchronous as long as its non-blocking?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I had focused more on the lower parts of the document where I had changed it to clearly state it's about being non-blocking, not async vs sync for much of it.

The only two places where truly async behavior is needed as I see it are:

  1. the Subscription.request method (and possible cancel)
  2. The need for a Subscriber to use request(n) in the first place which implies that somewhere down the chain it is async (buffering).

Experience with prototypes in RxJava have led me to believe the Subscription.request method should allow receiving -1 or some other value to represent infinite – just send me as fast as you can.

This is a very valid use case for a variety of things. For example, if I have a Publisher that represents a file and it's a choice of the Subscriber whether it is consumed synchronously or asynchronously, the Subscriber should be able to tell the Subscription to send without restriction.

Similarly, if I'm streaming metrics, I an do request(-1) and then just sample or throttle, since request(n) doesn't really make much sense in that use case. The data is flowing along at whatever rate it does and I always want to sample whatever the latest value is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose to discuss the topic of asynchrony in #46 and open new issues for discussing the other things: we should focus on fixing the README so that it matches the code again ASAP (and then update the website with correct links as well).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

focus on fixing the README so that it matches the code again ASAP (and then update the website with correct links as well).

Agreed, which was my hope with this PR even though I knew it would continue evolving over time.


It is the intention of this specification to allow the creation of many conforming implementations, which by virtue of abiding by the rules will be able to interoperate smoothly, preserving the aforementioned benefits and characteristics across the whole processing graph of a stream application.

Expand All @@ -34,67 +34,101 @@ In summary, Reactive Streams is a standard and specification for Stream-oriented

The Reactive Streams specification consists of the following parts:

**The SPI** defines the interoperablility layer between different implementations.

**The API** specifies the types that the users of Reactive Stream libraries use.
**The API** specifies the types to implement Reactive Streams and achieve interoperablility between different implementations.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe "types needed to [adequately?] implement Reactive Streams to achieve interoperability"?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about: “The API specifies the interfaces that define the interoperation layer of different Reactive Streams implementations.”


***The Technology Compatibility Kit (TCK)*** is a standard test suite for conformance testing of implementations.

Implementations are free to implement additional features not covered by the specification as long as they conform to the API and SPI requirements and pass the tests in the TCK.
Implementations are free to implement additional features not covered by the specification as long as they conform to the API requirements and pass the tests in the TCK.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just "pass the TCK".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d be okay either way.


#### Comparison with related technologies ####
### API Components ###

In contrast to reactive streams described in this document, a Future represents exactly one element (or a failure) that is produced asynchronosly while streams can provide a potentially unbounded number of elements.
The API consists of the following components that are required to be provided by Reactive Stream implementations:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this section removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a few problems with it while revising this:

  1. It's not comprehensive to all possible technologies or solutions, particularly when you leave the JVM world and try and take into account everything going on in Javascript, Go, Dart, .Net, etc.
  2. The "related technologies" are moving targets (such as the fact that Rx is working with this initiative to support all of this) thus baking any statements into this document then requires upkeep.
  3. This document and project should focus on what this new specification and contract is, not what other projects are.

In short, comparisons should live elsewhere, not in the specification and contract documentation which should be timeless.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, this is a good point; we should probably open the wiki for that purpose

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#49 created.


Compared to Rx, the SPI described here prescribes a mandatory, non-blocking way to handle back-pressure and requires the processing of an element by a dowstream component to be dispatched asynchronously.
- Publisher
- Subscriber
- Subscription

Iteratees are an abstraction used for consuming a stream, often for parsing it. In this sense they are not a stream transformation or combination tool in themselves.
A *Publisher* is a provider of a potentially unbounded number of sequenced elements, publishing them according to the demand received from its Subscriber(s).

### SPI Components ###
In response to a call to `Publisher.subscribe(Subscriber)` the possible invocation sequences for methods on the `Subscriber` are given by the following protocol:

The SPI consists of components that are required to be provided by Reactive Stream implementations but these interfaces should not be exposed to libraries or user code that *use* a Reactive Streams implementation. The reason for this is that the methods used on the SPI level have very strict and rather complex semantic requirements which are likely to be violated by end users.
```
onError | (onSubscribe onNext* (onError | onComplete)?)
```

The components of the SPI are:
- The number of `onNext` events emitted by a `Publisher` to a `Subscriber` will at no point in time exceed the cumulative demand that has been signaled via that `Subscriber`’s `Subscription`.
- A `Publisher` can send less events than requested and terminate the `Subscription` by calling `onComplete` or `onError`.
- Events sent to a `Subscriber` can only be sent sequentially (no concurrent notifications).
- If a `Publisher` fails it must emit an `onError`.
- If a `Publisher` terminates successfully (finite stream) it must emit an `onComplete`.
- If a Publisher signals either `onError` or `onComplete` on a `Subscriber`, that `Subscriber`’s `Subscription` must be considered canceled.
- Once a terminal state has been signaled (`onError`, `onNext`) no further events can be sent.
- Upon receiving a `Subscription.cancel` request it should stop sending events as soon as it can.
- `Subscription`'s which have been canceled should not receive subsequent `onError` or `onComplete` events, but implementations will not be able to strictly guarantee this in all cases due to the intrinsic race condition between actions taken concurrently by `Publisher` and `Subscriber`.
- The `Publisher.subscribe` method can be called as many times as wanted as long as it is with a different `Subscriber` each time. It is up to the `Publisher` whether underlying streams are shared or not. In other words, a `Publisher` can support multi-subscribe and then choose whether each `Subscription` is unicast or multicast.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it clear that "a different Subscriber" literally means: "a different Subscriber instance"?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think this is sufficiently clear; with what could it be confused?

- A `Publisher` can reject calls to its `subscribe` method if it is unable or unwilling to serve them (e.g. because it is overwhelmed or bounded by a finite number of underlying resources, etc...). It does this by calling `onError` on the `Subscriber` passed to `Publisher.subscribe` instead of calling `onSubscribe`".
- A `Publisher` should not throw an `Exception`. The only legal way to signal failure (or reject a `Subscription`) is via the `Subscriber.onError` method.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we generalize that to “none of the methods on Publisher/Subscription/Subscriber are allowed to throw non-fatal exceptions; failure is signaled exclusively via the onError callback”?

(where there should be a footnote declaring VirtualMachineError (sans StackOverflowException), ThreadDeath, InterruptedException, LinkageError, NotImplementedError as “fatal”)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm open to that. Not updating yet as I think we require more discussion due to the footnote bit.

Rx borrowed from Scala in the definition of what to throw: ReactiveX/RxJava#748 (comment) implemented here https://github.com/Netflix/RxJava/blob/master/rxjava-core/src/main/java/rx/exceptions/Exceptions.java#L56

If something breaks the contract it has to throw. For example, if onError throws there is no other choice.

I think it's correct to state that the contract is that nothing should throw (with the given exceptions) but implementations also need to be practical and handle user error and assume things can be thrown in out-of-contract scenarios.

- The `Subscription.request` method must behave asynchronously (separate thread, event loop, trampoline, etc) so as to not cause a StackOverflow since `Subscriber.onNext` -> `Subscription.request` -> `Subscriber.onNext` can recurse infinitely. This allows a `Subscriber` to directly invoke `Subscription.request` and isolate the async responsibility to the `Subscription` instance which has responsibility for scheduling events.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a little tricky because, using the RingBuffer as an example, it would be impossible to fulfill this requirement as-is. I would deadlock the thread if I tried to checkout another slot in the RingBuffer while in the event thread of the RingBuffer itself. The only solution to this is what we do in Reactor which is simulate tail recursion by placing a task to be dispatched onto a Queue that will be emptied after the current event handling method has returned.

It also doesn't seem optimal to enforce crossing a thread boundary here. When using the RingBuffer I only ever use a single thread so I can certainly make task execution asynchronous but I can't make it run in another thread (nor would I want to since I'm gaining a lot of performance by not context-switching).

If we're going to mandate crossing an asynchronous boundary here I think it should be fairly explicit what we expect. I don't feel like the current text does enough to explain what's expected and why.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is certainly one of the more tricky things I came across in the implementation. First of all, I am not sure that only allowing async boundary is sensible. For testing purpose, the flow behavior should be the same synchronously. E.g simple RxJava Observable.from(xxx) flow or Reactor Streams.defer(xxx). It is certainly optimized towards async, but what does that mean is we should have a -1 marker to let the Subscription dismisses the backpressure and just fully drain its source. If the publisher detects such capacity on one of its subscriptions, it should never buffer/queue but directly call the subscriber.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also doesn't seem optimal to enforce crossing a thread boundary here

By async this does not mean crossing thread boundaries. If the producer and consumer are on the same thread this can be achieved via trampolining for example.

I think #46 is a good discussion to continue on this. I won't update this text for now as it feels like it needs further clarification.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should have a -1 marker to let the Subscription dismisses the backpressure and just fully drain its source

I would definitely like to add this to the contract.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change forecloses the decision in #46; I would strongly prefer to keep the current wording (requiring fully asynchronous behavior of all specified methods) until we have properly settled that point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wording of this README can and will change as we continue progressing.

This pull request adds far more detail than currently exists and brings us closer to agreement so I suggest merging this and moving forward instead of continuing to bike shed on details that should be modified after and holding up everything else.



A *`Subscriber`* is a component that accepts a sequenced stream of elements provided by a `Publisher`. At any given time a `Subscriber` might be subscribed to at most one `Publisher`. It provides the callback `onNext` to be called by the upstream `Publisher`, accepting an element that is to be processed or enqueued without blocking the `Publisher`.

- `Subscriber` can be used once-and-only-once to subscribe to a `Publisher`.

- Publisher
- Subscriber
- Subscription

A *Publisher* is a provider of a potentially unbounded number of sequenced elements, publishing them according to the demand received from its Subscriber(s). A Publisher can serve multiple subscribers subscribed dynamically at various points in time. In the case of multiple subscribers the Publisher should respect the processing rates of all of its subscribers (possibly allowing for a bounded drift between them). It must eventually clean up its resources after all of its subscribers have been unsubscribed and shut down. A Publisher will typically support fanning out to multiple Subscribers in order to support the dynamic assembly of processing networks from building blocks that can freely be shared.
A `Subscriber` communicates demand to the `Publisher` via a *`Subscription`* which is passed to the `Subscriber` after the subscription has been established. The `Subscription` exposes the `request(int)` method that is used by the `Subscriber` to signal demand to the `Publisher`.

A *Subscriber* is a component that accepts a sequenced stream of elements provided by a Publisher. At any given time a Subscriber might be subscribed to at most one Publisher. It provides the callback onNext to be called by the upstream Producer, accepting an element that is to be asynchronously processed or enqueued without blocking the Producer.
- A `Subscription` can be used once-and-only-once to represent a subscription by a `Subscriber` to a `Publisher`.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why "only-once" behavior is required here. This would seem to be a spot in the architecture that could benefit from reusing a Subscription to avoid GC issues. I could see using it "at-most-once" but I'm not sure "once-and-only-once" is necessary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, it should be only one instance active at a time (allowing Subscriptions pooling for optimizing things such as flatMap and other dynamic Subscription creation).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of life-cycle here. Object pooling under the covers is fine, as long as it retains the same semantics of garbage collection that ensures each lifecycle starts it as unsubscribed, it is used by only 1 Subscriber/Producer until unsubscribed and then freed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed with @benjchristensen here: it is the exposed the semantics that matters

- Calls from a `Subscriber` to `Subscription.request(int n)` can be made directly since it is the responsibility of `Subscription` to handle async dispatching.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does “directly” mean in this context? If you refer to the point that the Subscriber should be allowed to call this from onNext then that is a change wrt. the status quo that we would have to discuss separately; that discussion would depend on us deciding on #46 first.

OTOH I don’t see a need for this point in the currently proposed text: it could be removed from this PR without adverse effects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A consumer should be able to be coded like this:

onNext(t) {
  queue.add(t);
  subscription.request(1);
}

The onNext here is completely async as it's just queuing the item for later processing and then invokes request which is an async call since the Subscription itself guarantees it is async. We don't need double scheduling in both the Consumer and Subscription. Only one of those places need to be async and thus this simplifies the Consumer and contract by clearly describing which implementation needs to be async.

Discussion about allowing a consumer to directly invoke subscription.request is referenced in these places:

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clarification, I agree. To avoid the kind of misunderstanding I had I propose to reword to: “Subscription.request() may be called synchronously from within the Subscriber.onNext() callback as long as the processing of the transmitted data element occurs asynchronously.”


A Subscriber communicates demand to the Publisher via a *Subscription* which is passed to the Subscriber after the subscription has been established. The Subscription exposes the requestMore(int) method that is used by the Subscriber to signal demand to the Publisher. For each of its subscribers the Publisher obeys the following invariant:
For each of its subscribers the `Publisher` obeys the following invariant:

*If N is the total number of demand tokens handed to the Publisher P by a Subscriber S during the time period up to a time T, then the number of onNext calls that are allowed to be performed by P on S before T must be less than or equal to N. The number of pending demand tokens must be tracked by the Producer separately for each of its subscribers.*
*If N is the total number of demand tokens handed to the `Publisher` P by a `Subscriber` S during the time period up to a time T, then the number of `onNext` calls that are allowed to be performed by P on S before T must be less than or equal to N. The number of pending demand tokens must be tracked by the `Producer` separately for each of its subscribers.*

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Me no understand. 😕

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The producer must check the capacity on the subscription/subscriber before handing over any onNext. Until Subscription#request is effectively called, the capacity remains null at first. However onSubscribe will probably generate a request(xxx) from the subscriber which will lead to adding to its capacity the requested number then signaling the Publisher to broadcast its onNext events from its buffer (if any, if it is a cold/replayable stream).


Subscribers that do not currently have an active subscription may subscribe to a Publisher. The only guarantee for subscribers attached at different points in time is that they all observe a common suffix of the stream, i.e. they receive the same elements after a certain point in time but it is not guaranteed that they see exactly the same number of elements. This obviously only holds if the subscriber does not cancel its subscription before the stream has been terminated.
`Subscriber`s that do not currently have an active subscription may subscribe to a `Publisher`. The only guarantee for subscribers attached at different points in time is that they all observe a common suffix of the stream, i.e. they receive the same elements after a certain point in time but it is not guaranteed that they see exactly the same number of elements. This obviously only holds if the subscriber does not cancel its subscription before the stream has been terminated.

> In practice there is a difference between the guarantees that different publishers can provide for subscribers attached at different points in time. For example Publishers serving elements from a strict collection (“cold”) might guarantee that all subscribers see *exactly* the same elements (unless unsubscribed before completion) since they can replay the elements from the collection at any point in time. Other publishers might represent an ephemeral source of elements (e.g. a “hot” TCP stream) and keep only a limited output buffer to replay for future subscribers.

At any time the Publisher may signal that it is not able to provide more elements. This is done by invoking onComplete on its subscribers.
At any time the `Publisher` may signal that it is not able to provide more elements. This is done by invoking `onComplete` on its subscribers.

> For example a Publisher representing a strict collection signals completion to its subscriber after it provided all the elements. Now a later subscriber might still receive the whole collection before receiving onComplete.
> For example a `Publisher` representing a strict collection signals completion to its subscriber after it provided all the elements. Now a later subscriber might still receive the whole collection before receiving onComplete.

### API components ###
### Asynchronous vs Synchronous Processing ###

The purpose of the API is to provide the types that users interact with directly. SPI methods and interfaces should not be exposed expect for the purpose of writing Reactive Streams implementations.
The Reactive Streams API prescribes that all processing of elements (`onNext`) or termination signals (`onError`, `onComplete`) do not *block* the `Publisher`. Each of the `on*` handlers can process the events synchronously or asynchronously.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As noted above: please do not conflate too many things into one pull request. These changes require proper discussion and should not be mixed into a pull request that should only bring the README up to date on the API removal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how this sentence is conflating anything. It is derived from the discussions had with many people on the various issues and PRs this README has been through to get to this point. If it is holding up this entire PR I'll delete the entire sentence as it's not an important one. Your call.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I would prefer to leave the changes to “async vs. sync” out of this PR since we have an ongoing discussion about that aspect which will eventually result in a PR of its own. AFAICS these changes are not necessary to be done in haste because the current wording is not in conflict with the source code.


The API counterpart for Publisher is *Producer* and for Subscriber is *Consumer*. The combination of these two—a stream processing element with asynchronous input and output—is called *Processor*.
For example, this `onNext` implementation does synchronous transformation and enqueues the result for further asynchronous processing:

The only operation supported by any Producer–Consumer pair is their ability to establish a connection for the purpose of transferring the stream of elements from Producer to Consumer; this is achieved by the method `produceTo()`. Concrete implementations of Reactive Streams are expected to offer a rich set of combinators and transformations, but these are not the subject of this specification. The reason is that implementations shall have the freedom to formulate the end-user API in an idiomatic fashion for the respective platform, language and use-case they target.
```java
void onNext(T t) {
queue.offer(transform(t));
}
```

In addition there is one method each on Producer and Consumer to obtain a reference to the underlying Publisher or Subscriber, respectively. These are necessary for implementations, but is not to be considered end-user API.
In a push-based model such as this doing asynchronous processing, back-pressure needs to be provided otherwise buffer bloat can occur.

### Asynchronous processing ###
In contrast to communicating back-pressure by blocking the publisher, a non-blocking solution needs to communicate demand through a dedicated control channel. This channel is provided by the `Subscription`: the `Subscriber` controls the maximum amount of future elements it is willing receive by sending explicit demand tokens (by calling `request(int)`).

The Reactive Streams SPI prescribes that all processing of elements (onNext) or termination signals (onError, onComplete) happens outside of the execution stack of the Publisher. This is achieved by scheduling the processing to run asynchronously, possibly on a different thread. The Subscriber should make sure to minimize the amount of processing steps used to initiate this process, meaning that all its SPI-mandated methods shall return as quickly as possible.
Expanding on the `onNext` example above, as the queue is drained and processed asynchronously it would signal demand such as this:

In contrast to communicating back-pressure by blocking the publisher, a non-blocking solution needs to communicate demand through a dedicated control channel. This channel is provided by the Subscription: the subscriber controls the maximum amount of future elements it is willing receive by sending explicit demand tokens (by calling requestMore(int)).
```java
// TODO replace with fully functioning code example rather than this pseudo-code snippet
void process() {
eventLoop.schedule(() -> {
T t;
while((t = queue.poll()) != null) {
doWork(t);
if(queue.size() < THRESHOLD) {
subscription.request(queue.capacity());
}
}
})
}
```

#### Relationship to synchronous stream-processing ####

This document describes asynchronous, non-blocking backpressure boundaries but in between those boundaries any kind of synchronous stream processing model is permitted. This is useful for performance optimization (eliminating inter-thread synchronization) and it conveniently transports backpressure implicitly (the calling method cannot continue while the call lasts). As an example consider a section consisting of three connected Processors, A, B and C:
This document defines a protocol for asynchronous, non-blocking backpressure boundaries but in between those boundaries any kind of synchronous stream processing model is permitted. This is useful for performance optimization (eliminating inter-thread synchronization) and it conveniently transports backpressure implicitly (the calling method cannot continue while the call lasts). As an example consider a section consisting of three connected Processors, A, B and C:

(...) --> A[S1 --> S2] --> B[S3 --> S4 --> S5] --> C[S6] --> (...)

Expand Down