Reactive programming concept
The beginning of the 21st century was marked by the appearance of large systems which a huge number of users work with. Every second the number of requests to the system is calculated not in thousands, but millions. Have you ever thought about how well-known social networks, large banking systems, and popular trading platforms are implemented? Why do they quickly and reliably cope with a large number of requests to the system? In this article, you will learn about the principles that underlie the systems that never fail and are capable of serving billions of customers. Such systems are called reactive.
The characteristics that a reactive system should have are described in the reactive programming manifest, which was published in 2014.
Characteristics of a reactive system:
- Responsiveness.
- Sustainability.
- Elasticity.
- Focus on messaging.
Responsiveness
Responsiveness means that the system responds promptly to a user’s requests under any conditions. A continuous dialogue is maintained with a client through responsiveness and interactivity. This allows a user to work more productively and feel the reliability of the service.
The most common responsiveness threat to a system is when some part of the system is overloaded and cannot process user’s requests within certain time frames. Therefore, the user’s work with the service gets annoying and uncomfortable. As a result of this, some services, such as trading platforms, lose customers and profits.
The “Circuit Breaker” template can help solve this problem, with its help the availability and performance of the service is monitored and, if necessary, in a situation where the service has been in unstable state for a long time, a “Circuit Breaker” is triggered, switching the service to another operating mode that does not use this service.
For example, take a social network. On the profile page, in addition to information about the user, there is a list of published posts. Imagine an architecture where a separate microservice is responsible for the posts. When you enter a user’s page, a backend is requested to obtain information about the user and the list of posts. Imagine that the service which is responsible for publishing is out of order. Then the “Circuit Breaker” will switch the backend to the operating mode without using the microservice responsible for the posts. And it will return to the user only user’s information. Thus, we will avoid the situation when an error occurs when a user requests, due to the fact that the microservice responsible for posts is not available.
The result of systems responsiveness errors is loss of customers. Users interact more intensively with “responsive” applications, which leads to large volumes of purchases. In some areas, for example: medicine, the inability to process requests within certain time limits is equivalent to a complete system failure. Many applications quickly become useless if they stop meeting time requirements. For example, an application that performs trading operations may lose a current transaction if it does not manage to respond on time.
Sustainability
One of the most critical scenarios of the system work is application failure. While the service is not available, the company loses profit, in the long run this can lead to poor reputation and customer dissatisfaction, and this will harm business even more. It is important to note that the solution to the problem of the system failure should be worked out from the very beginning of the system architecture development.
The main solutions to the system failure problem are:
- Replication is storing of several copies of a system, its data or functionality in different places. Geographic location of replicas must be consistent with the scale of the system; for example, a global postal service should serve all customers from several countries.
- Separation and isolation means that in case of failure of one module or service which interaction takes place with, the system must be able to adapt to the operating mode without it.
Elasticity
A reactive application can be extended up to needed scale. This is achieved by giving the application elasticity, a property that allows the system to stretch or contract (add or remove nodes) on demand. In addition, this architecture makes it possible to expand or contract (deploy on more or less processors) without the need to redesign or rewrite the application. Elasticity minimizes the cost of functioning in the cloud, while we pay only for what we really use.
Scalability also helps manage risks: too little equipment can lead to customers’ dissatisfaction and loss, and too much will simply be inactive (along with staff) and lead to unnecessary costs. Also, a scalable application reduces the risk of a situation where equipment is available, but the application cannot use it.
An event-oriented system based on asynchronous messaging is the foundation of scalability. Weak connectivity and location independence of components and subsystems allow you to deploy the system on many nodes, while remaining within the same software model with the same semantics. Adding new nodes increases the system throughput. In terms of implementation, there should be no difference between deploying a system on more cores or more nodes in a cluster or data center.
Messaging focus
In a reactive application, the components interact with one other by sending and receiving messages, the discrete pieces of information describing the facts. These messages are sent and received in asynchronous and non-blocking mode. Event-oriented systems are more prone to push models than pull or poll. I.e. they push data to their consumers when data becomes available, rather than wasting resources by constantly requesting or waiting for data.
Reactive Programming Tools
After reviewing the concepts underlying reactive programming, it’s time to get to know the basic tools and language constructs for reactive systems implementing.
Green threads
Green threads are essentially a simulation of threads. The virtual machine takes care of switching between different green threads, and the machine itself works as a single OS thread. This has several advantages. OS threads are relatively expensive. In addition, switching between native threads is much slower than switching between green threads. This all means that in some situations, green threads are much more profitable than native threads. A system can support far more green than OS threads.
Green threads are asynchronous and non-blocking, but they do not support message passing. They do not support horizontal scaling to different network nodes. Also, they do not provide any mechanisms to ensure fault tolerance, therefore, developers are forced to independently implement the processing of potential failures.
CSP
Communicating Sequential Processes (CSP) is the mathematical encapsulation of several processes or threads in a single process that interact through a message. The uniqueness of CSP is that two processes or threads must not know anything about each other, they are perfectly separated in terms of sending and receiving messages, but are still connected in terms of transmitted value. Instead of accumulating messages in the queue, the rendezvous principle is applied: for message exchange one side must be ready to send it, and the other one to receive. Thus, message exchange is always synchronized.
The CSP model is asynchronous and non-blocking. It supports rendezvous style messaging and is scalable for multicore systems, however it does not support horizontal scaling. Also, it does not provide for fault tolerance mechanisms.
Future and Promise
Future is a reference to a value or error code that may get available (read only) at some point in time.
Promise is a single-write corresponding descriptor that provides access to value. Analogs of Future and Promise objects are implemented in most popular languages: Java, C#, Ruby, Python, etc.
The function that returns the result asynchronously, creates a Promise object, initializes asynchronous processing, sets up the resulting callback function, which at some point fills Promise with data, and returns Future object associated with Promise to the calling component. Then, the calling component can attach some code to Future that will be executed when values appear in this object.
All Future implementations provide a mechanism for turning a block of code into a new Future object so that it executes code passed to it in another thread and places the final result inside the associated Promise object. Thus, Future provides an easy way to make code asynchronous. Future objects return either the result of successful calculations or an error.
Future and Promise objects are asynchronous and non-blocking, but they do not support messaging. Nevertheless, they are able to scale vertically, but cannot horizontally. They also provide fault tolerance mechanisms at individual Future objects level.
Reactive Extensions
Reactive Extensions or Rx is a library, or rather a set of libraries that allow you to work with events and asynchronous calls in a compositional style. The library came from the .NET world. Then it was transported to popular platforms: Java, Ruby, JS, Python, etc. This library combines two thread control patterns: an observer and an iterator. Both are connected to handling a potentially unknown number of elements or events.
The point of Rx library is to write a cycling construct that responds to events. In its semantics, this is similar to threading, when incoming data is constantly processed by iteration.
Rx library provides tools for processing data threads in an asynchronous manner. Its current implementation is scaled vertically, but not horizontally. It does not provide mechanisms for delegating failure processing, but allows destroying a failed threading processing container.
The last tool for reactive programming is the actor model, which will be described in the next chapter using Akka as an example.
Actor model
The actor model was invented back in 1973 by Carl Hewitt. The main concept of the actor model is that the computational unit in the system is the “actor”. When using this model, all the design and implementation of the system is built around the point of the actor. Therefore, the concept of an actor model can be concisely expressed in the phrase: “Everything is an actor.”
The programming language built on the basis of the actor model is Erlang. The most popular frameworks implementing actor model are Akka, Orbit, Quasar. Further, in the article, we will consider the actor model using Akka framework as an example.
To make it clear that the actor is behind the entity, we turn to the parts which it consists of:
- State.
- Behavior.
- Mailbox.
- Child actors.
- Supervisor strategy.
State
Actor objects typically contain fields that reflect the state of the actor. These data constitute the value of the actor, and they must be protected from direct influence from other actors.
One of the concepts of actors in Akka is that there is no direct reference to an exemplar of an actor class – it is impossible to call an actor method. The only way to interact between actors is through asynchronous communication, which will be described later.
A significant advantage of actors follows from this in the development of reactive systems – there is no need to synchronize access to the actor using locks, because they are asynchronous de facto. Consequently, a developer writes logic of work of an actor, without worrying about concurrency problems at all.
Since the state is critically important to the actions of the actor, the presence of an inconsistent state is fatal. Thus, if an actor fails and supervisor restarts it, the state of the actor will be reset to its original state, as when the actor was first created. This allows to solve the problem of fault tolerance of the actor, allowing the system to self repair.
However, it is possible to configure an actor so that it can automatically recover to a state before restart.
Behavior
Behavior is a function that determines actions that should be taken in response to a message, for example, forward a request, if the client is authorized, reject it, etc.
This behavior may be changed over time, for example, because different clients receive authorization over time or because an actor can switch to “no service” mode. The behavior is changed depending on changes in state variables that are taken into account in the logic of actor’s work, and the function itself can be changed at runtime.
When the actor restarts, its behavior is reset to the initial one. However, with the help of configurations, behavior of an actor to a state before restart can be automatically restored.
Mailbox
The goal of an actor is to process messages that were sent from other actors within the system or message that come from external systems. The element that connects the sender and the recipient is the actor’s mailbox: each actor has exactly one mailbox where senders put their messages to. Due to the fact that actor exemplars can be executed on several threads at the same time, it might seem that messages do not have a specific processing order, but this is not so. Sending multiple messages from one source to one specific actor will put them in the queue in the same order.
There are different options for mailboxes implementations, it is FIFO by default: the order of messages processed by the actor corresponds to the order which they were queued in. In most cases, this is the best choice, but applications may need to prioritize some messages over others.
The algorithm of placing messages into the mailbox can be configured and queued depending on the priority of the messages or using another custom algorithm. When using such a queue, the order of processed messages will, of course, be determined by the queue algorithm, and it will be no FIFO at all.
An important feature which differs Akka from some other implementations of the actor model is that the current behavior of the actor must always process the next message from the queue. But, when configured out of the box, validation (whether the actor with the current behavior can process the incoming message) does not occur. Denial to process a message is usually considered a failure.
Child Actors
Each actor is potentially a supervisor: if it creates child actors to delegate subtasks, it automatically controls them. The list of subsidiary actors is maintained in the context of the actor (it is worth noting that the second generation descendants, “grandchildren”, aren’t directly accessible) and is available to him. Changes in the list are made by creating (context.actorOf (…)) or stopping (context.stop (child)) child actors. In fact, actions to create and terminate child actors occur asynchronously, so they do not block their supervisor.
Supervisor strategy
The last part of actor is the strategy of handling of failures in subsidiary actors. The standard strategies for a failure in a child actor are:
- resume the work of a subordinate actor, save his condition;
- resume the work of a subordinate actor, restore its standard state;
- stop the work of a subordinate actor;
- pass the failure up (not recommended, used in exceptional situations).
Since this strategy is conceptually important for the actor and his subsidiary actors, it cannot be changed after actor creation.
Taking into account that for each actor there is only one such strategy, this means, if there is a need to apply different strategies for subordinate actors, they should be grouped under intermediate supervisor actors with appropriate strategies. It is good practice to restructure actor systems in accordance with the division of tasks into subtasks.
Akka is asynchronous, non-blocking and it supports messaging. It is scaled both vertically and horizontally (via Akka Distributed Data). To support fault tolerance, they have tracking mechanisms. It meets all the requirements for reactive system’s creation.
Work with actors in Akka
For following examples, you need to connect Akka-actor library. We’ll do it using maven:
<dependencies>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-actor_2.12</artifactId>
<version>2.5.16</version>
</dependency>
</dependencies>
The standard way to create an actor in Akka is to implement the AbstractActor interface. There are also other types of interfaces for creating an actor, such as TypedActor or UntypedActor.
UntypedActor provides a standard interface for processing messages using onReceive method. TypedActor must first describe an interaction interface, and then implement it using an actor.
To describe actor behavior, you need to implement receiveBuilder method () .
Actor example:
import akka.actor.AbstractActor;
import akka.actor.ActorRef;
import akka.actor.Props;
import com.lightbend.akka.sample.Printer.Greeting;
public class Greeter extends AbstractActor {
static public Props props(String message, ActorRef printerActor) {
return Props.create(Greeter.class, () -> new Greeter(message, printerActor));
}
static public class WhoToGreet {
public final String who;
public WhoToGreet(String who) {
this.who = who;
}
}
static public class Greet {
public Greet() {
}
}
private final String message;
private final ActorRef printerActor;
private String greeting = "";
public Greeter(String message, ActorRef printerActor) {
this.message = message;
this.printerActor = printerActor;
}
@Override
public Receive createReceive() {
return receiveBuilder()
.match(WhoToGreet.class, wtg -> {
this.greeting = message + ", " + wtg.who;
})
.match(Greet.class, x -> {
printerActor.tell(new Greeting(greeting), getSelf());
})
.build();
}
}
The prop’s method is a factory method and is responsible for creating an actor exemplar.
Props is a configuration class that specifies parameters for actors creating. It is recommended to use this class to build an actor.
This example passes parameters that the actor needs while building. The first parameter is the message that will be used while creating greeting messages. The second parameter is an ActorRef object, which is a reference to the actor that processes the output of the greeting.
CreateReceiver method defines behavior: how an actor should respond to various messages that it receives. Access or change of internal state of an actor is completely protected by thread. CreateReceive method should process the messages that user expects. In the case of Greeter, it expects two types of messages: WhoToGreet and Greet. The first type of messages will update actor state, the latter will send a greeting message to printerActor actor.
It is impossible to create an actor object using new in Akka. Instead, an ActorRef object is created using factory method system.actorOf, which is used to interact with the created actor. This method takes 2 arguments: a Prop’s configuration object and an actor name.
An example of an actor creating:
final ActorRef printerActor =
system.actorOf(Printer.props(), "printerActor");
final ActorRef howdyGreeter =
system.actorOf(Greeter.props("Howdy", printerActor), "howdyGreeter");
final ActorRef helloGreeter =
system.actorOf(Greeter.props("Hello", printerActor),"helloGreeter");
final ActorRef goodDayGreeter =
system.actorOf(Greeter.props("hi", printerActor),"hiGreeter");
Actors respond and are controlled by messages and do nothing until they receive a message. This ensures that a sender will not wait until his message has been processed by a recipient. Instead, a sender places a message in a recipient’s mailbox and is free to do other work.
Perhaps you are wondering what an actor does when it is not processing messages? It is in a state of suspension when it doesn’t consume any resources but memory. Showing effective nature of actors again.
To send a message to an actor, tell method in ActorRef is used.
An example of sending a message to an actor: the first argument is an object to send, the second argument is a sender’s ActorRef (a link to a sender, which can be answered if necessary).
howdyGreeter.tell(new WhoToGreet("Akka"), ActorRef.noSender());
howdyGreeter.tell(new Greet(), ActorRef.noSender());
Conclusion
This article serves as a brief excursion into the world of reactive programming, allowing you to verify its advantages, relevance and being demand in the modern world of development. Not every system must meet these standards. But if in the future your system has to process a huge number of requests, then the concepts that underlie the foundation of reactive programming should be taken into account at the initial stage of system design.