Reactive Streams in Java: Using Project Reactor
Tags: reactivestreams, projectreactor, javareactive, eventdriven
Published: 2024-06-19 22:17:43

### Introduction

In the world of modern application development, the ability to handle asynchronous data streams efficiently is critical. Reactive Streams provide a powerful approach to managing this, and Project Reactor, a library for building non-blocking applications on the JVM, is at the forefront of this paradigm. This article will delve into the concepts of Reactive Streams, explore Project Reactor in depth, and provide practical examples to help you harness the power of asynchronous data streams in Java.

### Understanding Reactive Streams

Reactive Streams is a standard for asynchronous stream processing with non-blocking backpressure. The main goal is to enable the development of reactive applications by providing a common API for handling asynchronous data streams.

### How Does the Server Make It Possible for Reactive Streams to Work?

Reactive Streams is a specification for asynchronous stream processing with non-blocking backpressure. Servers that support reactive streams enable this functionality by adhering to principles and mechanisms that manage data flow efficiently. Here’s a detailed breakdown of how servers facilitate the operation of reactive streams:

#### 1. Non-Blocking I/O

**Non-blocking I/O (Input/Output)** is a fundamental aspect that enables reactive streams. In a non-blocking I/O model, server threads do not get blocked while waiting for I/O operations (like reading from a network socket) to complete. Instead, the server can continue processing other tasks while waiting for the I/O operation to finish.

**Implementation in Servers**:
- **Netty**: An asynchronous event-driven network application framework for Java that provides non-blocking I/O operations.
- **Vert.x**: A toolkit for building reactive applications on the JVM, utilizing non-blocking I/O for handling a large number of connections with minimal threads.

#### 2. Event Loop Model

The event loop model allows a server to manage numerous connections concurrently with a small number of threads. This is achieved by continuously polling for events and dispatching them to the appropriate handlers.

**Key Components**:
- **Event Loop**: A loop that listens for and dispatches events or messages in a program.
- **Callbacks**: Functions that get called when an event occurs (e.g., data is received, a connection is established).

![Netty Event loop](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/awk3c68oioog52kk3h1f.png)

**Example**: Node.js, which uses the libuv library to implement an event-driven, non-blocking I/O model.

#### 3. Backpressure Management

Backpressure is the mechanism by which a system regulates the flow of data between producers and consumers. In a reactive stream, consumers can signal how much data they can handle, and producers must respect these signals to prevent overwhelming the consumers.

**Backpressure Mechanisms**:
- **Request-N Method**: Consumers request a specific number of items from the producer.
- **Buffers**: Temporarily store data when the producer is faster than the consumer.

**Implementation in Project Reactor**:
- **BaseSubscriber**: A base class for implementing backpressure-aware subscribers in Reactor.
- **Buffering Operators**: Operators like `buffer`, `window`, and `onBackpressureBuffer` help manage backpressure by controlling data flow.

#### 4. Asynchronous Processing

Asynchronous processing ensures that tasks can be executed without waiting for other tasks to complete, thus making efficient use of system resources and improving responsiveness.

**Reactive Programming Libraries**:
- **Reactor**: Provides abstractions like `Mono` and `Flux` for asynchronous programming in Java.
- **RxJava**: A Java implementation of Reactive Extensions, providing a similar model for asynchronous data streams.

#### 5. Scheduler Management

Schedulers in reactive programming control the execution context of the data streams, determining on which thread or thread pool the tasks will run.

**Types of Schedulers**:
- **Immediate Scheduler**: Executes tasks immediately on the current thread.
- **Single Scheduler**: Runs tasks on a single dedicated thread.
- **Elastic Scheduler**: Dynamically creates threads as needed and reuses idle threads.
- **Parallel Scheduler**: Uses a fixed pool of workers for parallel processing.

**Usage in Project Reactor**:
- `publishOn` and `subscribeOn` operators to switch the execution context.

#### Example Code: Non-Blocking I/O with Project Reactor

Here’s a basic example demonstrating non-blocking I/O with Project Reactor:

```java
import reactor.core.publisher.Flux;
import reactor.netty.http.server.HttpServer;

public class ReactiveServer {
    public static void main(String[] args) {
        HttpServer.create()
                  .port(8080)
                  .route(routes ->
                      routes.get("/stream", (request, response) ->
                          response.sendString(Flux.interval(Duration.ofSeconds(1))
                                                   .map(i -> "Data chunk " + i + "\n"))))
                  .bindNow()
                  .onDispose()
                  .block();
    }
}
```

In this example:
- **Netty-based HTTP Server**: Uses Reactor Netty to create an HTTP server.
- **Reactive Route**: Defines a route `/stream` that streams data chunks at one-second intervals.
- **Non-Blocking Data Flow**: The data is emitted asynchronously using `Flux.interval`.

#### Core Concepts of Reactive Streams

- **Publisher**: Produces a potentially unbounded number of sequenced elements, publishing them according to the demand received from its Subscriber(s).
- **Subscriber**: Consumes elements produced by the Publisher, receiving notifications of new data, errors, or completion.
- **Subscription**: Represents a one-to-one lifecycle of a Subscriber subscribing to a Publisher.
- **Processor**: A component that acts as both a Subscriber and a Publisher, often used to transform data between the source and the final Subscriber.

#### Benefits of Reactive Streams

- **Non-blocking**: Handles requests asynchronously without blocking threads.
- **Backpressure**: Manages flow control by allowing subscribers to dictate how much data they can handle.
- **Composability**: Easily combine multiple streams to build complex asynchronous data pipelines.
- **Error Handling**: Built-in mechanisms to manage errors gracefully.

### Project Reactor

Project Reactor is a fully non-blocking foundation for building reactive applications on the JVM. It is based on the Reactive Streams specification and provides a rich set of operators for composing asynchronous and event-driven programs.

#### Key Components of Project Reactor

- **Flux**: A reactive sequence that can emit zero to many elements.
- **Mono**: A reactive sequence that emits zero or one element.
- **Schedulers**: Control the execution context of reactive streams, allowing fine-grained control over concurrency and parallelism.

### Getting Started with Project Reactor

To start using Project Reactor, include the following dependencies in your `pom.xml` for Maven:

```xml
<dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-core</artifactId>
    <version>3.4.8</version>
</dependency>
```

For Gradle, add:

```groovy
implementation 'io.projectreactor:reactor-core:3.4.8'
```

![Flux Mono streams mindmap](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ec4x371r3155pky9oyhg.png)


#### Creating a Flux

A `Flux` can emit multiple items, complete, or signal an error. Here's a simple example of creating and subscribing to a `Flux`:

```java
import reactor.core.publisher.Flux;

public class FluxExample {
    public static void main(String[] args) {
        Flux<String> flux = Flux.just("Hello", "World", "From", "Project", "Reactor");

        flux.subscribe(System.out::println,
                       error -> System.err.println("Error: " + error),
                       () -> System.out.println("Completed"));
    }
}
```

#### Creating a Mono

A `Mono` represents a single-valued or empty result. Here's an example of creating and subscribing to a `Mono`:

```java
import reactor.core.publisher.Mono;

public class MonoExample {
    public static void main(String[] args) {
        Mono<String> mono = Mono.just("Hello Mono");

        mono.subscribe(System.out::println,
                       error -> System.err.println("Error: " + error),
                       () -> System.out.println("Completed"));
    }
}
```

### Advanced Features of Project Reactor

#### Combining Streams

Project Reactor provides powerful operators for combining multiple streams. Here’s an example using `merge` and `zip`:

```java
import reactor.core.publisher.Flux;

public class CombiningStreamsExample {
    public static void main(String[] args) {
        Flux<String> flux1 = Flux.just("A", "B", "C");
        Flux<String> flux2 = Flux.just("1", "2", "3");

        // Merge two fluxes
        Flux<String> mergedFlux = Flux.merge(flux1, flux2);
        mergedFlux.subscribe(System.out::println);

        // Zip two fluxes
        Flux<String> zippedFlux = Flux.zip(flux1, flux2, (s1, s2) -> s1 + s2);
        zippedFlux.subscribe(System.out::println);
    }
}
```
### Backpressure Handling in Project Reactor

Backpressure is a critical concept in reactive programming that deals with controlling the flow of data between a producer (which emits data) and a consumer (which processes data) to prevent the consumer from being overwhelmed by the producer. Project Reactor provides several mechanisms to handle backpressure effectively:

#### Backpressure Strategies

**Buffering**: Collects all emitted items in a buffer until the consumer is ready to process them. This can be done using operators like `onBackpressureBuffer`.

   ```java
   Flux.range(1, 100)
       .onBackpressureBuffer(10)
       .subscribe(System.out::println);
   ```

   In this example, `onBackpressureBuffer(10)` specifies a buffer size of 10. If the buffer is full, the producer will be paused until space is available.

**Dropping**: Drops items if the consumer cannot keep up. This can be achieved using `onBackpressureDrop`.

   ```java
   Flux.range(1, 100)
       .onBackpressureDrop(item -> System.out.println("Dropped: " + item))
       .subscribe(System.out::println);
   ```

   Here, items that cannot be processed immediately are dropped, and a message is printed for each dropped item.

**Latest**: Keeps only the latest item, discarding the rest until the consumer is ready to process the next item. Use `onBackpressureLatest` to achieve this.

   ```java
   Flux.range(1, 100)
       .onBackpressureLatest()
       .subscribe(System.out::println);
   ```

   This approach ensures that the consumer always processes the most recent item, discarding intermediate items if necessary.

**Error**: Propagates an error if the producer cannot emit items due to backpressure. This can be handled using `onBackpressureError`.

   ```java
   Flux.range(1, 100)
       .onBackpressureError()
       .subscribe(
           System.out::println,
           error -> System.err.println("Error: " + error)
       );
   ```

   When the buffer is full, this method throws an error, which can be handled by the subscriber.

#### Backpressure Handling with `BaseSubscriber`

Project Reactor’s `BaseSubscriber` class allows fine-grained control over backpressure by explicitly requesting items.

```java
import reactor.core.publisher.BaseSubscriber;
import reactor.core.publisher.Flux;

public class BackpressureExample {
    public static void main(String[] args) {
        Flux.range(1, 100)
            .subscribe(new BaseSubscriber<Integer>() {
                @Override
                protected void hookOnSubscribe(Subscription subscription) {
                    request(1); // Request the first item
                }

                @Override
                protected void hookOnNext(Integer value) {
                    System.out.println(value);
                    request(1); // Request the next item after processing the current one
                }
            });
    }
}
```

In this example:
- The `hookOnSubscribe` method requests the first item.
- The `hookOnNext` method processes each item and then requests the next one, effectively controlling the flow of items.

### Error Handling in Project Reactor

Error handling is an essential aspect of reactive programming, allowing applications to manage and recover from failures gracefully. Project Reactor provides several mechanisms for error handling:

#### Error Handling Operators

**onErrorReturn**: Provides a fallback value if an error occurs.

   ```java
   Flux<Integer> flux = Flux.just(1, 2, 0, 4)
                            .map(i -> 10 / i)
                            .onErrorReturn(-1);

   flux.subscribe(System.out::println);
   ```

   In this example, if a division by zero occurs, the `onErrorReturn` operator provides `-1` as a fallback value.

**onErrorResume**: Switches to an alternative `Publisher` when an error occurs.

   ```java
   Flux<Integer> flux = Flux.just(1, 2, 0, 4)
                            .map(i -> 10 / i)
                            .onErrorResume(e -> Flux.just(-1, -2, -3));

   flux.subscribe(System.out::println);
   ```

   If an error occurs, the `onErrorResume` operator switches to an alternative `Flux` emitting `-1`, `-2`, and `-3`.

**onErrorMap**: Transforms the error into another exception.

   ```java
   Flux<Integer> flux = Flux.just(1, 2, 0, 4)
                            .map(i -> 10 / i)
                            .onErrorMap(e -> new RuntimeException("Custom exception: " + e.getMessage()));

   flux.subscribe(System.out::println, error -> System.err.println("Error: " + error));
   ```

   This example converts the original exception into a custom `RuntimeException` with a detailed message.

**retry**: Retries the sequence when an error occurs.

   ```java
   Flux<Integer> flux = Flux.just(1, 2, 0, 4)
                            .map(i -> 10 / i)
                            .retry(1); // Retry once

   flux.subscribe(System.out::println, error -> System.err.println("Error: " + error));
   ```

   The `retry` operator retries the sequence once if an error occurs. You can specify the number of retries.

#### Global Error Handling

For more centralized error handling, you can use the `doOnError` method to log errors or perform other actions globally.

```java
Flux<Integer> flux = Flux.just(1, 2, 0, 4)
                         .map(i -> 10 / i)
                         .doOnError(error -> System.err.println("Error occurred: " + error.getMessage()))
                         .onErrorReturn(-1);

flux.subscribe(System.out::println);
```

In this example, `doOnError` logs the error message before the `onErrorReturn` operator provides a fallback value.

### Use Cases of Reactive Streams with Project Reactor

#### Real-time Data Processing

Project Reactor is ideal for real-time data processing scenarios, such as financial tickers, live sports scores, or social media feeds. 

**Example**: A live stock ticker application that processes and displays stock prices in real-time.

```java
import reactor.core.publisher.Flux;

import java.time.Duration;

public class StockTicker {
    public static void main(String[] args) {
        Flux.interval(Duration.ofSeconds(1))
            .map(tick -> "Stock price at tick " + tick + ": " + (100 + Math.random() * 10))
            .subscribe(System.out::println);
    }
}
```

#### Microservices Communication

Reactive Streams facilitate efficient communication between microservices. Project Reactor can be used to build non-blocking REST APIs and manage inter-service communication.

**Example**: A service that aggregates data from multiple microservices and provides a consolidated response.

```java
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

public class AggregatorService {
    private final WebClient webClient = WebClient.create();

    public Mono<String> aggregateData() {
        Mono<String> service1 = webClient.get().uri("http://service1/data").retrieve().bodyToMono(String.class);
        Mono<String> service2 = webClient.get().uri("http://service2/data").retrieve().bodyToMono(String.class);

        return Mono.zip(service1, service2, (data1, data2) -> "Combined Data: " + data1 + ", " + data2);
    }

    public static void main(String[] args) {
        AggregatorService service = new AggregatorService();
        service.aggregateData().subscribe(System.out::println);
    }
}
```

### Conclusion

Reactive Streams and Project Reactor offer a robust framework for handling asynchronous data streams in Java. By leveraging these technologies, developers can build highly responsive, resilient, and scalable applications. Whether you are dealing with real-time data processing, microservices communication, or complex event-driven systems, Project Reactor provides the tools and abstractions needed to succeed.