Add gossip replication between nodes

Nodes run anti-entropy over a TCP link: each advertises its version
vector, the peer replies with the log entries it lacks, and ingest is
idempotent so re-delivery and reconnect after a split both converge.
The engine is shared behind a mutex; config gains [gossip] and [[peer]].
This commit is contained in:
Jean Chevronnet 2026-07-12 07:05:07 +00:00
parent 82e41e95b2
commit d0556ebe8c
No known key found for this signature in database
7 changed files with 319 additions and 20 deletions

View file

@ -1,13 +1,18 @@
use std::sync::Arc;
use anyhow::Result;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;
use tokio::sync::Mutex;
use crate::engine::db::Db;
use crate::engine::Engine;
use crate::proto::{NetAction, Protocol};
// One uplink session: connect, handshake + burst, then translate lines forever.
pub async fn run(mut proto: Box<dyn Protocol>, mut engine: Engine, addr: &str) -> Result<()> {
// The engine is shared with the gossip layer, so it is locked per operation and
// never held across the registration key-stretching await.
pub async fn run(mut proto: Box<dyn Protocol>, engine: Arc<Mutex<Engine>>, addr: &str) -> Result<()> {
let stream = TcpStream::connect(addr).await?;
let (read, mut write) = stream.into_split();
let mut lines = BufReader::new(read).lines();
@ -15,7 +20,8 @@ pub async fn run(mut proto: Box<dyn Protocol>, mut engine: Engine, addr: &str) -
for line in proto.handshake() {
send(&mut write, &line).await?;
}
for action in engine.startup_actions() {
let startup = engine.lock().await.startup_actions();
for action in startup {
for line in proto.serialize(&action) {
send(&mut write, &line).await?;
}
@ -24,21 +30,23 @@ pub async fn run(mut proto: Box<dyn Protocol>, mut engine: Engine, addr: &str) -
while let Some(line) = lines.next_line().await? {
tracing::debug!(dir = "<<", %line);
for event in proto.parse(&line) {
for action in engine.handle(event) {
let actions = engine.lock().await.handle(event);
for action in actions {
let outs = match action {
// Registration: derive the password off the reactor so the
// ~1s of key stretching can't stall the link, after a cheap
// gate that rejects taken names and rate-limits floods.
NetAction::DeferRegister { account, password, email, reply } => {
match engine.pre_register_check(&account, &reply) {
let pre = engine.lock().await.pre_register_check(&account, &reply);
match pre {
Some(rejection) => rejection,
None => {
let iterations = engine.scram_iterations();
let iterations = engine.lock().await.scram_iterations();
let creds = tokio::task::spawn_blocking(move || {
Db::derive_credentials(&password, iterations)
})
.await?;
engine.complete_register(&account, creds, email, reply)
engine.lock().await.complete_register(&account, creds, email, reply)
}
}
}