Watch
0
0
Fork
You've already forked hyperhive
0

hive-matrix-mcp: retry a secondary account that failed to come up

The daemon's two-minute refresh compared the store's linked account
names with the names it booted with. A secondary that was named but did
not come up - no token at bring-up, a token the homeserver rejected
(M_UNKNOWN_TOKEN), or a homeserver still down after the ~52 s boot
retries - kept its name in the set, so nothing ever noticed it again:
it stayed down until the daemon restarted for some other reason, and
an operator re-linking a rejected account changed no name.

Each such secondary is now tried again on the same two-minute period,
one per tick, after one refresh and then doubling waits capped at
sixteen minutes. One that comes up joins the registry (now behind a
lock) and its sync loop joins the running set; the primary and the
other accounts are not restarted. The store is still only read.

The startup todo wipe moves out of bring_up_account to run once before
any sync loop: run per account, a late secondary would have cleared the
todos the running accounts' sweeps had already deduped.

Part of #5017 (slice M).
This commit is contained in:
atlas 2026-10-13 20:57:12 +02:00 • committed by mara
commit 7f26238c87
7 changed files with 335 additions and 100 deletions

View file

@ -26,7 +26,7 @@
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::{Arc, RwLock};
use anyhow::Context as _;
use matrix_sdk::Client;
@ -276,11 +276,11 @@ pub struct AccountStatus {
}
/// Account name → live `Client` map plus the primary-account name used
/// when a request omits `account`. Built once at daemon startup from
/// the accounts that successfully restored a session.
/// when a request omits `account`. Built at daemon startup from the
/// accounts that restored a session; a secondary that restores later joins it.
pub struct Registry {
primary: String,
by_name: HashMap<String, Arc<Client>>,
by_name: RwLock<HashMap<String, Arc<Client>>>,
}
/// Pick which account name a request resolves to, or an error. Pure (no
@ -314,19 +314,29 @@ impl Registry {
pub fn new(primary: String) -> Self {
Self {
primary,
by_name: HashMap::new(),
by_name: RwLock::default(),
}
}
/// Register a restored client under `name`.
pub fn insert(&mut self, name: String, client: Client) {
self.by_name.insert(name, Arc::new(client));
pub fn insert(&self, name: String, client: Client) {
self.by_name
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(name, Arc::new(client));
}
/// The restored clients by account name.
fn clients(&self) -> std::sync::RwLockReadGuard<'_, HashMap<String, Arc<Client>>> {
self.by_name
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
/// Whether any account restored successfully.
#[must_use]
pub fn is_empty(&self) -> bool {
self.by_name.is_empty()
self.clients().is_empty()
}
/// Snapshot every restored account: name, homeserver, user id, and
@ -338,7 +348,7 @@ impl Registry {
#[must_use]
pub fn list(&self) -> Vec<AccountStatus> {
let mut out: Vec<AccountStatus> = self
.by_name
.clients()
.iter()
.map(|(name, client)| AccountStatus {
name: name.clone(),
@ -370,11 +380,12 @@ impl Registry {
/// configured, or when the named account (or the primary, if `None`)
/// has no live client — e.g. an unknown name, or the primary failed to
/// restore at startup.
pub fn resolve(&self, account: Option<&str>) -> Result<&Arc<Client>, String> {
let mut names: Vec<&str> = self.by_name.keys().map(String::as_str).collect();
pub fn resolve(&self, account: Option<&str>) -> Result<Arc<Client>, String> {
let clients = self.clients();
let mut names: Vec<&str> = clients.keys().map(String::as_str).collect();
names.sort_unstable();
let name = pick_name(account, &self.primary, &names)?;
self.by_name.get(name).ok_or_else(|| {
clients.get(name).cloned().ok_or_else(|| {
format!(
"unknown matrix account {name:?}; available accounts: [{}]",
names.join(", ")
@ -385,7 +396,8 @@ impl Registry {
/// Publish the live-account snapshot ([`Registry::list`]) to `path`.
/// Thin wrapper over [`write_accounts_snapshot`] (split out so the
/// atomic-write logic is unit-testable without a live `Client`).
/// Called once at daemon startup after all restores.
/// Called at daemon startup after all restores, and again each time a
/// secondary comes up later.
///
/// # Errors
/// Propagates filesystem errors from the atomic write.
@ -410,11 +422,12 @@ impl Registry {
/// Atomically write `accounts` as pretty-printed JSON to `path`
/// (`<path>.tmp` + rename) so a dashboard reader never sees a partial
/// file. Idempotent — skips the rewrite when the on-disk content already
/// matches, keeping the mtime stable. Used for the boot-time publish.
/// matches, keeping the mtime stable. Used for the boot-time publish and
/// when a secondary comes up later.
///
/// The daemon rebuilds this file fresh on every boot, and restarts itself
/// when the store's linked accounts change, so the file lists exactly the
/// accounts that restored at the last start. Real-time liveness is conveyed by the file mtime,
/// accounts that have restored since the last start. Real-time liveness is conveyed by the file mtime,
/// which the daemon advances on a timer via
/// [`heartbeat_accounts_snapshot`] — a stalled mtime means the daemon is
/// down, so a reader can treat an old snapshot as stale.