20 février 2013

Backward compatibility hell

Vous connaissiez le ClassLoader hell, le ClassPath hell, je vous souhaite de ne jamais avoir à gérer le Backward Compatibility Hell.



Sur un malentendu, je me suis retrouvé maintainer du plugin Git pour Jenkins. Gratifiant parce que c'est l'un des plugins les plus installés, difficile parce que c'est plus un amas de pull-requests qu'un véritable plugin, et ça continue ...

J'ai donc voulu prendre le diable taureau par les cornes et entamer un début de tentative de refactoring du plugin, en commençant par isoler les opérations Git de bas niveau. Le plugin est conçu autour d'une GitAPI, qui a priori assure ce rôle, sauf que l'interface contient des fonctions hérétoclites et surtout fait un mix entre les aspects pur Jenkins et les spécificités Git. Entre autre, elle prenait parfois en charge la gestion des esclaves de build (remoting) mais pas toujours ...

Bref, le chat sur les genoux, voici un boulot digne d'un vendredi. Tous les testent passent, et hop, je lance une release -> 1.27

Ca c'était donc le vendredi soir, et dès le samedi je découvre une ignoble régression, parce que la suite de test ... est perfectible. -> 1.28

Lundi, je reçois quelques mails et contacts sur #irc par les maintainers d'autres plugins. En effet, ce code est aussi utilisé par le plugin gerrit, git-parameter, ou cloudbees validated-merge (et peut être d'autres ?).

Le soucis ici, c'est que les plugins Jenkins ne reposent pas sur un moteur de modularisation (genre OSGi, JigSaw, JBoss Modules ...) et donc TOUTE classe publique d'un plugin peut potentiellement être utilisée ailleurs et ne doit donc JAMAIS retirer quoi que ce soit. Sauf que, comme beaucoup de monde, les développeurs ont tendance à mettre public un peu tout, ne serait-ce que pour pouvoir dispatcher leurs classes dans des packages spécialisés par fonctionnalité (quid du "friend" en java ?)

Autant dire qu'on se traine pas mal de casseroles. -> 1.29.

Fort de cette boulette, j'ai passé ma journée de mardi à reprendre entièrement mon refactoring, à isoler proprement le code Git dans un nouveau plugin utilitaire "git-client", partagé avec les autres plugins, et à tester la compatibilité avec les autres plugins. Cela à nécessité de définir une nouvelle API toute propre :


  • D'un côté un GitClient avec les méthodes supportées, contrôlées par une suite de test
  • De l'autre la "vielle" GitAPI avec les autres méthodes, toutes dépréciées (sauf si un auteur de plugin se fait connaître)
  • tout en retournant un systématiquement composant GitAPI, même si l'API ne promet qu'un GitClient, histoire de rester backward-compatible à l'exécution.


Après quelques pas mal d'heures, on arrive donc au couple Git-plugin 1.2.0 (ça valait bien un changement de version mineure) + Git-Client-plugin 1.0.2

Ma première conclusion, c'est qu'il va falloir que je fasse très attention pour la suite, parce que du refactoring il y en à besoin ...



Cela m'a amené à regarder comment les autres font pour gérer ce problème ... et il n'y a pas de "bonne" solution afaik. Au mieux, j'ai trouvé la pratique de définit une interface Foo, puis Foo2 pour les nouvelles méthodes, puis Foo3 ... mais quoi qu'on fasse il faut se coltiner l'existant ! @Deprecated ou pas, le code de la v1.0 doit rester en place...


A côté de ma participation à Jenkins, il y a aussi la partie Cloud de Cloudbees. Ici, pour savoir si un plugin est utilisé par l'un de nos clients, on fait l'équivalent d'un grep (en un peu plus long). On peut donc à loisir virer quelque chose, quitte à trouver un contournement pour les 2 pelés qui ont activé la fonction qu'il ne fallait pas.

Ma seconde conclusion est donc que, pour un développement productif et donc un Time-to-Market réduit, le mode SaaS est sans commune mesure avec l'approche "produit" traditionnelle.

5 commentaires:

Fabszn a dit…

Hello,

Très intéressant et instructif cet article. Merci

Je crois qu'il y a une petite coquille sur la version de git-plugin + git-client.

Git-plugin ne devrait pas être en version 1.3.0 ?

nicolas deloof a dit…

La version 1.3.0 n'existe pas encore (c'est la branche de développement)

Fabszn a dit…

OK au temps pour moi.

svanespen a dit…

Bonjour,

Article très intéressant.

Je suppose que pour l'approche des interfaces Foo, Foo2, Foo3, je dois remplacer tout mes appels à Foo2 par des appels à Foo3 si je souhaite mettre mon code client à jour ?
Ou bien la solution implique-t-elle de faire des appels soit à Foo2, soit à Foo3 en fonction des méthodes invoquées ?


Aussi, pour limiter le problème de la multitude de classes publiques, est-ce que faire une librairie destinée uniquement à servir d'API publique ne permettrait pas de limiter la "casse".
Cela n'empêcherait pas d'avoir plein de classes publiques mais tout ce qui serait en dehors de l'API publique ne serait pas tenu d'être supporté.

nicolas deloof a dit…

l'approche "API publique" vs implémentation ne fonctionne que si les développeurs ne viennent pas hacker ton implémentation, profitant des classes publiques. Que penser par exemple des nombreux frameworks qui utilisent com.sun.Usafe ?