Si vous avez lu mon dernier article, consacré à FineDB, vous savez que l’une des évolutions que j’avais en tête était d’ajouter un thread dont le rôle aurait été de surveiller les connexions qui duraient depuis trop longtemps. Une manière de gérer les inactivités et de couper les connexions inutiles.
J’ai fait une implémentation complète de ce système. Cela imposait quelques modifications au code existant :
- Gérer une option supplémentaire, pour définir la durée de timeout (avec une valeur par défaut à 90 secondes).
- Ajouter un timestamp dans chaque thread, contenant la date de dernière activité − lecture ou écriture − du thread.
- Créer un thread supplémentaire, qui parcours la liste des threads à intervale régulier. Ceux dont l’inactivité dure depuis un temps supérieur au timeout voient leur connexion coupée.
J’ai rencontré un premier problème. Lorsqu’une connexion était détectée comme trop longue, la socket équivalente était fermée avec la fonction close(). Malheureusement, cela n’est pas suffisant ; le thread de communication restait bloqué à son appel à la fonction read(). Le fait de fermer la socket avec close() envoie l’information au client situé à l’autre bout de la connexion, mais cela ne change rien pour le read() local, qui attend toujours de recevoir des données.
L’astuce est de faire un appel à shutdown() au lieu de close(), en lui donnant le flag SHUT_RDWR. En fermant d’un coup les « deux côté » de la socket (c’est-à-dire autant en lecture qu’en écriture), read() sort en retournant une erreur, ce qui permet de gérer le soucis.
En parallèle de ça, Armel Fauveau m’a conseillé via Twitter d’utiliser TCP keepalive, pour éviter d’avoir un tel thread de garbage collecting. Malheureusement, cette technique ne permet que de vérifier que la connexion est toujours établie entre le client et le serveur ; si le client est parti dans les choux (ou qu’il a simplement décidé de ne rien faire et de consommer une connexion pour rien), il n’y a aucune différence.
Mais la suggestion semblait censée, alors j’ai voulu creuser la question. Il avait raison, ce serait étonnant que rien n’existe au niveau réseau pour gérer les timeouts. J’ai donc trouvé, c’est au final assez simple : la fonction setsockopt() avec les options SO_RCVTIMEO et SO_SNDTIMEO. La littérature sur la question n’est pas débordante, mais le minimum vital est assez facile à trouver.
L’une des choses à laquelle il faut faire attention, quand on utilise ces options, est de ne pas les activer en continue. Imaginons que l’on place un timeout de 10 secondes, aussi bien en lecture qu’en écriture. Le serveur va passer le plus clair de son temps à attendre des données. Une fois qu’il les aura reçues, le compteur de timeout en lecture va repartir de zéro ; mais celui d’écriture continuera à tourner. Donc si le temps d’attente suivi du temps de traitement des données est supérieur au timeout d’écriture, la socket va être fermée alors que d’un point de vue applicatif tout se déroule comme prévu. De la même manière, après avoir reçu des données, le temps de les traiter puis d’envoyer un retour au client peut être plus long que le timeout de lecture.
Là, le truc est de placer le timeout sur la socket juste avant l’opération système bloquante (read() ou write()), puis de le retirer juste après. Ainsi, on s’assure que ce ne sont que les délais de communication client-serveur qui sont pris en compte par les timeouts.
Je viens de mettre à disposition une version du code qui implémente cette évolution.
Au passage, les évolutions de FineDB sont listées sur le site du projet.
Pour ceux que ça intéresse, j’ai ajouté une commande PING au protocole, permettant de tester si une connexion au serveur est toujours effective. Je l’utilise dans l’outil en ligne de commande, pour tester automatiquement la connexion avant l’envoi d’une requête, pour reconnecter automatiquement si nécessaire (c’est une option désactivable).
Pour le reste, les commandes de base sont toutes implémentées, y compris le support des transactions. La bibliothèque cliente et l’outil en ligne de commande sont à jour par rapport aux fonctionnalités du serveur.
Bonjour, j ai été confronté au même problème et je me demandais qu’est ce qu il se passe si le client envoie une commande qui pour x ou y raisons, finedb mets plus de « timeout » secondes pour répondre ? (Disque dur lents ou autre)
Car le client n est pas inactif il attends juste une réponse.
Autre cas, l envoie d une commande prends beaucoup trop de temps (grosse quantité de donnée et faible bande passante par exemple)
Comment tu as fait pour gérer ces cas ?
Je viens d’ajouter un paragraphe dans l’article, pour expliquer ça. Il ne faut pas laisser les timeouts tout le temps. Juste au moment où on veut lire ou écrire sur la socket. Ça fait un peu plus de code, mais c’est infiniment plus efficace.
J’avais pas pensé à cette solution, mais dans mon cas il s’agit uniquement de traitement asynchrone j’ai un thread qui ne fait que lire, un thread qui fait le traitement et un thread qui écrit.
Donc dans mon cas, ca n’aurait pas pu marcher, et j’ai du rajouté un « flag » pour signaler si on a recu des choses à traiter sur ce socket..
Mmh… je ne suis pas sûr de bien comprendre ton besoin, mais dans tous les cas il est possible de mettre un timeout sur la socket juste avant de faire le read(), pour s’assurer qu’il ne dure pas indéfiniment, puis de retirer le timeout. Pareil en écriture.