La semaine qui a suivi lithair v0.12.0 n’a presque pas produit de code. Sept PRs ont été mergées, et six d’entre elles sont des fichiers de configuration et de la documentation : un Dockerfile, une unit systemd, des manifests Kubernetes, un guide de capacity planning, un runbook de sauvegarde/restauration, un playbook de montée de version.

C’est le travail dont personne ne parle jamais dans un framework. Tout le monde écrit sur les features. Personne n’écrit sur « et maintenant, comment tu l’exploites ? ». Cette semaine-là, c’était pourtant exactement le sujet — et le seul vrai commit de code de la série le résume mieux que tout : apprendre au serveur à s’éteindre proprement.

Le problème : on ne sait jamais éteindre

Tout serveur sait démarrer. Très peu savent s’arrêter. Quand on tue un processus serveur — Ctrl-C, kill, un redéploiement, un nœud Kubernetes qui recycle un pod — les requêtes en cours de traitement meurent avec lui. Pour un client, ça ressemble à une connexion coupée au milieu d’une réponse. Pour une écriture, ça peut être pire.

L’analogie qui dit tout : c’est la fermeture d’un restaurant. Un restaurant qui ferme bien ne jette pas les clients dehors au milieu du plat. Il fait trois choses, dans l’ordre :

  Fermer un restaurant            Éteindre un serveur

  1. On n'accepte plus            1. On arrête d'accepter de
     de nouvelles tables             nouvelles connexions

  2. On laisse les clients        2. On laisse les requêtes
     attablés finir leur repas       en vol se terminer

  3. On éteint les lumières       3. Le processus se termine

Sauter l’étape 2, c’est la différence entre une fermeture et une expulsion. Et jusqu’à v0.12.0, serve() ne proposait que l’expulsion : la boucle d’acceptation tournait pour toujours, et le seul moyen de l’arrêter était de tuer le processus.

La solution : un futur, et une boucle qui l’écoute

L’API ajoutée (PR #114) mire ce que fait axum — c’est volontaire, autant reprendre un pattern que l’écosystème connaît déjà :

LithairServer::new()
    .with_port(8080)
    .with_model::<Article>("./data/articles", "/api/articles")
    .serve_with_graceful_shutdown(async {
        tokio::signal::ctrl_c().await.ok();   // ← le signal d'extinction
    })
    .await

Le paramètre est un futur : n’importe quel événement asynchrone — un Ctrl-C, un SIGTERM envoyé par systemd ou Kubernetes, un canal interne. Tant qu’il ne se résout pas, le serveur tourne. Quand il se résout, la séquence du restaurant s’enclenche : la boucle d’acceptation s’arrête (tokio::select! entre « accepter une connexion » et « le futur d’extinction »), les connexions en vol disposent d’une fenêtre de grâce de 5 secondes pour se terminer, puis le processus rend la main.

Le détail d’implémentation qui compte : serve() — l’API historique — devient un délégué d’une ligne :

pub async fn serve(self) -> Result<()> {
    self.serve_with_graceful_shutdown(std::future::pending::<()>()).await
}

std::future::pending() est un futur qui ne se résout jamais. Autrement dit : l’ancien comportement (tourner pour toujours) est devenu un cas particulier du nouveau — celui où le signal d’extinction n’arrive jamais. Zéro breaking change, le code existant est inchangé octet pour octet, et il n’y a plus qu’un seul corps de boucle à maintenir au lieu de deux.

Pourquoi maintenant : le batch « exploitation »

Ce commit n’est pas tombé seul. Il est la pièce code d’un lot entièrement tourné vers l’exploitation :

  • Docker + docker-compose (#110) et systemd + Kubernetes (#111) — les trois façons standard de faire tourner un binaire en production. Et c’est précisément là que le graceful shutdown cesse d’être un luxe : systemd et Kubernetes envoient SIGTERM à chaque redéploiement. Un serveur qui ne sait pas l’écouter se fait expulser à chaque mise à jour.
  • Sauvegarde, restauration, PITR (#116) et montée de version (#117) — les runbooks qu’on cherche à 2h du matin. Écrits avant d’en avoir besoin.
  • Capacity planning (#113) — combien de RAM pour combien d’items, la question que le modèle memory-first de lithair impose de se poser avant, pas après.
  • CONTRIBUTING + SECURITY (#109) — la porte d’entrée pour quelqu’un d’autre que l’auteur.

Pris un par un, aucun de ces fichiers n’est excitant. Pris ensemble, ils changent la nature du projet : lithair n’est plus seulement un framework dont les features marchent — c’est un logiciel qu’on peut déployer, sauvegarder, mettre à jour et éteindre sans lire le code source.

Ce qu’on a appris

Un framework n’est pas fini quand les features marchent. Il est fini quand on peut l’éteindre proprement — et le redémarrer, le sauvegarder, le faire monter de version, en suivant une page de doc plutôt qu’en fouillant les sources. La feature la plus importante de la semaine tient en une signature de fonction, et ce qu’elle dit vraiment, c’est : ce serveur respecte ce qui est en train de se passer à l’intérieur de lui.

Reste une honnêteté à poser : la fenêtre de grâce est fixe (5 secondes), et le drainage des tâches internes (auto-compaction, flush du WAL) est un suivi documenté, pas encore câblé. C’est noté dans le code, au TODO près. La porte est posée ; les finitions suivront.