Blocking safe by default (Implementation Details)
As described in the section Blocking Safe By Default, ServiceTalk, by default, allows users to write blocking code when interacting with ServiceTalk. This document describes the details of the implementation and is addressed to audiences who intend to know the internals of how this is achieved.
It is not required to read this document if you just want to use ServiceTalk. |
Asynchronous Sources
Everything inside ServiceTalk is somehow connected to one of the three asynchronous sources, viz., Publisher
, Single
and Completable
. Since these sources are the building blocks for program control flow if they provide safety
guarantees for blocking code execution these guarantees apply outside the scope of preventing blocking code from
executing on an EventLoop thread. This approach is designed to make the task of ensuring we don’t block the EventLoop
threads less error-prone, and also allows for certain optimizations around thread context propagation and re-use.
Threads and asynchronous sources
An asynchronous source has two important decisions to make about thread usage:
-
Which thread or executor will be used to do the actual task related to a source. eg: for an HTTP client, the task is to send an HTTP request and read the HTTP response.
-
Which thread or executor will be used to interact with the
Subscriber
corresponding to its `Subscription`s.
Part 1. above is not governed by the
ReactiveStreams specification
and hence sources are free to use any thread. ServiceTalk typically will use Netty’s EventLoop
to perform the actual
task.
Part 2. defines all the interactions using the ReactiveStreams specifications, i.e. all methods in Publisher
,
Subscriber
and Subscription
. The ReactiveStreams specification requires that signals are not delivered
concurrently,
but doesn’t have any restrictions about which threads are used. This means the same thread maybe used for all signal
deliveries for a given Subscriber
, but it is also valid to use any thread (as long as no concurrency is introduced).
ServiceTalk concurrency APIs are used to define which executor will be used for an asynchronous source for Part 2,
which is typically an application Executor
.
Offloading and asynchronous sources
ServiceTalk uses the Executor
abstraction to specify the source of threads to be used for the delivery of signals from an asynchronous source. The
default signal offloading, if any, used by an asynchronous source is determined by the source. For example, the HTTP
sources, in addition to allowing for specification of an offloading executor, provide both direct control of the
offloading via
ExecutionStrategy
and may also influenced by the
computed execution strategy.
Applications with asynchronous, blocking, or computationally expensive tasks can also offload those tasks to specific
Executor
. The subscribeOn(Executor)
and publishOn(Executor)
operators will cause offloading execution from the
default signal delivery thread to a thread from the provided Executor
. The below diagram illustrates the interaction
between an asynchronous source, its Subscriber
, its operators, and the Executor
.
During Subscriber
method execution, the result publication and termination signals, the Executor
active at the
source is inherited by all operators unless there is a reason to switch to a different Executor
. The switch to another
executor, offloading, is done for a couple of reasons; unless configured to not offload ServiceTalk will offload from
the Netty EventLoop
thread as necessary in order to allow user code to block.
During subscribe()
the execution will offload at the subscribeOn()
operator and transition execution from the
subscribing thread to an Exeuctor
thread. The subscribing thread will be able to continue while the subscribe
operation asynchronously continues on an Executor
thread.
The diagram shows a typical case, when a result is available at the source it will begin publication on the receiving
Netty EventLoop thread. Assuming that the default ServiceTalk offloading has been disabled, then offloading will only
happen at the publishOn()
operator during Subscriber
signals and will transition execution from the EventLoop thread
to an Executor
thread. Once the Subscriber
signal is offloaded the EventLoop thread will be available again for
executing other I/O tasks while the response is asynchronously processed on the Executor
thread.