Reactive Streams

16/12/2020

Reactive Manifesto

Reactive Manifesto

RESPONSIVE

Responsive systems focus on providing rapid and consistent response times

RESPONSIVE

Responsive systems focus on providing rapid and consistent response times

Reactive Manifesto

ELASTIC

RESPONSIVE

Responsive systems focus on providing rapid and consistent response times

RESPONSIVE

Responsive systems focus on providing rapid and consistent response times

Reactive systems can react to changes and stay responsive under varying workload

Reactive Manifesto

ELASTIC

RESPONSIVE

Responsive systems focus on providing rapid and consistent response times

In case the system faces any failure, the system should stay responsive

RESPONSIVE

Responsive systems focus on providing rapid and consistent response times

Reactive systems can react to changes and stay responsive under varying workload

RESILIENT

Reactive Manifesto

ELASTIC

RESPONSIVE

Responsive systems focus on providing rapid and consistent response times

In case the system faces any failure, the system should stay responsive

RESPONSIVE

Responsive systems focus on providing rapid and consistent response times

Reactive systems can react to changes and stay responsive under varying workload

RESILIENT

MESSAGE DRIVEN

Message passing between systems ensure loose coupling and isolation

JDK9 java.util.concurrent.Flow

asynchronous stream processing with non-blocking backpressure

Asynchronous ≠ Non-Blocking

Asynchronous

Jean-Mich ask something to  Didier, Didier is not able to answer now  but Didier will call him back when he will have the answer

Non-Blocking

Jean-Mich ask something to Didier, Didier is not able to answer. Jean-Mich has to call back Didier some time after to get the response.

Backpressure

Backpressure

Backpressure

Problem : When the Consumer cannot handle the throughput provided by the Publisher

 

 

Backpressure

=

Subscriber can control the Publisher throughput

JDK9 java.util.concurrent.Flow

A Subscriber subscribe to a Publisher

3 interfaces :

  • Publisher
  • Subscriber
  • Suscription

Concurrency-agnostic

JDK9 java.util.concurrent.Flow


public interface Publisher<T> {
    void subscribe(Subscriber<? super T> s);
}


public interface Subscriber<T> {

    void onSubscribe(Subscription s);

    void onNext(T t);

    void onError(Throwable t);

    void onComplete();
}


public interface Subscription {

    public void request(long n);

    public void cancel();
}

Implement Flow API

Array Publisher

new ArrayPublisher<>(List.of("Pikachu","Salameche","Bulbizarre","Carapuce"))
		.subscribe(new Flow.Subscriber<>() {...});
    
    
/*
01:02:14.367 [main] INFO com.tahitiste.reactive.ArrayPublisherTest - onNext : Pikachu
01:02:14.370 [main] INFO com.tahitiste.reactive.ArrayPublisherTest - onNext : Salameche
01:02:14.370 [main] INFO com.tahitiste.reactive.ArrayPublisherTest - onNext : Bulbizarre
01:02:14.370 [main] INFO com.tahitiste.reactive.ArrayPublisherTest - onNext : Carapuce
01:02:14.370 [main] INFO com.tahitiste.reactive.ArrayPublisherTest - Complete !
*/

Implement Flow APi

Interval Publisher

new ColdIntervalPublisher(Duration.ofSeconds(1)).subscribe(new Flow.Subscriber<>() {...});

/* prints :
01:05:01.256 [pool-1-thread-1] INFO com.tahitiste.reactive.IntervalPublisherTest - tick received
01:05:02.252 [pool-1-thread-1] INFO com.tahitiste.reactive.IntervalPublisherTest - tick received
01:05:03.252 [pool-1-thread-1] INFO com.tahitiste.reactive.IntervalPublisherTest - tick received
01:05:04.252 [pool-1-thread-1] INFO com.tahitiste.reactive.IntervalPublisherTest - tick received
01:05:05.252 [pool-1-thread-1] INFO com.tahitiste.reactive.IntervalPublisherTest - tick received
01:05:06.252 [pool-1-thread-1] INFO com.tahitiste.reactive.IntervalPublisherTest - tick received
01:05:07.252 [pool-1-thread-1] INFO com.tahitiste.reactive.IntervalPublisherTest - tick received
*/

Improve Flow APi

Map/Filter operators

new ArrayPublisher<>("Alex","Manu","Yunus","Sylvain")
                .filter(this::isMeilleurCoderDeFrance)
                .subscribe(new Flow.Subscriber<>() {...});
 new ArrayPublisher<>(List.of("ho", "hi", "ca", "cu", "pi"))
                .map(value -> value + value)
                .subscribe(new Flow.Subscriber<String>() {...});

Flow API implementation

Project Reactor

Project Reactor

  • Maintained by Pivotal, firm behind Spring
  • Reactor Netty for TCP/HTTP (Server/Client)
  • Used by Spring in its reactive stack :
    • Spring WebFlux (equivalent of Spring MVC)
    • Spring Data (Mongo,Redis,R2DBC)
  • Reactor for Kafka

java.Flow => Project Reactor

java.util.concurrent.Flow reactor.core
N values Publisher Flow
1 value Publisher Mono

No need to implement Subscriber/Subscription

java.Flow => Project Reactor

  • Composability and readability

  • Data as a flow manipulated with a rich vocabulary of operators

  • Nothing happens until you subscribe

  • Backpressure or the ability for the consumer to signal the producer that the rate of emission is too high

  • High level but high value abstraction that is concurrency-agnostic

Project Reactor : construct

Text

Mono<String> emptyMono = Mono.empty();
Flux<String> emptyFlux = Flux.empty();

Mono<String> mono = Mono.just("Bioserenity");
Flux<String> flux = Flux.just("PSG", "Barca", "Liverpool");

Flux.range(1, 100); // from 1 to 100

Flux.fromStream(Stream.of("PSG", "Barca", "Liverpool"));
Flux.fromIterable(List.of("PSG", "Barca", "Liverpool"));

Mono<String> stringMono = Mono.fromCallable(() -> "hello");
Mono<Void> voidMono = Mono.fromRunnable(() -> System.out.println("Hello"));

Mono<String> completableFutureMono = Mono.fromFuture(new CompletableFuture<>());

Project Reactor : transform/combine

flux.map(...) 
mono.map(...)

flux.filter(...)
mono.filter(...)
        
        

Demo in previous implementation

Project Reactor : transform/combine

flux.map(...) 
mono.map(...)

flux.filter(...)
mono.filter(...)
        
        

Project Reactor : transform/combine

flux.map(...) 
mono.map(...)

flux.filter(...)
mono.filter(...)
        
        
Flux<Event> eventList;

Mono<List<Event>> collectedEventList = eventList.collectList();

Project Reactor : transform/combine

flux.map(...) 
mono.map(...)

flux.filter(...)
mono.filter(...)
        
        
Flux<Event> bffEvents = getBffEvents();
Flux<Event> mortaraEvents = getMortaraEvents();

Flux<Event> mergedEvents = Flux.merge(bffEvents,mortaraEvents);
Flux<Event> eventList;

Mono<List<Event>> collectedEventList = eventList.collectList();

Project Reactor : transform/combine

flux.map(...) 
mono.map(...)

flux.filter(...)
mono.filter(...)
        
        
Flux<User> userListToSave;
Flux<SavedUser> saveUserList = userListToSave.flatMap(user -> saveUser(user));


Mono<User> user = getUser(userId)
Flux<Record> userRecordList = user.flatMapMany(user -> getRecords(user));
Flux<Event> bffEvents = getBffEvents();
Flux<Event> mortaraEvents = getMortaraEvents();

Flux<Event> mergedEvents = Flux.merge(bffEvents,mortaraEvents);
Flux<Event> eventList;

Mono<List<Event>> collectedEventList = eventList.collectList();

Project Reactor : Subscribe

 

Flux.just(1, 2, 3).subscribe(new Subscriber<Integer>() {
    @Override
    public void onSubscribe(Subscription subscription) {
        subscription.request(Long.MAX_VALUE);
    }

    @Override
    public void onNext(Integer integer) {
        LOGGER.info("{}", integer);
    }

    @Override
    public void onError(Throwable t) {
    }

    @Override
    public void onComplete() {
        LOGGER.info("complete ! ");
    }
});

Project Reactor : Subscribe

 

Flux.just(1, 2, 3).subscribe(new Subscriber<Integer>() {
    @Override
    public void onSubscribe(Subscription subscription) {
        subscription.request(Long.MAX_VALUE);
    }

    @Override
    public void onNext(Integer integer) {
        LOGGER.info("{}", integer);
    }

    @Override
    public void onError(Throwable t) {
    }

    @Override
    public void onComplete() {
        LOGGER.info("complete ! ");
    }
});
Flux.just(1, 2, 3).subscribe(integer -> LOGGER.info("{}", integer));

Flux.just(1, 2, 3).subscribe(
        integer -> LOGGER.info("{}", integer),
        error -> LOGGER.error("{}", error),
        () -> LOGGER.info("complete !")
);

Project Reactor :

do side effects

LOGGER.info("Start");

Flux.just(1, 2, 3)
        .doOnNext(integer -> LOGGER.info("side effect : {}", integer))
        .doFirst(() -> LOGGER.info("first side effect"))
        .doOnComplete(() -> LOGGER.info("complete "))
        .subscribe();
        
/* LOGS
16:02:43.472 [main] INFO com.tahitiste.reactive.FluxTest - Start
16:02:43.551 [main] INFO com.tahitiste.reactive.FluxTest - first side effect
16:02:43.553 [main] INFO com.tahitiste.reactive.FluxTest - side effect : 1
16:02:43.554 [main] INFO com.tahitiste.reactive.FluxTest - side effect : 2
16:02:43.554 [main] INFO com.tahitiste.reactive.FluxTest - side effect : 3
16:02:43.554 [main] INFO com.tahitiste.reactive.FluxTest - complete 
*/

Simple example :

Create a load test

int numberOfRequestToRun = 1000;
int numberOfConcurrentRequest = 100;

Flux<Mono<String>> requests = Flux
        .range(0, numberOfRequestToRun)
        .map(unused -> httpClient // <= Netty reactive HTTPClient
                .get()
                .uri("uri")
                .responseContent()
                .aggregate()
                .asString()
        );

// we run concurrently
Flux<String> responseFlux = Flux.merge(requests, numberOfConcurrentRequest);

// 
List<String> responses = responseFlux.collectList().block();

Concurrency

JDK Flow / Reactor

are

concurrency-agnostic

LOGGER.info("Start");
Flux.just(1, 2, 3, 4, 5).subscribe(integer -> LOGGER.info("{}", integer));

/* logs : 
15:55:30.361 [main] INFO com.tahitiste.reactive.FluxConstruction - Start
15:55:30.461 [main] INFO com.tahitiste.reactive.FluxConstruction - 1
15:55:30.462 [main] INFO com.tahitiste.reactive.FluxConstruction - 2
15:55:30.462 [main] INFO com.tahitiste.reactive.FluxConstruction - 3
15:55:30.462 [main] INFO com.tahitiste.reactive.FluxConstruction - 4
15:55:30.462 [main] INFO com.tahitiste.reactive.FluxConstruction - 5
*/

Concurrency

JDK Flow / Reactor

are

concurrency-agnostic

Concurrency

But Reactor has some helpers : block()

 

LOGGER.info("Start");

String value = Mono.just("hello").block();

LOGGER.info("{}", value);

int lastValue = Flux.just(1, 2, 3).blockLast();

LOGGER.info("{}", lastValue);

int firstValue = Flux.just(1, 2, 3).blockFirst();

LOGGER.info("{}", firstValue);

/* LOGS : 
16:13:13.077 [main] INFO com.tahitiste.reactive.FluxTest - Start
16:13:13.126 [main] INFO com.tahitiste.reactive.FluxTest - hello
16:13:13.191 [main] INFO com.tahitiste.reactive.FluxTest - 3
16:13:13.191 [main] INFO com.tahitiste.reactive.FluxTest - 1
*/

// Its executed on the same thread ! 

Concurrency

But Reactor has some helpers : parallel()

 

LOGGER.info("start main");
Flux.range(1, 5)
        .parallel(3)
        .runOn(Schedulers.parallel())
        .subscribe(i -> LOGGER.info("{}", i));

LOGGER.info("end main");

Thread.sleep(1000);

/* LOGS
16:49:03.333 [main] INFO com.tahitiste.reactive.FluxTest - start main
16:49:03.451 [main] INFO com.tahitiste.reactive.FluxTest - end main
16:49:03.451 [parallel-1] INFO com.tahitiste.reactive.FluxTest - 1
16:49:03.451 [parallel-2] INFO com.tahitiste.reactive.FluxTest - 2
16:49:03.451 [parallel-3] INFO com.tahitiste.reactive.FluxTest - 3
16:49:03.454 [parallel-2] INFO com.tahitiste.reactive.FluxTest - 5
16:49:03.454 [parallel-1] INFO com.tahitiste.reactive.FluxTest - 4

Concurrency

But Reactor has some helpers : subscribeOn()

 

Flux.just(1, 2, 3)
        .subscribeOn(Schedulers.newSingle("executor"))
        .subscribe(value -> LOGGER.info("{}", value));
        
        
/* LOGS : 
16:15:11.833 [main] INFO com.tahitiste.reactive.FluxTest - Start
16:15:11.914 [main] DEBUG reactor.util.Loggers - Using Slf4j logging framework
16:15:11.932 [executor-1] INFO com.tahitiste.reactive.FluxTest - 1
16:15:11.934 [executor-1] INFO com.tahitiste.reactive.FluxTest - 2
16:15:11.934 [executor-1] INFO com.tahitiste.reactive.FluxTest - 3
*/

// Its executed on the executor thread ! 

When you want to subscribe on another thread

Concurrency

But Reactor has some helpers : publishOn()

 

Flux.just(1, 2, 3)
        .publishOn(Schedulers.newSingle("executor"))
        .subscribe(value -> LOGGER.info("{}", value));
        
        
/* LOGS : 
16:15:11.833 [main] INFO com.tahitiste.reactive.FluxTest - Start
16:15:11.914 [main] DEBUG reactor.util.Loggers - Using Slf4j logging framework
16:15:11.932 [executor-1] INFO com.tahitiste.reactive.FluxTest - 1
16:15:11.934 [executor-1] INFO com.tahitiste.reactive.FluxTest - 2
16:15:11.934 [executor-1] INFO com.tahitiste.reactive.FluxTest - 3
*/

// Its executed on the executor thread ! 

When you want to publish on another thread

Concurrency

Blocking calls => warning !

 

LOGGER.info("Start");

Mono<String> blockingCall = Mono.fromRunnable(() -> {
    try {
        Thread.sleep(1000);
        LOGGER.info("end of sleep");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

blockingCall.subscribe();

LOGGER.info("sleeping");

/* LOGS 

16:30:51.377 [main] INFO com.tahitiste.reactive.FluxTest - Start
16:30:52.427 [main] INFO com.tahitiste.reactive.FluxTest - end of sleep
16:30:52.428 [main] INFO com.tahitiste.reactive.FluxTest - sleeping

main thread was blocked by blockingCall

*/

Concurrency

Blocking calls => execute call in scheduler

 

LOGGER.info("Start");

Mono<String> blockingCall = Mono.fromRunnable(() -> {
    try {
        Thread.sleep(1000);
        LOGGER.info("end of sleep");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

blockingCall
        .subscribeOn(Schedulers.single())
        .subscribe();

LOGGER.info("sleeping");

/* LOGS 
16:28:51.322 [main] INFO com.tahitiste.reactive.FluxTest - Start
16:28:51.388 [main] INFO com.tahitiste.reactive.FluxTest - sleeping
16:28:52.389 [single-1] INFO com.tahitiste.reactive.FluxTest - end of sleep

main thread was not blocked by blockingCall
*/
  • Single thread => Scheduler.single()
  • elastic thread pool => Schedulers.boundedElastic()
  • fixed thread pool => Schedulers.parallel()
  • Schedulers.fromExecutorService()

Concurrency

Concurrency

Performance ?

Concurrency

Performance ?

Not more performant than classic java code with

Threads / Future / ExecutorService / CompletableFuture / ...

=> because it uses these concepts under the hood

Concurrency

Performance ?

Not more performant than classic java code with

Threads / Future / ExecutorService / CompletableFuture / ...

=> because it uses these concepts under the hood

But it provide tools to made asynchronous programming easier

 

Backpressure ?

public interface Subscription {

    public void request(long n);

    public void cancel();
}

Backpressure ?

public interface Subscription {

    public void request(long n);

    public void cancel();
}
  • buffer() => buffer elements
  • onBackpressureDrop() => publisher drop if no enough request
  • ...

Logging : wait what ?

Mono<String> getComputation() {
	LOGGER.info("start Computation");
    return Mono.just("hello !");
}

public runComputation  {

    Mono<String> computationMono = getComputation();

    Thread.sleep(1000);

    LOGGER.info("subscribe computation");

    computationMono.doOnNext(value -> LOGGER.info("{}", value))
            .subscribe();


    Thread.sleep(10000);
}

/* LOGS 
17:12:15.814 [main] INFO com.tahitiste.reactive.FluxTest - start Computation
17:12:16.862 [main] INFO com.tahitiste.reactive.FluxTest - subscribe computation
17:12:16.869 [main] INFO com.tahitiste.reactive.FluxTest - hello !

getComputation log is shown before the effective subscription of the computation
*/

Logging : wait what ?

Mono<String> getComputation() {
    return Mono.fromRunnable(() -> LOGGER.info("start Computation"))
            .then(Mono.just("hello !"));
}

public runComputation  {

    Mono<String> computationMono = getComputation();

    Thread.sleep(1000);

    LOGGER.info("subscribe computation");

    computationMono.doOnNext(value -> LOGGER.info("{}", value))
            .subscribe();


    Thread.sleep(10000);
}

/* LOGS 
17:10:41.131 [main] INFO com.tahitiste.reactive.FluxTest - subscribe computation
17:10:41.142 [main] INFO com.tahitiste.reactive.FluxTest - start Computation
17:10:41.142 [main] INFO com.tahitiste.reactive.FluxTest - hello !

getComputation log is shown after the effective subscription of the computation
*/

Tests

Flux<String> computation = getComputation();

String actualResult = computation.block();

String expectedResult = "expected";

assertEquals(expectedResult,actualResult);

Make blocking code ;)

Tests

Flux<String> computation = getComputation();

String actualResult = computation.block();

String expectedResult = "expected";

assertEquals(expectedResult,actualResult);

Make blocking code ;)

For more control : StepVerifier

Flux<String> flux = Flux.just("Max","Alex","Yunus");

StepVerifier.create(flux)
        .expectNext("Max")
        .expectNextCount(2)
        .expectComplete()
        .verify();

Conclusion

  • Concurrency is simple : no more callback / no more future.get()
  • Composition and readability
  • Think from the start about communication issues : back-pressure, retry, buffer

Conclusion

  • Concurrency is simple : no more callback / no more future.get()
  • Composition and readability
  • Think from the start about communication issues : back-pressure, retry, buffer

BUT

 

 

  • less simple than classic imperative programming
  • Another framework to learn
  • Use it once, use it everywhere

Reactive Stream

By maximeleprince

Reactive Stream

  • 187