NACA : une solution pour migrer vos legacy COBOL vers Java ?

Nous avons eu une présentation sur NACA au JUGL, la session est en ligne sur Parleys. On ne peut pas dire que cela a attiré les foules : nous étions 3. Pourtant, il est plus que probable que nous côtoyons encore longtemps des legacies COBOL (le nombre de lignes de code COBOL continue de croitre de 5 milliards chaque année d'après Microfocus ) et la même question se répétera : "est-ce raisonnable/envisageable de migrer cette application cruciale pour notre activité ?".

NACA est un framework opensource pour transcoder du code COBOL (plutôt IBM & CICS) en fichier java (un 'truc compilable' avec javac et qui s'exécute dans une JVM) - je ne parle pas, intentionnellement, de programme Java. Cela comprend un transcodeur, un runtime et des outils de tests isofonctionnels. Le runtime émule les appels CICS et les verbes COBOL. Le transcodeur génère un code Java qui est le plus proche possible du COBOL pour permettre d'utiliser les compétences de l'équipe existante - quitte à avoir du mal à convaincre des développeurs Java de travailler sur ce code -. NACA s'exécute dans un container de Servlet ou plus simplement dans une JVM. Il est né d'un important projet de migration qui visait à sortir une application du mainframe pour faire des économies sur le run. Une application plutôt orientée TP que batch. C'était une application mature avec très peu d'évolution.

La version OpenSource n'a pas été mise à jour depuis 2 ans et demi et ne le sera plus. Elle est fonctionnelle (une société: a choisi de partir dessus après un POC) mais les améliorations (nouveaux verbes COBOL, émulation de la partie transactionnelle de CICS, etc.) et certains outils n'ont pas été reversés dans le tronc. Les évolutions sont maintenant la propriété de la société montée par les créateurs de l'outil : Eranea.

Mon avis en version courte

Les + :

  • Pour les projets qui correspondent au cadre, il y a un modèle de pricing qui permet de financer le projet sur les économies de licences du mainframe
  • En plus de l'outillage les auteurs sont clairs sur la méthode et les cas où l'utiliser (et ceux où il ne faut pas l'utiliser)
  • Les outils et la méthode ont déjà été testé sur un projet : un gros projet a été fait avec, un autre en cours. D'autres projets (<10) utilisent le code NACA disponible en Open Source, sans passer par Eranea.
  • Il est possible à la marge d'utiliser certains framework java (les exemples cités : exposition d'un webservice, génération de pdf, etc.)
  • On diminue l'effet tunnel en migrant rapidement les premiers utilisateurs (on commence par ceux qui utilisent un petit nombre d'écrans)
  • Pour les équipes informatiques en place, le message est clair : la plateforme sera abandonnée. Cela évite l'effet de la nouvelle application qui traine à se mettre en place et l'ancienne qui continue à évoluer, plus vite que le code n'est migré, car il reste plus facile d'écrire des fonctionnalités dans l'ancien environnement mature et maitrisé.

Les - :

  • Le code transcodé en Java n'est pas un programme Java : perte du typage à la compilation, les types Runtime ne sont pas ceux du langage/jdk, il n'y a plus de pile d'appels (et donc plus d'Exception) et la gestion de la mémoire est faite à la main (façon COBOL) au lieu d'utiliser le GC.
  • Première conséquence : je ne crois pas du tout que le code transcodé en Java (qui s'exécute dans une JVM) sera un intermédiaire facilitant ensuite la migration en programme Java (maintenable par un développeur Java 'classique'). Cela restera du code COBOL dans des fichiers '.java', définitivement.
  • Deuxième conséquence : je ne sais pas quel profil Java il est possible d'attirer sur ce genre de projet, moi je n'ai pas envie de travailler sur ce type de code
  • Il y aura peu de gain technique durant la migration, car la référence reste COBOL. Par exemple les calculs seront toujours fait avec une librairie COBOL au lieu de java.lang.Math, il sera difficile d'intégrer des frameworks au sein du logiciel (c'est possible à la marge).
  • Je n'ai pas vu de solution claire pour la partie batch. Les premiers projets concernés n'ont pas eu semble-t-il de souci sur cette partie car il s'agissait de systèmes principalement TP.

Méthodologie de migration :

Le modèle proposé par NACA, est de migrer le code COBOL en java par transcodage automatique. Durant le projet de migration la 'version Java' et la version COBOL cohabitent en parallèle run (sur une base de données DB2 commune). Les utilisateurs sont basculés par populations sur des périmètres applicatifs de plus en plus larges (ce qui suppose d'identifier ces populations). Cela a deux vertus principales: financer très tôt le projet et permettre de faire monter en charge progressivement la nouvelle plateforme de façon à l'optimiser.

Financer le projet : cela utilise le modèle de facturation du mainframe, les licences logicielles payées dépendent des ressources consommées, dès qu'une partie des utilisateurs quittent le mainframe. Ainsi le projet peut trouver un ROI très tôt, même si une partie seulement de la migration est effectuée.

Montée en charge progressive : la 'version Java' va monter en charge progressivement, il sera possible d'optimiser la plateforme (comprendre améliorer les algorithmes de transcodage, le paramétrage de la JVM, la base de données et l'infrastructure de production). La bascule finale se fera sur une version qui aura déjà fait ses preuves en production. Durant toute la migration, le code COBOL reste la référence et est totalement transcodé toutes les nuits. On ne change donc jamais le code généré. Si un bug n'existe que dans la version Java, il faut corriger le transcodeur, pas le code Java. Si un bug existe en Java et en COBOL, il faut corriger le code COBOL et regénérer le code Java.

Performances : Du coté des performances la démarche et de faire la montée en charge progressive pour améliorer la platforme. Du coté des performances pure d'une application Java sur plateforme Intel n'a pas à rougir des performances d'un mainframe. La scabilité est assurée de façon horizontale en ajoutant le nombre de serveurs nécessaire (on parle de quelques serveurs sur les projets mentionnés dans la présentation, par exemple de 3 pentiums biprocesseur pour remplacer un mainframe de 750'000 transactions par jour). Il ne faut pas oublié qu'il s'agit de migrer des applications COBOL qui sont déjà dans le même paradigme d'accès aux données que les programmes Java (SQL / DB2). Les optimisations faites sur les programmes COBOL restant relativement valable en Java/JDBC, ce qui ne serait pas vrai avec d'autres types de source de données (fichiers séquentiels).

Une ligne de COBOL donne une ligne de code en Java avec en commentaire la ligne COBOL qui est à l'origine du Java transcodé.

// Sur chaque ligne Java, son équivalent COBOL
DataSection workingstoragesection = declare.workingStorageSection();// (27) WORKING-STORAGE SECTION.
                                                                    // (28)
...
Var zpass = declare.level(1).var();                                 // (44) 01  ZPASS.
  public FUF1PA01 fuf1pa01 = FUF1PA01.Copy(declare.getProgram()); // (45)     COPY FUF1PA01.
                                                                    // (46)

Le code généré n'est pas maintenable par des 'Javaiste'

Gestion des variables

Les types manipulés ne sont pas des types Java mais ceux du COBOL : les chaines sont en ebcdic, pareil pour tous les types de base (float, int, etc.). En fait, les programmes continuent de gérer la mémoire à travers un buffer ( byte[ ] ) comme cela était fait en COBOL avec les risques de débordements et de fuites...

// Exemple de déclaration de variables
Var rs_Abenddb2 = declare.level(1).var() ;                       //(33)01 RS-ABENDDB2.
  Var rs_Constdb2 = declare.level(5).picX(1).valueSpaces().var();//(34)05  RS-CONSTDB2 PIC X VALUE SPACES.
  Var rs_Sqlcode = declare.level(5).pic9(3).valueZero().var() ;  //(35)05  RS-SQLCODE  PIC 9(3) VALUE ZERO

Pour les auteurs cette implémentation via un byte[] a été faite a posteriori après avoir rencontré des soucis avec le garbage collector. Je serai surpris que cela soit encore vrai sur les nouvelles JVM (et intéressé pour avoir plus d'information sur les problèmes rencontrés). En effet, une conséquence de cette gestion est d'instancier un objet à chaque accès en lecture/écriture vers le buffer pour le convertir en byte[] et d'appeler les fonctions de conversion. De mon point de vue on multiplie les objets en mémoire. Ce sont certes des instances de très courte vie (donc très facilement libéré par un GC générationnel), mais par contre les byte[] sont des objets de durée de vie plus longue.

Utilisation du Jdk

Le jdk n'est pas appelé dans le code transcodé, le code appelle le runtime NACA qui est implémenté avec le Jdk. Par ailleurs les réflexes valables en Java ne le sont plus dans un tel code. Un exemple ? Comment on convertit un caractère en sa valeur hexadécimal ?

En Java :

assertEquals("41", Integer.toHexString('A')); // 0x41 = 65

En Naca :

On fait une variable chaine, une autre numérique qui fait un 'redefine' de la première et hop c'est fait. Sauf que ça renvoie 0xC1 et pas 0x41... WTF ?!? Et oui, ces programmes manipulent des chaines EBCDIC. Ainsi le code du caractère 'A' est 0xC1 au lieu de 0x41 (ASCII)...

Les objets

Le code COBOL n'est pas orienté objet, le code transcodé non plus. (le runtime NACA est écrit en 'vrai Java' objet, mais ce n'est pas le cas des programmes qu'il exécute).

Les fonctions

Les fonctions sont dénaturées : en COBOL comme en Basic, il n'y a pas de pile d'appels et toutes les variables sont globales. On gère les débranchements avec une commande GOTO. Les programmes sont exécutés comme des scripts (on commence en haut et on déroule jusqu'au premier GOTO ou jusqu'à la fin du fichier (à la fin du fichier, le programme s'arrête). Pour émuler les perform (GOTO) l'approche a été d'utiliser des fonctions Java. Mais ce qui est particulier c'est que le framework NACA va simuler le comportement COBOL dans ces 'programmes Java' : une fois une fonction terminée, il va trouver la fonction qui suit directement la suivante (en terme de position dans le fichier java) et l'exécuter. Un peu déroutant pour un développeur Java.

/** Exemple de 2 fonctions qui vont s'enchainer */
// *************************************************        // (152)
// * S'AGIT IL D'UN EDITEUR ETRANGER ?            **        // (153)
// *************************************************        // (154)****************************
Paragraph p_Traiter_Jnl_Etranger = new Paragraph(this);     // (155) P-TRAITER-JNL-ETRANGER.
public void p_Traiter_Jnl_Etranger() {
    move(fuf1pa01.jnlcod, vtb8510e.jnlcod);                 // (156)     MOVE JNLCOD  OF ZPASS TO JNLCOD  OF DVTB8510E
    // **  MOVE EDIDAT  OF ZPASS     TO W-VALDATD           // (157)
    // **  MOVE 31                   TO W-VALDATD(7:2)      // (158)
    // **  MOVE CDEDTP  OF ZPASS     TO W-VALDATD           // (159)
    // **  MOVE W-VALDATD            TO VALDATD OF DVTB8510E// (160)***  MOVE W-VALDATD TO VALDATD OF DVTB8510E
    move(fuf1pa01.cdedtp, vtb8510e.valdatd);                // (161)     MOVE CDEDTP  OF ZPASS     TO VALDATD OF DVTB8510E
    move(fuf1pa01.jnlcods, vtb8510e.jnlcods);               // (162)     MOVE JNLCODS OF ZPASS     TO JNLCODS OF DVTB8510E
    perform(lect_Tb8510) ;                                  // (163)     PERFORM LECT-TB8510
    if (isEqual(vtb8510e.tvaadh.subString(4, 3), "-ET")) {  // (164)     IF TVAADH  OF DVTB8510E (4:3) = '-ET'
        // -- editeur etranger                              // (165)*-- editeur etranger
	move("AD", fuf1pa01.tvasapc);                       // (166)       MOVE 'AD' TO TVASAPC OF ZPASS
    }                                                       // (167)     END-IF.
}
                                                            // (168)
                                                            // (169)
// *************************************************        // (170)
// * LECTURE DE LA TABLE TB8510 POUR JNX PARTIELLEMENT VALIDES // (171)
// *************************************************        // (172)********************************************
Paragraph lect_Tb8510 = new Paragraph(this);        // (173) LECT-TB8510.
public void lect_Tb8510() {
    sql("SELECT TVAADH FROM VTB8510E  WHERE JNLCOD = #1 AND JNLCODS = #2 AND VALDATD "+	// (174) EXEC SQL
        "= #4")
        .into(vtb8510e.tvaadh)
        .param(1, vtb8510e.jnlcod)
        .param(2, vtb8510e.jnlcods)
        .param(3, vtb8510e.valdatd)
        .param(4, vtb8510e.valdatd)
        .onErrorGoto(sql_Error) ;
}

Bien évidement comme il n'y a que des variables globales, cela signifie que les fonctions ne définissent pas d'interface (pas de paramètre et elles retournent void).

Les exceptions

Mais comment NACA fait-il pour enchainer des fonctions (au sens GOTO) sans faire une StackOverFlowError au bout de quelques heures de fonctionnement ? L'astuce consiste à ne pas empiler d'appel lors d'un GOTO. Pour cela NACA utilise un mécanisme simple, astucieux mais fortement déstabilisant : la fonction 'perform' n'appelle pas la nouvelle fonction. Elle lance une 'GOTOException' (je ne connait pas le nom de la vrai classe) qui prend en paramètre le nom de la fonction à appeler. De cette façon l'appel en cours se termine, on remonte la stack d'un appel, l'exception est intercepté par le runtime NACA qui appelle ensuite la fonction. De cette façon il n'y a jamais plus d'un niveau d'appel dans la stack applicative. La conséquence c'est que les mécanismes d'exceptions Java ne pourront pas être utilisé sans risquer de gros effet de bord.

Compétences pour les développeurs

Pour un développeur Java qui arriverait sur un tel code, il lui faudra apprendre avant tout le COBOL, le 'framework NACA' et oublier le Jdk (les types, les exceptions, les API, l'encapsulation, etc.). Mais aussi les framework classiques qui ont besoin de ces mécanismes.

Et l'interface ?

Elle est écrite en GWT pour fournir une interface web la plus proche du 3270. Cela permet entre autre de facilité le changement pour les utilisateurs qui vont basculer. * La première version des écrans transcodés est en faite une belle implémentation d'un terminal 3270 en GWT (en plein écran, difficile de découvrir que c'est un navigateur). Cela permet de ne pas perdre les utilisateurs qui changent de plateforme (lors de la migration les utilisateurs sont basculés progressivement). En même temps, ils ont juste la même chose qu'avant mais dans un navigateur…

Accès aux données

Le code COBOL doit faire du SQL pour être transcodé en JDBC, le support VESAM est envisagé. Les autres technologies ne sont pas supportées.

Stratégie de migration suggérée par les auteurs

  • Faire une première passe de tests en générant le code COBOL sur un sous ensemble => POC fait souvent par le client avant de faire son choix
  • Prototype (quelques semaines/mois) - le run est 100% legacy - : Pour valider que les écrans se lancent sur un sous ensemble et que ça l'ensemble fonctionne bien. Cela permet également de tester le transcodage sur tout le code pour connaitre les parties du code qui ne peuvent être transcodées par l'outil.
  • Projet : de 6 à 18 mois - au début 100% du run en legacy et progressivement 100% en Java -
    • Selon les cas détectés à l'étape d'avant, on modifie le transcodeur ou le code source en COBOL pour pouvoir tout générer
    • Mise en place d'une UDD pour faire le transcodage complet chaque nuit:
      • Extraction des fichiers COBOL
      • Transcodage (30' sur laptop pour 4 millions de LOC)
      • Compilation (quelques minutes)
      • Exécution des tests isofonctionnels en boite noire
      • Packaging de l'application
    • Tunning du transcodeur et de la plateforme au fur et à mesure de la bascule des utilisateurs
    • Brancher le code sur une nouvelle implémentation des bibliothèques existantes en COBOL (exemple : le gestionnaire d'impression)

Batch

La démarche proposée est de migrer les Step un par un de l'ancienne plateforme vers la nouvelle, de la même façon que les écrans et les utilisateurs le sont. Ca, c'est pour la démarche, maintenant quels sont les apports de NACA sur la mise en œuvre ? NACA vous migrera vos programmes, il vous restera une grosse tache : émuler le reste de la plateforme batch et obtenir sur Linux/Java des performances proches de celle de vos batchs actuels. Et ça n'est pas rien. Il vous faudra ensuite brancher ces nouveaux 'Steps' au sein de vos chaines batchs existantes avec votre ordonnanceur.

Conclusion

Vous l'aurez compris l’intérêt principal de cette approche est de réduire les couts de RUN avant tout. Il vous faudra donc commencer par savoir si vos douleurs principales sont du coté du build (régressions, difficultés de recrutement, impossibilités techniques, perte de compétence sur les programmes existants, etc.) ou du coté du RUN (le coût d'exploitation de la plateforme). Si vos douleurs sont plutôt sur le 'build', NACA ne vous apportera aucune réponse. Au contraire, il risque de vous compliquer les choses, en devant faire accepter à vos développeurs COBOL d'utiliser l'écosystème Java et/ou de recruter des développeurs Java qui acceptent de faire du COBOL en Java (pas d'utilisation des types Java, pas de jdk, Exceptions dénaturées, pas d'encapsulation, etc.).

Si par contre vous avez principalement des douleurs du coté RUN, la promesse de NACA est de vous permettre de réduire rapidement (avant la fin du projet) le coût de la plateforme. Ce peut être un moyen de financer le vrai projet de migration (celui vous permettra d'avoir du vrai code Java). Car vous aurez un autre projet de migration si vous avez besoin de faire évoluer la plateforme : celui qui vous permettra de passer de code 'COBOL like' dans des fichiers Java vers une application Java (une application maintenable avec des compétences Java du marché).

Si vous ne souhaitez pas faire cette dernière migration, vous devrez probablement vous poser la question de conserver vos programmes COBOL en l'état et de les exécuter avec des émulateurs COBOL (plus des couches d'émulation CICS). Dans ce dernier cas il est possible de moderniser partiellement votre code, par exemple sur des tests unitaires (plus d'information dans ce ce post).