Principe RS-7 : « dès qu'on build un truc, plus personne n'a à le rebuild —
la seule chose à faire est l'optimisation. » (nexus/RepoVerse)
- bion-vc : horloges vectorielles + MvReg fork-visible (LA spec
xion-relativiste-v0 enfin codée — CRDT testé par permutations)
- bion-triplet : l'Adressage Génératif (gen_hash BLAKE3, coords, résidu ;
résidu vide quand déjà-su ; align décidable au bit)
- bion-tsoinlog: journal append-only rejouable (CRC32 maison, crash-recovery)
- bion-kv : magasin clé-valeur bitcask (compaction atomique, tombstones)
- bion-regex : moteur Thompson NFA linéaire (jamais exponentiel — Russ Cox)
- bion-git : mini-git content-addressed (SHA-1 maison + vecteurs officiels,
branches divergentes = le fork visible)
129 tests verts, clippy 0 warning, doc française = chaque bion est un cours.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
92 lines
4.3 KiB
Markdown
92 lines
4.3 KiB
Markdown
# bion-tsoinlog — le journal append-only rejouable
|
||
|
||
## Quoi
|
||
|
||
La **primitive de la machine à tsoins** : un journal d'événements sur disque où
|
||
l'on ne fait qu'**appender** (jamais modifier, jamais effacer) et que l'on peut
|
||
**rejouer** — en entier ou par tranche `[from, to)`. Chaque événement = un
|
||
`topic` (UTF-8) + un `payload` (octets opaques), et reçoit un numéro de séquence
|
||
`Seq` strictement croissant.
|
||
|
||
- **std-only**, zéro dépendance, zéro `unsafe`.
|
||
- Format binaire v1 minuscule et documenté : `magic "TSOINLG1"` puis
|
||
`len(u32 LE) · topic_len(u16 LE) · topic · payload · crc32(u32 LE)`.
|
||
- **CRC-32 (IEEE) implémenté maison** (table générée à la compilation, vecteur
|
||
canonique `"123456789" → 0xCBF43926` testé) — pas de lib, build-your-own.
|
||
- **Récupération après crash** : à l'ouverture, la queue tronquée ou au CRC faux
|
||
est détectée et retaillée ; l'historique valide survit toujours (testé en
|
||
tronquant et en corrompant le fichier à la main).
|
||
- `fsync` configurable (`Sync::Always` / `Sync::Never` + `sync()` manuel).
|
||
- Index en mémoire (offset de chaque record) reconstruit au scan d'ouverture →
|
||
`iter_from(seq)` et `replay` démarrent en seek direct, pas de re-scan.
|
||
|
||
## Pourquoi
|
||
|
||
C'est la structure au cœur de Kafka, des WAL de bases de données, de l'event
|
||
sourcing, de git — et de la machine à tsoins : **enregistrer le réel dans
|
||
l'ordre, pouvoir le revivre**. Le complément exact de `tsoin-codec`
|
||
(`~/xerboxion-rt/crates/tsoin-codec`) : le codec transforme un contenu en
|
||
coordonnée de Babel, le log mémorise durablement la *séquence* des événements.
|
||
On appende volontiers des tsoins-de-fil comme payloads ; le log, lui, ne
|
||
présuppose rien sur le contenu.
|
||
|
||
## Exemple
|
||
|
||
```rust
|
||
use bion_tsoinlog::{TsoinLog, Seq, Sync};
|
||
|
||
let mut log = TsoinLog::open("journal.tsoinlog")?; // Sync::Never par défaut
|
||
// ou : TsoinLog::open_with("journal.tsoinlog", Sync::Always)? // durable à chaque append
|
||
|
||
let s0 = log.append("capteur/temp", b"21.5")?; // → Seq(0)
|
||
let s1 = log.append("bus/emit", b"{\"topic\":\"leds\"}")?; // → Seq(1)
|
||
|
||
// Tout relire :
|
||
for rec in log.iter()? {
|
||
let rec = rec?;
|
||
println!("#{} [{}] {} octets", rec.seq.0, rec.topic, rec.payload.len());
|
||
}
|
||
|
||
// Rejouer une tranche [1, 2) (from inclus, to exclu, comme un Range) :
|
||
log.replay(Seq(1), Seq(2), |rec| { /* ré-appliquer l'événement */ })?;
|
||
# std::io::Result::Ok(())
|
||
```
|
||
|
||
## API publique (stable — rétrocompatibilité éternelle)
|
||
|
||
| Élément | Rôle |
|
||
|---|---|
|
||
| `TsoinLog::open(path)` / `open_with(path, Sync)` | ouvre/crée + scan + réparation |
|
||
| `append(topic, payload) -> io::Result<Seq>` | appende un événement |
|
||
| `iter()` / `iter_from(Seq)` | itère (instantané cohérent, seek O(1)) |
|
||
| `replay(from, to, f) -> io::Result<u64>` | rejoue `[from, to)`, retourne le compte |
|
||
| `sync()` / `set_sync(Sync)` | contrôle du `fsync` |
|
||
| `len()` / `is_empty()` / `next_seq()` | état du journal |
|
||
| `Seq(u64)` / `Record { seq, topic, payload }` / `Sync` | types de données |
|
||
| `crc32(&[u8]) -> u32` / `MAGIC` / `MAX_TOPIC_LEN` | briques exposées |
|
||
|
||
## Build-your-own-x correspondants
|
||
|
||
Dans [build-your-own-x](https://github.com/codecrafters-io/build-your-own-x) :
|
||
|
||
- **Build your own Database** — le log est un *write-ahead log* (WAL) minimal ;
|
||
- **Build your own Git** — un historique append-only adressé par position ;
|
||
- le CRC-32 maison est la brique commune à zip/gzip/PNG/Ethernet (« build your
|
||
own checksum » depuis la division polynomiale dans GF(2)).
|
||
|
||
## Comment l'optimiser (l'invitation au fork)
|
||
|
||
Le format v1 est volontairement le plus simple qui soit correct. Pistes, dans
|
||
l'ordre de rentabilité, **sans jamais casser la lecture des fichiers v1** :
|
||
|
||
1. **`Sync::EveryN(n)` / group commit** — amortir le `fsync` sur n appends ;
|
||
2. **index persistant** (fichier `.idx` side-car, régénérable) — ouverture O(1)
|
||
sur les très gros journaux au lieu du scan complet ;
|
||
3. **segments + compaction** — découper en fichiers de taille bornée, archiver
|
||
ou fusionner les vieux segments (le chemin vers Kafka) ;
|
||
4. **mmap en lecture** — itération zéro-copie ;
|
||
5. **CRC vectorisé** (slicing-by-8, ou `crc32` matériel SSE4.2) — même résultat,
|
||
~10× plus vite ;
|
||
6. **compression des payloads** — brancher `tsoin-codec` : appender la
|
||
coordonnée de Babel au lieu des octets bruts.
|