Brouillon : cet article n'est pas publié. Il n'apparaît pas dans le blog et n'est pas indexé.

· Par Équipe HeyData

Série : Concevoir des jobs Talend : les bonnes pratiques de Dale Anderson, relues pour la version 8 de Talend Studio · article 2 sur 7

Structurer un job Talend : canevas, jobs parent et enfants, tPreJob et tPostJob

Un job Talend se lit d’abord comme un dessin. Avant d’ouvrir le moindre composant, on voit sur le canevas d’où viennent les données, où elles vont et ce qui se passe en cas d’erreur. Encore faut-il que le dessin soit clair. Un job de quarante composants posés en zigzag, aux liens qui se croisent, se comprend mal et se corrige plus mal encore. La structure d’un job est donc la première bonne pratique.

Cet article synthétise et adapte à Talend Studio 8 les parties 1 et 3 de « Talend Job Design Patterns and Best Practices » de Dale Anderson (Talend, 2015-2016). Ses conseils restent des références : nous les relisons avec les outils d’aujourd’hui.

Le canevas se lit de haut en bas

Dale Anderson propose une règle simple : on construit de haut en bas, puis de gauche à droite. Le chemin normal du traitement va vers la droite et vers le bas ; un chemin qui part vers la gauche signale une erreur. Les liens ne doivent pas se croiser.

Un job sur trois niveaux numérotés. En 1, le sous-job tPreJob, libellé Initialiser, avec la note contextes, connexions. En 2, la boucle principale : tDBInput Lire, relié par row1 (Main) à tMap Transformer, relié par out1 (Main) à tDBOutput Écrire ; depuis tDBInput, un lien rouge OnComponentError part vers la gauche jusqu’à tWarn Signaler, puis OnComponentOk descend vers tDie Arrêter. En 3, le sous-job tPostJob, libellé Terminer, avec la note fermer, journaliser. Une légende associe le vert au chemin normal et le rouge au chemin d’erreur.

Initialiser, traiter, terminer : l’erreur se repère d’un coup d’œil parce qu’elle part vers la gauche.

L’auteur reconnaît que la disposition en zigzag est tentante, parce qu’elle est rapide à écrire : on aligne les composants vers la droite, puis on revient à gauche une ligne plus bas. Mais si un job a besoin de plusieurs lignes pour tenir à l’écran, c’est souvent qu’il en fait trop. Dans la même veine, il conseille d’aérer le canevas : des composants serrés les uns contre les autres se lisent mal.

Des jobs atomiques, orchestrés par un parent

Un gros job est difficile à comprendre, à déboguer et à maintenir. La solution consiste à le découper en jobs enfants qui ont chacun un seul objectif, et à les faire appeler par un job parent avec le composant tRunJob. Le parent orchestre, les enfants travaillent. Le découpage facilite aussi la gestion des erreurs : le parent décide de ce qui se passe quand un enfant échoue.

À gauche, le job parent job_main_ventes : un sous-job tPreJob ; un tRunJob Extraire relié par OnSubjobOk à un tRunJob Charger ; un sous-job tPostJob relié par OnComponentOk à un joblet de log. À droite, trois cadres imbriqués illustrent la profondeur des appels tRunJob : niveau 1 parent, niveau 2 enfant, niveau 3 petit-enfant, avec la mention 3 niveaux conseillés, 5 au plus. En dessous, une légende : tRunJob, appelé, est un job à part avec ses propres contextes ; un joblet, inclus, est intégré au code généré du job.

Le parent enchaîne les enfants ; le joblet, lui, fait partie du job qui l’utilise.

Jusqu’où découper ? Dale Anderson fixe un repère : pas plus de trois niveaux d’appels tRunJob imbriqués, cinq au grand maximum. Au-delà, la mémoire consommée, les paramètres à transmettre et le débogage deviennent difficiles à maîtriser. Le guide de cycle de développement de Qlik Talend donne un repère complémentaire : éviter plus de vingt composants par job.

Deux options de tRunJob méritent d’être connues :

  • Transmit whole context transmet au job enfant tous les contextes du parent, ce qui évite de les passer un à un ;
  • Use an independent process to run subjob lance l’enfant dans une JVM distincte, avec sa propre mémoire. Ce n’est pas du parallélisme à proprement parler, mais cela isole un enfant gourmand.

tRunJob ou joblet ?

La différence tient en deux mots : un job enfant est appelé, un joblet est inclus. Le job enfant reste un job à part, exécuté par tRunJob, avec ses propres contextes et son propre code généré. Le joblet est un fragment de job que Studio intègre au code du job qui l’utilise : il partage son contexte et son exécution.

Pour Dale Anderson, les bons cas d’usage des joblets sont peu nombreux, mais précieux quand on les trouve. Il en distingue deux formes :

  • le joblet classique reçoit des lignes par son entrée, les traite (mise à jour d’une table, écriture d’un log) et les renvoie par sa sortie ;
  • le joblet sans entrée ni sortie se contente d’agir dans son coin. L’exemple type est un tLogCatcher qui capte les erreurs du job pour les journaliser : nous le détaillons dans l’article sur les erreurs.

Pour qu’un joblet serve à plusieurs projets, rangez-le dans un projet de référence. Nous y revenons dans l’article sur l’industrialisation.

Des points d’entrée et de sortie explicites

Tout job commence et finit quelque part. Studio fournit deux composants pour le marquer :

  • tPreJob s’exécute avant le reste du job. C’est l’étape d’initialisation : charger les contextes, ouvrir les connexions, écrire un premier message de log ;
  • tPostJob s’exécute après, même si le job s’est arrêté sur un tDie ou sur une erreur avec l’option Die on error. C’est l’étape de clôture : fermer les connexions, faire le ménage, journaliser la fin.

tPreJob et tPostJob ne se relient pas au reste du job : Studio les exécute d’office, au début et à la fin. On relie seulement leurs propres composants, par exemple tPreJob vers un tDBConnection.

Les composants tWarn et tDie complètent ce dispositif. tWarn envoie un avertissement sans interrompre le job ; tDie l’arrête avec un code et un message. Tous deux alimentent tLogCatcher, ce qui en fait les outils naturels d’une gestion d’erreurs maîtrisée.

Identifier la boucle principale

Dans presque tout job, un traitement fait l’essentiel du travail : la boucle principale, généralement le premier flux de données dont la fin marque la fin du job. Des boucles secondaires peuvent s’y imbriquer, par exemple une itération sur des fichiers. Dale Anderson recommande d’identifier explicitement la boucle principale et d’ajouter un tWarn et un tDie sur le composant qui la pilote. On contrôle ainsi la sortie du job, au lieu de la subir.

Ce qui a changé depuis 2016

  • Les liens du canevas sont courbes depuis Talend 6. Ils évitent mieux les croisements, mais ne dispensent pas d’une disposition claire.
  • Le repère des vingt composants par job vient du guide de cycle de développement de Qlik Talend. Il complète la règle des trois niveaux d’appels.
  • Les jobs enfants se gèrent aujourd’hui avec Git, ce qui rend le découpage plus simple à suivre : chaque job a son historique.
  • Le module Job Script décrit par Dale Anderson est toujours livré avec Talend Studio 8 : un job peut s’écrire en texte, puis se générer.

À retenir

  • De haut en bas, puis de gauche à droite. Le chemin normal va à droite et en bas, l’erreur part à gauche.
  • Un job, un objectif. Découpez en jobs enfants appelés par un parent.
  • Trois niveaux d’appels. Cinq au plus, et vingt composants par job comme repère.
  • Appelé ou inclus. tRunJob appelle un job à part ; un joblet est intégré au job qui l’utilise.
  • Entrée et sortie explicites. tPreJob initialise, tPostJob clôt, même après un tDie.
  • La boucle principale sous contrôle. Un tWarn et un tDie sur le composant qui la pilote.

Essayer dans Talend Studio

Pour vérifier que tPostJob s’exécute même après un arrêt :

  1. Créez un job job_demo_enfant : un tPreJob relié par OnComponentOk à un tJava qui affiche System.out.println("Initialisation");.
  2. Ajoutez un tFixedFlowInput produisant une ligne, relié à un tDie (message « Arrêt volontaire », code 4).
  3. Ajoutez un tPostJob relié à un tJava qui affiche System.out.println("Clôture");.
  4. Exécutez le job : la console affiche « Initialisation », l’arrêt du tDie, puis « Clôture ».
  5. Créez un job job_demo_parent contenant un tRunJob qui appelle job_demo_enfant, avec Die on child error décoché. Ajoutez un tJava relié par OnSubjobOk qui affiche le code retour de l’enfant :
System.out.println("Code retour de l'enfant : " + globalMap.get("tRunJob_1_CHILD_RETURN_CODE"));

Le parent se termine normalement et lit le code 4 renvoyé par l’enfant.

Variante : partir d’un Job Script

Un Job Script est la description d’un job sous forme de texte : composants, réglages et liens. Studio sait générer un job à partir de ce texte, et produire le texte d’un job existant. Voici les deux jobs de la recette, prêts à coller. D’abord job_demo_enfant :

SCRIPT_VERSION=4.2,
JobType= Standard,
DEFAULT_CONTEXT: DEV,
ContextType { NAME: DEV }
addComponent {
  setComponentDefinition { TYPE: "tPrejob", NAME: "tPrejob_1", POSITION: 64, 64 }
  setSettings { LABEL : "Initialiser" }
}
addComponent {
  setComponentDefinition { TYPE: "tJava", NAME: "tJava_1", POSITION: 256, 64 }
  setSettings {
    CODE : "System.out.println(\"Initialisation\");",
    LABEL : "Initialisation"
  }
}
addComponent {
  setComponentDefinition { TYPE: "tFixedFlowInput", NAME: "tFixedFlowInput_1", POSITION: 64, 192 }
  setSettings {
    NB_ROWS : "1",
    USE_SINGLEMODE : "true",
    VALUES { SCHEMA_COLUMN : "message", VALUE : "\"demo\"" },
    LABEL : "Une ligne"
  }
  addSchema {
    NAME: "tFixedFlowInput_1", CONNECTOR: "FLOW"
    addColumn { NAME: "message", TYPE: "id_String", NULLABLE: true }
  }
}
addComponent {
  setComponentDefinition { TYPE: "tDie", NAME: "tDie_1", POSITION: 256, 192 }
  setSettings {
    MESSAGE : "\"Arrêt volontaire\"",
    CODE : "4",
    EXIT_JVM : "false",
    LABEL : "Arrêt volontaire"
  }
}
addComponent {
  setComponentDefinition { TYPE: "tPostjob", NAME: "tPostjob_1", POSITION: 64, 320 }
  setSettings { LABEL : "Clôturer" }
}
addComponent {
  setComponentDefinition { TYPE: "tJava", NAME: "tJava_2", POSITION: 256, 320 }
  setSettings {
    CODE : "System.out.println(\"Clôture\");",
    LABEL : "Clôture"
  }
}
addConnection {
  TYPE: "COMPONENT_OK", NAME: "OnComponentOk", LINESTYLE: 3,
  METANAME: "tPrejob_1", SOURCE: "tPrejob_1", TARGET: "tJava_1"
}
addConnection {
  TYPE: "FLOW", NAME: "row1", LINESTYLE: 0,
  METANAME: "tFixedFlowInput_1", SOURCE: "tFixedFlowInput_1", TARGET: "tDie_1"
}
addConnection {
  TYPE: "COMPONENT_OK", NAME: "OnComponentOk", LINESTYLE: 3,
  METANAME: "tPostjob_1", SOURCE: "tPostjob_1", TARGET: "tJava_2"
}

Puis job_demo_parent. Son tRunJob désigne l’enfant par son nom, mais surtout par PROJET:ID_DE_L_ENFANT : le nom technique du projet, en majuscules, et l’identifiant interne que Studio a donné à l’enfant à sa création. Sans cet identifiant, le tRunJob ne trouve pas le job à lancer.

SCRIPT_VERSION=4.2,
JobType= Standard,
DEFAULT_CONTEXT: DEV,
ContextType { NAME: DEV }
addComponent {
  setComponentDefinition { TYPE: "tRunJob", NAME: "tRunJob_1", POSITION: 64, 64 }
  setSettings {
    PROCESS : "job_demo_enfant",
    PROCESS:PROCESS_TYPE_PROCESS : "PROJET:ID_DE_L_ENFANT",
    PROCESS:PROCESS_TYPE_CONTEXT : "DEV",
    PROCESS:PROCESS_TYPE_VERSION : "Latest",
    DIE_ON_CHILD_ERROR : "false",
    TRANSMIT_WHOLE_CONTEXT : "true",
    LABEL : "Appeler l'enfant"
  }
}
addComponent {
  setComponentDefinition { TYPE: "tJava", NAME: "tJava_1", POSITION: 64, 192 }
  setSettings {
    CODE : "System.out.println(\"Code retour de l'enfant : \" + globalMap.get(\"tRunJob_1_CHILD_RETURN_CODE\"));",
    LABEL : "Lire le code retour"
  }
}
addConnection {
  TYPE: "SUBJOB_OK", NAME: "OnSubjobOk", LINESTYLE: 1,
  METANAME: "tRunJob_1", SOURCE: "tRunJob_1", TARGET: "tJava_1"
}

Pour les déployer, dans un projet de test :

  1. Dans le référentiel, Code › Job Scripts, clic droit Create JobScript (« Créer un script de Job ») ; nommez-le job_demo_enfant.

  2. Collez le script dans l’éditeur et enregistrez (Ctrl+S) : les erreurs de syntaxe sont soulignées.

  3. Cliquez sur Generate Job (« Générer un Job ») dans la barre d’outils : le job apparaît sous Job Designs.

  4. Relevez l’identifiant de l’enfant. Dans le workspace de Studio, ouvrez <PROJET>/process/job_demo_enfant_0.1.properties : c’est l’attribut id de la balise <TalendProperties:Property …>, de la forme _aBc123… (pas xmi:id). En ligne de commande :

    grep -o '<TalendProperties:Property [^>]*' job_demo_enfant_0.1.properties | grep -o ' id="[^"]*"'
  5. Dans le script du parent, remplacez PROJET:ID_DE_L_ENFANT, par exemple par DEMO:_aBc123…, puis créez et générez job_demo_parent comme l’enfant.

  6. Ouvrez le parent, vérifiez dans le tRunJob le job appelé et le contexte DEV, puis lancez-le. Le champ Job est vide ? L’identifiant est faux : resélectionnez l’enfant avec le bouton ….

  7. Si le menu n’apparaît pas, créez un job vide, ouvrez l’onglet Jobscript de son éditeur, collez le script et enregistrez.

La syntaxe des Job Scripts n’a pas de documentation publique complète. Pour connaître le nom exact d’un réglage, construisez le composant à la main, puis faites clic droit Export Job Script sur le job et comparez. L’identifiant change à chaque création de l’enfant : si vous le régénérez, régénérez aussi le parent. Enfin, un Job Script ne doit jamais contenir de mot de passe : les connexions passent par les métadonnées et les contextes du référentiel.

Sources

Tout le mérite du contenu original revient à Dale Anderson ; l’adaptation à Talend Studio 8 et ses éventuelles erreurs sont de HeyData. Cet article fait partie de notre série sur les bonnes pratiques de conception de jobs Talend : découvrir la série.