samedi 25 avril 2009

L'INTÉGRATION CONTINUE EST UN SYSTÈME MULTITACHE

Ce billet est consacré à une pratique de l'intégration continue et au parallèle que nous pouvons dresser entre le développement concurrent avec intégration continue et les systèmes multitâches.

3 IDÉES
  • Je vais commencer par vous expliquer comment nous pratiquons une forme sécurisée de l'intégration continue.
  • Ensuite, j'évoquerai un problème propre à l'intégration continue et je présenterai les solutions que nous avons mis en place pour y remédier.

INTÉGRATION SÉCURISÉE

Pour qu'un développeur de notre équipe contribue au projet en délivrant ses travaux, il utilise un script qui automatise les activités suivantes:
  1. Identification des modifications: Il s'agit du "commit" ou "checkin" des fichiers modifiés de l'espace personnel de travail. Un éditeur texte est ouvert pour que le développeur justifie ses modifications.
  2. Synchronisation de l'espace personnel: L'espace personnel de travail est synchronisé par rapport à la référence du code en y important les nouveautés de la référence du code.
  3. Build et test de l'espace personnel: Le build est lancé et l'intégralité des tests xUnit sont exécutés dans l'espace personnel. Cette étape s'assure que les modifications du développeur sont compatibles de la référence du code.
  4. Synchronisation de la référence: Si le build et tous les tests ont réussi, alors les modifications de l'espace personnel sont délivrées dans la référence du code.
  5. Build et test de la référence: En moins d'une minute, l'outil d'intégration continue détecte la modification de la référence du code et lance un build incrémental avec rejeu de tous les tests xUnit. En général, le défaut type détecté par cette étape est un nouveau fichier source introduit dans le produit par le développeur et non géré par l'outil de gestion des versions.
Il s'agit d'une démarche de contribution sécurisée car le développeur ne peut délivrer des modifications incompatibles avec la référence du code. En effet, l'étape 3 interdit la livraison des modifications si le build ou le test de la version intégrée échoue. La référence du code est donc protégée contre l'injection de défauts.
Cette synchronisation est fluide car elle est automatisée et courte. Elle ne dure que de 2 à 3 minutes environ. Ainsi, chaque développeur se synchronise plusieurs fois par jour pour entretenir une intégration continue du produit en développement.

INTÉGRATION CONTINUE OU INTÉGRATION PERPÉTUELLE?

Néanmoins, il existe un point faible dans cette démarche, entre l'étape 2 et l'étape 4. Pour le comprendre, analysons le scénario suivant.
Supposons que Jacques et Paul soient deux développeurs de notre équipe. Jacques a terminé une étape de son travail et souhaite la délivrer dans la référence du code. Il lance le script d'intégration. A l'étape 2, il a récupéré chez lui les nouveautés de la référence du code. Il en est à l'étape 3: les tests sont en cours d'exécution dans son espace personnel.
Il se trouve que Paul avait commencé une synchronisation quelques minutes ou secondes avant Jacques. Alors que Jacques en est encore à l'étape 3, Paul en est déjà à l'étape 4: il délivre le résultat de son travail dans la référence du code.
Maintenant, c'est au tour de Jacques de passer à l'étape 4 et de délivrer son travail. Mais la référence du code a changée car elle a été enrichie des travaux de Paul. Donc, Jacques vient de finir son intégration et pourtant il n'est déjà plus synchronisé avec le version de référence du code! Il ne peut donc pas délivrer son travail et il doit reprendre son intégration depuis le début, à l'étape 1.

Ce petit scénario n'est pas bien grave. Jacques n'a perdu que 3 minutes. Maintenant, comprenez que notre équipe est composée de plus de 15 développeurs qui se synchronisent plusieurs fois par jour. Ce scénario de collision a donc une forte probabilité d'occurrence et l'intégration de Jacques peut durer des heures. Pour Jacques, l'intégration continue est devenu une intégration perpétuelle.

LE PROBLÈME

Nous nous sommes rendu compte qu'un seul développeur de l'équipe peut intégrer à la fois. L'intégrateur doit bénéficier d'un accès exclusif à la référence du code pour intégrer ses travaux sans être perturbé par les modifications d'autres développeurs. L'outil de gestion de configuration de gère pas cette exclusion mutuelle car l'intégration est composée de plusieurs commandes atomiques de gestion de versions.

GIZMO, OU L'EXCLUSION MUTUELLE EN INTÉGRATION

Depuis plusieurs projets, nous avons décidé de régler ce problème à l'aide d'une simple peluche, baptisée Gizmo par l'équipe. Pour intégrer ses travaux, un développeur doit poser cette unique peluche sur son écran. A la fin de sa synchronisation, il libère la référence du code en retirant le Gizmo de son moniteur. Ainsi, nous avons très simplement organisé l'intégration exclusive des travaux dans la référence du code.
Il se trouve que la pratique du Gizmo a deux autres bénéfices très importants expliqués en détail dans la description de nos pratiques d'équipe.

LE VERROU NUMÉRIQUE D'INTÉGRATION

La pratique du Gizmo suffit pour assurer l'exclusion mutuelle des développeurs en intégration dans le cas où l'équipe partage le même bureau. En effet, il faut visualiser où est Gizmo et pouvoir le prendre. Or, certains (rares) contributeurs à notre projet ne sont pas dans notre open-space. Nous avons alors décidé de doubler la peluche Gizmo par un verrou numérique. La séquence automatisée par le script d'intégration est donc devenue:
  1. [Nouveau] Recherche du verrou. Si la référence du code est verrouillée en intégration, la séquence est interrompue et on demande au développeur d'intégrer plus tard;
  2. [Nouveau] Verrouillage de la référence du code.
  3. Identification des changements;
  4. Synchronisation de l'espace personnel;
  5. Build et test de l'espace personnel;
  6. Synchronisation de la référnce du code (si les tests ont réussi!);
  7. [Nouveau] Libération du verrou;
  8. Build et test de la référence du code par l'outil d'intégration continue.
Le verrou est un simple fichier créé et supprimé dans la référence du code. Ce verrou numérique ne remplace pas pour autant le Gizmo en peluche. En effet, l'utilisation de la peluche présente deux autres bénéfices très intéressants (se référer à la description de nos pratiques d'équipe).

DÉVELOPPEMENT CONCURRENT AVEC INTÉGRATION CONTINUE ET SYSTÈME MULTITACHE

Pour comprendre la nécessité d'un verrou d'intégration, il suffit de faire un parallèle avec la programmation concurrente et les systèmes miltitâches.

Une équipe qui pratique le développement concurrent est un système multitâche. En effet, chaque développeur (ou binôme) est assimilable à un thread concurrent qui mène ses activités en parallèle des autres.

Si les développeurs pratiquent l'intégration continue, alors ils se partagent une ressource. Il s'agit de la référence du code. La séquence de synchronisation avec la référence (étapes 1 à 4) est donc une section critique. Un seul développeur à la fois doit la dérouler. Puisqu'il s'agit d'une section critique, il convient d'en assurer une utilisation exclusive à l'aide d'un mécanisme d'exclusion mutuelle, comme les mutex ou les sémaphores. La peluche baptisée Gizmo ou le verrou numérique sont en fait un sémaphore (à un jeton) tel qu'il en existe dans les OS temps-réel pour gérér l'exclusion mutuelle des thread dans les sections critiques.

Autrement dit:
Les développeurs sont les threads concurrents, la référence du code est la ressource partagée, la séquence d'intégration est la section critique et le verrou d'intégration est le mutex. Le développement avec intégration continue est un système multitâche.
Une fois que ce parallèle est admis, alors les règles de la programmation concurrente s'appliquent également pour l'intégration continue. En effet, le temps passé en section critique doit être aussi court que possible car une section critique est un goulet d'étranglement. La pratique de l'exclusion mutuelle en intégration créé une file d'attente d'intégration qui peut pénaliser les travaux si la durée d'intégration est longue ou si l'équipe est nombreuse.

COMMENT FONT LES AUTRES ÉQUIPES?

Je m'étonne toujours de constater que d'autres équipes (même chez nous) ne semblent pas être confrontées à ce problème. Cela n'est pas une question de taille de l'équipe car nous avons instauré cette pratique pour la première fois lors d'un projet développé à 4 développeurs organisés en 2 binômes. Ce problème me semble indissociable du développement concurrent avec intégration continue.
Alors, comment faites-vous?

17 commentaires:

  1. Salut Manu,
    on dirait que vous travaillez avec une seule branche ? Est-ce que l'article suivant de H Kniberg pourrait vous être utile ?

    http://www.infoq.com/news/2008/04/kniberg-agile-version-control

    A+
    Bruno

    RépondreSupprimer
  2. Salut Bruno,
    nous avons une branche pour la référence du code et une branche par développeur.
    Ansi, les dévéloppeurs ne se bloquent pas mutuellement et ils peuvvent réellement développer en parallèle. Par contre, ils partagent une ressource: la branche de la référence du code.
    Il me semble que c'est une organisation très classique.

    RépondreSupprimer
  3. Salut,
    N'est ce pas exactement ce que veut corriger Team City (http://www.jetbrains.com/teamcity/), à savoir ne reporter dans la branche commune que lorsque l'intégration continue est au vert ? (http://linsolas.developpez.com/articles/outil/integration-continue/teamcity/)
    Personnellement nous n'avons que peu rencontrer ces problèmes de synchronisation dans les différents projets sur lesquels je suis passé même avec une équipe de plus de 13 personnes. Le tout est de committer souvent et par petits bouts et ça se passait bien.

    RépondreSupprimer
  4. Salut ManU,
    Dans le projet Scrum multi équipes (20 pers en tout) que j'ai connu, la synchronisation se faisait oralement et les équipes ou sous-équipes avertissaient les autres oralement lors du "Daily Scrum of Scrum". Le but étant d'avoir une exclusivité pour merger une branche sur le trunk. Qu'il existe des pratiques différentes selon les équipes, ça ne m'étonne pas; mais si les divergences ou déséquilibres deviennent trop fort, on est tenté de faire de la normalisation (règles externes); une autre attitude consiste à faire danser les chaises. Offrir la flexibilité de changer de projet, équipe, ça peut résoudre pas mal de chose (harmoniser de façon interne).
    @+

    RépondreSupprimer
  5. @Emmanuel (java in the Alps)
    A mon avis, plus on délivre souvent et plus le problème risque d'arriver. A mon avis, le problème est que la synchronisation/deliver d'un développeur n'est pas une action atomique de gestion de configuration. Comme il ne s'agit pas d'une action atomique, plusieurs développeurs peuvent dérouler la même séquence en même temps.

    @Emmanuel (homoagilis)
    OK, donc vous auusi vous gériez une exclusivité pour intégrer ses modifs dans le tronc commun. Cela me rassure sur le fait que vous aussi avez rencontré le problème et vous aussi avez dû vous organiser pour le résoudre.

    RépondreSupprimer
  6. Ce commentaire a été supprimé par l'auteur.

    RépondreSupprimer
  7. Dommage que ce commentaire ait été supprimé, je l'aimais bien ;o)

    RépondreSupprimer
  8. Je poste ici des question que l'on m'a posé sur la mailing liste XP France et que j'ai trouvé très pertinentes:

    > Par cette pratique, y a-t-il toujours
    > besoin d'intervenir en urgence pour réparer l'intégration ?

    L'erreur classique qui reste est le développeur qui délivre des fichiers source non-gérés en configuration. Les tests passent en local parce que les nouveaux sources y sont. Par contre, les tests ne passent pas ailleurs car les fichiers n'y sont pas.

    > Y a-t-il
    > d'ailleurs toujours besoin d'un "serveur" d'intégration ?

    Je pense que oui, pour plusieurs raisons:
    1.Pour détecter la classe d'erreur décrite ci-dessus;
    2.Pour disposer d'une version de référence intégrée, testée, identifiée et livrable;
    3.Parce que ceintures, bretelles et airbags ;o)

    RépondreSupprimer
  9. @ManU:
    En quoi le commit n'est il pas atomique avec un outil de gestion de conf moderne (svn, mercurial, gn it) ? L'ensemble des fichiers est lié à une revision donc tu ne peux avoir de modification que sur du code 'extérieur' à celui du développeur qui commite. Le commit fréquent fait qu'il ne concerne que peu de fichiers sources donc les risques de collision sont faibles et tout le monde est presque tout le temps à jour (ce qui les réduit d'autant plus). L'intégration continue qui se lance sur chaque commit(ou presque) te sert de ceinture puisque la phase compilation/tests unitaires de couvre contre l'oubli de fichiers, la modification d'API etc.
    Ensuite les tests d'intégration garantissent la partie fonctionnement de ton logiciel.

    RépondreSupprimer
  10. Salut Manu des Alpes,

    le commit d'outils tels que ClearCase ou Mercurial (et les autres) est en effet atomique. Par contre l'intégration du travail personnel dans la référence du code n'est pas atomique puisque cette activité est composée d'au moins 4 étapes atomiques (en prenant le vocabulaire Mercurial et ClearCase):
    1.Commit/Checkin, ou l'officialisation des changements;
    2.Pull/Rebase, ou l'import des nouveautés de la référence, aussi appelé synchronisation de l'espace personnel;
    3.Rejeu des tests (rien à voir avec la géconf);
    4.Push/Deliver, ou l'export des modification vers la référence du code, aussi appelé synchronisation de la référence.

    Les étapes 1, 2 et 3 concernent les outils de gestion de configuration. Chacune de ces étapes est atomique mais la succession de ces étapes ne l'est pas - surtout en insérant le rejeu des tests au milieu.

    Tu es d'accord?
    En fait, je suppose que nous avons une utilisation assez différente de la gestion de configuration, d'où notre mutuelle incompréhension. Je regrette de ne pas arriver à être plus clair dans mes propos ...

    RépondreSupprimer
  11. Oui je suis d'accord avec toi même si pour moi tu ne fais pas les choses dans le bon ordre :
    1/ checkout / update/ pull local
    2/ compilation / tests unitaires (de l'ordre de quelques minutes max)
    3/ commit
    4/ Le super serveur d'intégration continue fait son taff
    Le risque de collision intervient durant les étapes 2 - 3, voir essentiellement 2 d'où la nécessité d'avoir un jeu de tests unitaires rapides (la compilation normalement ça va assez vite). Après je déporte les tests d'intégration sur le serveur d'intégration continue.
    J'espère avoir été clair sur mon utilisation du SCM.
    Emmanuel

    RépondreSupprimer
  12. @Manu des Alpes,
    en fait, nous avons un vocabulaire différent, mais nous faisons exactement la même chose et dans le même ordre!
    Il reste tout de même une légère différence: tu ne joues que les tests unitaires en local alors que nous jouons tous nos tests xUnit (unitaires + intégration) en local.
    Donc toit aussi tu peux te faire interrompre entre les étapes 1 et 3 par un autre développeur qui intègre.

    RépondreSupprimer
  13. @ManU de la vallée ;o)
    Oui mais dans la pratique cela arrive très rarement car tout est fait pour que les étapes 1 à 3 soient courtes. Passer les tests d'intégration en local augmentant la durée de collision probable tu te retrouves 'coincé'. C'est pour moi l'intérêt du serveur d'intégration continue que de me libérer des tests d'intégrations sachant que c'est couteux en temps.
    Dans Team City ils ont un commit en deux temps : commit sur le serveur d'intégration continue qui fait un réel commit sur le SCM une fois l'intégration complètement réalisée. C'est lui qui gère le 'token' (du moins à ce que j'en ai compris en lisant la doc).

    RépondreSupprimer
  14. Pas mal le coup de la peluche ! Je n'y avais jamais pensé... Ca ne dégénère pas en "course au Gizmo" parfois lorsqu'un développeur a fini et que deux autres se l'arrachent ? ;)

    Dans notre équipe tout se joue à la négociation orale, sachant que le développeur (ou la paire) ayant un "petit commit vite fait" à passer sera quasiment toujours prioritaire. D'où la nécessité aussi de dire stop lorsqu'il devient urgent de laisser la place (et le temps) à un plus "gros commit" affectant une large partie de la base de code.

    On avait aussi pensé écrire une file d'attente d'intégration sur un tableau blanc, mais matériellement la tenir à jour est un peu contraignant.

    RépondreSupprimer
  15. @Guillaume,
    en effet, la "course au Gizmo" arrive parfois. Je ne pense pas que cela soit grave. Cela fait même partie du folklore qui permet à l'équipe de se créer un identité d'équipe et de se souder.

    RépondreSupprimer
  16. Ce commentaire a été supprimé par l'auteur.

    RépondreSupprimer
  17. J'ai l'impression qu'il y a deux écoles qui "s'affrontent" sur l'intégration continue :
    1) seuls les TU et quelques tests d'intégrations sont lancés dans l'espace du développeurs. Les tests d'intégration sont déroulés sur le serveur d'intégration. L'avantage est le gain de temps mais on augmente le risque de "casser le build" ce qui n'arrive jamais dans le cas 2
    2) tous les tests sont lancés dans l'espace du développeurs et la livraison ne se fait que dans le cas où le stream est vert. On garantit de ne jamais casser le build mais cela peut prendre du temps.
    A une époque j'étais 100% pour la solution 2 (c'est celle qu'on utilise aujourd'hui). Cependant, depuis qu'on doit garantir la compilation avec plusieurs compilateurs (1/2h par compil) la solution 1 amène tout son intérêt.
    Une petite astuce puisque tu sembles aussi utiliser la solution 2 : il n'est pas nécessaire de faire les builds dans l'espace de référence.
    En effet si je reprends tes 5 étapes :
    1 Identification des modifications
    2 Synchronisation de l'espace personnel:
    3 Build et test de l'espace personnel
    4 Synchronisation de la référence
    5 Build et test de la référence

    Il faut alors rajouter une étape "0" : recherche de fichiers privés + fichiers hijacked dans UCM (fichier privé surchargeant une fichier en conf)

    Dans ce cas, à la fin de l'étape 2 tu as dans le stream du dévellopeur :
    _ la base
    _ le delta issu des autres livraison qui sont arrivés via le rebase
    _ le delta du développeurs (ou des pairs) correspondant au travail à livrer
    Et si on réfléchit c'est magique : cela correspond à ce qui va se trouver dans l'espace de référence une fois l'étape 3 (livraison vers l'espace de reférence) effectuée. Donc si c'est vert chez le développeur, c'est vert dans la référence.
    Tu peux donc supprimer l'étape 5 et mettre le vérou entre la 1 et la 4.
    Intellectuellement, je trouve cette solution plus élégante que 2 builds et j'aime bien cette notion de décentralisation de Clearcase UCM qui est l'exemple parfait d'un gestionnaire de conf trop centralisé (avec encore la notion d'intégrateur). Le stream d'intégration ou l'espace de référence devient alors conceptuel : il n'a plus de réalité physique.

    Patrice Van de Velde

    RépondreSupprimer