Ruby on Rails, Passenger et MySQL : interrompre un thread sans détruire le ConnectionPool

Imaginons. Une application Rails dispose d’une action qui lance la génération d’un gigantesque CSV. Afin de ne pas attendre le retour de l’action, on place la génération du CSV dans un thread et on fait retourner un message à l’utilisateur type « Ne vous inquiétez pas on s’occupe de tout ». Et on part tranquillement pour une heure de génération, relax.

Un petit bout de JavaScript va appeler périodiquement une autre action tant que le fichier ne sera pas prêt. Il changera un bouton de couleur en y ajoutant un lien de téléchargement dès que ce sera fini. Mais subitement, l’utilisateur change d’avis, modifie quelques options sur l’interface et demande la génération d’un nouveau fichier.

Comment on tue ce thread sans tout péter ?

La génération du CSV devant recommencer, on souhaite éviter de gâcher des ressources serveur et l’écrasement de notre fichier de sortie.

Je suis un développeur bourrin : Thread.kill

Je vais ressortir mon schéma favoris qui illustre comment le module Apache Passenger se charge de load balancer les requêtes qu’il reçoit :
passenger-request_load_balancing
L’application Rails est démarrée plusieurs fois dans des processus séparés. De ce fait, en production, Thread.list peut ne pas lister le thread dans lequel notre premier CSV est en cours de génération : tout dépend dans quelle application tombe notre requête.

Mettons que notre développeur bourrin ait de la chance, tombe aléatoirement dans le bon processus et qu’il tranche net notre thread. La connexion à la base de données ouverte par l’application Rails dans le thread pendouille sans avoir été déconnectée proprement, elle sera définitivement inutilisable jusqu’à l’arrêt de ce processus Passenger.

L’ORM ActiveRecord est configuré par défaut pour disposer d’un pool de 5 connexions à la base de données. Lorsque les 5 connexions seront bloquées, tout accès à la base de données lèvera l’erreur suivante :

#<Mysql2::Error: This connection is in use by: #<Thread:0x007f9d913dd6b0 dead>>

Je suis un développeur ignoble : je pousse le thread à se suicider

La parade consiste alors à faire lire régulièrement un booléen « dois_arreter » au thread. Dès que le booléen passe à true, la boucle dans le thread s’interrompt et le thread se laisse mourir. Il suffit de mettre à jour ce booléen pour interrompre proprement le thread, et MySQL est un bon endroit pour stocker cette variable commune à tous les threads.

Même problème en revanche concernant la connexion à MySQL. Elle n’est plus en cours d’utilisation par un thread mort, comme disait l’erreur précédente, mais elle n’est pour autant pas refermée, donc pas libérée pour retourner dans le ConnectionPool. Après 5 décès de threads, l’erreur suivante apparait :

#<ActiveRecord::ConnectionTimeoutError: could not obtain a database connection within 5.000 seconds (waited 5.000 seconds)>

On ajoute le bout de code suivant juste avant la fin du thread et le tour est joué :

if (ActiveRecord::Base.connection && ActiveRecord::Base.connection.active?)
  ActiveRecord::Base.connection.close
end

Suite et fin

Bon, après, libre à vous d’identifier plus précisément les threads de l’utilisateur en joignant un identifiant au booléen. Libre aussi à vous de définir si le lancement d’un nouveau thread doit attendre la mort de tous ses prédécesseurs, par exemple, en ajoutant la suppression de la ligne de BDD avant la mort du thread et en attendant que toutes les lignes aient disparues pour lancer le nouveau.

Que faire si Apache redémarre et interrompt un thread qui n’aura pas le temps d’effacer son entrée en BDD ? Comment supprimer toutes les lignes d’états de thread au démarrage de l’appli, alors que Passenger démarre et arrête des processus à tout moment ? On peut éventuellement stocker le timestamp de démarrage du serveur.

Sous Linux :

ls -dl --full-time /proc/$(ps axo pid,cmd,user | grep apache | grep root | cut -d" " -f2) | cut -d" " -f6,7

Sous Mac :

ps axo pid,user,lstart,command | grep "rails s" | grep -v "grep

Et voilà, jusque là nous on s’en est sorti, si vous rencontrez d’autres problèmes n’hésitez pas à les partager en commentaire 🙂

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.