Spring Boot not so dumb dry-run

Dans le cadre d’une mission à forte valeur métier, et pour garantir la communication entre les fournisseurs et notre équipe de développement, celui que j'appellerais le responsable data travaille sur des fichiers d’échange, au format Excel.

Les fichiers Excel sont remplis par les fournisseurs et sont ensuite récupérés par le responsable data. S'ensuit une étape de transformation dans un format plus simple à intégrer dans l’application. A défaut d’un back office, par manque de temps et surtout pour d’autres priorités business, les fichiers d’intégration sont intégrés à chaque démarrage de l’application. De fait, les fichiers suivent le cycle de nos développements. En revanche, le responsable data étant garant de ceux-ci, il était important qu’il soit autonome dans l’intégration des changements de données.

Nous avons pris le parti de le faire monter en compétence sur les Pull Requests. Ainsi, il était en mesure de modifier les fichiers qui l’intéressaient et de proposer une modification à faire valider auprès de l’équipe, depuis l’interface Github.

Ce qu’il faut bien comprendre, c’est que l’intégration est un véritable geste métier. Elle vient avec ses règles implémentées dans le code pour par exemple garantir que la grille tarifaire couvre l'ensemble du nouveau catalogue de produit. Ces règles d’intégration restent vraies, que l’on intègre les données au démarrage de l’application comme c’est le cas ici, ou que l’on fasse des intégrations pendant le run, au travers d’un back office.

Cependant, bien que ces vérifications soient implémentées, la mise à jour de ces données reste à la charge de l'équipe de développement. Comment fluidifier ce processus ? Comment rendre la main au responsable data sur l'évolution des catalogues ?

Dry-run naïf mais efficace

Force est de constater qu’Internet ne nous a pas beaucoup aidé à trouver une solution simple pour pallier notre problème somme toute simple : comment être sûr que l’application va démarrer correctement ?

Tentative 1 : Le test d’intégration Spring

La première solution que nous avons essayée a été le test d’intégration Spring. Malheureusement, le contexte d’exécution étant celui d’un test, les ressources de tests sont chargées et viennent modifier le comportement réel de l’application. Cette possibilité n’est donc pas satisfaisante.

Tentative 2 : Démarrer l’application dans la CI

Dans un second temps, nous avons choisi d’intégrer un job dédié à notre CI. C'est-à-dire qu’on démarre l’application dans le processus d’intégration continue. Si l’application tourne, c’est que ça marche. Du coup est-ce que ça marche ? Oui et même trop bien, puisque le job ne s’arrête pas tant que l’application tourne. Mais on avance !

La solution : Dry-run dans la CI

La notion de dry-run nous est finalement venu à l’esprit.
Le dry-run est un démarrage sans effets de bords. C'est-à-dire que nous allons tester intentionnellement un comportement pouvant poser problème, dans des conditions réelles, sans pour autant persister les modifications éventuelles de l’application qui pourraient être engendrées par la défaillance.
Notre application étant en Spring Boot, voici ce que nous avons fait :

@SpringBootApplication
@Slf4j
public class ApplicationStartup {
  public static void main(String[] args) {
    SpringApplication.run(ApplicationStartup.class, args);
    if (isDryRun(args)) {
      log.info("Dry run completed");
      System.exit(0);
    }
  }

  private static boolean isDryRun(String[] args) {
    return Arrays.asList(args).contains("--dry-run");
  }
}

Nous avons ajouté une option sur la ligne de commande. Ainsi, l’application démarre complètement pour s’éteindre tout de suite après sans erreur. Le job imaginé dans notre seconde tentative peut désormais s’arrêter.

Parenthèse sur le CommandLineRunner

En développant la solution, nous avons essayé d’implémenter un CommandLineRunner.

@Component
public final class DryRun implements CommandLineRunner {
  @Override
  public void run(String... args) throws Exception {
    … code goes here …
  }
}

A première vue, le CommandLineRunner nous offre la possibilité d’isoler le comportement de dry-run. Pour autant, son exécution intervient trop tôt dans le cycle de démarrage de l’application Spring. L’implémentation précédente reste donc la solution qui garantit le démarrage total de l’application avant d’être éteinte.

Intégration continue des nouvelles données

Nous avons donc un moyen simple de vérifier que l’application sera fonctionnelle après chaque développement et nous pouvons l’intégrer dans notre CI. Dans notre cas, nous sommes sous Github Actions.

dry-run:
  needs: [ build ]
  runs-on: [ self-hosted, Linux, standard ]
  steps:
    - [... some step to get the jar ...]
    - name: Dry-run application
      run: java -jar application.jar --dry-run

Si le démarrage de l’application échoue au démarrage, la ligne de commande java nous envoie un code d’erreur 1. La step “dry-run” est alors en erreur et le pipeline s’arrête. La PR de notre responsable data est donc en échec.

Avec des erreurs suffisamment explicites, le responsable data est capable de comprendre la raison de l’erreur. Et s’il faut creuser, un développeur peut l’assister.

En conclusion

Ajouter la procédure dans le cycle de développement garantit que les nouvelles données fournisseurs ne viendront pas corrompre l’intégrité du logiciel et prévient au plus tôt des erreurs éventuelles. L’autonomie de notre responsable data est ainsi garantie grâce à une solution peu coûteuse à mettre en place et qui permet de ne pas polluer les développeurs avec de la mise à jour de données. D’autre part, la solution étant très simple, elle est volontairement jetable pour être remplacée par un back office.