URL: https://linuxfr.org/users/krunch/journaux/zeromq-et-les-mangoustes Title: ZeroMQ et les mangoustes Authors: Krunch Date: 2012年11月10日T17:45:25+01:00 Tags: zeromq, zmq, gdb, debugging, qa, rpm et coup_de_gueule Score: 36 [On m'a récemment demandé d'expliquer pourquoi je trouve que ZeroMQ est un sujet de rigolade.](https://linuxfr.org/news/de-tout-de-rien-des-bookmarks-du-bla-bla-45#comment-1406453) En guise d'introduction, j'aimerais préciser qu'un panel d'experts concertés sur le sujet ont eu des réactions similaires lorsqu'ils ont été exposés à deux lignes de ZeroMQ :> < Krunch> 1149 event.data.connected.addr = (char *)addr_;> < Krunch> 1150 event.data.disconnected.fd = fd_;> < Krunch> avec event.data qui est un union> < s> ahah> < n> nice> < n> http://i.imgur.com/pejxB.gif Le reste de ce journal décrit mon contexte et comment j'ai été amené à abhorrer ZeroMQ (ci-après désigné « zmq » mais je vais aussi mentionner 0mq et ØMQ histoire d'avoir tous les mots clés). Initialement je voulais finir par une tentative de généralisation qui m'aurait permis d'insulter plus ou moins subtilement toute personne possédant un compte github mais ça sera pour une autre fois. # [serpents et mangoustes](http://thedailywtf.com/Articles/Psychic-Code.aspx) # Je ne suis pas vraiment un développeur. J'écris du code mais de manière générale, mon rôle est plutôt de faire en sorte que les développeurs puissent en écrire. Dans le contexte de ce journal, ça veut dire que je ne développe pas d'application qui va utiliser zmq et de par ce fait, je me tape le popotin de la qualité de l'API, des performances top moumoutes et de l'architecture révolutionnaire du bousin. De mon point de vue, je veux un logiciel qui fait ce qui est écrit sur la boite, qui soit packagé ou packageable facilement et qui reste relativement stable sur la durée de vie de l'application/système. # pourquoi je bois # Ma découverte de zmq a commencé quand il a fait sont apparition dans une liste de composants qu'ils m'ont été demandé d'intégrer à un système que je maintiens. Le dit système étant basé sur CentOS 5, mon premier réflexe a été de vérifier si zmq n'était pas déjà dans la distribution de base ou un repo tierce « de confiance ». On trouve effectivement un paquet [zeromq-2.1.9-1.el5 dans EPEL](http://dl.fedoraproject.org/pub/epel/5/x86_64/repoview/zeromq.html). Il s'avère cependant que ce paquet (crée il y a environ un an) est « trop ancien », que l'API qu'il expose n'est plus compatible avec celui pour lequel les développeurs veulent travailler et qu'il n'est plus maintenu par les développeurs de zmq. Un point de moins pour la stabilité donc. Après quelques tentatives de négociations je cède et me lance dans le packaging de La Chose. Je commence donc par examiner le site web de zmq pour obtenir la dernière version stable. Il s'avère que la version recommandée est une release candidate (3.2.0-rc1). Peu engageant mais soit. Par bonheur, un fichier .spec (ce qui décrit comment construire les RPMs) est fourni. Malheureusement il n'est pas maintenu et ne fonctionne pas. Plutôt que d'essayer de le corriger, je reprend donc le .spec du paquet EPEL qui fonctionne immédiatement quand je le fais pointer vers la bonne version de l'archive (et j'ai plus confiance en les mainteneurs EPEL pour écrire des fichiers .spec corrects qu'en la plupart des autres développeurs). Mission accomplie donc. Sauf que quelques jours plus tard on se rend compte que la création des RPMs zmq échoue une fois sur quarante deux. Plus précisément, un des tests intégrés segfault occasionnellement. Examinons donc le core : ``` tests$ gdb .libs/lt-test_shutdown_stress core.12342 [...] [New Thread 12912] [New Thread 12911] [New Thread 12910] [...] Program terminated with signal 11, Segmentation fault. #0 zmq::session_base_t::monitor_event (this=0x2b304179d000, event_=256) at session_base.cpp:279 279 socket->monitor_event (event_, args); (gdb) list zmq::session_base_t::monitor_event [...] 275 void zmq::session_base_t::monitor_event (int event_, ...) 276 { 277 va_list args; 278 va_start (args, event_); 279 socket->monitor_event (event_, args); (gdb) l 280 va_end (args); 281 } [...] (gdb) p socket 1ドル = (zmq::socket_base_t *) 0x0 ``` On a visiblement un object session_base_t dont le membre socket est NULL de manière inattendue. Voyons ce qui se trame plus haut dans le stack : ``` (gdb) up #1 0x00002b304014a487 in zmq::tcp_connecter_t::close (this=0x2b30413cd000) at tcp_connecter.cpp:291 (gdb) l zmq::tcp_connecter_t::close ... 280 void zmq::tcp_connecter_t::close () 281 { 282 zmq_assert (s != retired_fd); 283 #ifdef ZMQ_HAVE_WINDOWS ... 288 #else 289 int rc = ::close (s); 290 if (unlikely (rc == 0)) 291 session->monitor_event (ZMQ_EVENT_CLOSE_FAILED, endpoint.c_str(), zmq_errno()); 292 errno_assert (rc == 0); 293 #endif 294 session->monitor_event (ZMQ_EVENT_CLOSED, endpoint.c_str(), s); (gdb) l 295 s = retired_fd; 296 } ``` Pour les gens qui sont allergique au C++ (comme moi), il peut être utile de préciser que "::close" fait juste référence au symbole "close" en dehors de tout namespace C++ (je suis sûr que cette phrase n'est pas strictement correcte mais j'ai pas mes specs sur moi). Ce qui signifie qu'à la ligne 289 on exécute l'appel système close(2) sur le file descriptor "s". Si cet appel réussi, on fait un truc relatif à ZMQ_EVENT_CLOSE_FAILED (truc dans lequel on segfault). Si vous avez l'impression que ça n'a pas de sens, c'est normal. Je suis pas sûr de la raison exacte du segfault mais cette méthode ne devrait vraisemblablement pas être appelée avec ces arguments à ce moment là. Un examen du code se trouvant dans le repo git suggère que le problème a été corrigé ou en tout case que le code concerné a été réécrit : ``` 297 void zmq::tcp_connecter_t::close () 298 { 299 zmq_assert (s != retired_fd); 300 #ifdef ZMQ_HAVE_WINDOWS ... 303 #else 304 int rc = ::close (s); 305 errno_assert (rc == 0); 306 #endif 307 socket->event_closed (endpoint.c_str(), s); 308 s = retired_fd; 309 } ``` Il n'y a d'ailleurs plus d'appel à session->monitor_event donc plus de raison de segfaulter de cette manière. Je retourne donc sur le site web pour voir si une nouvelle rc ou même la release est disponible et corrige le problème. J'y trouve bien une nouvelle rc mais pour une nouvelle version. Apparemment, 3.2.0 n'a jamais été releasé. On passe directement de 3.2.0-rc1 à 3.2.1-rc2 qui contient le changement cité ci-dessus et bien d'autres (dont des modifications d'API). On a donc affaire à un projet qui ne fait que des release candidates et qui ne corrige pas les bugs dans une « release » donnée. Soit. Si la dernière release candidate corrige le problème, n'en n'introduit pas d'autre et reste compatible, ça me va. Niveau compatibilité, c'est déjà raté puisque le [changelog](https://raw.github.com/zeromq/zeromq3-x/master/NEWS) indique des renommages de symboles mais on peut espérer que ça demandera pas trop de travail à adapter. Je récupère donc la dernière release candidate, adapte le fichier .spec et tente de builder les RPMs. Le test qui posait problème passe mais maintenant il y en a deux autres qui échouent systématiquement. On a donc affaire à un projet qui release avec des régressions... Examinons quand même ce qui échoue exactement : ``` zeromq-3.2.1$ make check [...] test_connect_delay running... Rerunning with DELAY_ATTACH_ON_CONNECT Running DELAY_ATTACH_ON_CONNECT with disconnect lt-test_connect_delay: test_connect_delay.cpp:80: void* server(void*): Assertion `rc != -1' failed. /bin/sh: line 5: 18715 Aborted (core dumped) ${dir}$tst FAIL: test_connect_delay zeromq-3.2.1$ gdb tests/.libs/lt-test_connect_delay tests/core.18715 [...] [New Thread 10358] [New Thread 10357] [New Thread 10354] [New Thread 10353] [New Thread 10350] [...] Core was generated by `/tmp/zeromq-3.2.1/tests/.libs/lt-test_connect_delay'. Program terminated with signal 6, Aborted. #0 0x00002ab8d7bec285 in raise () from /lib64/libc.so.6 (gdb) bt #0 0x00002ab8d7bec285 in raise () from /lib64/libc.so.6 #1 0x00002ab8d7bedd30 in abort () from /lib64/libc.so.6 #2 0x00002ab8d7be5706 in __assert_fail () from /lib64/libc.so.6 #3 0x00000000004012b3 in server () at test_connect_delay.cpp:80 #4 0x00002ab8d721577d in start_thread () from /lib64/libpthread.so.0 #5 0x00002ab8d7c8fc1d in clone () from /lib64/libc.so.6 (gdb) frame 3 #3 0x00000000004012b3 in server () at test_connect_delay.cpp:80 80 assert (rc != -1); (gdb) list 75 76 usleep (200000); 77 78 memset (&buffer, 0, sizeof(buffer)); 79 rc = zmq_recv (socket, &buffer, sizeof(buffer), ZMQ_DONTWAIT); 80 assert (rc != -1); 81 82 // Start closing the socket while the connecting process is underway. 83 rc = zmq_close (socket); 84 assert (rc == 0); (gdb) p errno You can't do that without a process to debug. TLS symbol `errno' not resolved for non-TLS program. You should compile the program with `gcc -pthread'. ``` Du coup je me retrouve à rebuilder le test avec un printf(strerror(errno)) pour voir l'erreur (si quelqu'un voit comment récupérer errno depuis ce core sans rebuilder, je suis preneur) et écrire [un rapport de bug](https://zeromq.jira.com/browse/LIBZMQ-462) pas complètement inutile. C'est quand même dommage d'avoir un test qui fail sans qu'on puisse comprendre pourquoi rien qu'en regardant std{out,err} mais bon. L'autre test qui échoue ressemble à ceci : ``` zeromq-3.2.1$ make check [...] lt-test_monitor: test_monitor.cpp:81: void* req_socket_monitor(void*): Assertion `!strcmp (event.data.disconnected.addr, addr)' failed. /bin/sh: line 5: 12547 Aborted (core dumped) ${dir}$tst FAIL: test_monitor Le code concerné : 35 const char *addr; .. 42 zmq_event_t event; .. 57 switch (event.event) { .. 79 case ZMQ_EVENT_DISCONNECTED: 80 assert (event.data.disconnected.fd != 0); 81 assert (!strcmp (event.data.disconnected.addr, addr)); ``` Si on cherche la définition de zmq_event_t on trouve ceci : ``` 291 /* Socket event data (union member per event) */ 292 typedef struct { 293 int event; 294 union { 295 struct { 296 char *addr; 297 int fd; 298 } connected; ... 331 struct { 332 char *addr; 333 int fd; 334 } disconnected; 335 } data; 336 } zmq_event_t; ``` Et en cherchant un peu, on trouve des trucs ainsi : ``` 1144 void zmq::socket_base_t::event_disconnected (const char *addr_, int fd_) 1145 { 1146 zmq_event_t event; 1147 if (!(monitor_events & ZMQ_EVENT_DISCONNECTED)) return; 1148 event.event = ZMQ_EVENT_DISCONNECTED; 1149 event.data.connected.addr = (char *)addr_; 1150 event.data.disconnected.fd = fd_; 1151 monitor_event (event); 1152 } ``` On a donc data qui est une union et une écriture consécutive sur deux membres de l'unions. Si vous avez des notions de C, ça devrait vous donner des envies de meurtre. Quand on regarde les méthodes alentours on se rend compte que ce motif est répété à plusieurs reprises. Techniquement c'est correct (et n'est pas directement lié au problème déclenché par le test) car tous les membres de l'union sont des structs aux membres de mêmes noms et tailles dans le même ordre mais si le développeur se permet d'écrire des choses pareilles, j'ai assez peu envie de continuer de creuser. J'ai donc remonté ces deux bugs (dont un était [déjà connu](https://zeromq.jira.com/browse/LIBZMQ-450)) et ai soulevé le problème sur IRC mais ça n'a pas l'air d'émouvoir beaucoup les développeurs. Du coup, soit je commence à fixer des bugs zmq, soit je désactive les tests (puisque de toute façon les développeurs zmq ne les utilisent visiblement pas). Soit je convainc mes développeurs de se trouver une bibliothèque d'IPC moins bancale. J'espère que le récit de mes mésaventures permet de comprendre pourquoi je n'aime pas zmq et si, quand j'ai le choix, je déciderai de ne pas l'utiliser. Il ne s'agit pas juste du fait qu'il y a des bugs ou même des régressions (ça arrive pour tous les logiciels). Mon problème est plutôt que le peu de code que j'ai examiné sent le copié/collé pas testé à 200 mètres et que le processus de développement/test/release du projet ressemble beaucoup trop à une vaste blague pour que je décide de baser quoi que ce soit qui devrait fonctionner plus de quelques semaines sur une telle bibliothèque. # Dallas # Un ami qui suit ce genre de choses a tenté de me décrire l'histoire de zmq. J'ai pas tout suivi et je ne me risquerais pas à tenter de résumer cette histoire ici. Ça m'a juste beaucoup rappelé les séries télévisées que ma grand mère regardais et j'ai l'impression que ça pourrait en partie expliquer les problèmes cités ci-dessus.