Taming the Camel

Le but de cet article est de pousser un peu plus la compréhension des mécanismes cachés de Camel, "under the hood"! D'où ce titre "racoleur" : Dompter Camel, en référence au grand nombre d'articles de présentation de Camel que l'on peut trouver sur le net intitulés "Riding the Camel"... :)

La documentation du site officielle, quoique plutôt claire sur les fonctionnalités offertes par l'outil, occulte plus généralement l'aspect technique de leur utilisation. Je veux plus particulièrement parler de ce qui se passe dans la JVM selon les routes mises en place.

Après avoir développé une première application sur les seules bases de la documentation et du livre "Camel in action", de nombreux problèmes sont survenus liés à l'existence de multiples threads dans la JVM, issus des différentes routes créées. En effet, même si lors de la première conception il était apparu assez évident de créer ces différentes routes (effectuant des opérations bien particulières), il est devenu rapidement compliqué de les orchestrer pour réaliser le flux de traitements métier souhaité.

En cherchant un peu plus précisément dans la documentation autour de cette problématique, on tombe rapidement sur la description de plusieurs mécanismes permettant de mieux gérer l'enchainement des routes :

  • le plus simple est l'ajout sur une route de l'instruction .noAutoStartup() pour indiquer qu'elle ne doit pas se lancer au démarrage de l'application. Elle pourra être démarrée par la suite avec : getContext().startRoute("myRoute")
  • l'utilisation de l'instruction ".onCompletion()" qui permet d'écrire la suite de la route à exécuter uniquement à la fin de la route précédente. Il est alors possible de conditionner le déroulement avec par exemple ".onFailureOnly()" pour prendre en compte les cas d'erreurs ou encore le test d'un prédicat ".onWhen()" pour conditionner la suite des opérations. Pour plus de détails voir : http://camel.apache.org/oncompletion.html
  • l'injection au sein des routes d'un système de synchronisation, basé sur le concept de "UnitOfWork" : son principe est de regrouper un ensemble de tâches à exécuter lors d'un échange. Ceci offre la possibilité d'ajouter (ou de supprimer) l'appel à une ou plusieurs actions exécutées séquentiellement à la fin de l'échange. Voici donc le modèle objet entrant en jeu :
    L'objet "Synchronization" possède deux méthodes utilisées comme callback par "UnitOfWork" dans la méthode "done()" (invoquée à la fin de l'Exchange). L'objet "UnitOfWork" est propre à chaque échange (injecté par un processeur particulier).
    Exemple d'utilisation :
    from("direct:download")
    .process(new Processor() {
     @Override
     public void process(Exchange e) throws Exception {
      e.getUnitOfWork().addSynchronization(new MySynchro());
     }
    }).to("file:...");
    
    ...
    
    public class MySynchro implements Synchronization {
     public void onComplete(Exchange e) {
      ...
     }
     
     public void onFailure(Exchange e) {
      ...
     }
    }
    

La différence fondamentale entre "onCompletion" et Synchronization est le modèle de thread utilisé. Synchronization implique l'usage du même thread pour réaliser les tâches spécifiées, qui sera donc bloqué jusqu'à leur fin. Au contraire, "onCompletion" transfert l'échange (une copie en réalité) à un thread indépendant (celui de la route suivante).

Mais ces différentes options, même si elles apportent la possibilité de garder la main sur le séquencement des opérations, ne résolvent toujours pas le problème de l'apparition de multiples threads dans la JVM... Pour palier à ceci, et c'est à partir de ce moment que l'on se rend compte des lacunes de la documentation, il existe une syntaxe précise qui permettra d'enchainer les traitement au sein d'un même thread :

from("file://C:/test").to("direct:sameThread");

from("direct:sameThread").to("...");

L'utilisation de "direct:" spécifie une exécution continue des opérations, tandis que "seda:" permet de réaliser des traitements asynchrones. Mais le point commun - essentiel ! - à cet appel par système d'ID sur les routes est, vous l'aurez compris, l'utilisation d'un seul thread pour la totalité des instructions.

Voici un tableau comparatif de ce qui se passe dans la JMV (via la JConsole) dans tous ces cas de figures :

from("file://C:/test").to("...");
from("file://C:/test/unzipped").to("...");
from("direct:sameThread").to("...");
Deux threads créés à partir des routes utilisant "file://"
from("file://C:/test").to("...").process(new Processor() {
 @Override
 public void process(Exchange exchange) throws Exception {
   getContext().startRoute("idleRoute");
  }
 });
from("file://C:/test/unzipped").routeId("idleRoute").noAutoStartup().to("...");
from("direct:sameThread").to("...");
Un seul thread au démarrage de l'application Un deuxième thread démarré dès l'appel à la deuxième route
from("file://C:/test").to("direct:sameThread");
from("direct:sameThread").to("...");
Présence du seul thread de l'application

Pour conclure, vous pouvez voir qu'il est très important de connaître le fonctionnement interne de Camel afin d'optimiser la composition de l'application. Ceci permet d'une part de limiter les problèmes de concurrence lors du déroulement des routes et d'autre part de créer un système plus "léger" qui facilitera notamment l'extinction du contexte Camel de par le nombre limité de threads.

Sur ce, en selle!


Fichier(s) joint(s) :

0 commentaires: