Un besoin récurrent est d'exécuter un traitement en boucle. Mulesoft propose essentiellement trois mécanismes pour cela: le composant "ForEach", le composant "Batch" et... le composant "UntilSucessfull". Si vous devez exécuter une boucle de type "While" aucune de ces trois solutions ne convient:
- La boucle "ForEach" parcourt une collection et donc le nombre d'itérations est déterminé à priori. Or l'intérêt d'une boucle "While" est justement de réévaluer constamment la condition d'arrêt, celle-ci dépendant généralement du résultat de l'exécution de la dernière itération, résultat soumis éventuellement à toute sorte d'aléas. Comme Mulesoft ne propose pas de collection qui "s'auto-construisent", "ForEach" ne convient pas.
- Le "Batch" est un raffinement de la logique "ForEach", structurant le traitement à appliquer à chaque objet traité. Il présente cependant la même limitation que "ForEach": la définition à priori de la collection à traiter.
- Le comportement d'"UntilSuccesfull" est proche de celui souhaité, mais ce composant est spécialisé dans le traitement d'erreurs et son détournement pour implémenter une boucle "While" provoque de nombreux effets de bords : une "continuation" est matérialisée par une exception (et donc logger comme telle), interférence avec les "vraies" exceptions, restauration automatique du message d'entrée à chaque tentative, etc.
Diverses autres solutions sont proposées sur Internet comme l'appel récursif d'un flot dont l'arrêt est contrôlé par un "Choice". Cette solution fonctionne si le nombre d'itérations est très réduit (une grosse dizaine ?). De plus, le "prix" en terme de performance est élevé.
Une autre solution est d'utiliser un mécanisme de messagerie (style VM): on poste un message dans une queue. Le flot de traitement écoute la queue et exécute le fonction demandée. Si cette fonction dépose un message dans le queue, alors la boucle "While" continue, sinon, elle s'arrête. Cette solution présente des désavantages importants: caractère fondamentalement asynchrone, parallélisation problématique, messages orphelins en cas d'incidents.
Je pense disposer d'une solution très supérieure, même si elle présente quelques limites dues à "l'acharnement philosophique" 😁 des développeurs de la plateforme Mulesoft (je m'en explique après).
L'idée est d'utiliser DataWeave (qui est LA solution pour la plupart des problèmes présentés dans ce blog). Nous allons traiter ici le cas récurent d'une API REST qui fournit une liste paginée d'objets, chaque page contenant dans sa description l'URL de la page suivante. Partons du principe que nous avons un sous flow (nommé "flow") capable de traiter une page. Ce sous flot reçoit l'URL de la page à traiter et renvoie - si elle existe - l'URL de la page suivante (et null sinon). Ces URL transitent par le payload (c'est indispensable comme expliqué après). La boucle "while" est matérialisée par un "TransformMessage" dont le script est le suivant:
%dw 2.0
import * from dw::core::Strings
output application/json
fun loookup(n, url) = log(url) then if (sizeOf(url)==0) null else substring(url, 0, sizeOf(url)-1)
@TailRec fun while(n, url) = if (url == null) 0 else n while (n loookup url)
---
while("flow", "/my/url/to/invoke")
Afin que l'exemple puisse être exécutée dans le DataWeave playground, la fonction lookup a été renommée loookup (avec 3 o) et une implémentation de test lui est donné : elle renvoie l'URL reçue diminuée du dernier caractère, histoire d'avoir une fin. Là, n'est pas l'important.
La ligne utile est celle qui commence par @TailRec. C'est une récursion dite TCO (Terminal Call Optimization), c'est à dire que la toute dernière chose que fait la fonction "while" est de se rappeler. Dans ce cas, l'exécuteur (DataWeave dans notre cas) est capable de transformer l'appel récursif en une itération ! Aucun débordement de pile n'est à craindre. Cet avantage n'est pas une coquetterie technique: l'empilement de fonctions dans DataWeave n'est pas infini (de l'ordre de 256 ?) et donc, un long processus peut parfaitement provoquer un débordement de pile.
Cette solution semble "parfaite": répondant exactement au besoin sans effet de bord type génération de messages ou d'erreurs qui déborderaient de la boucle, exécution synchrone, comportement exacte d'une boucle "While". C'est presque vrai.
Presque parce qu'il y a un "Mais". Afin de préserver l'aspect "fonctionnel pur" de DataWeave, les concepteurs du langage ont imposé à la méthode lookup une limitation de taille: celle de ne pas partager le Message Mulesoft: un nouveau message est crée pour chaque invocation de lookup. Adieu donc les partages d'attributs et de variables. Lookup se comporte un peu comme l'appel d'un flot via un Requêteur HTTP. On lui passe un payload, il renvoie un payload, seul mécanisme d'échange d'information. Si donc des informations doivent être remontées vers l'appelant, elles doivent être incluses dans le payload.
_______________________________________________________________________________
A recurring need is to execute processing in a loop. Mulesoft essentially offers three mechanisms for this: the "ForEach" component, the "Batch" component and... the "UntilSucessfull" component. If you need to execute a "While" loop, none of these three solutions is suitable:
- The "ForEach" loop traverses a collection, so the number of iterations is determined a priori. But the point of a "While" loop is to constantly re-evaluate the stopping condition, which generally depends on the result of the last iteration, a result that may be subject to all kinds of contingencies. As Mulesoft does not offer "self-constructing" collections, "ForEach" is not suitable.
- Batch" is a refinement of "ForEach" logic, structuring the processing to be applied to each object processed. However, it has the same limitation as "ForEach": the a priori definition of the collection to be processed.
- The behavior of "UntilSuccesfull" is close to that desired, but this component is specialized in error processing and its detour to implement a "While" loop causes numerous side effects: a "continuation" is materialized by an exception (and therefore logged as such), interference with "real" exceptions, automatic restoration of the input message on each attempt, etc.
Various other solutions are proposed on the Internet, such as the recursive call of a flow whose stop is controlled by a "Choice". This solution works if the number of iterations is very small (a dozen or so?). Moreover, the "price" in terms of performance is high.
Another solution is to use a messaging mechanism (VM-style): you post a message in a queue. The processing flow listens to the queue and executes the requested function. If this function posts a message in the queue, the "While" loop continues, otherwise it stops. This solution has major drawbacks: fundamentally asynchronous nature, problematic parallelization, orphan messages in the event of incidents.
I think I have a very superior solution, even if it has a few limitations due to the "philosophical relentlessness" 😁 of the developers of the Mulesoft platform (I'll explain later).
The idea is to use DataWeave (which is THE solution for most of the problems presented in this blog). We'll deal here with the recurring case of a REST API that provides a paginated list of objects, each page containing in its description the URL of the next page. Let's assume that we have a sub-flow (named "flow") capable of processing a page. This sub-flow receives the URL of the page to be processed and returns - if it exists - the URL of the next page (and null if it doesn't). These URLs pass through the payload (this is essential, as explained below). The "while" loop is materialized by a "TransformMessage" whose script is as follows:
%dw 2.0
import * from dw::core::Strings
output application/json
fun loookup(n, url) = log(url) then if (sizeOf(url)==0) null else substring(url, 0, sizeOf(url)-1)
@TailRec fun while(n, url) = if (url == null) 0 else n while (n loookup url)
---
while("flow", "/my/url/to/invoke")
So that the example can be run in the DataWeave playground, the lookup function has been renamed loookup (with 3 o's) and given a test implementation: it returns the URL received, minus the last character, just to make sure there's an end. That's not the point.
The useful line is the one beginning with @TailRec. This is a so-called TCO (Terminal Call Optimization) recursion, i.e. the very last thing the "while" function does is recall. In this case, the executor (DataWeave in our case) is able to transform the recursive call into an iteration! No stack overflow to worry about. This advantage is not a technical quirk: the stack of functions in DataWeave is not infinite (of the order of 256?), so a long process can perfectly well cause a stack overflow.
This solution seems "perfect": it responds exactly to the need, with no side-effects such as the generation of messages or errors that would overflow the loop, synchronous execution and the exact behavior of a "While" loop. This is almost true.
Almost because there's a "But". In order to preserve the "pure functionality" of DataWeave, the language's designers imposed a major limitation on the lookup method: that of not sharing the Mulesoft Message: a new message is created for each invocation of lookup. So good-bye attribute and variable sharing! Lookup behaves a little like calling a flowvia an HTTP Requester. You pass it a payload, and it returns a payload, the only mechanism for exchanging information. If any information needs to be sent back to the caller, it must be included in the payload.
Aucun commentaire:
Enregistrer un commentaire