An intro to Reactive Extensions

An intro to Reactive Extensions

An not so short introduction to Reactive Extensions

https://www.youtube.com/watch?v=YnL1dPyFyhg

05162bc961c3654218bf1839974a4f35?s=128

Benoît Quenaudon

March 07, 2017
Tweet

Transcript

  1. 2.

    Agenda • Why Reactive? ◦ Problem with Android ◦ Problem

    with Back-end • What is Reactive Extensions? ◦ Observables ◦ Operators ◦ Schedulers • Use Cases ◦ API gateway pattern ◦ Data writing across distributed systems
  2. 5.

    Why Reactive? #android interface UserManager { User getUser(); void setName(String

    name); void setAge(int age); } UserManager um = new UserManager(); System.out.println(um.getUser()); um.setName("Bo Jackson"); System.out.println(um.getUser());
  3. 6.

    interface UserManager { User getUser(); void setName(String name); // <--

    now async void setAge(int age); // <-- now async }
  4. 8.

    interface UserManager { User getUser(); void setName(String name, Runnable callback);

    void setAge(int age, Runnable callback); } UserManager um = new UserManager(); System.out.println(um.getUser()); um.setName("Bo Jackson", new Runnable() { @Override public void run() { System.out.println(um.getUser()); } });
  5. 9.

    interface UserManager { User getUser(); void setName(String name, Listener listener);

    void setAge(int age, Listener listener); interface Listener { void success(User user); void failure(IOException exception); } }
  6. 10.

    UserManager um = new UserManager(); System.out.println(um.getUser()); um.setName("Bo Jackson", new UserManager.Listener()

    { @Override public void success() { System.out.println(um.getUser()); } @Override public void failure(IOException exception) { // TODO show the error... } });
  7. 11.

    UserManager um = new UserManager(); System.out.println(um.getUser()); um.setName("Bo Jackson", new UserManager.Listener()

    { @Override public void success() { System.out.println(um.getUser()); } @Override public void failure(IOException exception) { // TODO show the error… } }); um.setAge(54, new UserManager.Listener() { @Override public void success() { System.out.println(um.getUser()); } @Override public void failure(IOException exception) { // TODO show the error… } });
  8. 12.

    UserManager um = new UserManager(); System.out.println(um.getUser()); um.setName("Bo Jackson", new UserManager.Listener()

    { @Override public void success() { System.out.println(um.getUser()); um.setAge(54, new UserManager.Listener() { @Override public void success() { System.out.println(um.getUser()); } @Override public void failure(IOException exception) { // TODO show the error… } }); } @Override public void failure(IOException exception) { // TODO show the error… } });
  9. 13.

    public final class UserActivity extends Activity { private final UserManager

    um = new UserManager(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.user); TextView tv = (TextView) findViewById(R.id.username); tv.setText(um.getUser().toString()); um.setName("Bo Jackson", new UserManager.Listener() { @Override public void success() { tv.setText(um.getUser().toString()); } @Override public void failure(IOException exception) { // TODO show the error… } }); } }
  10. 14.

    public final class UserActivity extends Activity { private final UserManager

    um = new UserManager(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.user); TextView tv = (TextView) findViewById(R.id.username); tv.setText(um.getUser().toString()); um.setName("Bo Jackson", new UserManager.Listener() { @Override public void success() { tv.setText(um.getUser().toString()); } @Override public void failure(IOException exception) { // TODO show the error… } }); } }
  11. 15.

    public final class UserActivity extends Activity { private final UserManager

    um = new UserManager(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.user); TextView tv = (TextView) findViewById(R.id.username); tv.setText(um.getUser().toString()); um.setName("Bo Jackson", new UserManager.Listener() { @Override public void success() { if (isDestroyed()) { tv.setText(um.getUser().toString()); } } @Override public void failure(IOException exception) { // TODO show the error… } }); } }
  12. 16.

    public final class UserActivity extends Activity { private final UserManager

    um = new UserManager(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.user); TextView tv = (TextView) findViewById(R.id.username); tv.setText(um.getUser().toString()); um.setName("Bo Jackson", new UserManager.Listener() { @Override public void success() { if (isDestroyed()) { tv.setText(um.getUser().toString()); } } @Override public void failure(IOException exception) { // TODO show the error… } }); } }
  13. 17.

    public final class UserActivity extends Activity { private final UserManager

    um = new UserManager(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.user); TextView tv = (TextView) findViewById(R.id.username); tv.setText(um.getUser().toString()); um.setName("Bo Jackson", new UserManager.Listener() { @Override public void success() { if (isDestroyed()) { tv.setText(um.getUser().toString()); } } @Override public void failure(IOException exception) { // TODO show the error… } }); } }
  14. 18.

    public final class UserActivity extends Activity { private final UserManager

    um = new UserManager(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.user); TextView tv = (TextView) findViewById(R.id.username); tv.setText(um.getUser().toString()); um.setName("Bo Jackson", new UserManager.Listener() { @Override public void success() { runOnUiThread(new Runnable() { @Override public void run() { if (isDestroyed()) { tv.setText(um.getUser().toString()); } } }); } @Override public void failure(IOException exception) { // TODO show the error… } }); }
  15. 19.

    public final class UserActivity extends Activity { private final UserManager

    um = new UserManager(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.user); TextView tv = (TextView) findViewById(R.id.username); tv.setText(um.getUser().toString()); um.setName("Bo Jackson", new UserManager.Listener() { @Override public void success() { runOnUiThread(new Runnable() { @Override public void run() { if (isDestroyed()) { tv.setText(um.getUser().toString()); } } }); } @Override public void failure(IOException exception) { // TODO show the error… } }); }
  16. 21.
  17. 25.

    Problems • Low spec devices • Why not leverage the

    servers? ⇒ Client = one request only
  18. 30.

    Try • java.util.concurrent.Future ◦ Future.get() is blocking… ◦ Future<List<Future<T>>> •

    How about Reactive Programming? ◦ Observable<T> ⇒ Reactive Programming for the win
  19. 35.

    ReactiveX • Extension of Observable Pattern • Declarative Composition of

    Streams • Abstraction of ◦ low-level threading, ◦ Synchronization, ◦ Thread-safety, ◦ concurrent data structures, ◦ and non-blocking I/O.
  20. 38.

    Observable Pattern • Listen to Stream = Subscribing • Only

    async ◦ event ⇒ funcA() ◦ error ⇒ funcB() ◦ complete ⇒ funcC() • funcA, B, C = Observers = Consumer • Stream = Subject = Observable
  21. 40.

    event Iterable (pull) Observable (push) retrieve data T next() onNext(T)

    discover error throws Exception onError(Exception) complete !hasNext() onCompleted()
  22. 43.

    Observables • Usually do work when start/stop listening • One

    event, many events or empty • Terminates with an error or completion • May never terminate
  23. 47.

    Creating Observables • RxJava ◦ Observable.just("Bo Jacks"); ◦ Observable.fromArray(array); ◦

    Observable.fromIterable(list); ◦ Observable.create(...); • RxJS ◦ Rx.Observable.just(42) ◦ Rx.Observable.from(iterable) ◦ Rx.Observable.create(subscribe)
  24. 48.
  25. 56.

    Filtering Operators • Debounce • Distinct • ElementAt • Filter

    • First • IgnoreElements • Last • Sample • Skip • SkipLast • Take
  26. 59.

    So on... • Error Handling Operators ◦ catch, retry •

    Observable Utility Operators ◦ delay, timeout, observeOn, subscribeOn, etc • Conditional and Boolean Operators ◦ all, skipUntil, contains, etc • Mathematical and Aggregate Operators ◦ average, count, max, etc • etc
  27. 62.

    Schedulers getDataFromNetwork() .skip(10) .take(5) .map(s -> s + " transformed")

    .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread())) .subscribe(s -> println s)
  28. 63.

    Schedulers • subscribeOn: ◦ Everything from top to the next

    observeOn run on my thread. • observeOn: ◦ Everything below me run on my thread.
  29. 64.

    Schedulers getDataFromNetwork() .skip(10) .take(5) .map(s -> s + " transformed")

    .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread())) .subscribe(s -> println s) IO Main
  30. 65.
  31. 66.

    RxJS: 事前 [1, 2, 3].map(x => x + 1) //

    [2, 3, 4] [ [1], [2, 3], [], [4] ].concatAll() // [1, 2, 3, 4]
  32. 67.

    RxJS getElementDrags = function(elmt) { return elmt.mouseDowns. map(mouseDown => document.mouseMoves.

    takeUntil(document.mouseUps)). concatAll(); } getElementDrags(image). forEach(pos => image.position = pos);
  33. 68.

    D D D map(mouseDown => document.mouseMoves .takeUntil(document.mouseUps)) U U U

    concatAll() forEach(pos => image.position = pos) Operator Operator Observer M M M M M M M M M M
  34. 69.

    RxJS getElementDrags = function(elmt) { return elmt.mouseDowns. map(mouseDown => document.mouseMoves.

    takeUntil(document.mouseUps)). concatAll(); } getElementDrags(image). forEach(pos => image.position = pos);
  35. 70.
  36. 71.

    RxJava Observable<String> editText; editText .filter(text -> text.length > 2) .debounce(250,

    MILLISECONDS) .map(text -> api.search(text)) .subscribe(result -> showResult(result));
  37. 72.

    filter(text -> text.length > 2) map(text -> api.search(text)) result ->

    showResult(result) Operator Operator Observer k debounce(250, MILLISECONDS) Operator ka kai kaiz kaize kaizen kai kaiz kaize kaizen kaiz kaizen 250ms 250ms
  38. 73.

    RxJava Observable<String> editText; editText .filter(text -> text.length > 2) .debounce(250,

    MILLISECONDS) .map(text -> api.search(text)) .subscribe(result -> showResult(result));
  39. 74.
  40. 76.

    Hot vs Cold Observable • Cold ◦ Start to work

    on subscription • Hot ◦ Start to work on creation
  41. 78.
  42. 79.

    Background • Moore’s Law Is Dead. Now What? • We

    need the power. ◦ multi-devices, IoT, Big Data ◦ For organizations' scalability
  43. 80.

    Background • Microservices ◦ ≒ distributed systems • Need API-based

    collaboration • Need combination of multi tasks
  44. 81.

    Complexity • How do we combine multi data? • How

    do we resolve dependencies between tasks? • Concurrency / multi-threading / blocking • Network • Retry strategy / Message delivery reliability • Eventual consistency • Anti-fragile / Fault tolerant
  45. 86.

    API as a Stream public interface AccountApi { @POST("api/account/login") Observable<LoginOutputForm>

    login(@Body LoginInputForm inputForm); } • Retrofit & RxJava2 Adapter • If you don't use these adapters, you can wrap API responses by yourself
  46. 88.

    Service as a Stream @Service @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class

    AccountService { private final ApiRegistry apiRegistry; public Observable<LoginOutputForm> login(String account, String password) { AccountApi accountApi = apiRegistry.of(AccountApi.class); return accountApi.login(new LoginInputForm(account, password)); } }
  47. 90.

    @RequestMapping(path = "/api/example1", method = RequestMethod.POST) public ReadMergeOutputForm run(@Validated @RequestBody

    ReadMergeInputForm inputForm) { return accountService .login(inputForm.getAccount(), inputForm.getPassword()) .flatMap( loginOutputForm -> recommendationService .get(loginOutputForm.accountId()) ) .zipWith( videoService.get() , (recommendationOutputForm, videoOutputForm) -> new ReadMergeOutputForm( videoOutputForm.getVideos() , recommendationOutputForm.getRecommendations() ) ) .blockingSingle(); }
  48. 91.

    @RequestMapping(path = "/api/example1", method = RequestMethod.POST) public ReadMergeOutputForm run(@Validated @RequestBody

    ReadMergeInputForm inputForm) { return accountService .login(inputForm.getAccount(), inputForm.getPassword()) .flatMap( loginOutputForm -> recommendationService .get(loginOutputForm.accountId()) ) .zipWith( videoService.get() , (recommendationOutputForm, videoOutputForm) -> new ReadMergeOutputForm( videoOutputForm.getVideos() , recommendationOutputForm.getRecommendations() ) ) .blockingSingle(); }
  49. 92.

    @RequestMapping(path = "/api/example1", method = RequestMethod.POST) public ReadMergeOutputForm run(@Validated @RequestBody

    ReadMergeInputForm inputForm) { return accountService .login(inputForm.getAccount(), inputForm.getPassword()) .flatMap( loginOutputForm -> recommendationService .get(loginOutputForm.accountId()) ) .zipWith( videoService.get() , (recommendationOutputForm, videoOutputForm) -> new ReadMergeOutputForm( videoOutputForm.getVideos() , recommendationOutputForm.getRecommendations() ) ) .blockingSingle(); }
  50. 93.

    @RequestMapping(path = "/api/example1", method = RequestMethod.POST) public ReadMergeOutputForm run(@Validated @RequestBody

    ReadMergeInputForm inputForm) { return accountService .login(inputForm.getAccount(), inputForm.getPassword()) .flatMap( loginOutputForm -> recommendationService .get(loginOutputForm.accountId()) ) .zipWith( videoService.get() , (recommendationOutputForm, videoOutputForm) -> new ReadMergeOutputForm( videoOutputForm.getVideos() , recommendationOutputForm.getRecommendations() ) ) .blockingSingle(); }
  51. 94.

    @RequestMapping(path = "/api/example1", method = RequestMethod.POST) public ReadMergeOutputForm run(@Validated @RequestBody

    ReadMergeInputForm inputForm) { return accountService .login(inputForm.getAccount(), inputForm.getPassword()) .flatMap( loginOutputForm -> recommendationService .get(loginOutputForm.accountId()) ) .zipWith( videoService.get() , (recommendationOutputForm, videoOutputForm) -> new ReadMergeOutputForm( videoOutputForm.getVideos() , recommendationOutputForm.getRecommendations() ) ) .blockingSingle(); }
  52. 95.

    @RequestMapping(path = "/api/example1", method = RequestMethod.POST) public ReadMergeOutputForm run(@Validated @RequestBody

    ReadMergeInputForm inputForm) { return accountService .login(inputForm.getAccount(), inputForm.getPassword()) .flatMap( loginOutputForm -> recommendationService .get(loginOutputForm.accountId()) ) .zipWith( videoService.get() , (recommendationOutputForm, videoOutputForm) -> new ReadMergeOutputForm( videoOutputForm.getVideos() , recommendationOutputForm.getRecommendations() ) ) .blockingSingle(); }
  53. 97.

    return accountService .login(inputForm.getAccount(), inputForm.getPassword()) .map(loginOutputForm -> { if (!loginOutputForm.isSuccess()) {

    throw new RuntimeException(); } return loginOutputForm; }) .flatMap( loginOutputForm -> recommendationService .get(loginOutputForm.accountId()) ) .zipWith( videoService.get() , (recommendationOutputForm, videoOutputForm) -> new ReadMergeOutputForm( videoOutputForm.getVideos() , recommendationOutputForm.getRecommendations() ) ) .blockingSingle();
  54. 99.

    return accountService .login(inputForm.getAccount(), inputForm.getPassword()) .map(loginOutputForm -> { if (!loginOutputForm.isSuccess()) {

    throw new RuntimeException(); } return loginOutputForm; }) .flatMap( loginOutputForm -> recommendationService .get(loginOutputForm.accountId()) .onErrorReturnItem(RECOMMENDATIONS_ON_ERROR) ) .zipWith( videoService.get() , (recommendationOutputForm, videoOutputForm) -> new ReadMergeOutputForm( videoOutputForm.getVideos() , recommendationOutputForm.getRecommendations() ) ) .blockingSingle();
  55. 101.

    return accountService .login(inputForm.getAccount(), inputForm.getPassword()) .map(loginOutputForm -> { if (!loginOutputForm.isSuccess()) {

    throw new RuntimeException(); } return loginOutputForm; }) .flatMap( loginOutputForm -> recommendationService .get(loginOutputForm.accountId()) .onErrorReturnItem(RECOMMENDATIONS_ON_ERROR) ) .zipWith( videoService.get().subscribeOn(Schedulers.computation()) , (recommendationOutputForm, videoOutputForm) -> new ReadMergeOutputForm( videoOutputForm.getVideos() , recommendationOutputForm.getRecommendations() ) ) .blockingSingle();
  56. 102.
  57. 105.

    Consistency in a single system • Can use the transaction

    • e.g. ◦ When a corporation data is created, an address data is also created ◦ The corporation object manages the address object's life cycle
  58. 106.

    Consistency in a single system @Transactional(rollbackFor = {Exception.class}) public Corporation

    create(Corporation anonymousCorporation, Address anonymousAddress) { Corporation corporation = corporationRepository.save(anonymousCorporation); Address unidentifiedAddress = Address.builder() .corporationId(corporation.getId()) .state(anonymousAddress.getState()) .phone(anonymousAddress.getPhone()) .build(); Address address = addressRepository.save(unidentifiedAddress); return Corporation.aggregate(corporation, address); }
  59. 107.

    Eventual Consistency for multi-services • We couldn't use the transaction

    for the data consistency in HTTP protocol based on multi-services collaboration • Instead of the transaction, we often use the eventual consistency • We need take care of some things: ◦ Idempotency ◦ Message delivery reliability
  60. 108.

    Scenario • End user inputs corporation and address data •

    Service A enqueues with end users' input data • Worker processes the queue ◦ Worker is responsible for guaranteeing for message delivery reliability • Save corporation data in the service A with unique key message • Service A requests Service B to save address data via Web API ◦ Retry till successful response from Service B ⇒ Has to be Idempotent
  61. 109.
  62. 110.
  63. 111.
  64. 112.
  65. 113.
  66. 114.
  67. 115.
  68. 116.
  69. 117.
  70. 118.
  71. 122.
  72. 123.
  73. 124.

    Objects life cycle • Requirements ◦ When a corporation data

    is created, an address data should also be created.
  74. 126.

    Repository public Corporation save(Corporation corporation) { Corporation saved = findByMessageUniqueKey(corporation.getMessageUniqueKey());

    if (saved == null) { return insert(corporation); } Corporation adjusted = Corporation.of(saved.getId(), corporation.getName(), corporation.getMessageUniqueKey()); return update(adjusted); }
  75. 127.

    Wrap Repository using Observables @Service @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class

    CorporationService { private final CorporationRepostory corporationRepository; public Observable<Corporation> save(Corporation corporation) { return Observable.fromCallable(() -> corporationRepository.save(corporation)); } }
  76. 129.

    Web APIs public interface AddressApi { @GET("api/service-b/address/{key}") Observable<Response<GetAddressOutputForm>> get(@Path("key") String

    key); @POST("api/service-b/address/create") Observable<Response<CreateAddressOutputForm>> create(@Body CreateAddressInputForm inputForm); }
  77. 130.

    Wrap Web API for idempotency public Observable<Address> save(Address anonymousAddress) {

    AddressApi addressApi = apiRegistry.of(AddressApi.class); Observable<Response<GetAddressOutputForm>> getAddressResp$ = addressApi.get(anonymousAddress.getMessageUniqueKey()); return getAddressResp$.flatMap(getAddressResp -> { if (getAddressResp.isSuccessful()) { return Observable.just(getAddressResp.body().getAddress()); } if (getAddressResp.code() != 404) { new UnsupportedOperationException(getAddressResp.errorBody().string()); } return addressApi.create( new CreateAddressInputForm(anonymousAddress.getCorporationId(), anonymousAddress.getState(), anonymousAddress.getMessageUniqueKey())) .map(createAddressOutputFormResp -> { Long addressId = createAddressOutputFormResp.body().getAddressId(); return Address.of(addressId, anonymousAddress); }); }); }
  78. 131.

    Compose idempotent APIs public Observable<Corporation> execute(CreateCorporationCommand aCommand) { Corporation anonymousCorporation

    = Corporation.anonymous( aCommand.getCorporationName() , aCommand.getMessageUniqueKey() ); return corporationService.save(anonymousCorporation).flatMap( corporation -> { Address anonymousAddress = Address.anonymous( corporation.getId() , aCommand.getState(), aCommand.getMessageUniqueKey() ); return addressService.save(anonymousAddress) .map(address -> Corporation.aggregate(corporation, address)); }); }
  79. 132.

    Conclusion • Rx’s APIs provide us high level abstraction •

    We write how we process data • We can modify our code for: ◦ error handling, ◦ logging, ◦ multi-threading and so on • The more we break into appropriate components, the easier the testing
  80. 134.

    Resources • Rx official website ◦ http://reactivex.io/ • The introduction

    to Reactive Programming you've been missing ◦ https://gist.github.com/staltz/868e7e9bc2a7b8c1f754 • RxJava ◦ https://github.com/ReactiveX/RxJava • RxJS ◦ https://github.com/ReactiveX/RxJS • Reactive Fault Tolerant Programming with Hystrix and RxJava ◦ https://goo.gl/2BrwPd • Functional Reactive Programming with RxJava • Ben Christensen ◦ https://www.youtube.com/watch?v=_t06LRX0DV0 • Exploring RxJava 2 for Android • Jake Wharton ◦ https://www.youtube.com/watch?v=htIXKI5gOQU