RxJava from the trenches

Post on 13-Apr-2017

296 views 1 download

Transcript of RxJava from the trenches

mindloops

RXJAVA: FROM THE TRENCHES

PETER HENDRIKS @PETERHENDRIKS80 PETER@MINDLOOPS.NL

Agenda

• Intro

• Why we want RxJava

• RxJava in Practice

• Future / conclusion

What is ReactiveX?

“A library for composing asynchronous and event-based programs by using observable

sequences” - Erik Meijer

What is RxJava?

• Port of ReactiveX C# framework to Java

• Open source, lead by Netflix

• 1.0.0 release november 2014, 2.0.0 last week :)

• ReactiveX is currently available in 14 programming languages

RxJava Survival Guide

// In your own code, convert results in a fluent way...Observable<String> descriptionObservable = eventObservable.map(event -> event.getDescription());

// e.g. Database, services, keyboard, messaging etc.MyEventProducer eventProducer = new MyEventProducer();

// use some custom adapter code to adapt to an RxJava "Observable"Observable<MyEvent> eventObservable = eventProducer.toObservable();

// ... And compose with other ObservablesObservable<Success> storeObservable = eventObservable.flatMap(event -> storeEvent(event));private Observable<Success> storeEvent(MyEvent event) { // stores the event asynchronously...}

// Start flowing by subscribing and do something with final results storeObservable.subscribe(myEvent -> {/* do something */}, error -> LOG.error("Oops", error));

Case: Sanoma Learning

• Web e-learning platform

• Many users

• Co-development: multiple agile product teams

Case: Sanoma Learning

• AngularJS, Java 8, Vert.x, MongoDB, AWS S3, Redis

• Suite of micro-services

Microservices met Vert.x- Tim van Eindhoven, Zaal 4, 11:30

Why RxJava

WHY RXJAVA

Why we want RxJava

• Avoid “callback hell”

• Out of the box support

• Streaming performance benefits

Simplicity of the POJAM• A simple, blocking Plain Old JAva Method

• Predictable execution flow

• Fully built-in result and error handling

• Easy to composepublic List<QuestionView> listQuestions(String studentId, String segmentId) { List<Question> questions = contentService.findBySegmentId(segmentId); List<QuestionView> result = new ArrayList<>(); for(Question question : questions) { Progress progress = progressService.findByQuestionId(studentId, question.getId()); result.add(new QuestionView(question, progress)); } return result;}

Vert.x callback exampleMessageConsumer<String> consumer = eventBus.consumer("news.uk.sport"); consumer.handler(message -> { try { System.out.println("I have received a message: " + message.body()); message.reply("how interesting!"); } catch (Exception e) { message.fail(500, e.getMessage()); }});sendNewsMessage("Yay! Someone kicked a ball across a patch of grass", ar -> { if (ar.succeeded()) { System.out.println("Received reply: " + ar.result().body()); } else { ar.cause().printStackTrace(); }});

private void sendNewsMessage(String message, Handler<AsyncResult<Message<Object>>> replyHandler) { eventBus.send("news.uk.sport", message, replyHandler);}

Callback hellvoid listQuestions(String studentId, String segmentId, Handler<AsyncResult<List<QuestionView>>> resultHandler) { findBySegmentId(segmentId, ar -> { if (ar.succeeded()) { List<QuestionView> result = new ArrayList<>(); List<Question> questions = ar.result().body(); for(Question question : questions) { findByQuestionId(studentId, question.getId(), progressAr -> { if (ar.succeeded()) { Progress progress = progressAr.result().body(); result.add(new QuestionView(question, progress)); questions.remove(question); if (questions.isEmpty()) { resultHandler.handle(Future.<List<QuestionView>>succeededFuture(result)); } } else { resultHandler.handle(Future.<List<QuestionView>>failedFuture(ar.cause())); } }); } } else { resultHandler.handle(Future.<List<QuestionView>>failedFuture(progressAr.cause())); } } );} private void findBySegmentId(String segmentId, Handler<AsyncResult<Message<List<Question>>>> handler) {}private void findByQuestionId(String studentId, String questionId, Handler<AsyncResult<Message<Progress>>> handler) {}

Solution: RxJava as “Promise API”

public Observable<QuestionView> listQuestions(String studentId, String segmentId) { return contentService.findBySegmentId(segmentId) .concatMap(question -> progressService.findByQuestionId(studentId, question.getId()) .map(progress -> new QuestionView(question, progress)));}

RxJava: one Promise API to rule them all

• We want to use one promise API everywhere to fix callback hell

• RxJava is directly supported in Vert.x and the MongoDB Java driver

Typical request processing overhead

Thread

Request

Response (List)

Domain data (List)

public List<QuestionView> listQuestions(String studentId, String segmentId) { List<Question> questions = contentService.findBySegmentId(segmentId); List<Question> result = new ArrayList<>(); for(Question question : questions) { Progress progress = progressService.findByQuestionId(studentId, question.getId()); result.add(new QuestionView(question, progress)); } return result;}

Streaming == less intermediate data needed

public Observable<QuestionView> listQuestions(String studentId, String segmentId) { return contentService.findBySegmentId(segmentId) .flatMap(question -> progressService.findByQuestionId(studentId, question.getId()) .map(progress -> new QuestionView(question, progress)));}

Thread

Request

Response (List)

Domain data (element)

RXJAVA IN PRACTICE

Expect a learning curve

“Simple” promises vs RxJava

CompletableFuture Observable

What emits 1 event or 1 error emit 0 or more events and/or 1 error

When Immediately scheduled for execution

already executing, or only after subscription

Where Event can be read at any time after completion Event is available once

Learn to stream first!• RxJava will force you to do it

• It helps to be comfortable with Java 8 Stream API first

• Java 8 Stream API still very useful for POJAM code

public Observable<QuestionView> listQuestions(String studentId, String segmentId) { return contentService.findBySegmentId(segmentId) .flatMap(question -> progressService.findByQuestionId(studentId, question.getId()) .map(progress -> new QuestionView(question, progress)));}

Hot vs Cold Observables// HOTMyEventProducer eventProducer = new MyEventProducer();// some other code...Observable<MyEvent> hotObservable = Observable.create(subscriber -> { // Listen to producer here.});hotObservable.subscribe(myEvent -> {/* do something with event */});

// COLDObservable<MyEvent> coldObservable = Observable.create(subscriber -> { MyEventProducer eventProducer = new MyEventProducer(); // Listen to producer here.});// some other code...coldObservable.subscribe(myEvent -> {/* do something with event */});

Examples of hot and cold// HOT exampleHttpServer server = vertx.createHttpServer();Observable<HttpServerRequest> requests = server.requestStream().toObservable();// some other coderequests.subscribe(request -> System.out.println(request.absoluteURI()));

//COLD exampleMongoClient mongoClient = MongoClients.create();MongoCollection<Document> questionCollection = mongoClient.getDatabase("contents").getCollection("questions");Observable<Document> allQuestions = questionCollection.find().toObservable();// some other codeallQuestions.subscribe(question -> System.out.println(question));

Always end with subscribe somewhere// wrong way to check value @Testpublic void storeAnswerStoresAnswer() { Observable<QuestionView> questionView = answerController.storeAnswer("1", "test"); questionView.map(view -> assertThat(view, is(testView)));}

// use test subscriber to test observables in isolation @Testpublic void storeAnswerStoresAnswer() { Observable<QuestionView> questionView = answerController.storeAnswer("1", "test"); TestSubscriber<QuestionView> ts = new TestSubscriber<>(); questionView.subscribe(ts); ts.awaitTerminalEvent(); ts.assertNoErrors(); ts.assertValue(testValue);}

0 or more events dilemma// POJAMpublic QuestionView storeAnswer(String questionId, String textValue) { Question question = contentService.findByQuestionId(questionId); if (question == null) { throw new IllegalStateException("question not found"); } Answer answer = validateAnswer(question, textValue); progressService.store(questionId, answer); return new QuestionView(question, answer);}

// RxJava wrongpublic Observable<QuestionView> storeAnswer(String questionId, String textValue) { return contentService.findByQuestionId(questionId) .flatMap(question -> { Answer answer = validateAnswer(question, textValue); return progressService.store(questionId, answer) .map(x -> new QuestionView(question, answer)); });}

// RxJava with switchpublic Observable<QuestionView> storeAnswer(String questionId, String textValue) { return contentService.findByQuestionId(questionId) .switchIfEmpty(Observable.defer(() -> Observable.error(new IllegalStateException("question not found")))) .flatMap(question -> { Answer answer = validateAnswer(question, textValue); return progressService.store(questionId, answer) .map(x -> new QuestionView(question, answer)); });}

Mapping “always empty” resultspublic Observable<Void> storeResult(String answer) { System.out.println("Do some async operation without result."); return Observable.empty();} public Observable<String> processRequest(String answer) { // This does not work right return storeResult(answer) .map(x -> "success!");}

public interface MongoCollection<TDocument> { /** * @return an Observable with a single element indicating when the operation has completed or with either a com.mongodb.DuplicateKeyException or com.mongodb.MongoException */ Observable<Success> insertOne(TDocument document);} // Return non-null event to be compatible with RxJava 2.xpublic enum Success { SUCCESS}

Will change in RxJava 2.xBetter distinction between 0, 1,

optional and multiple results

Make it easy to do the right thing

Only use RxJava when needed• POJAM code still the best for a lot of your code

• Use RxJava only for async composition/streaming

public boolean isAnswerCorrect(Question question, String answer) { // some complicated synchronised checking logic here return true; } public Observable<String> processRequest(String questionId, String answer) { return contentService.findByQuestionId(questionId) .map(question -> isAnswerCorrect(question, answer)) .map(result -> result ? "Good job!" : "Better luck next time!");}

Separate subscribe code• Separate and reuse subscribe code from observable chains

// BAD: subscription mixed with observable codepublic void processRequest(HttpServerRequest request, String questionId, String answer) { contentService.findByQuestionId(questionId) .map(question -> isAnswerCorrect(question, answer)) .map(result -> result ? "Good job!" : "Better luck next time!") .subscribe(result -> request.response().end(result));}

// GOOD: simple observable code, easy to test and cannot forget to subscribepublic Observable<String> processRequest(String questionId, String answer) { return contentService.findByQuestionId(questionId) .map(question -> isAnswerCorrect(question, answer)) .map(result -> result ? "Good job!" : "Better luck next time!");}

Async code and crappy stack tracespublic static void main(String[] args) { Observable.empty() .observeOn(Schedulers.io()) .toBlocking() .first();}

Exception in thread "main" java.util.NoSuchElementException: Sequence contains no elementsat rx.internal.operators.OperatorSingle$ParentSubscriber.onCompleted(OperatorSingle.java:131) at rx.internal.operators.OperatorTake$1.onCompleted(OperatorTake.java:53) at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber.pollQueue(OperatorObserveOn.java:195) at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber$1.call(OperatorObserveOn.java:162) at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(<…>.java:180) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(<…>.java:293) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745)

Band-aid: add context to logging/exceptions

// generic wrapping codepublic void processRequest(HttpServerRequest request, Function<HttpServerRequest, Observable<String>> processor) { Observable<String> observable = processor.apply(request); observable.subscribe( result -> processResult(request, result), error -> processError(request, error));} private void processError(HttpServerRequest request, Throwable error) { LOG.error("An error occurred processing: " + request.uri(), error); request.response().setStatusCode(500).setStatusMessage(error.getMessage()).end();}

FUTURE OF RXJAVA

The Future of RxJava looks promising

• Increased adoption for “Reactive Streams”

• RxJava 2 solves some API weirdness and simplifies simple event handling

• Strong adoption in Android and JavaScript

• Spring 5 goes big on reactive, including RxJava support

Spring example@RestControllerpublic class RxJavaController { private final RxJavaService aService; @Autowired public RxJavaController(RxJavaService aService) { this.aService = aService; } @RequestMapping(path = "/handleMessageRxJava", method = RequestMethod.POST) public Observable<MessageAcknowledgement> handleMessage(@RequestBody Message message) { System.out.println("Got Message.."); return this.aService.handleMessage(message); }}

RxJava 2.xReactive Streams(Backpressure) No Backpressure

0..n items, complete, error Flowable Observable

0..1 item, complete, error Maybe

1 item, error Single

complete, error Completable

CONCLUSION

RxJava in practice- Learning to stream - Hot vs Cold observables - Always subscribe - 0 or more events dilemma - Dealing with always empty results Make it easy

- Still use POJAM code - Separate subscribe code - Make up for stack trace

Why we want RxJava- Avoid Callback Hell - Streaming helps performance - Out of the box support

PETER HENDRIKS @PETERHENDRIKS80 PETER@MINDLOOPS.NL