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:
parent
2fb6b39f9f
commit
7f26238c87
7 changed files with 335 additions and 100 deletions
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue