4 Malgré sa popularité croissante (en anglais) en tant que langage de programmation open source, il est encore possible de dénicher quelques bugs savoureux dans Go. C’est ce qui est arrivé à Benoit Artuso, Software Architect chez Scality, et à son équipe, alors qu’ils travaillaient au développement et aux benchmarks de notre logiciel. Voici comment ils ont mis au jour (en anglais) le bug, et les enseignements de cette enquête. Commençons par le début : dans notre code, nous devons appeler un autre service via HTTP. Voici à quoi ressemble notre pseudo-code : function call_service(dataBuffer) result = http.Put(destination, dataBuffer) return result dataBuffer = "some data&" result = call_service(dataBuffer) print result C’est propre et assez simple. Mais le code reste le code, et il y a souvent un piège : cette fois, notre composant veut savoir quand dataBuffer peut être nettoyé et réutilisé. Au lieu du code ci-dessus, nous exécutons donc celui-ci : dataBuffer = get_an_existing_buffer("some data") result = call_service(dataBuffer) reuse(dataBuffer) print result Ce code fonctionne la plupart du temps. Mais pas toujours : certains tests échouent encore. Pire, l’équipe découvre qu’elle ne peut pas réutiliser le buffer immédiatement lorsque http.Put() a échoué. L’enquête continue donc. Nous cherchons la cause et imaginons un premier correctif : au lieu de réutiliser le buffer immédiatement, nous lui attachons un callback pour avertir le code lorsqu’il est libéré par le garbage collector. À ce stade, le coupable semble neutralisé. Certes, la consommation mémoire pendant les tests est un peu élevée, mais rien d’alarmant. Avance rapide jusqu’au moment de passer aux tests à grande échelle sur une vraie plateforme haute performance. Dans ces conditions, la consommation mémoire explose au-delà des niveaux acceptables, ce qui nous oblige à reporter quelques tests. En remontant le fil, il s’avère que le contournement précédent fait grimper la consommation mémoire. Une fois de plus, nous revenons à la planche à dessin pour une approche différente. Cette fois, nous utilisons dataBuffer pour exposer la méthode spécifique close() que http.Put() appellera (selon la documentation) lorsqu’il n’aura plus besoin du buffer. Nouveau code : dataBuffer = get_an_existing_buffer("some data") dataBuffer.close() = function () { reuse(dataBuffer) } result = call_service(dataBuffer) print result À ce stade, tout semble correct et conforme à la documentation. La consommation mémoire revient à des niveaux acceptables. Problème résolu ? Loin de là. Les tests à grande échelle affichent soudain de nombreuses erreurs, plus qu’auparavant. Pire encore, le logiciel semble allergique à ce code et plante très tôt. En examinant les logs de crash, il semble que close() soit appelé deux fois. En cherchant à localiser l’origine de ces erreurs, nous modifions le comportement de dataBuffer, ce qui conduit http.Put() à créer une requête sans Content-Length correct. Bien que la requête reste valide (au regard des RFC), le serveur refuse ce type de requête et répond par un code d’erreur approprié. Les enseignements de notre chasse aux bugs dans Go Go et sa bibliothèque standard sont excellents. La plupart de nos problèmes venaient du fait que nous ne faisions pas « ce qu’il fallait » en voulant repousser les limites, et de ce que cela impliquait pour le cycle de vie du buffer d’entrée. Chaque mot de la documentation de Go compte. Ne sautez pas une seule phrase ! Les tests sont vos amis, ne les négligez pas Lancez aussi souvent que possible des tests à grande échelle et de longue durée. C’est exactement ce que nous avons dans notre pipeline Ne laissez rien au hasard et signalez ce que vous trouvez. C’est bon pour l’écosystème Go et, à long terme, cela profite à tous. Le correctif s’avère simple et, à notre grand soulagement, les plantages cessent aussi. Notre hypothèse : si une erreur survient à un moment critique, la bibliothèque standard de Go réagit en fermant le buffer deux fois. Pour identifier le véritable coupable, nous reproduisons d’abord le problème à plus petite échelle, puis nous le réduisons jusqu’à obtenir un petit reproducteur tenant en un seul fichier, afin d’ouvrir un ticket sur le dépôt golang/go. Roulement de tambour : le bug se cache dans le serveur HTTP de la stdlib de Go (en anglais) ! Fin de l’histoire ? Pas du tout. Il n’existe pas encore de solution définitive. Nous espérons un correctif en amont, mais s’il n’arrive pas, nous envisagerons un contournement dans le code. Restez à l’écoute !