Traitement de la contre-pression avec RxJava

Faire face à la contre-pression avec RxJava

1. Vue d'ensemble

Dans cet article, nous verrons comment leRxJava library nous aide à gérer la contre-pression.

En termes simples - RxJava utilise un concept de flux réactifs en introduisantObservables, auxquels un ou plusieursObservers peuvent s'abonner. Dealing with possibly infinite streams is very challenging, as we need to face a problem of a backpressure.

Il n’est pas difficile de se retrouver dans une situation dans laquelle unObservable émet des éléments plus rapidement qu’un abonné ne peut les consommer. Nous examinerons les différentes solutions au problème de l’amortissement croissant des objets non consommés.

2. Observables chaud contreObservables froid

Tout d'abord, créons une fonction consommateur simple qui sera utilisée comme consommateur d'éléments deObservables que nous définirons plus tard:

public class ComputeFunction {
    public static void compute(Integer v) {
        try {
            System.out.println("compute integer v: " + v);
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Notre fonctioncompute() imprime simplement l'argument. La chose importante à noter ici est l'invocation d'une méthodeThread.sleep(1000) - nous le faisons pour émuler une tâche longue qui fera queObservable se remplira d'éléments plus rapidement queObserver peut consommer leur.

Nous avons deux types deObservables – Hot etCold - qui sont totalement différents en ce qui concerne la gestion de la contre-pression.

2.1. FroidObservables

UnObservable froid émet une séquence particulière d'items mais peut commencer à émettre cette séquence lorsque sonObserver le trouve convenable, et à quelque taux que leObserver désire, sans perturber l'intégrité du séquence. Cold Observable is providing items in a lazy way.

LeObserver ne prend des éléments que lorsqu'il est prêt à traiter cet élément, et les éléments n'ont pas besoin d'être mis en mémoire tampon dans unObservable car ils sont demandés de manière pull.

Par exemple, si vous créez unObservable basé sur une plage statique d'éléments de un à un million, ceObservable émettrait la même séquence d'éléments quelle que soit la fréquence à laquelle ces éléments sont observés:

Observable.range(1, 1_000_000)
  .observeOn(Schedulers.computation())
  .subscribe(ComputeFunction::compute);

Lorsque nous démarrons notre programme, les éléments seront calculés parObserver paresseusement et seront demandés de manière pull. La méthodeSchedulers.computation() signifie que nous voulons exécuter nosObserver dans un pool de threads de calcul enRxJava.

La sortie d'un programme sera constituée d'un résultat d'une méthodecompute() invoquée pour un par un élément à partir d'unObservable:

compute integer v: 1
compute integer v: 2
compute integer v: 3
compute integer v: 4
...

LesObservablesfroids n'ont pas besoin d'avoir une quelconque forme de contre-pression car ils fonctionnent en mode pull. Des exemples d'éléments émis par unObservable froid peuvent inclure les résultats d'une requête de base de données, d'une récupération de fichier ou d'une requête Web.

2.2. ChaudObservables

Un hotObservable commence à générer des éléments et les émet immédiatement lorsqu'ils sont créés. C'est contraire au modèle de traitement pull de ColdObservables. Hot Observable emits items at its own pace, and it is up to its observers to keep up.

Lorsque leObserver n'est pas capable de consommer des éléments aussi rapidement qu'ils sont produits par unObservable, ils doivent être mis en mémoire tampon ou gérés d'une autre manière, car ils rempliront la mémoire, provoquant finalementOutOfMemoryException.

Prenons un exemple deObservable, chaud qui produit un million d'articles à un consommateur final qui traite ces articles. Lorsqu'une méthodecompute() dans leObserver prend un certain temps pour traiter chaque élément, leObservable commence à remplir une mémoire avec des éléments, provoquant l'échec d'un programme:

PublishSubject source = PublishSubject.create();

source.observeOn(Schedulers.computation())
  .subscribe(ComputeFunction::compute, Throwable::printStackTrace);

IntStream.range(1, 1_000_000).forEach(source::onNext);

L’exécution de ce programme échouera avec unMissingBackpressureException car nous n’avons pas défini de moyen de gérer la surproduction deObservable.

Des exemples d'éléments émis par unObservable chaud peuvent inclure des événements de souris et de clavier, des événements système ou des cours de bourse.

3. Tampon de surproductionObservable

La première façon de gérer la surproduction deObservable est de définir une sorte de tampon pour les éléments qui ne peuvent pas être traités par unObserver.

Nous pouvons le faire en appelant une méthodebuffer():

PublishSubject source = PublishSubject.create();

source.buffer(1024)
  .observeOn(Schedulers.computation())
  .subscribe(ComputeFunction::compute, Throwable::printStackTrace);

Définir un tampon d'une taille de 1024 donnera àObserver un peu de temps pour rattraper une source surproductrice. Le tampon stockera les éléments qui n'ont pas encore été traités.

Nous pouvons augmenter la taille de la mémoire tampon pour laisser suffisamment de place aux valeurs produites.

Notez cependant qu'en général,this may be only a temporary fix comme le débordement peut encore se produire si la source surproduit la taille de tampon prévue.

4. Articles émis par lots

Nous pouvons regrouper des articles surproduits dans des fenêtres de N éléments.

LorsqueObservable produit des éléments plus rapidement queObserver ne peut les traiter, nous pouvons atténuer cela en regroupant les éléments produits ensemble et en envoyant un lot d'éléments àObserver qui est capable de traiter une collection d'éléments au lieu de l'élément un par un:

PublishSubject source = PublishSubject.create();

source.window(500)
  .observeOn(Schedulers.computation())
  .subscribe(ComputeFunction::compute, Throwable::printStackTrace);

L'utilisation de la méthodewindow() avec l'argument500, indiquera àObservable de regrouper les éléments dans les lots de taille 500. Cette technique peut réduire un problème de surproduction deObservable lorsqueObserver est capable de traiter un lot d'éléments plus rapidement que de traiter les éléments un par un.

5. Sauter des éléments

Si certaines des valeurs produites parObservable peuvent être ignorées en toute sécurité, nous pouvons utiliser l'échantillonnage dans un temps et des opérateurs de limitation spécifiques.

Les méthodessample() etthrottleFirst() prennent la durée comme paramètre:

  • La méthode sample() examine périodiquement la séquence des éléments et émet le dernier élément qui a été produit pendant la durée spécifiée en paramètre

  • La méthodethrottleFirst() émet le premier élément qui a été produit après la durée spécifiée en paramètre

La durée est une heure après laquelle un élément spécifique est sélectionné dans la séquence des éléments produits. Nous pouvons spécifier une stratégie pour gérer la contre-pression en sautant des éléments:

PublishSubject source = PublishSubject.create();

source.sample(100, TimeUnit.MILLISECONDS)
  .observeOn(Schedulers.computation())
  .subscribe(ComputeFunction::compute, Throwable::printStackTrace);

Nous avons précisé que la stratégie de saut d'éléments sera une méthodesample(). Nous voulons un échantillon d'une séquence d'une durée de 100 millisecondes. Cet élément sera émis vers lesObserver.

Rappelez-vous cependant que ces opérateurs ne réduisent le taux de réception de valeur que par lesObserver en aval et qu'ils peuvent donc encore conduire à desMissingBackpressureException.

6. Manipulation d'un tampon de remplissageObservable

Dans le cas où nos stratégies d'échantillonnage ou de mise en lots d'éléments n'aident pas à remplir un tampon,, nous devons implémenter une stratégie de gestion des cas où un tampon se remplit.

Nous devons utiliser une méthodeonBackpressureBuffer() pour éviter lesBufferOverflowException.

La méthodeonBackpressureBuffer() prend trois arguments: une capacité d'un tamponObservable, une méthode qui est appelée quand un tampon se remplit et une stratégie pour gérer les éléments qui doivent être supprimés d'un tampon. Les stratégies de débordement sont dans une classeBackpressureOverflow.

Il existe 4 types d’actions pouvant être exécutées lorsque la mémoire tampon est pleine:

  • ON_OVERFLOW_ERROR – c'est le comportement par défaut signalant unBufferOverflowException lorsque le tampon est plein

  • ON_OVERFLOW_DEFAULT – actuellement c'est le même queON_OVERFLOW_ERROR

  • ON_OVERFLOW_DROP_LATEST - si un débordement se produit, la valeur actuelle sera simplement ignorée et seules les anciennes valeurs seront livrées une fois que les demandesObserver en aval

  • ON_OVERFLOW_DROP_OLDEST - supprime l'élément le plus ancien du tampon et y ajoute la valeur actuelle

Voyons comment spécifier cette stratégie:

Observable.range(1, 1_000_000)
  .onBackpressureBuffer(16, () -> {}, BackpressureOverflow.ON_OVERFLOW_DROP_OLDEST)
  .observeOn(Schedulers.computation())
  .subscribe(e -> {}, Throwable::printStackTrace);

Ici, notre stratégie pour gérer le tampon débordant consiste à déposer l'élément le plus ancien dans un tampon et à ajouter le dernier élément produit par unObservable.

Notez que les deux dernières stratégies provoquent une discontinuité dans le flux lorsqu'elles suppriment des éléments. De plus, ils ne signaleront pasBufferOverflowException.

7. Suppression de tous les éléments surproduits

Chaque fois que leObserver en aval n'est pas prêt à recevoir un élément, nous pouvons utiliser une méthodeonBackpressureDrop() pour supprimer cet élément de la séquence.

On peut considérer cette méthode comme une méthodeonBackpressureBuffer() avec une capacité d'un buffer mis à zéro avec une stratégieON_OVERFLOW_DROP_LATEST.

Cet opérateur est utile lorsque nous pouvons ignorer en toute sécurité les valeurs d'une sourceObservable (comme les mouvements de souris ou les signaux de localisation GPS actuels) car il y aura plus de valeurs à jour plus tard:

Observable.range(1, 1_000_000)
  .onBackpressureDrop()
  .observeOn(Schedulers.computation())
  .doOnNext(ComputeFunction::compute)
  .subscribe(v -> {}, Throwable::printStackTrace);

La méthodeonBackpressureDrop() élimine un problème de surproduction deObservable mais doit être utilisée avec prudence.

8. Conclusion

Dans cet article, nous avons examiné un problème de surproduction deObservable et les moyens de gérer une contre-pression. Nous avons examiné les stratégies de mise en mémoire tampon, de mise en lots et de saut d'éléments lorsque leObserver n'est pas capable de consommer des éléments aussi rapidement qu'ils sont produits par unObservable.

L'implémentation de tous ces exemples et extraits de code peut être trouvée dans leGitHub project - il s'agit d'un projet Maven, il devrait donc être facile à importer et à exécuter tel quel.