← Retour au site Chargement…

rescriptum

Les contraintes

Développement

Généré le 30 août 2026

Les contraintes

Les contraintes

Ce sont des décisions, pas des oublis. Plusieurs ressemblent, vues de l’extérieur, à des améliorations évidentes. N’en changez aucune sans demander — et si vous en changez une, changez cette page et CLAUDE.md avec.

Asynchrone, sur tokio et hyper

La spécification d’origine demandait zéro dépendance et un thread par connexion. Les deux ont été écartées délibérément, une fois que l’exigence est devenue « absorber une rafale de provisioning professionnelle ». Un déploiement de 2 000 machines, ce sont 2 000 connexions quasi simultanées, et un thread chacune fait 2 000 piles sur une machine à 512 Mo.

Ce qui a survécu de la spec : aucun serde derive, aucun framework, et une liste de dépendances directes très courte. Voir architecture.

hyper directement, pas axum

axum ne donne aucun moyen de définir un délai de lecture des en-têtes, précisément le garde-fou anti-slowloris qui a motivé le passage à l’asynchrone. Le routage ici est un seul if sur la méthode et le chemin, donc un framework n’apporte rien et coûte la seule chose qui comptait.

Concurrence bornée, même si les tâches sont bon marché

Une connexion coûte des kilo-octets plutôt qu’un thread — c’est tout l’intérêt de la réécriture asynchrone. Mais bon marché n’est pas gratuit, et un accept non borné transforme quand même une rafale en épuisement mémoire.

Un Semaphore de RESCRIPTUM_MAX_CONNECTIONS plafonne les connexions en vol. Au-delà, le serveur écrit un 503 immédiat et ferme plutôt que de mettre en file : un client à qui on dit de réessayer s’en sort mieux qu’un client garé dans une file qui ne se videra pas.

Le travail sur le système de fichiers passe par spawn_blocking

read_dir et read sont des appels bloquants, et bloquer un thread worker asynchrone bloque toutes les autres connexions que ce thread pilote. Sur un NAS dont le disque dort, ce n’est pas théorique — un réveil de disque se compte en secondes, pas en millisecondes.

resolve() porte à la fois le parsing et l’E/S, et n’est jamais appelé qu’à l’intérieur d’un spawn_blocking. Un panic là renvoie un 500 ; il ne peut pas emporter le serveur.

Ne jamais paniquer sur une entrée malformée

Tout échec de parsing devient une réponse d’erreur plus une ligne de log. Écrivez le code comme s’il n’y avait pas de filet.

Il y en a un, délibérément : le profil release ne définit pas panic = "abort". Avec le déroulement, un panic est confiné à la connexion qui l’a causé au lieu de tuer un serveur en pleine installation. Coût mesuré sur ARMv7 : +2416 octets, +0,8 %. Ne le « ré-optimisez » pas.

Si la conception passe un jour à un pool de threads, ajoutez un catch_unwind à la frontière du worker — un thread de pool qui meurt en silence est pire que l’un ou l’autre.

[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
strip = true
# panic = "abort" est délibérément ABSENT

Le store ne décide de rien

Il rend le texte brut des documents et un jeton de version bon marché. Correspondance, chaînes extends, fusion, rendu et check vivent tous au-dessus et sont partagés.

Gardez-le ainsi. Dès qu’un backend se met à décider du comportement, les deux divergent — et tests/stores.rs cesse de pouvoir prouver le contraire.

L’organisation du stockage n’est pas l’URL

Répertoires et lignes de base sont un espace de recherche et doivent rester libres d’être réorganisés. Une URL est un contrat public gravé dans une ISO et ne doit pas bouger parce que quelqu’un a renommé un dossier. Une conception antérieure faisait du nom de répertoire le segment d’URL et a été écartée pour exactement cette raison.

La conséquence est que la clé d’un document est (identifiant, format), ce autour de quoi le schéma SQLite est construit.

Ne jamais construire un chemin de fichier à partir de données de requête

C’est le garde-fou contre la traversée de chemin, et il est structurel plutôt qu’une vérification : seules les entrées directes du répertoire de réponses sont lues. Les identifiants arrivant à l’API d’administration sont validés séparément, à la frontière de l’API et dans les deux stores, parce qu’export les retransforme en noms de fichiers.

Les réponses doivent être des documents valides

Avant la fusion, un fichier de réponse était servi comme des octets opaques, donc un fichier malformé atteignait l’installateur. Maintenant c’est un 500 avec l’erreur de parsing dans le log.

C’est le meilleur échec — un installateur qui reçoit du TOML à moitié valide échoue d’une façon bien plus déroutante — mais c’est un changement de comportement, et des fixtures écrites en pseudo-YAML ont cessé de fonctionner à ce moment-là.

Échouer bruyamment

Un groupe manquant, un template impossible à remplir, un document qui ne parse pas : tous sont des erreurs avec une raison, jamais une réponse au mieux.

Le raisonnement est toujours le même. Une réponse à moitié construite installe une machine de travers, et personne ne s’en aperçoit avant qu’elle ne tourne. Une installation ratée se remarque en quelques minutes.

Asymétries délibérées

Deux endroits où la symétrie évidente est fausse exprès :

Le jeton de réponse n’est jamais limité en débit ; celui d’administration siune baie peut se trouver derrière une seule adresse, donc l’exclure transforme un mauvais jeton en déploiement raté. Aucun installateur ne parle à l’API d’administration
Un jeton de réponse court avertit ; un jeton d’administration court empêche le démarragerefuser de démarrer laisserait un parc incapable de s’installer. Refuser de démarrer l’API d’administration ne coûte d’installation à personne

Ce que la spec demandait et n’a pas eu

plans/rescriptum-spec.md (dans le .gitignore, donc un contributeur ne l’aura pas) est le compte rendu de ce qui a été demandé au départ, pas une description de ce qui existe. Le projet l’a dépassée dans toutes les directions : multi-OS, sélecteurs, templating, une API d’administration, un store en base.

Trois écarts précis, tous listés ci-dessus : asynchrone plutôt qu’un thread par connexion, panic = "abort" omis, et le corps de requête parsé en JSON non typé pour en récolter des faits. Là où la spec et cette page divergent, c’est cette page qui a raison.