diff --git a/core/archipelago/src/server.rs b/core/archipelago/src/server.rs index d6f215e5..ad887e7e 100644 --- a/core/archipelago/src/server.rs +++ b/core/archipelago/src/server.rs @@ -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::() - ), - Err(e) => debug!( - "Periodic federation sync with {}: {}", - node.did.chars().take(20).collect::(), - 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