by jc-Qualitystreetjc-Qualitystreet atJanuary 23, 2012 06:15 PM
by jc-Qualitystreetjc-Qualitystreet atJanuary 23, 2012 06:15 PM
by jc-Qualitystreetjc-Qualitystreet atJanuary 22, 2012 11:21 PM
by jc-Qualitystreetjc-Qualitystreet atJanuary 19, 2012 10:48 AM
by Olivier RodriguesOlivier Rodrigues atJanuary 18, 2012 02:40 PM
La plupart des clients veulent à juste titre, savoir quel sera leur ROI s’il décidait d’investir dans le déploiement des pratiques agiles sur leur activité projet, de maintenance ou de réalisation de nouvelles applications.
Afin de pouvoir apporter des éléments concrets à cette problématique incontournable, je propose de m’appuyer sur un cas concret décrit principalement par :
Contexte client :
Un client possède un département informatique représentant 100 projets et 700 personnes dont environ 400 intervenants extérieur.
Le middle management est réfractaire à l’agilité et doit donc être convaincu par la Direction qui souhaite déployer les pratiques agiles et qui compte sur un fournisseur pour l’aider via une présentation officielle mettant en avant le ROI d’une telle démarche.
Budget du client :
Le budget annuel des projets (maintenance comprise) représente environ 20 M€.
Ce client peut dépenser 1 M€ pour déployer l’agilité sur son périmètre projet.
Natures de projet :
Les projets réalisés dans le département informatique du client se répartissent aujourd’hui de la façon suivante :
Organisation spécifique :
En fin d’année d’Octobre à Décembre, le client travaille sur les budgets de l’année suivante en identifiant ses nouveaux projets.
En début d’année suivante, le client déstaffe en masse les équipes projet de Janvier à Février (2 mois) durant lesquels ses équipes spécifient les nouveaux projets.
Début Mars, le client démarre les nouveaux projets et staffe à nouveau ses équipes avec de nouveaux intervenants extérieurs.
Demande du client :
Le client souhaite connaître l’ordre de grandeur du ROI lié à l’adoption de pratiques projets agiles.
Ce ROI est évalué par le client en nombre de projets qu’il peut réaliser avec un même budget annuel. Il souhaite par conséquent augmenter le nombre de projets réalisés en une année grâce à l’agilité et en parallèle, diminuer la part prise par les projets de maintenance.
Aucune information détaillée sur la taille actuelle des 100 projets réalisés annuellement n’est connue.
Aucune information détaillée sur la nature des activités de maintenance (corrective ou évolutive) n’est connue, ni sur la taille des “defect backlogs”.
Aucune information détaillée concernant les frais de structure et d’encadrement n’est connue.
Les hypothèses retenues proviennent de statistiques ou du contexte client devant être discutées avec le client à l’occasion d’interviews ou de réunions supplémentaires.
Taux de succès des projets agiles / projets traditionnels (www.agilemodeling.com de Scott W. Ambler)
Les tailles moyennes des équipes agiles se répartissent de la manière suivante :
78% des équipes agiles ont une taille d’équipe de 5 à 15 personnes. Cette taille de projet constitue la taille optimale pour l’agilité, évitant des organisations de management plus lourde et donc moins agiles pour des équipes importantes, et des équipes trop petites (moins de 5 personnes) pour conserver des pratiques agiles efficaces.
Dans 65% des cas environ, la satisfaction des clients a été améliorée par la mise en œuvre de l’agilité sur les projets. Il faut dire que statistiquement, il est courant de constater à posteriori que plus de 40% des fonctions livrées n’ont quasiment jamais été utilisées par les opérationnels. Enfin 56% des problèmes rencontrés en cours de projet proviennent des besoins exprimés.
« The state of agile development » Juin 2010, FrenchSUG : Analyse des méthodes agiles – 2009

Dans plus de 74% des cas, la productivité des équipes est améliorée par la mise en œuvre de pratiques agiles.
« The state of agile development » Juin 2010, FrenchSUG : Analyse des méthodes agiles – 2009
Dans plus de 68% des cas, la qualité des logiciels produits est améliorée.

« The state of agile development » Juin 2010, FrenchSUG : Analyse des méthodes agiles – 2009
1) Concernant la taille des équipes
L’agilité étant optimale sur des projets de taille moyenne (entre 5 et 15 personnes), un axe d’optimisation du ROI consisterait à :
2) Concernant la nature de la maintenance
Sur les activités de correction, un axe d’optimisation du ROI consisterait à :
Sur la base des hypothèses prises (30% de maintenance corrective à T0), on peut raisonnablement planifier une baisse de la maintenance corrective de 10% par an sur deux ans, à l’avantage de la maintenance évolutive. Cela se traduirait par une augmentation de la maintenance évolutive de 10% par an sur deux ans.
3) Concernant le succès des projets
Les statistiques démontrent que le taux de succès des projets agiles est au minimum de 15% par rapport aux projets traditionnels. Ce gain est obtenu par la diminution des coûts de non qualité et par l’augmentation de la productivité des équipes.
L’augmentation du taux de succès des projets d’environ 15% une fois les pratiques agiles déployées et maîtrisées se traduirait donc par une économie substantielle de budget. Ce budget peut être évalué par exemple en considérant un dépassement moyen en effort de 20% pour les projets en échec. Si les projets représentent 60% du budget, que les projets qui étaient en échec représentent 15% et que cet échec représente en moyenne 20% de dépassement, une économie de 1,8% serait possible (60% x 15% x 20%).
4) Amélioration de la satisfaction client
L’amélioration de la satisfaction client se traduira par la diminution du taux des fonctionnalités livrées mais jamais utilisées car remplacées par des fonctionnalités à plus haute valeur ajoutée. Cela survient grâce aux feedbacks réguliers et fréquents du métier lors des démonstrations de fin d’itération, mais également grâce à la capacité de l’équipe à être force de proposition car plus et mieux impliquée sur le produit. Il paraît donc raisonnable que le taux de non utilisation passe de 40% à 20% environ. Le budget récupéré correspondant devrait permettre soit d’améliorer la valeur du produit livré (15%), soit de le réinvestir sur de nouveaux projets (5%) et inciter le métier à confier plus de demandes au département informatique. Une économie de budget d’environ 5% peut probablement être atteinte.
5) Amélioration de la qualité logiciel et augmentation de la productivité des équipes
L’amélioration de la qualité des logiciels réalisés en agile se traduit par une baisse de la capacité de production consacrée à la correction des anomalies en cours de projet, renforçant encore l’augmentation de productivité des équipes agiles obtenues par la dynamique et le rythme projet.
Cette augmentation de productivité se traduit par une diminution des budgets nécessaires à la réalisation d’un certain périmètre de fonctionnalités ou par une augmentation de la taille logicielle livrée pour un certain budget.
Une augmentation de productivité de 10% à 20% par rapport à la productivité sur un projet traditionnel est courante et peut varier selon les cas. Elle est liée principalement à :
6) Réorganisation du travail du département informatique :
Le déstaffing des intervenants extérieurs deux mois de l’année pourrait être évité par la priorisation des projets et le lissage des activités de spécifications tout au long de l’année. Cela peut être réalisé avec la constitution de roadmap produit et de « product backlog » au fil de l’eau chaque fois que de nouveaux besoins émergent notamment à l’occasion des démonstrations et des livraisons de releases.
Il paraît raisonnable de considérer une économie de montée en compétence de 5% sur les 400/700 ième du budget, à condition de considérer que le coût des intervenants internes est identique à celui des intervenants externes (ce qui est forcément faux mais simplifie le calcul). Cela correspond à une économie de budget de 2,5%.
Il faut ajouter à cela, une réduction des coûts globaux d’infrastructure d’environ 15% (400p sur 10 mois = 4000 p.m => 4000 p.m./12 = 333 p => Ecart de 67p/400 = -16%).
Si on considère que les coûts d’infrastructure et d’encadrement représentent 200€ par mois et par personne, la réduction des coûts d’infrastructure d’environ 15% représenterait 15% de 200€ * 12 * 400p = 144 000 € / ans. Cette économie représenterait 0,7% du budget global qui est de 20 M d’Euros.
Activités de maintenance :
L’impact de la diminution progressive de la part de maintenance corrective (-10% par an sur deux ans) liée à l’application des pratiques agiles se traduirait sur le budget constant du département informatique par :
Activités projet :
L’impact de l’agilité sur les projets est positif et peut être quantifié dans le temps (entre 2012 et 2014) de la façon suivante :
En termes d’économies budgétaire, le tableau suivant décrit entre 2012 et 2014 les économies correspondantes :
Economie globale pressentie :
Les économies pressenties globalement se répartissent de la façon suivante :
Indicateurs projet :
Ces économies se concrétiser par une augmentation du nombre de projets pouvant être réalisée de la façon suivante :
Economies et indicateurs estimés en mode dégradée :
Si le périmètre de la maintenance n’était pas revu à la baisse et si l’organisation du travail restait inchangée (10 mois travaillés pour les intervenants externes), les économies réalisables par le seul jeu de l’agilité sur le périmètre projet seraient décrites de la façon suivante :
Les économies pressenties globalement se répartiraient de la façon suivante :
L’augmentation du nombre de projets se réaliserait de la façon suivante :
Retour sur investissement (ROI) :
Si nous partons de l’hypothèse que le client investit 1 M Euros en 2012 et en 2013, puis 500 K Euros en 2014 pour passer intégralement sa structure à l’agilité, le calcul de ROI nous donnerait :

L’investissement serait donc rentabilisé au bout de la seconde année, c’est-à-dire fin 2013.
En mode dégradé, le calcul de ROI nous donnerait :

L’investissement serait donc rentabilisé au bout de la troisième année, c’est-à-dire en 2014.
Quand on ne sait plus trop où trouver des formules magiques, il reste Harry Potter, Alohomora l’incantation qui ouvre les portes, les portes de la voie des statistiques.
Allez au boulot, on a du rangement à faire.
Cet article fait partie d’une suite qui commence avec cet article épisode 1.
First things first, charger nos librairies et nos données et on reprend où on en était dans l’épisode 3.
user=> (use '(incanter core io datasets stats))
nil
user=> (def data (read-dataset
"/Users/cfalguiere/Workspaces/Diapason/report-data/20111217-ETALON_WSW_TPS2011.csv" :header true))
#'user/data Il est grandement temps de faire une fonction qui charge les résultats. On peut être sorcier et fainéant.
user=> (defn read-results []
(read-dataset "/Users/cfalguiere/Documents/2012-01/RD-Clojure/Workspace/incanter/resources/sample-results.csv" :header true))
#'user/read-results N’oubliez pas les parenthèses, sinon au lieu du prince charmant vous auriez une jolie mangouste, jolie mais bon ça n’est pas ce que vous vouliez. En l’occurrence, vous ne voulez pas que data représente la fonction mais son résultat.
user=>(def data read-results)
#'user/data
user=> data
#<user$read_results user$read_results@6bd46c20> Donc voilà, le tour exécuté correctement et quelques donnnées présentes.
user=>(def data (read-results))
#'user/data Pour le moment j’expérimente interactivement, donc peu importe l’IDE. Lançons un éditeur de texte pour copier ce code et chargeons le dans le REPL.
(use '(incanter core io datasets stats))
(def result-file-name "/Users/cfalguiere/Documents/2012-01/RD-Clojure/Workspace/incanter/resources/sample-results.csv")
(defn read-results []
(read-dataset result-file-name :header true))
(def data (read-results)) J’aimerais bien indiquer le répertoire courant pour éviter tout ces chemins mais à la différence de R on ne peut pas changer le répertoire courant, parce que la JVM ne permet pas de le faire. Mais au fait quel serait mon répertoire courant ? Une rapide recherche sur Google indique ça :
user=> (. (java.io.File. ".") getCanonicalPath)
"/Applications"Un peu ésotérique. C’est plus clair avec un exemple plus évident comme la méthode toUpperCase de la classe String. Le . applique la méthode à l’objet.
user=>(. "aaa" toUpperCase)
"AAA"Il ne reste plus qu’à charger le programme et data est disponibles pour d’autres tours.
user=>(load-file "/Users/cfalguiere/Documents/2012-01/RD-Clojure/workspace/incanter/tuto1.clj")
#'user/data
user=> (nrow data)
209En général, j’aime bien avoir une vue d’ensemble de mes données : temps max, temps moyen, taux d’erreur …
Qu’est ce que j’ai là dedans ?
Le dataset est une map qui contient un vecteur de labels et une liste de lignes de données. La fonction col-names retourne les labels des colonnes (c’est équivalent (:column-names data))
user=> (col-names data)
[:lb :t :lt :ts :s :rc :rm :by :na]
user=> (:column-names data)
[:lb :t :lt :ts :s :rc :rm :by :na] Les labels JMeter ne sont pas des plus parlants pour des raisons de compacité du résultat.
Pour chaque relevé nous disposons des informations suivantes :
Les 4 premières lignes sont des séries numériques. Les autres sont des catégories (l’équivalent des factors pour R). Elles vont servir à filtrer (pour les statuts) ou à regrouper les données.
Le module stats fourni les statistiques habituelles. Pour faire quelques expériementations, on va générer une série de nombres dont la moyenne est connue. sample-normal construit une liste de 1000 nombres distribués selon une loi normale centrée sur 10 avec un écart-type de 1. Sans surprise, la moyenne est autour de 10 et l’écart type est autour de 1.
user=> (def mysample (sample-normal 1000 :mean 10 :std 1))
(10.099828740537856 9.903470850168006 9.52228847634955 9.003518490739022 9.171579274811812 ...
user=> (mean mysample)
9.941436842660105
user=> (sd mysample)
1.0007997747452984 Et min et max ?
Un magicien ne voit pas les choses aussi simplement. Le min est la valeur qui est inférieure à tous les autres relevés. Et symétriquement le max est la valeur qui est supérieure à tous les autres relevés.
Mais les magiciens aiment bien découper les données en tranches. Donc ils veulent aussi savoir quelle est la valeur qui sépare les relevés en deux groupes, inférieurs et supérieurs à une valeur qui s’appelle la médiane.
Pour améliorer le spectacle, les données sont découpées en quatre tranches par pas de 25% comme le montre le schéma de droite, ce sont des quartiles. On voit sur le schéma que la surface rouge qui représente les 25% les plus bas se termine vers 9, la surface bleu qui représente les 25% suivants se termine vers 10 etc.
Les valeurs de 25% sont arbitraires. La forme générale de cet agrégat est le quantile. La fonction quantile retourne le min, la valeur maximale pour 25% des relevés, 50% (la médiane) et 75%, et le max (le quantile 100%).
user=> (quantile mysample)
(6.078738192126153 9.284660278736457 9.9269193454763 10.609659921191085 12.899443382831647) Le monde étant ce qu’il est les sorciers qui font du test de charge ne s’intéressent pas aux quantiles des magiciens. En fait la seule chose qui les intéressent, c’est l’impact des 5% ou 10% de temps les plus longs. Donc ils calculent la valeurs maximale pour 90% ou 95% des relevés.
user=> (quantile mysample :probs 0.95)
11.591767882673627
user=> (quantile mysample :probs [0.9 0.95])
(11.278966306153395 11.591767882673627) Qu’est ce que ça donne sur nos données de test ?
user=> (mean ($ :t data))
1429.579831932773
user=> (sd ($ :t data))
2237.25502018713
On peut faire toutes ces opérations sur la même série de données en utilisant la fonction associée au dataset with-data. Elle factorise le dataset et permet de définir l’expression à utiliser, ici construire un tableau contenant les différentes métriques. $data représente la série à l’intérieur du with-data.
user=> (with-data ($ :t data)
[(mean $data)(sd $data)])
[1429.579831932773 2237.25502018713] Si on rajoute le quantile ça nous donne ce qui suit. Un bon début, mais on a un tableau des différents indicateurs plus une liste avec les quantiles.
user=> (with-data ($ :t data)
[(count $data)(mean $data)(sd $data)(quantile $data :probs[0 0.5 0.9 0.95 1])])
[119 1429.579831932773 2237.25502018713 (10.0 708.0 3393.4 4130.999999999982 13007.0)]
Mmm … un coup de baguette magique flatten et les données se retrouvent bien alignées dans le tableau.
user=> (with-data ($ :t data)
(flatten [(count $data)(mean $data)(sd $data)(quantile $data :probs[0 0.5 0.9 0.95 1])]) )
(119 1429.579831932773 2237.25502018713 10.0 708.0 3393.4 4130.999999999982 13007.0) Une variable pour stocker le résultat et on sauve
user=> (def stats
(with-data ($ :t data)
(flatten [(count $data)(mean $data)(sd $data)(quantile $data :probs[0 0.5 0.9 0.95 1])]) ) )
#'user/stats
user=> stats
(119 1429.579831932773 2237.25502018713 10.0 708.0 3393.4 4130.999999999982 13007.0) Quoique, pas tout de suite apparemment.
user=> (save 'stats "stats.csv")
java.lang.IllegalArgumentException: No method in multimethod 'save' for dispatch value: class clojure.lang.Symbol (NO_SOURCE_FILE:0)
Que peut bien être une multiméthode ?
Vous trouverez une réponse complète dans cet article sur le polymorphisme en Clojure. En quelques mots, c’est le mécanisme qui permet à une fonction de se comporter différement en fonction des paremètres qu’on lui passe (des nombres ou des chaînes par exemple).
Vous avez probablement déjà noté que les fonctions en clojure sont souvent définies pour plusieurs nombres d’arguments (l’arité de la fonction si vous voulez briller la nuit au prochain ParisJUG). Clojure permet qu’une fonction soit surchargée (overloaded en anglais) et elle peut ainsi avoir une définition différente selon le nombre d’arguments.
Ci-après un exemple très simple de surcharge qui crée une map rectangle avec 1 ou 2 paramètres. La forme a un seul paramètre utilise la forme a 2 paramètres pour construire un carré.
user=> (defn make-rectangle
([width] (make-rectangle width width))
([width height] {:width width :height height}))
#'user/make-rectangle
user=> (make-rectangle 20)
{:width 20, :height 20}
user=> (make-rectangle 10 20)
{:width 10, :height 20} Autre exemple plus compliqué, la multiplication implémentée de manière récursive en utilisant 4 formes :
(def mult
(fn this
([] 1)
([x] x)
([x y] (* x y))
([x y & more]
(apply this (this x y) more)))) La fonction est définie pour 0, 1, 2 arguments et un nombre quelconque d’arguments. Le & indique que ce qui se trouve à droite contient une liste d’arguments, et c’est par ce moyen qu’on définie une fonction variadique (pour quand vous ne brillerez plus assez avec arité).
Décryptons un peu cette multiplication. Tout d’abord, cet exemple n’utilise pas la macro defn, on a donc deux les étapes, décrire la fonction (fn) et la nommer (def). La multiplication utilise 4 variantes. Dans le cas général (4) je prend les 2 paramètres les plus à gauche, je leur applique “moi-même” (this x y) et j’applique ensuite “moi-même” en utilisant ce résultat et tous les paramètres qui restent à droite (more). Si je n’ai que 2 paramètres (3), je les multiple. Si je n’en ai plus qu’un (2), je le renvoie. Si je n’en ai plus (1) je renvoie 1.
Et pourquoi apply ? apply est utilisé dans les contextes où le nombre d’élements n’est pas connu à la compilation et donc typiquement lorsque l’on déconstruit une séquence. Par exemple
user=> (+ 1 2 3 4 5)
15
user=> (+ [1 2 3 4 5])
java.lang.ClassCastException (NO_SOURCE_FILE:0)
user=> (apply + [1 2 3 4 5])
15
L’addition du contenu de la séquence n’est pas possible directement. L’utilisation d’apply permet de déconstruire le tableau pour en lister les éléments et d’effectuer l’opération. Dans le cas de la fonction mult, apply permet de traiter more qui est une séquence.
Et ma multiméthode ?
La multiméthode est un mécanisme plus puissant qui permet de gérer le polymorphisme. La syntaxe est un peu différente et basée sur les verbes defmulti et defmethod.
L’exemple suivant montre une multiméthode “rencontre” entre différentes espèces. Si le paramètre 1 est un lapin et le paramètre 2 un lion, le comportement de “rencontre” est équivalent à la fonction “s’enfuit”. Dans le cas inverse, elle est équivalente à “mange”
(defmulti encounter (fn [x y] [(:Species x) (:Species y)])) (defmethod encounter [:Bunny :Lion] [b l] :run-away) (defmethod encounter [:Lion :Bunny] [l b] :eat)
L’exemple complet se trouve dans cet article sur le polymorphisme en Clojure.
Les multimethodes peuvent dispatcher en fonction des types des paramètres mais également sur des valeurs des arguments, leur nombre ou des méta-données selon la syntaxe.
Bref, “No method in multimethod 'save' for dispatch value: class clojure.lang.Symbol (NO_SOURCE_FILE:0)" signifie seulement “je n’ai pas d’implémentation de save pour le type que tu m’envoie”. Oui, je sais …, mais les magiciens ne vont pas dévoiler tous leurs tours si facilement.
stats est une liste et save est une fonction Incanter qui fonctionnera que sur une matrice ou un dataset.
Qu’à celà ne tienne, on va construire un dataset avec ces données. Il faut lui indiquer les noms de colonnes et les valeurs.
user=> stats
(119 1429.579831932773 2237.25502018713 10.0 708.0 3393.4
4130.999999999982 13007.0)
user=> (def statsds
(dataset ["count", "mean", "sd", "min", "median", "q90", "q95", "max"] stats) )
#'user/statsds
user=> statsds
#:incanter.core.Dataset{:column-names ["count" "mean" "sd" "min" "median" "q90" "q95" "max"],
:rows ({"count" 119} {"count" 1429.579831932773} {"count" 2237.25502018713} {"count" 10.0} {"count" 708.0} {"count" 3393.4} {"count" 4130.999999999982} {"count" 13007.0})}
user=> (view statsds)
Mmm, est ce bien ce que l’on veut ? Non, en fait le dataset n’est pas dans le bon sens. Il devrait avoir 1 ligne avec une colonne par indicateur.
Si vous allez sur la fonction dataset vous pouvez voir le source sous github. La fonction dataset attend une séquence de séquences ou une séquence de maps. Avec une simple liste, dataset a considéré que l’on voulait une liste de plusieurs lignes de 1 colonne.
La façon la plus simple de résoudre le problème est de transformer la liste en dataset en la transposant pour qu’elle constitue 1 ligne de plusieurs colonnes. Ensuite on lui ajoute des noms de colonnes.
user=> (def statsds (col-names
(to-dataset stats :transpose true) ["count", "mean", "sd", "min", "median", "q90", "q95", "max"]) )
#'user/statsds
user=> statsds
#:incanter.core.Dataset{:column-names ["count" "mean" "sd" "min" "median" "q90" "q95" "max"],
:rows ({"max" 13007.0, "q95" 4130.999999999982, "q90" 3393.4, "median" 708.0, "min" 10.0, "sd" 2237.25502018713, "mean" 1429.579831932773, "count" 119})}
user=> (view statsds)
user=> (save statsds "/Users/cfalguiere/Documents/stats.csv")
nil Il manque juste une dernière info utile le taux d’erreurs et le top n.
Pour le taux d”erreurs, on verra plus tard (surtout qu’il n’y en a pas dans le jeu de données) mais pour le top 5 des pires temps voici l’incantation trier-les-lignes-renverser-l-ordre-et-prendre-les-5-premiers
user=> (take 5 (reverse (sort-by :t (:rows data))))
({:na 1, :by 172777, :rm "OK", :rc 200, :s "true", :ts 1.324109405557E12, :lt 1398, :t 13007, :lb "/CategoryDisplay"}
{:na 1, :by 33235, :rm "OK", :rc 200, :s "true", :ts 1.324109282891E12, :lt 8967, :t 8981, :lb "/--product--.html"} ...
Et voilà d’autres tours de sorciers inscrits dans le grimoire. Dans l’épisode 5 on regroupera ces données pour avoir des résultats plus détaillés, par label par exemple et on fera des graphes de ces données.
by Olivier RodriguesOlivier Rodrigues atJanuary 13, 2012 11:28 AM
La société suédoise Tobii Technology vient de mettre au point leur technologie eye tracking (Gaze Interface), pour l’interface Metro de Windows 8. Le eye tracking couplé au touchpad des notebooks offre des perspectives très séduisantes pour rendre encore plus naturelle l’expérience utilisateur. Cette nouveauté sera présenté au CES à Las Vegas cette semaine.
Voici les slides de la présentation que j’ai faite aux JUGs de Toulouse et Bordeaux le 7 et 8 décembre.
Je voudrais remercier ces JUGs ainsi que les deux genigraph.fr qui a publié un compte rendu sur son blog. Le JUG de Toulouse a également fait un compte-rendu de la soirée.
Une session similaire a été enregistrée par le JUG de Lausanne.
by Olivier RodriguesOlivier Rodrigues atJanuary 06, 2012 10:22 AM
Ca faisait longtemps que je n’avais plus eu recours à cette technologie. La semaine dernière un client me pose un problème intéressant : “Comment dans une architecture distribuée je peux garantir qu’un message envoyé par un client sera délivré au serveur même si celui-ci est inaccessible momentanément du à des problèmes réseau ou autre ? “
Et la les vieux souvenirs remontent et MSMQ couplé à WCF parait la solution la plus appropriée. Et me voila lancé dans le POC que je vous présente dans cet article.
Mais avant un petit rappel sur technologie MSMQ.
C’est le nom (pour le coup pas très original) de la technologie de Message Queuing présente chez Microsoft sur les serveurs depuis Windows NT. Cette technologie a été généralisé aussi sur les plateformes workstation (Windows XP, Windows 7) et aussi sur Windows embeded.
Le but de MSMQ est d’assurer la fiabilité des échanges entre des applications qui s’exécutent à des moments différents et/ou en utilisant des environnement réseau hétérogènes.
En gros j’ai une application A qui doit envoyer des données à une application X. En utilisant MSMQ si l’application X n’est pas accessible au moment ou l’application A envoie les données, les données en question seront stockées dans la file d’attente locale de la machine qui exécute l’application A. Lorsque la file d’attente distante sera accessible les message lui seront remis depuis la file d’attente locale.![]()
Pour pouvoir mettre en œuvre ce type de scénario il faut que MSMQ soit actif sur les machines exécutant les applications.
MSMQ peut être utilisé pour des scenarios synchrones et asynchrones pour des applications critiques comme :
Dans cet exemple nous allons démontrer ce scénario dans une architecture client/serveur en utilisant WCF et MSMQ. Cet exemple à été réalisé et testé avec Windows 7 x64, Visual Studio 2010.
Le code est disponible sur github https://github.com/imihalcea/MSMQ-WCF-Post.
Pour démarrer j’ai préparé une solution Visual Studio avec un client et un serveur qui communiquent en netTcp. La structure est la suivante :
- Orders.Client – client WPF qui permet la saisie d’une commande et l’envoie au serveur.La classe “OrderFacade” se charge d’assurer l’envoi d’une commande via le proxy généré coté client.
1: public class OrdersFacade
2: {
3: public void SendOrder(Order o)
4: {
5: using (var client = new OrdersServiceClient())
6: {
7: client.Open();
8: client.PrepareOrder(o);
9: client.Close();
10: }
11: }
12: }
La configuration du service coté client dans App.config est la suivante :
1: <system.serviceModel>
2: <bindings>
3: <netNamedPipeBinding>
4: <binding name="NetNamedPipeBinding_IOrdersService" />
5: </netNamedPipeBinding>
6: </bindings>
7: <client>
8: <endpoint address="net.pipe://[server host name or ip]/" binding="netNamedPipeBinding"
9: bindingConfiguration="NetNamedPipeBinding_IOrdersService"
10: contract="IOrdersService" name="NetNamedPipeBinding_IOrdersService">
11: <identity>
12: <userPrincipalName value="Ionut-PC\Ionut" />
13: </identity>
14: </endpoint>
15: </client>
16: </system.serviceModel>
- Orders.Common – librairie définissant le contrat de données, la classe Order, qui est échangée entre le client et le serveur. Cette librairie est référencée par “Orders.Service” et “Orders.Client”. Le proxy du service utilisé coté client utilise la classe Order définie dans ce projet.
1: public class Order
2: {
3: public string ProductName { get; set; }
4: public int Quantity { get; set; }
5: }
- Orders.Server – application console hôte du service.
1: static void Main(string[] args){2: _resetEvent = new AutoResetEvent(false);3:4: host = new ServiceHost(typeof(Orders.Services.OrdersService));5: host.Open();6: ....
La configuration du service est la suivante :
1: <system.serviceModel>
2: <services>
3: <service name="Orders.Services.OrdersService" behaviorConfiguration="serviceBehavior">
4: <endpoint contract="Orders.Services.IOrdersService" binding="netNamedPipeBinding" />
5: <endpoint address="mex" contract="IMetadataExchange" binding="mexHttpBinding" />
6: <host>
7: <baseAddresses>
8: <add baseAddress="net.pipe://[server host name or ip]"/>
9: <add baseAddress="http://[server host name or ip]:8000"/>
10: </baseAddresses>
11: </host>
12: </service>
13: </services>
14: <behaviors>
15: <serviceBehaviors>
16: <behavior name="serviceBehavior">
17: <serviceMetadata/>
18: </behavior>
19: </serviceBehaviors>
20: </behaviors>
21: </system.serviceModel>
- Orders.Services – librairie définissant et implémentant le contrat de service “OrdersService”.
1: [ServiceContract]
2: public interface IOrdersService
3: {
4: [OperationContract]
5: void PrepareOrder(Order order);
6: }
7:
8: public class OrdersService:IOrdersService
9: {
10:
11: public void PrepareOrder(Order order)
12: {
13: Console.WriteLine("Prepare order : {0}, Qty:{1}", order.ProductName,order.Quantity);
14: }
15: }
Si le serveur n’est pas disponible au moment ou le client va exécuter la méthode “SendOrder” de la classe “OrderFacade” j’aurai une jolie “EndPointNotFoundException” et le client ne pourra pas envoyer le message au serveur. J’aurai un problème d’indisponibilité du service.
Vous l’aurez compris il faut qu’on fasse rentrer MSMQ dans la boucle, pour résoudre notre problème.
Etape 1: Au niveau de Windows installez MSMQ (sur la machine cliente et serveur).
Après l’activation de MSMQ vous disposez d’une console de management et monitoring des files d’attente.
Etape 2: Ajouter le support de MSMQ ou niveau du serveur.
a) MSMQ est supporté uniquement pour les contrat de service à sens unique. Il faut donc ajouter le paramètre IsOneWay=true.
1: [ServiceContract]
2: public interface IOrdersService
3: {
4: [OperationContract(IsOneWay = true)]
5: void PrepareOrder(Order order);
6: }
7:
8: public class OrdersService:IOrdersService
9: {
10:
11: public void PrepareOrder(Order order)
12: {
13: Console.WriteLine("Prepare order : {0}, Qty:{1}", order.ProductName,order.Quantity);
14: }
15: }
b) Au niveau de l’application console “OrdersServer” qui héberge le service il faut ajouter la référence vers l’assembly “System.Messaging”. Cette assembly du framework .NET fournit une API permettant le travail avec des files d’attente. Au démarrage du serveur il faut rajouter le code qui va créer automatiquement la file d’attente distante si elle n’existe pas :
1: public const string NomFileAttente = @".\private$\OrdersQueue";
2:
3: private static void CreateQueueIfNotExist()
4: {
5: if (!MessageQueue.Exists(NomFileAttente))
6: {
7: MessageQueue.Create(NomFileAttente, true);
8: }
9: }
c) Modifier le binding du service dans le projet “OrdersServer”.
1: <configuration>
2: <system.serviceModel>
3: <services>
4: <service name="Orders.Services.OrdersService" behaviorConfiguration="serviceBehavior">
5: <endpoint contract="Orders.Services.IOrdersService" binding="netNamedPipeBinding" />
6: <endpoint address="mex" contract="IMetadataExchange" binding="mexHttpBinding" />
7: <endpoint address="net.msmq://[server host name or ip]/private/OrdersQueue" contract="Orders.Services.IOrdersService" binding="netMsmqBinding" bindingConfiguration="netMsmq"/>
8: <host>
9: <baseAddresses>
10: <add baseAddress="net.pipe://[server host name or ip]"/>
11: <add baseAddress="http://[server host name or ip]:8000"/>
12: </baseAddresses>
13: </host>
14: </service>
15: </services>
16: <bindings>
17: <netMsmqBinding>
18: <binding name="netMsmq">
19: <security mode="None"/>
20: </binding>
21: </netMsmqBinding>
22: </bindings>
23: <behaviors>
24: <serviceBehaviors>
25: <behavior name="serviceBehavior">
26: <serviceMetadata/>
27: </behavior>
28: </serviceBehaviors>
29: </behaviors>
30: </system.serviceModel>
31: </configuration>
d) Compiler la solution et lancez l’application console “OrdersServer”. Attention il faut l’exécuter en mode administrateur.
e) Régénérez le proxy coté client.
Etape 3 : Lancez le service et le client testez la solution.
a) Vous pouvez arrêter l’application console “OrdersServer”, lui envoyer des messages depuis le client puis avec la console de management regardez comment les messages sont stockes dans la file d’attente distante.
b) Vous pouvez arrêter physiquement le serveur ou le débrancher du réseau, puis luis envoyer des messages depuis le client. Sur la machine client vous pouvez constater (avec l’aide de la console de management) que les messages sont stockés dans la file d’attente locale en attendant que la file d’attente distante soit disponible.
c) Rebranchez le serveur et vous allez constater que les messages vont arriver dans la file distante dans un délais de 2 à 3 min.
Grâce à la puissance de WCF, .NET, MSMQ nous arrivons a obtenir une solution client serveur fiable avec un minimum d’effort. Je vous fais remarquer qu’une fois le support de MSMQ activé, nous avons ajouté 3 lignes de code et modifié un fichier de config.
1) Queues in WCF: http://msdn.microsoft.com/en-us/library/ms731089.aspx
2) Introduction à WCF : http://www.amazon.fr/dp/284177449X
3) Programming WCF Services : http://www.amazon.fr/dp/B0068GHGTI
Continuons notre voyage dans les sortilèges d’Incanter avec l’incantation de Mary Poppins,
supercalifragilisticexpialidocious
Le balai arrive. En route pour le monde où nos tableaux de nombres vont se ranger tout seuls !
Cet article fait partie d’une suite. L”épisode 1 présentait quelques bases de Clojure et Incanter. L’épisode 2 présentait les types.
Nous allons manipuler un peu plus d’API et quelques documents vont être bien utiles.
Clojure ainsi que la librairie présentent l’API complète ainsi que des cheat sheets pour regrouper les commandes les plus fréquentes.
Les librairies sont organisées en modules. Pour utiliser une des formules magiques vous devez d’abord charger les librairies correspondantes.
user=> (use 'incanter.core)
nil
user=> (use 'incanter.io)
nil
user=> (use 'incanter.datasets)
nil
Un peu laborieux au bout d’un moment, non ?
use ou require sont des fonctions du package clojure.core qui chargent des librairies, c’est à dire un ensemble de ressources qui se trouvent dans un package Java. Un fichier Clojure à la racine de cette librairie sert de descripteur.
On a souvent à charger différentes librairies avec le même préfixe (par exemple incanter.core incanter.io …)
On peut réécrire ces lignes en plus compact en utilisant la forme suivante (prefix list).
user=> (use '(incanter core io datasets))
nil
Vous noterez que les librairies sont maintenant passées dans une liste. Le premier élément est le préfixe, les autres sont les librairies sans le prefixe. Attention, les noms passés dans la liste ne doivent pas contenir de . .
Et bien sûr une ’ pour empêcher l’évaluation de la liste.
Pour commencer, il vous faut quelques tableaux de nombres.
Incanter propose un moyen rapide de créer un dataset à partir d’un fichier CSV. Si vous n’avez pas de données sous la main, Incanter est livré avec quelques jeux de données (http://liebke.github.com/incanter/datasets-api.html) que l’on peur charger en utilisant la fonction get-dataset.
Commençons avec un résultat de test JMeter transformé en CSV. Un tir étalon pour ne pas avoir trop de données pour le moment.
user=> (def data (read-dataset "/Users/cfalguiere/Workspaces/Diapason/report-data/20111217-ETALON_WSW_TPS2011.csv" :header true))
#'user/data
Petite vérification, avons nous bien des données ? 
user=> (view data)
#<JFrame javax.swing.JFrame[frame1,0,22,400x600,invalid,layout=java.awt.BorderLayout,title=Incanter Dataset,resizable,normal,defaultCloseOperation=HIDE_ON_CLOSE,rootPane=javax.swing.JRootPane[,0,22,400x578,invalid,layout=javax.swing.JRootPane$RootLayout,alignmentX=0.0,alignmentY=0.0,border=,flags=16777673,maximumSize=,minimumSize=,preferredSize=],rootPaneCheckingEnabled=true]>
user=>
La fonction view permet de visualiser ce dataset.
La fonction de dénombrement Clojure est count.
user=> (count [1 2 3])
3
user=> (count "Incanter")
8
Donc voilà
user=> (count data)
2
Mmmmm … 2 … ???
Regardons de plus près à quoi ressemble ce dataset.
user=> data
#:incanter.core.Dataset{
:column-names [:lb :t :lt :ts :s :rc :rm :by :na],
:rows (
{:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"}
{:na 1, :by 28383, :rm "OK", :rc 200, :s "false", :ts 1.324109266396E12, :lt 8511, :t 8523, :lb "/CategoryDisplay"}
;...
{:na 1, :by 861, :rm "OK", :rc 200, :s "true", :ts 1.324109908066E12, :lt 21, :t 21, :lb "/AjaxLastViewedDisplay"}
)}
Le dataset est une map composée de 2 éléments :column-names qui contient tous les labels de colonnes dans un vector et :rows qui est une liste comportant une map par ligne de données.
On obtient un résultat plus pertinent en comptant les entrées de la liste. Incanter fournit aussi une fonction nrow qui compte correctement les lignes d’un dataset (n’oubliez pas que le dataset n’est pas une structure de données Clojure)
user=> (count (:rows data))
119
user=> (nrow data)
119Le count de l’élément :rows de data nous donne bien le même compte que le nrow du dataset.
Quelques incantations Clojure pour se faire la main. Affectons les lignes du dataset a une variable pour les manipuler plus facilement.
user=> (def datarows (:rows data))
#'user/datarowsDonne moi les deux premières lignes !
user=> (take 2 datarows)
({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 28383, :rm "OK", :rc 200, :s "false", :ts 1.324109266396E12, :lt 8511, :t 8523, :lb "/CategoryDisplay"})Donne moi tous les labels !
On va utiliser la fonction map pour appliquer à chaque ligne une fonction qui renvoie le label.
user=> (map :lb datarows)
("/" "/CategoryDisplay" "/--product--.html" ...Et tout à la fois !
user=> (take 2 (map :lb datarows))
("/" "/CategoryDisplay")
Incanter propose des fonctions de manipulation de dataset. La fonction utilisée pour extraire des données est sel plus souvent utiilisée via son alias $.
L’alias $ a plusieurs formes. Il peut être utilisé avec la colonne seule (identifié par sa position, son nom, ou une collection de ces indications), ou bien la ligne et la colonne. Le dernier paramètre est le dataset.
La fonction sel place le dataset en premier argument et spécifie l’axe par des keywords.
Donne moi tous les labels !
user=> ($ :lb data)
("/" "/CategoryDisplay" "/--product--.html" ...
user=> (sel data :cols 0)
("/" "/CategoryDisplay" "/--product--.html" ...
Un usage typique est celui-ci, extraire une série, ici le temps de réponse et calculer un agrégat tel que la moyenne :
user=> (mean ($ :t data) )
1429.579831932773Vous avez testé et ça ne marche pas, jeune sorcier ? C’est normal, il faut le package incanter.stats qui n’a pas été chargé pour le moment.
Donne moi les deux premières lignes !
Pour la fonction sel le paramètre qui suit :rows indique soit un numéro particulier de ligne, soit une liste de numéros de lignes, soit un range qui va générer cette liste de numéros de lignes.
Lorqu’on utilise $ le premier paramètre représente la ligne (ou les lignes) et le second la colonne.
user=> (sel data :rows (range 2) )
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 28383, :rm "OK", :rc 200, :s "false", :ts 1.324109266396E12, :lt 8511, :t 8523, :lb "/CategoryDisplay"})}
user=> (sel data :rows '(0 1))
Ce qui est équivalent à :
user=> ($ (range 2) :all data)
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 28383, :rm "OK", :rc 200, :s "false", :ts 1.324109266396E12, :lt 8511, :t 8523, :lb "/CategoryDisplay"})}
Vous noterez le :all. La syntaxe oblige à spécifier deux paramètres si l’on veut indiquer des lignes, les lignes puis les colonnes. :all indique que toutes les colonnes doivent être conservées.
Donne moi les labels des cinq premières lignes !
user=> user=> ($ (range 5) :lb data)
("/" "/CategoryDisplay" "/--product--.html" ...
user=> (sel data :rows (range 5) :cols :lb)
("/" "/CategoryDisplay" "/--product--.html" ...
Toutes sortes de combinaisons sont possibles. L’expression suivante retourne un dataset ne comportant plus que les colonnes sélectionnées et les deux premières lignes.
user=> ($ (range 2) [:lb :t] data)
#:incanter.core.Dataset{:column-names [:lb :t], :rows ({:t 808, :lb "/"} {:t 8523, :lb "/CategoryDisplay"})}
S’il y a plusieurs colonnes, le résultat est un dataset, mais s’il n’y a qu’une ligne la structure de données est également différente. Le résultat est une liste et non un map. Les éléments sont ordonnés conformément aux colonnes du dataset.
user=> (sel data :rows 0)
("/" 808 794 1.324109256665E12 "false" 200 "OK" 27985 1)
user=> (:column-names data)
[:lb :t :lt :ts :s :rc :rm :by :na]
Pour finir, il existe un mot clé :not qui permet d’exprimer des listes par exclusion. Dans l’exemple suivant, l’expression conserve toutes les colonnes sauf :by et :na pour les deux premières lignes.
user=> ($ (range 2) [:not :by :na] data)
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm], :rows ({:rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:rm "OK", :rc 200, :s "false", :ts 1.324109266396E12, :lt 8511, :t 8523, :lb "/CategoryDisplay"})}
Attention jeune sorcier ça se complique.
Pour analyser les résultats on aura souvent besoin de retrouver des sous ensemble sur des critères particuliers, tous les relevés pour une url données (:lb), les relevés avec des temps très élevés (:t), ou les relevés en erreur (statut :s, return code :rc, message :rm).
Les collections Clojure ont une fonction filter filter. La ligne suivante donne tous les nombres pairs du vecteur [1 2 3 4 5 6].
user=> (filter even? [1 2 3 4 5 6])
(2 4 6)Dans le cas plus général on ne pourra pas utiliser une fonction existante et il faudra écrire sa propre fonction filtre.
user=> (defn filterAlice [name] (= name "Alice"))
#'user/filterAlice
user=> (filter filterAlice ["Alice", "Bob", "Charles"])
("Alice")
D’une manière générale on utilisera plutôt une fonction anonyme. Ce qui nous donne :
user=> (filter (fn[name](= name "Alice")) ["Alice", "Bob", "Charles"])
("Alice")
C’est un peu verbeux et vous trouverez plus souvent une forme abrégée utilisant #.
user=> (filter #(= % "Alice" ) ["Alice", "Bob", "Charles"])
("Alice")# est appelé la macro dispatch et a plusieurs effets décrits ici. Celui qui nous intéresse ici est #(...) qui est un équivalent de (fn [args] (...)).
user=> ( #(+ 1 %) 2 )
3
user=> ( #(+ 1 %1 %2) 2 3 )
6
% représente l’argument, ou les arguments, de la fonction anonyme, ici le paramètre :name implicite.
Cette fonction anonyme appliquée sur la liste de prénom donne bien le même résultat.
user=> (filter #(= % "Alice") ["Alice", "Bob", "Charles"])
("Alice")
Et sur mes données maintenant ?
Donne moi toutes les lignes dont le label est “/” !
user=> (filter #(= % "/") datarows)
()
C’est vide ? Normal, chaque ligne de la liste est une map. Il faut donc récupérer le :lb.
user=> (filter #(= (:lb %) "/") datarows)
({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 27974, :rm "OK", :rc 200, :s "false", :ts 1.324109395646E12, :lt 209, :t 213, :lb "/"} {:na 1, :by 27994, :rm "OK", :rc 200, :s "false", :ts 1.324109537182E12, :lt 247, :t 252, :lb "/"} {:na 1, :by 27983, :rm "OK", :rc 200, :s "false", :ts 1.324109682199E12, :lt 344, :t 349, :lb "/"} {:na 1, :by 27980, :rm "OK", :rc 200, :s "false", :ts 1.32410980267E12, :lt 212, :t 217, :lb "/"})
Autre critère, les temps supérieurs à 10s ? Changeons simplement de fonction pour #(> (:t %) 10000).
user=> (filter #(> (:t %) 10000) datarows)
({:na 1, :by 172777, :rm "OK", :rc 200, :s "true", :ts 1.324109405557E12, :lt 1398, :t 13007, :lb "/CategoryDisplay-REF"} {:na 0, :by 33268, :rm "OK", :rc 200, :s "true", :ts 1.324109406957E12, :lt 11602, :t 11607, :lb "http://www.witre.se/spannare_85915M.html?leafcode=85919&fromSearch=true"})
Et pour finir comment exprimer la négation ? Par exemple, trouver tous les relevés dont le statut n’est pas OK.
user=> (count (filter #(= (:rm %) "OK") datarows))
97
user=> (count (filter #(not= (:rm %) "OK") datarows))
22
user=> (count (remove #(= (:rm %) "OK") datarows))
22
Le total fait bien 119. Deux solutions sont possibles, remove ou filter avec l’expression complémentaire. La fonction remove n’altère pas la liste initiale.
Incanter propose la fonction query-dataset. Cette fonction a elle aussi un alias $where.
Les prédicats de query-dataset peuvent être exprimés dans un langage voisin du langage de requête de MongoDB.
Donne moi tous les relevés de la home page !
Ces relevés sont ceux dont la colonne :lb vaut “/”.
user=> (query-dataset data {:lb "/"} )
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 27974, :rm "OK", :rc 200, :s "false", :ts 1.324109395646E12, :lt 209, :t 213, :lb "/"} {:na 1, :by 27994, :rm "OK", :rc 200, :s "false", :ts 1.324109537182E12, :lt 247, :t 252, :lb "/"} {:na 1, :by 27983, :rm "OK", :rc 200, :s "false", :ts 1.324109682199E12, :lt 344, :t 349, :lb "/"} {:na 1, :by 27980, :rm "OK", :rc 200, :s "false", :ts 1.32410980267E12, :lt 212, :t 217, :lb "/"})}
La même chose exprimée avec l’alias $where
user=> ($where {:lb "/"} data)
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 27974, :rm "OK", :rc 200, :s "false", :ts 1.324109395646E12, :lt 209, :t 213, :lb "/"} {:na 1, :by 27994, :rm "OK", :rc 200, :s "false", :ts 1.324109537182E12, :lt 247, :t 252, :lb "/"} {:na 1, :by 27983, :rm "OK", :rc 200, :s "false", :ts 1.324109682199E12, :lt 344, :t 349, :lb "/"} {:na 1, :by 27980, :rm "OK", :rc 200, :s "false", :ts 1.32410980267E12, :lt 212, :t 217, :lb "/"})}
On peut aussi exprimer plusieurs critères. Ci-dessous, les temps entre 8s et 10s.
user=> (query-dataset data {:t {:$gt 8000 :$lt 10000} } )
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ({:na 1, :by 28383, :rm "OK", :rc 200, :s "false", :ts 1.324109266396E12, :lt 8511, :t 8523, :lb "/CategoryDisplay"} {:na 1, :by 33235, :rm "OK", :rc 200, :s "true", :ts 1.324109282891E12, :lt 8967, :t 8981, :lb "/--product--.html"} {:na 1, :by 30959, :rm "OK", :rc 200, :s "false", :ts 1.324109547672E12, :lt 8787, :t 8792, :lb "/CategoryDisplay"})}
Ou bien les code retours qui ne font pas partie de la liste acceptée, 200 (OK) ni 302 (redirect).
user=> (query-dataset data {:rc {:$nin #{200 302} } } )
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ()}
user=> (query-dataset data {:rm {:$nin #{"OK" "Found"} } } )
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ()}
Les opérateurs disponibles dans les requêtes sont :$gt, :$lt, :$gte, :$lte, :$eq, :$ne, :$in, :$nin, $fn.
Les prédicats peuvent également être définis de manière classique par une fonction anonyme. La fonction s’applique à la ligne.
user=> (query-dataset data #(= (:lb %) "/" ) )
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 27974, :rm "OK", :rc 200, :s "false", :ts 1.324109395646E12, :lt 209, :t 213, :lb "/"} {:na 1, :by 27994, :rm "OK", :rc 200, :s "false", :ts 1.324109537182E12, :lt 247, :t 252, :lb "/"} {:na 1, :by 27983, :rm "OK", :rc 200, :s "false", :ts 1.324109682199E12, :lt 344, :t 349, :lb "/"} {:na 1, :by 27980, :rm "OK", :rc 200, :s "false", :ts 1.32410980267E12, :lt 212, :t 217, :lb "/"})}
Si vous avez déjà des fonctions dans vos grimoires, vous pouvez bien sûr les utiliser directement.
user=> (defn isHomePage [row] (= (:lb row) "/"))
#'user/isHomePage
user=> (query-dataset data isHomePage )
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 27974, :rm "OK", :rc 200, :s "false", :ts 1.324109395646E12, :lt 209, :t 213, :lb "/"} {:na 1, :by 27994, :rm "OK", :rc 200, :s "false", :ts 1.324109537182E12, :lt 247, :t 252, :lb "/"} {:na 1, :by 27983, :rm "OK", :rc 200, :s "false", :ts 1.324109682199E12, :lt 344, :t 349, :lb "/"} {:na 1, :by 27980, :rm "OK", :rc 200, :s "false", :ts 1.32410980267E12, :lt 212, :t 217, :lb "/"})}
Voilà, beaucoup de sortilèges a expérimenter en attendant l’épisode 4. Attention à ne pas faire trop de dégâts !