mardi 20 février 2024

Création d'un document JSON par étape / Step-by-step creation of a JSON document

Générer un fichier de données est à priori une tâche aisée avec Mulesoft. Sur fichier et par SFTP, divers composants permettent d'écrire (Write - mode OVERWRITE) ou de compléter (Write - mode APPEND) le contenu d'un fichier. Un besoin récurrent est de créer un fichier au format JSON car l'usage de CSV - plus simple - est parfois insuffisant car très fragile:

  1. Le choix d'un séparateur est parfois un dilemme cornélien (quel est le caractère imprimable dont on n'est sûr qu'il ne sera JAMAIS utilisé, dans un environnement de plus en plus international)
  2. Comment intégrer dans un fichier des données de type texte (contenant des retours à la ligne) que CSV interprète faussement comme étant plusieurs lignes de données distinctes
  3. CSV est mal adapté pour contenir des données structurées (des objets contenant une ou plusieurs sous listes).

Le problème est que s'il est très facile de créer un fichier contenant une liste au format JSON en une seule opération, le faire en plusieurs appels n'est pas évident. En effet, écrire plusieurs sous listes au format JSON ne constitue pas un document bien formé car JSON impose qu'un document ne contienne qu'un seul objet racine (objet ou tableau). Or constituer une unique liste a écrire d'un seul coup n'est pas toujours possible:

  1. soit parce que la liste est trop longue (des centaines de milliers ou des millions de lignes)
  2. soit parce que les données à collecter le sont dans des processus successifs et non dans un unique processus (par lectures successives d'une source de données par exemple)

Construire une liste JSON par enrichissements successifs est en fait assez simple à réaliser. Comme bien des développeurs se sentent démunis face à ce problème, je fournis ici la manière de faire. Nous allons construire trois sous-flots:

  1. un flot d'ouverture qui entame la liste. Ce flot doit être appelé en premier lieu, une unique fois (obligatoire),
  2. un flot d'enrichissement qui peut être appelé autant de fois que nécessaire (éventuellement pas du tout),
  3. un flot de clôture qui ferme la liste. Ce flot doit être appelé en dernier lieu, une unique fois (obligatoire).

Le flot d'ouverture doit ressembler à celui-ci:


Le code Dataweave qui assure le formatage du contenu à écrire est le suivant:

%dw 2.0
output text/plain
---
"[" ++ (payload map write($, "application/json") reduce ($$ ++ ",\n" ++ $))

Bien que ne tenant que sur quatre lignes, cette snippet mérite quelques explications:

  1. Le format de sortie est "text/plain" afin que Mulesoft ne nous entrave pas pour des questions de format. En effet, l'objet que nous allons écrire, sera incomplet et donc rejeté par JSON.
  2. La structure du tableau (c'est à dire le "[" ouvrant et le "," qui sépare les objets de la liste sont ajoutés par la snippet elle-même. La description des objets est elle déléguée à la fonction DataWeave "Write".
  3. L'écriture se fait en mode OVERWRITE pour s'assurer que le contenu du fichier est correctement réinitialisé.
  4. Noter l'absence du "]" fermant, permettant de venir enrichir le document après ce premier appel. Noter aussi que pour le document doit bien formé, il ne manque que ce "]".

Le flot d'enrichissement doit ressembler à celui-ci:


Le code Dataweave qui assure le formatage de l'enrichissement est le suivant:

%dw 2.0
output text/plain
---
",\n" ++ (payload map write($, "application/json") reduce ($$ ++ ",\n" ++ $))

 Cette snippet ressemble beaucoup à la précédente. Pointons les quelques différences:

  1. La continuité de la structure du tableau, c'est à dire le "," qui permet de rajouter des produits à la liste déjà présente et le "," qui sépare les objets de la suite de la liste sont ajoutés par la snippet elle-même.
  2. L'écriture se fait en mode APPEND pour que le contenu déjà présent ne soit pas écrasé.
  3. Noter l'absence des "[" ouvrant et "]" fermant, permettant de s'inscrire dans l'enrichissement du document.

Le flot de clôture est beaucoup plus simple. On se contente, en me APPEND, d'écrire le "]" fermant.

Ce système fonctionne quelque soit la taille de la liste (y comprit une liste vide) à deux conditions:

  1. qu'un segment de liste ne soit pas trop grand (et "tienne" dans une chaine de caractères DataWeave),
  2. que le flot d'ouverture ne présente une liste vide QUE si la liste globale est réellement vide, sinon, elle doit contenir au moins un objet.
  3. que le flot d'enrichissement ne présente JAMAIS une liste vide (elle doit contenir au moins un objet).

________________________________________________________________________

Generating a data file is, on the face of it, an easy task with Mulesoft. On file and via SFTP, various components enable you to write (Write - OVERWRITE mode) or complete (Write - APPEND mode) the contents of a file. A recurring need is to create a file in JSON format, as the simpler CSV format is sometimes insufficient due to its fragility:

  1. Choosing a separator is sometimes a Cornelian dilemma (which printable character can you be sure will NEVER be used, in an increasingly international environment).
  2. How to integrate text data (containing line breaks) into a file, which CSV misinterprets as several separate lines of data?
  3. CSV is ill-suited to holding structured data (objects containing one or more sub-lists).

The problem is that while it's very easy to create a file containing a list in JSON format in a single operation, doing so in several calls is not straightforward. Indeed, writing several sub-lists in JSON format does not constitute a well-formed document, as JSON requires that a document contain only one root object (object or array). However, it's not always possible to write a single list all at once:

  1. either because the list is too long (hundreds of thousands or millions of lines)
  2. or because the data to be collected are collected in successive processes rather than in a single process (by successive readings of a data source, for example).

Building a JSON list by successive enrichments is actually quite simple to achieve. As many developers find themselves at a loss when confronted with this problem, I've provided a step-by-step guide here. We're going to build three sub-flows:

  1. an opening stream that starts the list. This stream must be called first, once only (mandatory),
  2. an enrichment stream, which can be called as many times as necessary (or not at all),
  3. a closing flow, which closes the list. This stream must be called last, once only (mandatory).

The opening flow should look like this:


The Dataweave code used to format the content to be written is as follows:

%dw 2.0
output text/plain
---
"[" ++ (payload map write($, "application/json") reduce ($$ ++ ",\n" ++ $))

Although only four lines long, this snippet deserves some explanation:

  1. The output format is "text/plain", so that Mulesoft doesn't hinder us with format questions. Indeed, the object we're about to write will be incomplete and therefore rejected by JSON.
  2. The structure of the array (i.e. the opening "[" and the "," separating the objects in the list) is added by the snippet itself. Object description is delegated to the DataWeave "Write" function.
  3. Writing is done in OVERWRITE mode to ensure that file contents are correctly reset.
  4. Note the absence of the closing "]", allowing the document to be enriched after this first call. Note also that, for the well-formed document, only the "]" is missing.

The enrichment flow should look like this:


The Dataweave code used for enrichment formatting is as follows:

%dw 2.0
output text/plain
---
",\n" ++ (payload map write($, "application/json") reduce ($$ ++ ",\n" ++ $))

This snippet is very similar to the previous one. Let's point out the few differences:

  1. The continuity of the table structure, i.e. the "," that allows products to be added to the list already present and the "," that separates objects from the rest of the list are added by the snippet itself.
  2. Writing is done in APPEND mode, so that existing content is not overwritten.
  3. Note the absence of opening and closing "[" and "]", allowing you to take part in the enrichment of the document.

The closing flow is much simpler. In APPEND mode, you simply write the closing "]".

This system works regardless of the size of the list (including an empty list) on two conditions:

  1. a list segment is not too large (and "fits" into a DataWeave string),
  2. the opening stream presents an empty list ONLY if the global list is truly empty, otherwise it must contain at least one object.
  3. the enrichment flow NEVER presents an empty list (it must contain at least one object).


Regroupement et Structuration / Grouping and structuring

Voici un article qui devrait vous être très utile: comment transformer un CSV en une liste d'objets structurés. On me dira: mais c'est très simple avec Mulesoft, il suffit de passer de application/csv à application/json et le tour et joué. C'est le cas, en effet, pour des objets strictement tabluaires, c'est à dire que chaque ligne représente un objet qui ne contient que des champs à valeur litérale. Mais quand est-il si l'objet est structuré, c'est à dire si N lignes de CSV représente UN objet contenant un champ évalué avec une sous liste de N objets ? Là, il faut effectuer une transformation. C'est le principe de celle-ci dont il est question dans cet article.

Entrons dans le concret. Soit l'exemple sylvestre suivant, qui décrit des arbres dans une foret. Les colonnes communes sont forest, tree, trunk et root. Les informations à mettre dans les objets secondaires (les branches des arbres) sont branch et leave. Enfin, les informations qui permettent d'identifier les arbres sont forest et tree (et qui sont aussi des données communes). Donc à l'entrée, on a ça:

forest,tree,trunk,root,branch,leaves
f,A,12cm,3m,1,110
f,A,12cm,3m,2,220
F,A,18cm,8m,1,150
F,A,18cm,8m,2,270
f,B,110cm,12m,1,310
f,B,110cm,12m,2,320
f,B,110cm,12m,3,340
f,C,16cm,4m,1,230

Et en sortie, on veut obtenir cela:

[
{
"forest": "f",
"tree": "A",
"trunk": "12cm",
"root": "3m",
"branches": [
{
"branch": "1",
"leaves": "110"
},
{
"branch": "2",
"leaves": "220"
}
]
},
{
"forest": "F",
"tree": "A",
"trunk": "18cm",
"root": "8m",
"branches": [
{
"branch": "1",
"leaves": "150"
},
{
"branch": "2",
"leaves": "270"
}
]
},
{
"forest": "f",
"tree": "B",
"trunk": "110cm",
"root": "12m",
"branches": [
{
"branch": "1",
"leaves": "310"
},
{
"branch": "2",
"leaves": "320"
},
{
"branch": "3",
"leaves": "340"
}
]
},
{
"forest": "f",
"tree": "C",
"trunk": "16cm",
"root": "4m",
"branches": [
{
"branch": "1",
"leaves": "230"
}
]
}
]

La fonction qui permet de construire les objets structurés est la suivante. Elle a été écrite afin d'être générique dans le cas d'objets ne contenant qu'une sous liste d'objets simples (donc non structurés à leur tour) afin de pouvoir être utilisée tel quel. C'est le cas le plus courant et elle devrait donc être suffisante dans la majorité des cas. Il faudra l'adapter pour qu'elle puisse générer des objets contenant plusieurs sous listes, ou des objets qui ont des listes d'objets possédant eux même leur propre sous liste.

Cette fonction prend quatre paramètres:

  1. lst: la liste des objets tabulaires bruts provenant de CSV
  2. id: un tableau contenant les noms des champs identifiants les objets
  3. common: un tableau contenant les noms des champs commun (c'est à dire à placer dans les objets racines). Les champs qui ne sont pas dans cette liste commune sont automatiquement affectés aux objets inclus.
  4. sublist: le nom du champ de l'objet principal qui désigne la liste des objets inclus.

%dw 2.0
output application/json
fun group(lst, id, common, sublist) = lst
groupBy ((ln) -> (id reduce (fld, acc="")-> acc ++ ln[fld] ++ ","))
mapObject ((o, i) -> (i):(
(common reduce (v, acc={})->acc ++ (v):o[0][v]))
++ (sublist): o map (b) -> b -- common
)
pluck $
---
group(payload, ["forest", "tree"],
    ["forest", "tree", "trunk", "root"], "branches")

Le principe de cette fonction est le suivant:

  1. Les lignes CSV sont d'abord regroupées en fonction d'une "clé" qui est la concaténation des valeurs des champs d'identification (par groupBy)
  2. Ensuite chaque sous liste obtenue dans l'étape précédente est passée à une réduction qui copie les champs "communs" de la première ligne dans un objet
  3. Les champs communs de chaque ligne de la sous liste sont retranchés (b -- common) et la sous liste est ajoutée sous la forme d'un champ de l'objet créé lors de l'étape précedevte (++ (sublist):)  
  4. Par un appel à pluck, on transforme la Map obtenue par GroupBy en une liste.
________________________________________________________________________

Here's an article that should come in very handy: how to transform a CSV into a list of structured objects. Some people will say: but it's very simple with Mulesoft, you just have to switch from application/csv to application/json and you're done. This is indeed the case for strictly tabular objects, i.e. where each line represents an object containing only literal fields. But what if the object is structured, i.e. if N CSV lines represent ONE object containing a field evaluated with a sub-list of N objects? In this case, a transformation is required. That's what this article is all about.

Let's get down to the nitty-gritty. Consider the following sylvan example, which describes trees in a forest. The common columns are forest, tree, trunk and root. Secondary objects (tree branches) are branch and leave. Finally, the information used to identify the trees is forest and tree (which are also common data). So this is the input:

forest,tree,trunk,root,branch,leaves
f,A,12cm,3m,1,110
f,A,12cm,3m,2,220
F,A,18cm,8m,1,150
F,A,18cm,8m,2,270
f,B,110cm,12m,1,310
f,B,110cm,12m,2,320
f,B,110cm,12m,3,340
f,C,16cm,4m,1,230

And that's what we want to achieve:

[
{
"forest": "f",
"tree": "A",
"trunk": "12cm",
"root": "3m",
"branches": [
{
"branch": "1",
"leaves": "110"
},
{
"branch": "2",
"leaves": "220"
}
]
},
{
"forest": "F",
"tree": "A",
"trunk": "18cm",
"root": "8m",
"branches": [
{
"branch": "1",
"leaves": "150"
},
{
"branch": "2",
"leaves": "270"
}
]
},
{
"forest": "f",
"tree": "B",
"trunk": "110cm",
"root": "12m",
"branches": [
{
"branch": "1",
"leaves": "310"
},
{
"branch": "2",
"leaves": "320"
},
{
"branch": "3",
"leaves": "340"
}
]
},
{
"forest": "f",
"tree": "C",
"trunk": "16cm",
"root": "4m",
"branches": [
{
"branch": "1",
"leaves": "230"
}
]
}
]

The function used to construct structured objects is as follows. It has been written to be generic in the case of objects containing only a sub-list of simple objects (i.e. unstructured in turn), so that it can be used as is. This is the most common case, and should therefore be sufficient in the majority of cases. It will need to be adapted so that it can generate objects containing several sub-lists, or objects that have lists of objects themselves possessing their own sub-lists.

This function takes four parameters:

  1. lst: the list of raw tabular objects from CSV
  2. id: an array containing the names of the fields identifying the objects
  3. common: an array containing the names of common fields (i.e. to be placed in the root objects). Fields not in this common list are automatically assigned to the included objects.
  4. sublist: the name of the main object field that designates the list of included objects.

%dw 2.0
output application/json
fun group(lst, id, common, sublist) = lst
groupBy ((ln) -> (id reduce (fld, acc="")-> acc ++ ln[fld] ++ ","))
mapObject ((o, i) -> (i):(
(common reduce (v, acc={})->acc ++ (v):o[0][v]))
++ (sublist): o map (b) -> b -- common
)
pluck $
---
group(payload, ["forest", "tree"],
    ["forest", "tree", "trunk", "root"], "branches")

This function works as follows:

  1. CSV rows are first grouped according to a "key" which is the concatenation of the values of the identifying fields (by groupBy).
  2. Next, each sub-list obtained in the previous step is passed to a reduction that copies the "common" fields of the first line into an object.
  3. The common fields of each line of the sublist are subtracted (b -- common) and the sublist is added as a field of the object created in the previous step (++ (sublist):)  
  4. A call to pluck transforms the Map obtained by GroupBy into a list.

lundi 19 février 2024

Un essai de formalisation de la qualité / An attempt to formalize quality

La maîtrise de la plateforme Mulesoft en général et de Dataweave en particulier étant en soi un sujet difficile, bien des développeurs s'arrêtent à un unique objectif: arriver au résultat demandé en passant par pertes et profit tout le reste. Une telle attitude est catastrophique car dès qu'on tente d'utiliser ce qui est fait en condition réelle, c'est le carnage: incidents à répétition, temps de réponse très au delà du prohibitif, impossibilité de comprendre ce qui se passe, etc, etc.

Dans toute réalisation informatique la question de la qualité est INCONTOURNABLE et le seul fait que "ça marche" est à des kilomètres d'être suffisant. Pour prendre une analogie, c'est toute la différence entre les mots "comestible" et "cuisine". Mais comme la notion de "qualité" en informatique est bien trop vague pour n'être autre chose qu'une invocation et un vœu pieux, permettez moi d'en apporter ici une définition un peu plus structurée. Selon mon analyse, la qualité se divise en sept critères, donnés ici par ordre décroissant d'importance:

  1. La Robustesse
  2. La Performance
  3. La Justesse
  4. La Résilience
  5. L'Observabilité
  6. L'Evolutivité
  7. La Lisibilité

Avant d'entrer dans le détail de chacun de ses critères, arrêtons nous un instant sur cette notion de priorité qui peut légitimement interroger. Les deux premiers critères sont ESSENTIELS, c'est à dire que s'ils sont mal remplis:

  1. L'application n'est pas utilisable et
  2. Il y a une forte probabilité pour que l'application ne soit pas amendable (et donc qu'il faut tout jeter !)

Les deux critères suivants sont CRUCIAUX, c'est à dire que s'ils ne sont pas remplis:

  1. L'application n'est pas utilisable mais
  2. L'application est amendable (et donc on peut corriger les problèmes, ouf !)

Les deux derniers critères sont SERIEUX, c'est à dire que s'il ne sont pas remplis:

  1. L'application est néanmoins utilisable mais
  2. Sa pérennité est menacée à terme

Donc lorsqu'il y a des arbitrages à faire, il faut bien respecter ces priorités. Les critères cruciaux menacent tout, y compris le travail accomplis: ils ne peuvent ne aucun cas être négligés et doivent être résolus dès qu'ils se présentent. Les deux suivants sont indispensable, mais ne provoque généralement qu'une perte de temps. Les deux derniers sont un "(Very) Nice to Have". Passons maintenant ces critères en revue:

La robustesse: c'est la capacité de l'application à s'exécuter sans incident, c'est à dire ne pas planter, ne pas freezer, ne pas entrer dans des verrous mortels. Tout le monde comprend que cet aspect est indispensable. Ce qui est moins courant, c'est la conscience que parfois, la robustesse est tellement mal gérée, qu'il est nécessaire de tout refaire pour l'assurer, d'où le caractère absolument critique de ce critère.

La performance: c'est la capacité de l'application à être économe des ressources à sa disposition, qu'il s'agisse du temps d'exécution (la définition la plus commune de la performance), mais aussi la mémoire utilisée, la bande passante réseau, l'espace disque, etc. C'est un critère qui est souvent négligé par les développeurs comme étant accessoire ou amendable. C'est souvent le cas, mais pas toujours et parfois, retrouver de bonnes performances nécessite de revoir toute la conception, d'où le danger inhérent de ce critère.

La justesse: c'est la capacité de l'application de répondre correctement, c'est à dire de restituer les informations ou les commandes attendues par l'utilisateur. C'est la qualité "business" de l'application. C'est très souvent LE critère privilégié des développeurs, et même très souvent le SEUL assuré. Sans en négliger son importance, il ne faut pas oublier que les problèmes de ce type sont - sauf cas très exceptionnels - corrigeables.

La résilience: c'est la capacité de l'application de répondre aux sollicitations agressives, erronées ou défaillantes. C'est la robustesse "externe" en quelque sorte. Un des sujets majeurs de la résilience est la sécurité (sous toutes ses formes). L'autre gros sujet, c'est la disponibilité des autres pièces du SI qui doivent communiquer avec l'application. La résilience est très souvent un sujet qui négligé et le corriger consiste en fait à écrire le code nécessaire, car il est manquant

L'Observabilité: c'est la capacité de l'application à fournir des informations sur son fonctionnement. On y inclus le système de log, l'usage possible d'un débogueurs, la génération de rapport d'exécution, etc. De cette aspect, dépend la faculté des développeurs à identifier la raison d'un problème.

L'évolutivité: c'est la capacité de l'application à être étendue afin d'intégrer de nouvelles fonctionnalités sans remise en cause fondamentale.

La lisibilité: c'est la capacité de l'application à être comprise par un développeur qui n'a pas - ou peu - participer à son écriture.

Il arrive parfois que certains critères soient en contradiction avec d'autres. C'est en particulier le cas entre celui des performances (qui peut entrainer un accroissement de la complexité) et ceux de l'évolutivité et de la lisibilité (qui supportent assez mal l'accroissement de la complexité). Pour faire les choix pertinents, il est nécessaire de se référer aux priorités telles que données ci-dessus et la gravité des problèmes rencontrés (par exemple, s'il est licite d'accroitre la complexité pour améliorer significativement des performances réellement insuffisantes, il ne l'est pas forcément pour faire des optimisations pour une réalisation déjà suffisamment performante).

______________________________________________________________

Because mastering the Mulesoft platform in general, and Dataweave in particular, is in itself a difficult subject, many developers focus on a single objective: to achieve the required result, and write off everything else. Such an attitude is catastrophic, because as soon as you try to use what's been done in real-life conditions, it's carnage: repeated incidents, response times that are far beyond prohibitive, the impossibility of understanding what's going on, etc., etc.

In all IT projects, the question of quality is INCREDIBLE, and the mere fact that "it works" is miles from being sufficient. To take an analogy, this is the difference between the words "edible" and "cooking". But as the notion of "quality" in computing is far too vague to be anything more than an invocation and wishful thinking, allow me to provide here a slightly more structured definition. According to my analysis, quality can be divided into seven criterion, listed here in descending order of importance:

  1. Robustness
  2. Performance
  3. Correctness
  4. Resilience
  5. Observability
  6. Scalability
  7. Legibility

Before going into the details of each of these criteria, let's stop for a moment to consider the notion of priority, which may legitimately raise questions. The first two criteria are ESSENTIAL, i.e. if they are not met, the application cannot be used:

  1. The application is not usable and
  2. There's a high probability that the application won't be amendable (and therefore has to be thrown out!).

The next two criteria are CRUCIAL, i.e. if they are not met:

  1. The application is not usable, but
  2. The application is amendable (and so the problems can be corrected, phew!)

The last two criteria are SERIOUS, i.e. if they are not met:

  1. The application is still usable, but
  2. Its long-term viability is threatened
Robustness: this is the ability of the application to run without incident, i.e. not to crash, freeze or enter deadlocks. Everyone understands that this aspect is essential. What is less common is the awareness that sometimes robustness is so poorly managed that it's necessary to redo everything to ensure it, hence the absolutely critical nature of this criterion.

Performance: this is the ability of the application to be economical with the resources at its disposal, whether in terms of execution time (the most common definition of performance), but also memory used, network bandwidth, disk space, etc. It's a criterion that is often overlooked in the design of applications. This is a criterion that is often overlooked by developers as being incidental or amendable. This is often the case, but not always, and sometimes restoring good performance requires a complete rethink of the design, hence the inherent danger of this criterion.

Correctness: this is the ability of the application to respond correctly, i.e. to deliver the information or commands expected by the user. This is the application's "business" quality. It is very often THE preferred criterion for developers, and very often the ONLY one guaranteed. While not neglecting its importance, it should not be forgotten that problems of this type are - except in very exceptional cases - correctable.

Resilience: the application's ability to respond to aggressive, erroneous or faulty requests. It's a kind of "external" robustness. One of the major subjects of resilience is security (in all its forms). The other major issue is the availability of other parts of the IS that need to communicate with the application. Resilience is very often a neglected subject, and correcting it means writing the necessary code, because it's missing.

Observability: this is the ability of the application to provide information about its operation. This includes the logging system, the possible use of debuggers, the generation of execution reports, etc. The ability of developers to identify the reason for a problem depends on this aspect.

Scalability: this is the ability of the application to be extended to integrate new functionalities without fundamental changes.

Readability: this is the ability of the application to be understood by a developer who has had little or no involvement in writing it.

Sometimes, certain criteria are in contradiction with others. This is particularly the case between performance (which can lead to an increase in complexity) and scalability and readability (which don't cope well with an increase in complexity). To make the right choices, it is necessary to refer to the priorities given above and the seriousness of the problems encountered (for example, while it is legitimate to increase complexity in order to significantly improve performance that is really insufficient, it is not necessarily the case to make optimizations for a project that is already sufficiently efficient).

vendredi 9 février 2024

Logger les temps d'exécution / Time Execution Logging

Qui n'a pas rencontré de problème de performance en écrivant des traitements DataWeave un peu complexe? Pour résoudre de tels problèmes, il est nécessaire  de pouvoir faire afficher le temps d'exécution de la procédure problématique et ensuite suivre ce temps d'exécution. Bien des développeurs ignorent que DataWeave possède tout ce qui est nécessaire  pour implémenter une telle fonctionnalité:

  1. la méthode now() permet de récupérer une estampille temporelle
  2. la méthode log() permet d'émettre un message de log
  3. il est possible de passer une fonction comme paramètre d'une autre fonction

Nous allons voir tout ça. Prenons par exemple un traitement qui s'exécute dans un temps non négligeable (elle construit un objet de 10000 champs, mais l'intérêt de l'article n'est pas là).

%dw 2.0
output application/json
---
(0 to 10000) reduce(v, acc={})->(acc ++ ("value"++(v as Number)):v)

Pour monitorer le temps d'exécution de ce traitement, nous allons définir puis utiliser une fonction "eval" que vous pourrez utiliser telle quelle dans vos projet:

%dw 2.0
output application/json
fun Now() = now() then (t)->(t as Number)*1000 + t.milliseconds
fun eval(prs) =
Now() then (t1)-> prs() then (r)->
log(Now() -t1)
then r
---
eval(()->
(0 to 10000) reduce(v, acc={})->(acc ++ ("value"++(v as Number)):v)
)

Quelques commentaires sur cette snippet:

  •  D'abord, il a été nécessaire de redéfinir une fonction Now au dessus de la fonction standard now() car "now() as a Number" retourne un nombre de secondes et pas de millisecondes (ce que fait now()).
  • La fonction "eval" reçoit une fonction en paramètre qu'elle exécute au milieu d'une séquence de trois appels. Les deux appels qui l'encadrent invoquent Now() et en retirent un délai qui est loggé. Noter que "eval" renvoie la valeur obtenue par évaluation de la fonction passée en paramètre: son comportement est donc complètement générique et transparent.
  • Noter le "()->" qui suit l'invocation d'éval. Il sert à transformer l'expression que l'on veux monitorer en une fonction.
  • "eval" est utilisé ici pour monitorer toute l'expression. Rien n'empêche de l'utiliser pour monitorer un morceau de l'expression. On peut écrire par exemple:
%dw 2.0
output application/json
fun Now() = now() then (t)->(t as Number)*1000 + t.milliseconds
fun eval(prs) =
Now() then (t1)-> prs() then (r)->
log(Now() -t1)
then r
---
(0 to 10000) reduce(v, acc={})->
    (eval(()->(acc ++ ("value"++(v as Number)):v)))

__________________________________________________________________________

Who hasn't encountered performance problems when writing complex DataWeave processes? To solve such problems, it is necessary to be able to display the execution time of the problematic procedure and then track this execution time. Many developers are unaware that DataWeave has everything needed to implement such a feature:

  1. the now() method can be used to retrieve a time stamp
  2. the log() method can be used to send a log message
  3. a function can be passed as a parameter to another function.

Let's take a look at all this. Let's take, for example, a process that takes a considerable amount of time to run (it builds an object with 10,000 fields, but that's not the point of this article).

%dw 2.0
output application/json
---
(0 to 10000) reduce(v, acc={})->(acc ++ ("value"++(v as Number)):v)

To monitor the execution time of this process, we're going to define and use an "eval" function that you can use as is in your projects:

%dw 2.0
output application/json
fun Now() = now() then (t)->(t as Number)*1000 + t.milliseconds
fun eval(prs) =
Now() then (t1)-> prs() then (r)->
log(Now() -t1)
then r
---
eval(()->
(0 to 10000) reduce(v, acc={})->(acc ++ ("value"++(v as Number)):v)
)

A few comments on this snippet:

  •  Firstly, it was necessary to redefine a Now function above the standard now() function, as "now() as a Number" returns a number of seconds and not milliseconds (which is what now() does).
  • The "eval" function receives a function as a parameter, which it executes in the middle of a sequence of three calls. The two calls that flank it invoke Now() and retrieve a delay that is logged. Note that "eval" returns the value obtained by evaluating the function passed in parameter: its behavior is therefore completely generic and transparent.
  • Note the "()->" that follows the invocation of "eval". It is used to transform the expression you wish to monitor into a function.
  • "eval" is used here to monitor the whole expression. Nothing prevents you from using it to monitor a part of the expression. For example, you could write:
%dw 2.0
output application/json
fun Now() = now() then (t)->(t as Number)*1000 + t.milliseconds
fun eval(prs) =
Now() then (t1)-> prs() then (r)->
log(Now() -t1)
then r
---
(0 to 10000) reduce(v, acc={})->
    (eval(()->(acc ++ ("value"++(v as Number)):v)))




Diffusion d'une valeur par défaut / Diffusion of a default value

Comme c'est Vendredi, on va faire un exercice plutôt simple (mais qui provient d'un vrai besoin): diffuser une valeur d'un objet vers les objets dans une liste qu'il contient. On pose deux contraintes, histoire de se donner un petit peu de challenge:

  • La solution doit être au maximum indépendante de la structure de l'objet,
  • Les objets de la liste incluse peuvent déjà potentiellement posséder ledit champ qui donc devient prioritaire

Le champ à diffuser est en quelque une valeur par défaut qui ne sera prise en compte dans les objets de la liste incluse que lorsque ceux ci n'en dispose pas déjà. Par exemple, au départ nous avons:

{
"field": "a value",
"flag": "X",
"articles": [
{
"field": "a first value"
},
{
"field": "a second value",
"flag": "Y"
},
{
"field": "a third value"
}
]
}

Et nous voulons obtenir:

{
"field": "a value",
"articles": [
{
"field": "a first value",
"flag": "X"
},
{
"field": "a second value",
"flag": "Y"
},
{
"field": "a third value",
"flag": "X"
}
]
}

Nous pouvons voir que le champs "flag" a disparu de l'objet père pour apparaître dans les objets "articles". Noter que pour l'article "a second value" qui disposait déjà d'un champ "flag", celui-ci n'est pas modifié.

Le code qui fait la transformation est le suivant:

%dw 2.0
output application/json
fun shareOut(o) = o - "flag" - "articles" ++
"articles": o.articles map (a) -> a - "flag" ++
        "flag": (if (a.flag != null) a.flag else o.flag)
---
shareOut(payload)

Ce qui est notable est l'usage des opérateur "-" et "++" sur la structure d'un objet. Le premier permet de "retirer" un champ, le second d'en ajouter un. Le remplacement d'un champ se fait en le retirant plus en l'ajoutant (avec valeur modifiée). C'est ainsi qu'on peut rendre une transformation indépendante de la structure des objets sur lesquels elle s'applique.
_______________________________________________________________

As it's Friday, we're going to do a rather simple exercise (but one that comes from a real need): broadcast a value from an object to the objects in a list it contains. We'll set two constraints, just to give ourselves a bit of a challenge:

  • The solution must be as independent as possible of the object's structure,
  • The objects in the included list may already potentially possess the said field, which therefore takes priority.

The field to be distributed is a kind of default value that will only be taken into account by objects in the included list if they don't already have one. For example, initially we have:

{
"field": "a value",
"flag": "X",
"articles": [
{
"field": "a first value"
},
{
"field": "a second value",
"flag": "Y"
},
{
"field": "a third value"
}
]
}

And we want to get:

{
"field": "a value",
"articles": [
{
"field": "a first value",
"flag": "X"
},
{
"field": "a second value",
"flag": "Y"
},
{
"field": "a third value",
"flag": "X"
}
]
}

We can see that the "flag" field has disappeared from the parent object to appear in the "item" objects. Note that for the "a second value" item, which already had a "flag" field, this has not been modified.

The code that performs the transformation is as follows:

%dw 2.0
output application/json
fun shareOut(o) = o - "flag" - "articles" ++
"articles": o.articles map (a) -> a - "flag" ++
        "flag": (if (a.flag != null) a.flag else o.flag)
---
shareOut(payload)

What's particularly noteworthy is the use of the "-" and "++" operators on the structure of an object. The former is used to "remove" a field, the latter to add one. Replacing a field is done by removing it plus adding it (with modified value). This is how you can make a transformation independent of the structure of the objects to which it applies.

jeudi 8 février 2024

Accès par clé et Performance / Key access and Performance

Mulesoft est une plateforme qui offre des performances très honorables à partir du moment où l'on sait s'en servir. Le souci est qu'à part de vagues conseils très généraux (et peu utiles pour qui fait preuve d'un peu de bon sens), très peu d'articles sont disponibles sur Internet à ce sujet. Et bien en voici un pour combler ce manque et qui peut GRANDEMENT vous aider. Il y en aura d'autres lorsque je ferai de nouvelles "découvertes". Il s'agit d'un article à la fois concret et pointu, et qui sur les sujets traiter permet de gagner ENORMEMENT (x10, x100, x1000!) en performance.

Un besoin très, très récurent est de faire une recherche dans une liste à partir d'une clé. Par exemple, pour s'assurer qu'un objet d'un ID donné est présent et d'aller consulter les informations qu'il recèle. Pour effectuer cette recherche, la plupart des développeurs vont écrire quelque chose comme:

%dw 2.0
output application/json
fun search(list, id) = list filter (v)->v.id==id
---
payload search 86

Pour retrouver dans la liste en payload, l'objet d'ID 86. Voici une TRES TRES mauvaise implémentation de ladite fonctionnalité ! NE FAITES PAS CA ! Pourquoi ? Parce que Mulesoft va parcourir tous les éléments de la liste, pour n'en récupérer qu'un seul ! Si vous utilisez "search" à l'intérieur d'un "map" ou d'un "reduce", vous aller probablement exécuter un processus quadratique (type NxN) et vos performances vont s'effondrer. C'est le problème de performance N°1 dans la plupart des projets. C'est un vrai attrape nigaud: en test avec 10 à 100 éléments on a 100 à 10000 opérations élémentaires à exécuter et cela prend de quelques centaines de millisecondes au pire. En PROD, avec un million d'éléments, nous nous retrouvons avec 1000 milliards d'opérations !

La bonne solution est de créer une "HashMap" qui référence chaque objet en fonction de son ID et ensuite de les rechercher en fonction de l'ID qui sert de clé:

%dw 2.0
output application/json
var hashmap = payload groupBy $.id
fun search(hm, id) = hm[id][0]
---
payload map (o)->search(hashmap, o.id)

Qu'est ce qu'une "HashMap" dans Mulesoft ? Et bien, c'est tout simplement un objet. Une clé est le nom d'un champ de cet objet HashMap, les valeurs de ces champs, sont les objets de la liste initiale. Autant de champs, autant de clés et donc autant d'objets référencés. Comment transformer une liste en un objet et donc en une "HashMap" ? Facile, on utilise la fonction groupBy qui "regroupe" les objets en fonction d'un critère, ici tout simplement l'ID. La valeur de chaque champ est un tableau d'un seul élément (car les ID étant discriminants, le regroupement ne contient que l'objet propriétaire), d'où le [0] dans la fonction "search".

La recherche par HashCode est d'ordre constant (1.1 si certaines conditions sont remplies: la taille de la HashMap est un nombre premier, la fonction de hachage est uniforme, la table est remplie à 75%). Donc tout processus garde son ordre (X x ~1 = ~X), s'il fait référence à cette fonction de recherche. Noter qu'il faut compter l'ordre de la création de la HashMap dans la complexité de l'algorithme, qui est linéaire (ordre N) Ce qui donne dans notre cas N+N = 2N. Cela reste linéaire et donc performant (les esprits chagrins me feront remarquer que la recherche d'un unique élément dans la liste est plus performant avec "filter" et ils auront raison mais ...juste pour ce cas la !).

Tout va bien alors ? Ben non. Ou plutôt cela dépend d'un autre facteur: la plateforme d'implémentation. Si la HashMap est en "JSON", les performances sont catastrophiques (pour de grandes listes). En "Java", c'est très performant. Pourquoi ? Parce que la résolution de [id] en JSON provoque le parcourt de tous les champs (et donc cette recherche est d'ordre N), mais en Java, l'objet est une LinkedHashMap et l'accès est par hashcode (ordre constant). Le code présenté plus haut, donc n'est hélas pas performant car quadratique. Il faut écrire plutôt:

%dw 2.0
output application/java
---
payload groupBy $.id

Dans un premier Transform afin d'avoir la HashMap

%dw 2.0
output application/json
var hashmap = payload
fun search(hm, id) = hm[id][0]
---
payload map (o)->search(hashmap, o.id)

Que l'on utilise dans un second Transform.

J'ai écrit un petit programme de test qui mesure tout çà et je vous présente les résultats (le délai qui nous intéresse est celui qui sépare les timestamps C et D).
Si j'utilise JSON, voila ce que j'obtiens:


Que constate t'on ?
  • Pour une liste de 1000 éléments, le délai est de 83ms
  • Pour une liste de 10000 éléments, le délai est de 2,5 secondes
  • Pour une liste de 100000 éléments, le délai est de 4,47 minutes!
Passons à Java:

Que constate t'on ?
  • Pour une liste de 1000 éléments, le délai est de 67ms
  • Pour une liste de 10000 éléments, le délai est de 84 ms
  • Pour une liste de 100000 éléments, le délai est de 173 ms (si, si)
  • Pour une liste d'un million (!) d'éléments : 2.5 s
Conclusion: le choix de l'implémentation de "l'objet" qui nous sert de HashMap est CRUCIAL. On change d'ordre de l'algorithme et donc les performances bondissent avec la taille de la liste a créer. On passe de l'impossible au possible, rien de moins. Ma question est: pourquoi cette information qui devrait être sur le site de Mulesoft en première page, taille 50em, rouge clignotant est NULLE PART ? 
_________________________________________________________________________

Mulesoft is a platform that offers very respectable performance, provided you know how to use it. The problem is that, apart from vague general advice (of little use to anyone with a little common sense), there are very few articles on the subject available on the Internet. Well, here's one to fill the gap, and one that can help you a LOT. There will be more as I make new "discoveries". It's an article that's both concrete and to the point, and which, on the subjects dealt with, will help you gain ENORMOUSLY (x10, x100, x1000!) in performance.

A very, very recurrent need is to search a list using a key. For example, to ensure that an object with a given ID is present, and to consult the information it contains. To perform this search, most developers will write something like:

%dw 2.0
output application/json
fun search(list, id) = list filter (v)->v.id==id
---
payload search 86

To find the ID 86 object in the payload list. This is a VERY VERY poor implementation of this feature! DON'T DO IT! Why not? Because Mulesoft will go through all the elements in the list, and only retrieve one! If you use "search" inside a "map" or a "reduce", you'll probably run a quadratic process (NxN type) and your performance will plummet. This is the No. 1 performance problem in most projects. It's a real eye-catcher: in testing, with 10 to 100 elements, you have 100 to 10,000 elementary operations to execute, taking from a few hundred milliseconds at worst. In PROD, with a million elements, we end up with 1,000 billion operations!

The best solution is to create a "HashMap" that references each object according to its ID, and then search for them according to the ID that serves as the key:

%dw 2.0
output application/json
var hashmap = payload groupBy $.id
fun search(hm, id) = hm[id][0]
---
payload map (o)->search(hashmap, o.id)

What is a "HashMap" in Mulesoft? Well, it's simply an object. A key is the name of a field in this HashMap object, and the values of these fields are the objects in the initial list. As many fields, as many keys and therefore as many referenced objects. How do you transform a list into an object, and therefore into a HashMap? Easily, we use the groupBy function, which "groups" objects according to a criterion, in this case simply the ID. The value of each field is an array of a single element (because IDs are discriminating, the grouping contains only the owner object), hence the [0] in the "search" function.

HashCode search is of constant order (1.1 if certain conditions are met: the size of the HashMap is a prime number, the hash function is uniform, the table is 75% full). So any process keeps its order (X x ~1 = ~X), if it refers to this search function. Note that the order of the HashMap creation must be taken into account in the complexity of the algorithm, which is linear (order N). In our case, this gives N+N = 2N. It's still linear and therefore efficient (those with a grudge will point out that the search for a single element in the list is more efficient with "filter", and they'll be right, but that's just the case!)

So all's well then? Well, no. Or rather, it depends on another factor: the implementation platform. If the HashMap is in "JSON", performance is catastrophic (for large lists). In "Java", it performs very well. Why is this? Because resolving [id] in JSON causes all fields to be searched (and so this search is of order N), but in Java, the object is a LinkedHashMap and access is by hashcode (constant order). Unfortunately, the code presented above is not efficient, as it is quadratic. Instead, write:

%dw 2.0
output application/java
---
payload groupBy $.id

First Transform to obtain the HashMap

%dw 2.0
output application/json
var hashmap = payload
fun search(hm, id) = hm[id][0]
---
payload map (o)->search(hashmap, o.id)

Which we use in a second Transform.

I've written a little test program that measures all this, and here are the results (the delay we're interested in is the one between timestamps C and D).
If I use JSON, here's what I get:


What do we see?
  • For a list of 1000 elements, the delay is 83ms
  • For a list of 10000 elements, the delay is 2.5 seconds.
  • For a list of 100000 elements, the delay is 4.47 minutes!
Let's move on to Java:

What do we see?
  • For a list of 1000 elements, the delay is 67ms
  • For a list of 10000 elements, the delay is 84ms
  • For a list of 100,000 elements, the delay is 173 ms (yes, yes)
  • For a list of a million (!) elements: 2.5 s
Conclusion: the choice of implementation of the "object" that serves as our HashMap is CRUCIAL. We're changing the order of the algorithm, so performance jumps with the size of the list to be created. We go from the impossible to the possible, nothing less. My question is: why is this information, which should be on the front page of the Mulesoft website, size 50em, flashing red, NOWWHERE to be found?




vendredi 2 février 2024

Découpage d'un texte en ligne / Breaking text into lines

Nous allons traiter maintenant d'un sujet récurent et pas si simple. Il s'agit de découper un texte en lignes d'une longueur maximale définie tout en s'efforçant de respecter les césures entre les mots. Cas typique d'utilisation: découper une adresse en lignes d'adresses. Nous y intégrons une contrainte supplémentaire: si un mot dépasse la longueur de la ligne, alors il y aura césure à l'intérieur du mot, et ce pour chaque occurrence du nombre de caractères dans le mot (ainsi un mot de 100 caractères se verra découpé en 40, puis 40 puis 20 caractères).

Prenons à titre d'exemple un extrait du livre Harry Potter (dont les espaces d'une phrase ont été enlevés afin de créer un mot à découper:

Nearly ten years had passed since the Dursleys had woken up to find their nephew on the front step, but Privet Drive had hardly changed at all. ThesunroseonthesametidyfrontgardensandlitupthebrassnumberfourontheDursleys'frontdoor; it crept into their living room, which was almost exactly the same as it had been on the night when Mr. Dursley had seen that fateful news report about the owls. Only the photographs on the mantelpiece really showed how much time had passed.

Nous sommes censé obtenir cela (un tableau de lignes dont aucune ne dépasse 40 caractères).

[
"Nearly ten years had passed since the",
"Dursleys had woken up to find their",
"nephew on the front step, but Privet",
"Drive had hardly changed at all.",
"Thesunroseonthesametidyfrontgardensandli",
"upthebrassnumberfourontheDursleys'frontd",
"or; it crept into their living room,",
"which was almost exactly the same as it",
"had been on the night when Mr. Dursley",
"had seen that fateful news report about",
"the owls. Only the photographs on the",
"mantelpiece really showed how much time",
"had passed."
]

Le script DataWeave qui effectue la découpe est le suivant:

%dw 2.0
import * from dw::core::Strings
output application/json
var LINE_LENGTH = 40
fun cut(s, l) = if (sizeOf(s)<=l)
{
lines:[],
line:s
}
else
{
lines:[substring(s, 0, l)] ++ cut(substring(s, l+1,
            sizeOf(s)), l).lines,
line:cut(substring(s, l+1, sizeOf(s)), l).line
}
---
payload
splitBy(" ")
reduce ((v, a={
lines: [],
line: ""
}) ->
if (a.line == "" and sizeOf(v)<=LINE_LENGTH) {
lines: a.lines,
line: v
}
else
if (a.line != "" and sizeOf(a.line ++ " " ++ v)<=LINE_LENGTH) {
lines: a.lines,
line: a.line ++ " " ++ v
}
else // too big words 1/2
if (a.line != "" and sizeOf(v)>LINE_LENGTH) {
lines: (a.lines + a.line) ++ cut(v, LINE_LENGTH).lines,
line: cut(v, LINE_LENGTH).line
}
else // too big words 2/2
if (a.line == "" and sizeOf(v)>LINE_LENGTH) {
lines: a.lines ++ cut(v, LINE_LENGTH).lines,
line: cut(v, LINE_LENGTH).line
}
else
{
lines: a.lines + a.line,
line: v
})
then (v) -> v.lines + v.line

Par rapport avec ce que nous avons vu dans les billets précédents, ce script n'apporte pas de nouveautés techniques remarquables : c'est une utilisation avancée mais classique d'une réduction, pour peu que l'on sache écrire une réduction non triviale. Le texte est d'abord découpé en une série de mots grâce à splitBy. La liste de mots est ensuite passée à la réduction qui ne présente qu'une difficulté: le traitement des mots trop longs. Notez que l'accumulateur contient à la fois les lignes déjà élaborées ("lines") et la dernière line en cours d'élaboration: ("line"). La fonction de conclusion:

then (v) -> v.lines + v.line

ajoute la dernière ligne en cours d'élaboration à la liste. Je pense que vous pouvez utiliser ce code tel quel.
__________________________________________________________________

We're now going to deal with a recurrent and not-so-simple subject. It involves cutting text into lines of a defined maximum length, while respecting the hyphenation between words. Typical use case: cutting an address into address lines. An additional constraint is added: if a word exceeds the length of the line, the word is hyphenated for each occurrence of the number of characters in the word (so a 100-character word is split into 40, then 40, then 20 characters).

Let's take an excerpt from the Harry Potter book as an example (where the spaces in a sentence have been removed to create a word to be cut):

[
"Nearly ten years had passed since the",
"Dursleys had woken up to find their",
"nephew on the front step, but Privet",
"Drive had hardly changed at all.",
"Thesunroseonthesametidyfrontgardensandli",
"upthebrassnumberfourontheDursleys'frontd",
"or; it crept into their living room,",
"which was almost exactly the same as it",
"had been on the night when Mr. Dursley",
"had seen that fateful news report about",
"the owls. Only the photographs on the",
"mantelpiece really showed how much time",
"had passed."
]

The DataWeave script that performs the cut is as follows:

%dw 2.0
import * from dw::core::Strings
output application/json
var LINE_LENGTH = 40
fun cut(s, l) = if (sizeOf(s)<=l)
{
lines:[],
line:s
}
else
{
lines:[substring(s, 0, l)] ++ cut(substring(s, l+1,
            sizeOf(s)), l).lines,
line:cut(substring(s, l+1, sizeOf(s)), l).line
}
---
payload
splitBy(" ")
reduce ((v, a={
lines: [],
line: ""
}) ->
if (a.line == "" and sizeOf(v)<=LINE_LENGTH) {
lines: a.lines,
line: v
}
else
if (a.line != "" and sizeOf(a.line ++ " " ++ v)<=LINE_LENGTH) {
lines: a.lines,
line: a.line ++ " " ++ v
}
else // too big words 1/2
if (a.line != "" and sizeOf(v)>LINE_LENGTH) {
lines: (a.lines + a.line) ++ cut(v, LINE_LENGTH).lines,
line: cut(v, LINE_LENGTH).line
}
else // too big words 2/2
if (a.line == "" and sizeOf(v)>LINE_LENGTH) {
lines: a.lines ++ cut(v, LINE_LENGTH).lines,
line: cut(v, LINE_LENGTH).line
}
else
{
lines: a.lines + a.line,
line: v
})
then (v) -> v.lines + v.line

Compared with what we've seen in previous posts, this script doesn't bring any remarkable technical novelties: it's an advanced but classic use of a reduction, provided you know how to write a non-trivial reduction. The text is first split into a series of words using splitBy. The list of words is then passed to the reduction function, which presents only one difficulty: dealing with words that are too long. Note that the accumulator contains both the lines already processed ("lines") and the last line being processed: ("line"). The conclusion function:

then (v) -> v.lines + v.line

adds the last line in progress to the list. I think you can use this code as is.

Pourquoi ce blog ? / Why this blog?

Mulesoft est un ESB du monde Salesforce utilisé pour construire des flots permettant aux pièces logicielles d'un Système d'Informati...