Il y a une plaque dans chaque ascenseur : 8 personnes — 630 kg. Elle ne dit pas que la cabine se rompt à la neuvième. Elle dit qu’un ingénieur a chargé, mesuré, et signé une enveloppe : voici ce que je garantis, dans ces limites, avec une marge. La plaque n’est pas un aveu de faiblesse — c’est précisément ce qui permet de monter sans y penser.

Le cluster de lithair n’avait pas sa plaque.

Le code, lui, tournait déjà : réplication multi-nœuds, élection d’un leader, bascule quand un nœud tombe. Tout ça existait et fonctionnait. Mais à la question « est-ce que je peux m’appuyer dessus en production ? », il n’y avait pas de réponse documentée — juste « ça a l’air de marcher chez moi ». Le cinquième et dernier pilier de v0.13.0 donne enfin cette réponse. Et il la donne avec ses limites écrites dessus.

Ce qui a été livré : la preuve, puis le runbook

Le pilier #104 (PR #127, commit 2253e26) ne contient presque pas de code nouveau. Il contient un run de stress et le document qui en découle.

Un cluster à trois nœuds, soumis à plusieurs phases de charge, dont une endurance de 140 000 écritures à concurrence 64 sur 11,3 minutes :

MesureRésultat
Écritures totales (toutes phases)170 200, répliquées à l’identique sur les 3 nœuds
Drops / panics / divergences de réplication0 / 0 / 0
Latence (endurance)p50 296 ms, p99 534 ms — stable
RSS du leader sur l’endurance135 → 307 MB

Verdict : sur 170 200 écritures, zéro perte, zéro panic, zéro divergence d’état entre les nœuds.

Mais le chiffre qui compte le plus n’est pas un succès — c’est un plafond. L’enveloppe a un bord dur : ~210 à 240 écritures/s par leader en mode écriture simple. Au-delà, la latence à haute concurrence n’est pas de l’instabilité : c’est de la file d’attente pure contre ce plafond. Et le léger déclin observé sur l’endurance (241 → 206 ops/s) se trace à la croissance de l’état en mémoire (le RSS qui monte) — conforme au modèle memory-first de lithair, pas une fuite.

C’est ça, la plaque de l’ascenseur. Pas « c’est rapide ». Mais « voici le débit que je tiens, voici pourquoi il bouge un peu, et voici à partir d’où vous sortez de la zone testée ».

Le runbook qui ne se cache pas

Le vrai livrable du pilier, c’est docs/operations/cluster.md — un runbook qui documente l’implémentation telle qu’elle est, pas telle qu’on aimerait qu’elle soit :

  • L’élection n’est pas du Raft. Pas de timeouts randomisés, pas de quorum de votes. C’est statique : le plus petit ID de nœud vivant devient leader. Corollaire écrit noir sur blanc dans le runbook : incluez toujours un nœud d’ID 0. Détection de panne à 5 s, heartbeats toutes les ~1,7 s.

  • La fenêtre dual-leader au rejoin — la partie que le runbook appelle littéralement « the honest part ». Quand un ancien leader revient après une coupure, il y a un bref instant où deux nœuds se croient leader :

  t0   node 0 = leader            node 1, node 2 = followers
  t1   node 0 tombe        ✗      node 1 prend la main (plus petit ID vivant)
  t2   node 0 revient      ↩      node 0 ET node 1 se croient leader  ← fenêtre dual-leader
  t3   réconciliation             un seul leader, l'état converge

Cette fenêtre est connue, documentée, et non bloquante — parce que la protection contre le split-brain committé ne vient pas de l’élection, elle vient de l’ailleurs : un write n’est commit qu’avec un accusé de réception de la majorité. Deux leaders peuvent accepter des writes une fraction de seconde ; un seul peut les committer.

  • Les limites, nommées une par une. _bulk n’a pas de vraie sémantique batch en mode cluster (v0.13.0). /_admin/schema/sync est un stub : il logue et renvoie l’état courant, il ne réconcilie rien. La suite BDD du cluster existe mais ne tourne pas encore en CI. Aucune de ces lignes n’est agréable à écrire. Toutes sont dans le doc.

Les cinq piliers, en neuf jours

Le pilier cluster est le dernier d’une série lancée début juin — cinq chantiers dont aucun n’ajoutait de fonctionnalité produit, et qui tous répondaient à la même question : peut-on opérer ça ?

PilierSujetFermé
#106Runbook d’exploitation (Docker, systemd, k8s, capacity planning)06-09
#107Observabilité (tracing, OTLP, X-Request-ID)06-10
#105Couverture BDD sur modèles métier réalistes (Invoice, Document, User, Order)06-11
#108Gouvernance (code de conduite, roadmap v1.0, politique de dépréciation)06-11
#104Stabilité cluster — gate v1.0 G106-12

Les deux premiers ont déjà eu leur récit ici : s’éteindre proprement puis raconter ce qui se passe. Le pilier #105 mérite une note au passage : écrire des tests BDD sur de vrais modèles métier a fait sortir deux bugs silencieux que v0.13.0 corrige — un #[retention] qui ignorait les budgets durée-seule, un #[db(fk=)] silencieusement écrasé. Le genre de bug qu’on ne voit pas en relisant, qu’on ne voit qu’en exerçant.

Et l’intendance a suivi : v0.13.0 a été taguée le 11 juin et publiée le jour même sur crates.io — trois crates, zéro drift entre le tag git et le registre public. Un détail, mais c’est le détail qui fait la différence entre « j’ai sorti une version » et « tu peux en dépendre ».

Le choix : « stable dans une enveloppe documentée », pas « prêt pour la production »

J’aurais pu écrire « le cluster est production-ready ». La phrase est plus vendeuse et techniquement défendable. Je ne l’ai pas écrite.

Le runbook dit : stable within a documented envelope. La nuance n’est pas de la modestie — elle est falsifiable. « Production-ready » ne se vérifie pas ; c’est une affirmation qu’on croit ou pas. « Stable jusqu’à ~240 écritures/s par leader sur 3 nœuds, avec une fenêtre dual-leader au rejoin et ces trois limites nommées » se vérifie : n’importe qui peut relancer le stress test et confronter le chiffre. Un opérateur décide les yeux ouverts, pas sur la foi d’un adjectif.

Ce qu’on a appris

La stabilité cluster n’était pas un problème de code. Le code tournait avant que le pilier ne commence. Le travail dur, c’était d’écrire ce qui est vrai — y compris les morceaux qui ne brillent pas : la fenêtre dual-leader, le plafond à 240, les stubs. Un runbook qui cache ses limites est pire que pas de runbook : il donne une confiance que rien ne soutient.

Petit clin d’œil du run lui-même : le stress test a débusqué un bug de bruit. Le reap des connexions keep-alive inactives de hyper loggait à ERROR — sous charge, ça noyait les vraies erreurs. Corrigé en debug. Le test n’a pas seulement mesuré l’enveloppe ; il a nettoyé le tableau de bord qui sert à la lire.

La suite : les trois derniers verrous avant v1.0

Le pilier #104 était la gate G1. Trois autres restent ouvertes avant qu’un contrat d’API stable v1.0 existe :

  • #128 (G2) — le parser de macros doit émettre une erreur de compilation sur un token inconnu, au lieu de l’ignorer en silence (ce qui a causé les bugs #75/#122).
  • #129 (G3) — audit de la surface API publique + politique MSRV : distinguer stable / instable / caché avant de signer le contrat.
  • #126 (G4) — benchmarks reproductibles et baselines publiées : débit CRUD, latences p50/p99/p99.9, croissance mémoire, cold-start — comparés à Axum+SQLite et Actix+Postgres.

G1 répondait à « le cluster tient-il ? ». G4 répondra à « tient-il face à quoi ? ». Entre les deux, lithair sait maintenant faire la chose la plus rare pour un framework jeune : nommer son enveloppe sans la maquiller.