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:

  1. 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.

  2. 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.

Offloading

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.