DevoxxFR 2018: Spéléo Reactor

DevoxxFR 2018: Spéléo Reactor

Fda20bf9d9c85c4390ca7237beba45a2?s=128

Simon Baslé

April 18, 2018
Tweet

Transcript

  1. Spéléo Reactor @simonbasle Dans les profondeurs de la Programmation Réactive

  2. Au Programme

  3. • introduction / reactive streams • map • la fusion

    • concatMap • la “drain loop”
  4. Introduction

  5. Programmation Réactive

  6. “Composer des séquences asynchrones, basées sur les événements avec des

    opérateurs non-bloquants”
  7. Reactive Streams

  8. Reactive Streams standard d’interopérabilité JDK 9 Flow Publisher Subscriber Subscription

  9. Standard d’interopérabilité

  10. Aussi dans JDK 9 (Flow)

  11. Publisher produit la donnée

  12. Subscriber consomme la donnée

  13. Publisher Subscriber pousse des éléments produit consomme feedback

  14. subscribe(Subscriber<T>) Publisher Subscriber

  15. onNext(T) onError(Throwable) onComplete() Publisher Subscriber

  16. Publisher Subscriber pousse des éléments produit consomme feedback “0 à

    N onNext suivi d’au plus 1 (onComplete ou onError)”
  17. onNext(T) onError(Throwable) onComplete() Publisher Subscriber

  18. + onSubscribe(Subscription<T>) onNext(T) onError(Throwable) onComplete() Publisher Subscriber

  19. Subscription requête et annulation

  20. Subscriber au Publisher: “je suis disposé à traiter N éléments,

    envoie les-moi dès que possible”
  21. C’est la Backpressure

  22. à cet effet le Publisher fourni une Subscription au Subscriber

  23. grâce à elle, le Subscriber peut notifier de sa capacité

    de traitement via Subscription#request(long)
  24. Publisher Subscriber pousse des éléments produit consomme request(n) backpressure

  25. Modèle dit “Asynchronous Push-Pull”

  26. Modèle dit “Asynchronous Push-Pull”

  27. Modèle dit “Asynchronous Push-Pull” Le Subscriber demande N éléments au

    Publisher (notamment à la souscription)
  28. Modèle dit “Asynchronous Push-Pull”

  29. Modèle dit “Asynchronous Push-Pull” Le Publisher envoie au max le

    nombre demandé, en asynchrone (au fil de l’eau)
  30. Diagramme à billes (“marble diagram”)

  31. None
  32. onNext

  33. onComplete

  34. la séquence

  35. direction du “temps”

  36. plus de délai entre ces deux onNext

  37. une séquence qui tombe en erreur (onError)

  38. Contraintes intéressantes de Concurrence

  39. les onNext, ainsi que les événements terminaux, sont en série

  40. les onNext, ainsi que les événements terminaux, sont en série

    ie. pas d’appel parallèle à onNext pendant un onNext
  41. request & cancel peuvent se produire en parallèle (de onNext

    et d’eux même) req(2) req(7) req(1)
  42. can I have an API though? Publisher Subscriber

  43. Publisher Subscriber

  44. Reactor

  45. Reactor et son vocabulaire d’opérateurs Reactive Streams de bout en

    bout Flux Mono Riche API d’opérateurs
  46. Tous les types sont des Publisher

  47. Flux pour 0-N éléments

  48. None
  49. Mono pour 0-1 élément

  50. valué vide en erreur

  51. Large vocabulaire d’Opérateurs

  52. None
  53. Autres Concepts Importants

  54. Autres Concepts Importants souscription vers le haut, données vers le

    bas rien ne se passe avant `subscribe()` enchaîner les opérateurs
  55. souscription de bas en haut données de haut en bas

  56. Flux/Mono generator operator operator operator

  57. Flux/Mono generator Subscriber operator operator operator

  58. Flux/Mono generator Subscriber operator operator operator état propre à chaque

    Subscription Sub Sub Sub
  59. Flux/Mono generator Subscriber operator operator operator le flux de données

    commence Sub Sub Sub
  60. les opérateurs sont à la fois des Publisher et des

    Subscriber
  61. rien ne se passe tant qu’on n’a pas subscribe( )

  62. (dans la plupart des cas) la source des données ne

    commence à émettre que lors de la souscription
  63. crée une chaine de subscribers dédiés propage la 1ère requête,

    qui “démarre” la source
  64. enchaîner les opérateurs

  65. chaque opérateur retourne une nouvelle instance qui décore l’opérateur précédent.

  66. il faut donc enchaîner les appels (le style “fluent API”)

  67. Première descente: map

  68. L’opérateur map que fait l’opérateur ? à quoi ressemble le

    code ?
  69. que fait l’opérateur?

  70. Flux<R> map(Function<T, R> f)

  71. Flux<R> map(Function<T, R> f)

  72. Flux<R> map(Function<T, R> f)

  73. Flux<R> map(Function<T, R> f)

  74. Flux<R> map(Function<T, R> f)

  75. Flux<R> map(Function<T, R> f)

  76. T R map( )

  77. T R map( )

  78. à quoi ressemble le code?

  79. le Publisher l’opérateur, instancié par la méthode Flux#map

  80. final class FluxMap<T, R> extends FluxOperator<T, R> { final Function<?

    super T, ? extends R> mapper; FluxMap(Flux<? extends T> source, Function<? super T, ? extends R> mapper) { super(source); this.mapper = Objects.requireNonNull(mapper, "mapper"); } @Override @SuppressWarnings("unchecked") public void subscribe(CoreSubscriber<? super R> actual) { source.subscribe(new MapSubscriber<>(actual, mapper)); } }
  81. final class FluxMap<T, R> extends FluxOperator<T, R> { final Function<?

    super T, ? extends R> mapper; FluxMap(Flux<? extends T> source, Function<? super T, ? extends R> mapper) { super(source); this.mapper = Objects.requireNonNull(mapper, "mapper"); } @Override @SuppressWarnings("unchecked") public void subscribe(CoreSubscriber<? super R> actual) { source.subscribe(new MapSubscriber<>(actual, mapper)); } }
  82. le MapSubscriber

  83. static final class MapSubscriber<T, R> implements InnerOperator<T, R> { final

    CoreSubscriber<? super R> actual; final Function<? super T, ? extends R> mapper; boolean done; Subscription s; MapSubscriber(CoreSubscriber<? super R> actual, Function<? super T, ? extends R> mapper) { this.actual = actual; this.mapper = mapper; } ...
  84. static final class MapSubscriber<T, R> implements InnerOperator<T, R> { final

    CoreSubscriber<? super R> actual; final Function<? super T, ? extends R> mapper; boolean done; Subscription s; MapSubscriber(CoreSubscriber<? super R> actual, Function<? super T, ? extends R> mapper) { this.actual = actual; this.mapper = mapper; }
  85. final CoreSubscriber<? super R> actual; Subscription s; ... @Override public

    void onSubscribe(Subscription s) { if (Operators.validate(this.s, s)) { this.s = s; actual.onSubscribe(this); } }
  86. final CoreSubscriber<? super R> actual; boolean done; ... @Override public

    void onComplete() { if (done) { return; } done = true; actual.onComplete(); }
  87. final CoreSubscriber<? super R> actual; boolean done; ... @Override public

    void onError(Throwable t) { if (done) { return; } done = true; actual.onError(t); }
  88. static final class MapSubscriber<T, R> implements InnerOperator<T, R> { ...

    Subscription s; ... @Override public void request(long n) { s.request(n); } @Override public void cancel() { s.cancel(); } }
  89. son onNext le coeur de métier de l’opérateur

  90. @Override public void onNext(T t) { if (done) { return;

    } R v; try { v = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null value."); } catch (Throwable e) { onError(e); return; } actual.onNext(v); }
  91. @Override public void onNext(T t) { if (done) { return;

    } R v; try { v = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null value."); } catch (Throwable e) { onError(e); return; } actual.onNext(v); }
  92. pause 15min

  93. La Fusion map peut être fusionné comment la fusion est-elle

    implémentée ? quel impact sur la chaîne d’opérateurs ?
  94. map peut être Fusionné

  95. le subscriber est aussi une Queue réutilisée par chaque opérateur

    fusionné en dessous
  96. remplace des queues internes peut remonter jusqu’à la source (eg.

    Flux.fromIterable)
  97. opérateur source opérateur 2 opérateur 3 sans fusion

  98. opérateur source opérateur 2 opérateur 3 avec fusion

  99. Impact sur le code interface Fuseable

  100. final class FluxMapFuseable<T, R> extends FluxOperator<T, R> implements Fuseable {

    final Function<? super T, ? extends R> mapper; FluxMapFuseable(Flux<? extends T> source, Function<? super T, ? extends R> mapper) { super(source); this.mapper = Objects.requireNonNull(mapper, "mapper"); } @Override public void subscribe(CoreSubscriber<? super R> actual) { source.subscribe(new MapFuseableSubscriber<>(actual, mapper)); }
  101. final class FluxMapFuseable<T, R> extends FluxOperator<T, R> implements Fuseable {

    final Function<? super T, ? extends R> mapper; FluxMapFuseable(Flux<? extends T> source, Function<? super T, ? extends R> mapper) { super(source); this.mapper = Objects.requireNonNull(mapper, "mapper"); } @Override public void subscribe(CoreSubscriber<? super R> actual) { source.subscribe(new MapFuseableSubscriber<>(actual, mapper)); }
  102. static final class MapFuseableSubscriber<T, R> implements InnerOperator<T, R>, QueueSubscription<R> {

    final CoreSubscriber<? super R> actual; final Function<? super T, ? extends R> mapper; boolean done; QueueSubscription<T> s; int sourceMode; MapFuseableSubscriber(CoreSubscriber<? super R> actual, Function<? super T, ? extends R> mapper) { this.actual = actual; this.mapper = mapper; }
  103. static final class MapFuseableSubscriber<T, R> implements InnerOperator<T, R>, QueueSubscription<R> {

    final CoreSubscriber<? super R> actual; final Function<? super T, ? extends R> mapper; boolean done; QueueSubscription<T> s; int sourceMode; MapFuseableSubscriber(CoreSubscriber<? super R> actual, Function<? super T, ? extends R> mapper) { this.actual = actual; this.mapper = mapper; }
  104. négociation de la fusion

  105. @Override public int requestFusion(int requestedMode) { int m; if ((requestedMode

    & Fuseable.THREAD_BARRIER) != 0) { return Fuseable.NONE; } else { m = s.requestFusion(requestedMode); } sourceMode = m; return m; }
  106. @Override public int requestFusion(int requestedMode) { int m; if ((requestedMode

    & Fuseable.THREAD_BARRIER) != 0) { return Fuseable.NONE; } else { m = s.requestFusion(requestedMode); } sourceMode = m; return m; }
  107. null comme signal de fusion poll comme mécanisme

  108. @Override public void onNext(T t) { if (sourceMode == ASYNC)

    { actual.onNext(null); } else { if (done) { return; } R v; try { v = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null value."); } catch (Throwable e) { onError(e); return;
  109. @Override public void onNext(T t) { if (sourceMode == ASYNC)

    { actual.onNext(null); } else { if (done) { return; } R v; try { v = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null value."); } catch (Throwable e) { onError(e); return;
  110. @Override @Nullable public R poll() { T v = s.poll();

    if (v != null) { //can throw exceptions return Objects.requireNonNull(mapper.apply(v)); } return null; }
  111. @Override @Nullable public R poll() { T v = s.poll();

    if (v != null) { //can throw exceptions return Objects.requireNonNull(mapper.apply(v)); } return null; }
  112. @Override public boolean isEmpty() { return s.isEmpty(); } @Override public

    void clear() { s.clear(); } @Override public int size() { return s.size(); }
  113. Deuxième descente: concatMap

  114. l’opérateur concatMap que fait-il ? opérateurs 1-to-N opérateur interne et

    Subscriber interne
  115. que fait l’opérateur?

  116. Flux<U> concatMap( Function<T, Publisher<U>> f)

  117. Flux<U> concatMap( Function<T, Publisher<U>> f)

  118. Flux<U> concatMap( Function<T, Publisher<U>> f)

  119. ça ressemble à une fonction qui pourrait être passée à

    map
  120. essayons...

  121. T map( )

  122. T map( )

  123. T map( )

  124. T map( ) ceux-ci sont “inertes” Flux<Publisher<R>>

  125. map n’as aucune connaissance particulière sur le type <R>

  126. ici <R> est en fait Publisher<U> seulement map ne sait

    pas qu’il doit y souscrire...
  127. T concatMap( )

  128. concatMap( )

  129. concatMap( )

  130. concatMap( ) sous-flux souscrits et propagés Flux<U>

  131. concatMap sait quoi faire d’un Publisher<U> il y souscrit, et

    propage ses éléments
  132. concatMap concatène: pas de souscription au sous-Publisher suivant tant que

    le précédent n’as pas terminé
  133. opérateurs 1-to-N concatMap vs flatMap

  134. concatMap( )

  135. flatMap( )

  136. à quoi ressemble le code?

  137. opérateur de base Publisher classique

  138. final class FluxConcatMap<T, R> extends FluxOperator<T, R> { final Function<?

    super T, ? extends Publisher<? extends R>> mapper; final Supplier<? extends Queue<T>> queueSupplier; final int prefetch; FluxConcatMap(Flux<? extends T> source, Function<? super T, ? extends Publisher<? extends R>> mapper, Supplier<? extends Queue<T>> queueSupplier, int prefetch) { super(source); if (prefetch <= 0) throw new IllegalArgumentException("prefetch <= 0"); this.mapper = Objects.requireNonNull(mapper, "mapper"); this.queueSupplier = Objects.requireNonNull(queueSupplier); this.prefetch = prefetch; } @Override public void subscribe(CoreSubscriber<? super R> actual) { source.subscribe(new ConcatMapImmediate<>(s, mapper, queueSupplier, prefetch)); }
  139. final class FluxConcatMap<T, R> extends FluxOperator<T, R> { final Function<?

    super T, ? extends Publisher<? extends R>> mapper; final Supplier<? extends Queue<T>> queueSupplier; final int prefetch; FluxConcatMap(Flux<? extends T> source, Function<? super T, ? extends Publisher<? extends R>> mapper, Supplier<? extends Queue<T>> queueSupplier, int prefetch) { super(source); if (prefetch <= 0) throw new IllegalArgumentException("prefetch <= 0"); this.mapper = Objects.requireNonNull(mapper, "mapper"); this.queueSupplier = Objects.requireNonNull(queueSupplier); this.prefetch = prefetch; } @Override public void subscribe(CoreSubscriber<? super R> actual) { source.subscribe(new ConcatMapImmediate<>(s, mapper, queueSupplier, prefetch)); }
  140. ConcatMapImmediate Subscriber principal avec un peu plus d’état, coordonne les

    sous-Publisher
  141. static final class ConcatMapImmediate<T, R> { final CoreSubscriber<? super R>

    actual; final Function<? super T, ? extends Publisher<? extends R>> mapper; final Supplier<? extends Queue<T>> queueSupplier; final int prefetch; final int limit; Subscription s; int consumed; volatile Queue<T> queue; volatile boolean done; volatile boolean cancelled; volatile boolean active; volatile Throwable error; volatile int wip; volatile int guard; int sourceMode;
  142. static final class ConcatMapImmediate<T, R> { final CoreSubscriber<? super R>

    actual; final Function<? super T, ? extends Publisher<? extends R>> mapper; final Supplier<? extends Queue<T>> queueSupplier; final int prefetch; final int limit; Subscription s; int consumed; volatile Queue<T> queue; volatile boolean done; volatile boolean cancelled; volatile boolean active; volatile Throwable error; volatile int wip; volatile int guard; int sourceMode;
  143. static final AtomicReferenceFieldUpdater<ConcatMapImmediate, Throwable> ERROR = AtomicReferenceFieldUpdater.newUpdater(ConcatMapImmediate.class, Throwable.class, "error"); static

    final AtomicIntegerFieldUpdater<ConcatMapImmediate> WIP = AtomicIntegerFieldUpdater.newUpdater(ConcatMapImmediate.class,"wip"); static final AtomicIntegerFieldUpdater<ConcatMapImmediate> GUARD = AtomicIntegerFieldUpdater.newUpdater(ConcatMapImmediate.class, "guard"); final ConcatMapInner<R> inner;
  144. @Override public void onSubscribe(Subscription s) { if (Operators.validate(this.s, s)) {

    this.s = s; if (s instanceof Fuseable.QueueSubscription) { if (m == Fuseable.SYNC) { ... } else if (m == Fuseable.ASYNC) { ... } else { queue = queueSupplier.get(); } } else { queue = queueSupplier.get(); } actual.onSubscribe(this); s.request(Operators.unboundedOrPrefetch(prefetch)); } }
  145. ... if (s instanceof Fuseable.QueueSubscription) { Fuseable.QueueSubscription<T> f = (Fuseable.QueueSubscription<T>)

    s; int m = f.requestFusion(Fuseable.ANY); if (m == Fuseable.SYNC) { sourceMode = Fuseable.SYNC; queue = f; done = true; actual.onSubscribe(this); drain(); return; } else if (m == Fuseable.ASYNC) { sourceMode = Fuseable.ASYNC; queue = f; } else { queue = queueSupplier.get(); } } ...
  146. ... if (s instanceof Fuseable.QueueSubscription) { Fuseable.QueueSubscription<T> f = (Fuseable.QueueSubscription<T>)

    s; int m = f.requestFusion(Fuseable.ANY); if (m == Fuseable.SYNC) { sourceMode = Fuseable.SYNC; queue = f; done = true; actual.onSubscribe(this); drain(); return; } else if (m == Fuseable.ASYNC) { sourceMode = Fuseable.ASYNC; queue = f; } else { queue = queueSupplier.get(); } } ... drain( )? on va y revenir...
  147. @Override public void onComplete() { done = true; drain(); }

    @Override public void request(long n) { inner.request(n); } @Override public void cancel() { if (!cancelled) { cancelled = true; inner.cancel(); s.cancel(); } }
  148. @Override public void onError(Throwable t) { if (Exceptions.addThrowable(ERROR, this, t))

    { inner.cancel(); if (GUARD.getAndIncrement(this) == 0) { t = Exceptions.terminate(ERROR, this); if (t != TERMINATED) { actual.onError(t); } } } else { Operators.onErrorDropped(t, actual.currentContext()); } }
  149. @Override public void onError(Throwable t) { if (Exceptions.addThrowable(ERROR, this, t))

    { inner.cancel(); if (GUARD.getAndIncrement(this) == 0) { t = Exceptions.terminate(ERROR, this); if (t != TERMINATED) { actual.onError(t); } } } else { Operators.onErrorDropped(t, actual.currentContext()); } }
  150. @Override public void onNext(T t) { if (sourceMode == Fuseable.ASYNC)

    { drain(); } else if (!queue.offer(t)) { onError(Exceptions.BACKPRESSURE_ERROR_QUEUE_FULL); } else { drain(); } }
  151. @Override public void onNext(T t) { if (sourceMode == Fuseable.ASYNC)

    { drain(); } else if (!queue.offer(t)) { onError(Exceptions.BACKPRESSURE_ERROR_QUEUE_FULL); } else { drain(); } } on va y revenir je vous dis...
  152. pour l’instant, précisons juste que dans le drain( ) on

    va attacher le inner au Publisher créé par la fonction
  153. @Override public void onNext(T t) { if (sourceMode == Fuseable.ASYNC)

    { drain(); } else if (!queue.offer(t)) { onError(Exceptions.BACKPRESSURE_ERROR_QUEUE_FULL); } else { drain(); } }
  154. le inner appelle par la suite son parent via l’API

    suivante
  155. @Override public void innerNext(R value) { if (guard == 0

    && GUARD.compareAndSet(this, 0, 1)) { actual.onNext(value); if (GUARD.compareAndSet(this, 1, 0)) { return; } Throwable e = Exceptions.terminate(ERROR, this); if (e != TERMINATED) { actual.onError(e); } } }
  156. @Override public void innerComplete() { active = false; drain(); }

  157. @Override public void innerError(Throwable e) { if (Exceptions.addThrowable(ERROR, this, e))

    { s.cancel(); if (GUARD.getAndIncrement(this) == 0) { e = Exceptions.terminate(ERROR, this); if (e != TERMINATED) { actual.onError(e); } } } else { Operators.onErrorDropped(e, actual.currentContext()); } }
  158. ConcatMapInner récupère les signaux des sous-Publisher s’assure que la backpressure

    est respectée
  159. static final class ConcatMapInner<R> extends Operators.MultiSubscriptionSubscriber<R, R> { final FluxConcatMapImmediate<?,

    R> parent; long produced; ConcatMapInner(FluxConcatMapSupport<?, R> parent) { super(Operators.emptySubscriber()); this.parent = parent; } @Override public void onNext(R t) { produced++; parent.innerNext(t); } ...
  160. static final class ConcatMapInner<R> extends Operators.MultiSubscriptionSubscriber<R, R> { final FluxConcatMapImmediate<?,

    R> parent; long produced; ConcatMapInner(FluxConcatMapSupport<?, R> parent) { super(Operators.emptySubscriber()); this.parent = parent; } @Override public void onNext(R t) { produced++; parent.innerNext(t); } ...
  161. static final class ConcatMapInner<R> extends Operators.MultiSubscriptionSubscriber<R, R> { ... @Override

    public void onError(Throwable t) { long p = produced; if (p != 0L) { produced = 0L; produced(p); } parent.innerError(t); } ... }
  162. static final class ConcatMapInner<R> extends Operators.MultiSubscriptionSubscriber<R, R> { ... @Override

    public void onComplete() { long p = produced; if (p != 0L) { produced = 0L; produced(p); } parent.innerComplete(); } } }
  163. au plus profond: la drain loop

  164. Au plus profond: la `drain loop` gérer les race conditions

    work stealing
  165. void drain() { if (WIP.getAndIncrement(this) == 0) { for (;

    ; ) { if (cancelled) return; if (!active) { ... } if (WIP.decrementAndGet(this) == 0) { break; } } } } }
  166. void drain() { if (WIP.getAndIncrement(this) == 0) { for (;

    ; ) { if (cancelled) return; if (!active) { ... } if (WIP.decrementAndGet(this) == 0) { break; } } } } }
  167. if (cancelled) return; if (!active) { boolean d = done;

    T v; try { v = queue.poll(); } catch (Throwable e) { actual.onError(e); return; } boolean empty = v == null; if (d && empty) { actual.onComplete(); return; } ...
  168. ... if (!empty) { Publisher<? extends R> p; try {

    p = Objects.requireNonNull(mapper.apply(v)); } catch (Throwable e) { actual.onError(e); return; } if (sourceMode != Fuseable.SYNC) { ... } active = true; p.subscribe(inner); } } if (WIP.decrementAndGet(this) == 0) { break; }
  169. ... if (sourceMode != Fuseable.SYNC) { int c = consumed

    + 1; if (c == limit) { consumed = 0; s.request(c); } else { consumed = c; } } active = true; p.subscribe(inner); } } if (WIP.decrementAndGet(this) == 0) { break; }
  170. work stealing

  171. void drain() { if (WIP.getAndIncrement(this) == 0) { for (;

    ; ) { if (cancelled) return; if (!active) { ... } if (WIP.decrementAndGet(this) == 0) { break; } } } } }
  172. thread 1 thread 2 thread 3 sans work stealing onNext(3)

    empile 3 1, 2, 3
  173. thread 1 thread 2 thread 3 sans work stealing onNext(3)

    empile 3 request( 1 ) émet 1 2, 3
  174. request( 2 ) thread 1 thread 2 thread 3 sans

    work stealing émet 2, 3 onNext(3) empile 3 request( 1 ) émet 1
  175. thread 1 thread 2 thread 3 sans work stealing onNext(3)

    empile 3 bloqué draine bloqué draine
  176. thread 1 thread 2 thread 3 avec work stealing onNext(3)

    empile 3 1, 2, 3
  177. thread 1 thread 2 thread 3 avec work stealing onNext(3)

    empile 3 request( 1 ) émet 1 2, 3
  178. request( 2 ) thread 1 thread 2 thread 3 avec

    work stealing onNext(3) empile 3 request( 1 ) émet 1 émet 2, 3
  179. request( 2 ) thread 1 thread 2 thread 3 avec

    work stealing onNext(3) empile 3 request( 1 ) émet 1 émet 2, 3 met juste la requête à jour et quitte la boucle WIP cette drainLoop a “gagné”
  180. Conclusion

  181. Questions?

  182. Merci !