fix(01-05): delete the redundant second periodic federation sync loop (FED-02)

Two near-identical periodic federation sync loops were running side by
side. git history shows the overlap was accidental, not load-bearing: the
30-minute loop landed first (8dd57bcb, 2026-04-19), and the 90s loop
landed later (837cc028, 2026-06-19) describing itself as "new 90s
periodic federation auto-sync (none existed)" — its author simply hadn't
seen the existing one. Running both doubled the write-race exposure
against nodes.json that plan 01-01 locked down.

The 90s loop survives; it already did strictly more (per-peer sync-result
recording, asymmetry self-heal). The deleted loop's one unique behavior —
refresh_federation_mesh_peers() after a completed pass (#42), which pushes
newly-learned names/roster into the live mesh peer table so chat contacts
refresh without a restart — is preserved at the tail of the survivor. That
call is a local, idempotent re-seed from nodes.json with no network I/O,
so running it per-pass rather than per-half-hour is cheap.

Also carried over: MissedTickBehavior::Delay, so a pass delayed by suspend
or heavy load resumes the cadence instead of firing a burst of catch-up
ticks. And node-load errors are now logged and skipped explicitly rather
than swallowed by a catch-all, so an empty roster and an unreadable one
are no longer indistinguishable.

Not carried over: the deleted loop's 5s per-peer stagger. Its stated
reason was avoiding concurrent connects against the Tor SOCKS proxy, but
both loops iterate peers sequentially and await each sync, so there were
never concurrent connects to stagger; keeping it would only push a
multi-peer pass past the 90s cadence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-02 14:13:18 -04:00
co-authored by Claude Opus 5
parent dad40c23f1
commit 937d836c53
+48 -69
View File
@@ -498,18 +498,46 @@ impl Server {
// timer so renamed nodes and roster changes propagate WITHOUT a manual
// "Sync" click. Each sync now fast-fails a dead FIPS path and falls back
// to Tor (~3-5s), so a full pass over a handful of peers is quick.
//
// This is the ONE periodic federation sync loop. A second, near-identical
// 30-minute loop used to run alongside it and was deleted in FED-02:
// `git log` shows the 30-min loop landed first (8dd57bcb, 2026-04-19,
// "periodic sync every 30 minutes") and this 90s loop landed later
// (837cc028, 2026-06-19) describing itself as "new 90s periodic
// federation auto-sync (none existed)" — the author simply hadn't seen
// the existing one. The redundancy was accidental, not load-bearing, and
// it doubled the write-race exposure against nodes.json that plan 01-01
// locked down. The deleted loop's one unique behavior — refreshing the
// live mesh peer table after a pass (#42) — is preserved at the tail of
// this loop below.
{
let data_dir = config.data_dir.clone();
let state = state_manager.clone();
// Carried over from the deleted 30-min loop (#42): push the
// names/roster learned during the pass into the live mesh peer
// table so chat contacts refresh without a restart.
let rpc = api_handler.rpc_handler().clone();
tokio::spawn(async move {
// Delay the first pass so Tor/onion publishing settles after boot.
tokio::time::sleep(Duration::from_secs(20)).await;
let mut interval = tokio::time::interval(Duration::from_secs(90));
// Carried over from the deleted loop: after a stall (suspend,
// heavy load) don't fire a burst of catch-up ticks back-to-back,
// just resume the cadence from now.
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
interval.tick().await;
// Zero federated nodes is a clean no-op: nothing is read
// further, nothing is written, and no sync error is recorded
// against anyone. Same for a failed load — we simply have no
// roster to act on this tick.
let nodes = match crate::federation::load_nodes(&data_dir).await {
Ok(n) if !n.is_empty() => n,
_ => continue,
Ok(n) if n.is_empty() => continue,
Ok(n) => n,
Err(e) => {
debug!(error = %e, "federation auto-sync: node load failed");
continue;
}
};
let (snap, _) = state.get_snapshot().await;
let local_did =
@@ -610,6 +638,12 @@ impl Server {
total = nodes.len(),
"federation auto-sync pass complete"
);
// After syncing every peer, push the names/roster just
// learned (into nodes.json) into the live mesh peer table
// so chat contacts refresh without a restart (#42). Moved
// here from the deleted 30-min loop — this is the behavior
// that loop uniquely carried.
rpc.refresh_federation_mesh_peers().await;
}
});
}
@@ -861,73 +895,18 @@ impl Server {
});
}
// Periodic federation state sync — every 30 min we call
// federation::sync_with_peer on each Trusted peer. Without this
// users had to manually click Sync for `fips_npub`/transport
// badge/state updates to propagate; now it happens in the
// background. Staggers peers with a 5s delay so we don't thunder
// the Tor SOCKS proxy. Sync itself already prefers FIPS.
{
let data_dir = config.data_dir.clone();
let state = state_manager.clone();
let rpc = api_handler.rpc_handler().clone();
tokio::spawn(async move {
// First run 60s after boot to let onboarding settle.
tokio::time::sleep(Duration::from_secs(60)).await;
let mut interval = tokio::time::interval(Duration::from_secs(1800));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
interval.tick().await;
let Ok(nodes) = crate::federation::load_nodes(&data_dir).await else {
continue;
};
if nodes.is_empty() {
continue;
}
let (data, _) = state.get_snapshot().await;
let Ok(local_did) =
crate::identity::did_key_from_pubkey_hex(&data.server_info.pubkey)
else {
continue;
};
let identity_dir = data_dir.join("identity");
let Ok(node_identity) =
crate::identity::NodeIdentity::load_or_create(&identity_dir).await
else {
continue;
};
for node in &nodes {
if node.trust_level == crate::federation::TrustLevel::Untrusted {
continue;
}
match crate::federation::sync_with_peer(
&data_dir,
node,
&local_did,
|bytes| node_identity.sign(bytes),
)
.await
{
Ok(_) => debug!(
"Periodic federation sync ok: {}",
node.did.chars().take(20).collect::<String>()
),
Err(e) => debug!(
"Periodic federation sync with {}: {}",
node.did.chars().take(20).collect::<String>(),
e
),
}
tokio::time::sleep(Duration::from_secs(5)).await;
}
// After syncing every peer, push the names/roster just
// learned (into nodes.json) into the live mesh peer table
// so chat contacts refresh without a restart (#42).
rpc.refresh_federation_mesh_peers().await;
}
});
}
// (FED-02) The redundant second periodic federation sync loop that used
// to live here — 30-minute cadence, otherwise a near-duplicate of the
// 90s loop above — has been deleted. See that loop's comment for the
// git-history evidence that its overlap was accidental. Its one unique
// behavior, `rpc.refresh_federation_mesh_peers()` after a completed
// pass (#42), now runs at the tail of the surviving loop.
//
// Not carried over: the deleted loop's 5s per-peer stagger. Its stated
// reason was "don't thunder the Tor SOCKS proxy with concurrent
// connects", but both loops iterate peers sequentially and await each
// sync, so there were never concurrent connects to stagger. Re-adding
// it would only push a multi-peer pass past the 90s cadence.
// Container health monitoring — auto-restart unhealthy containers
// Respects webhook config: skips when disabled or ContainerCrash not subscribed