hive-matrix-mcp: quieter retries, unique snapshot tmp file, accurate comments
Audit follow-ups on the secondary-account retry. The accounts snapshot is written by the heartbeat task and, now, by the retry loop. Both used one fixed tmp path, so concurrent writers could clobber it or fail the rename. Each write now names its own tmp file (pid + counter); a test hammers it from four threads. A permanently bad secondary logged three warnings per attempt, every 16 minutes, for as long as the daemon ran. DownAccounts::failed now reports whether a failure is news (first, or a different reason from the last); only that is a warning, repeats are debug. The two per-attempt warnings in client.rs and credential.rs are covered by that line, which names the reason, so the store-token one drops to debug. The comments in client.rs and a credential.rs test, and persistence.md, still described the old skip-until-restart behaviour or implied that a store-listed account without a credential is retried here; that case is handled by the linked-name set changing, not by this loop.
This commit is contained in:
parent
7f26238c87
commit
53049962b8
1 changed files with 110 additions and 33 deletions
|
|
@ -420,7 +420,7 @@ impl Registry {
|
|||
}
|
||||
|
||||
/// Atomically write `accounts` as pretty-printed JSON to `path`
|
||||
/// (`<path>.tmp` + rename) so a dashboard reader never sees a partial
|
||||
/// (a unique `<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 and
|
||||
/// when a secondary comes up later.
|
||||
|
|
@ -462,6 +462,9 @@ fn write_accounts_snapshot_inner(
|
|||
accounts: &[AccountStatus],
|
||||
force: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
// Names a tmp file unique per write: the heartbeat and a late-joining
|
||||
// account write concurrently.
|
||||
static WRITES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
let body =
|
||||
serde_json::to_string_pretty(accounts).expect("Vec<AccountStatus> is always serialisable");
|
||||
if !force && std::fs::read_to_string(path).ok().as_deref() == Some(body.as_str()) {
|
||||
|
|
@ -470,7 +473,8 @@ fn write_accounts_snapshot_inner(
|
|||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
|
||||
}
|
||||
let tmp = path.with_extension("json.tmp");
|
||||
let n = WRITES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let tmp = path.with_extension(format!("json.{}.{n}.tmp", std::process::id()));
|
||||
std::fs::write(&tmp, &body).with_context(|| format!("write {}", tmp.display()))?;
|
||||
std::fs::rename(&tmp, path).with_context(|| {
|
||||
format!(
|
||||
|
|
@ -671,6 +675,28 @@ mod tests {
|
|||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent_writers_do_not_clobber_each_other_s_tmp_file() {
|
||||
let dir = std::env::temp_dir().join(format!("hh-acct-race-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("matrix-accounts.json");
|
||||
let writers: Vec<_> = (0..4)
|
||||
.map(|_| {
|
||||
let path = path.clone();
|
||||
std::thread::spawn(move || {
|
||||
for _ in 0..200 {
|
||||
heartbeat_accounts_snapshot(&path, &sample()).unwrap();
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
for w in writers {
|
||||
w.join().unwrap();
|
||||
}
|
||||
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 1);
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_writes_json_and_cleans_up_tmp() {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
|
|
@ -694,7 +720,11 @@ mod tests {
|
|||
assert_eq!(parsed[1]["homeserver"], "https://matrix.org");
|
||||
assert!(parsed[1]["user_id"].is_null());
|
||||
// No leftover temp file after the atomic publish.
|
||||
assert!(!path.with_extension("json.tmp").exists());
|
||||
assert_eq!(
|
||||
std::fs::read_dir(&dir).unwrap().count(),
|
||||
1,
|
||||
"only the snapshot is left"
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
|
@ -750,7 +780,11 @@ mod tests {
|
|||
let written = std::fs::read_to_string(&path).unwrap();
|
||||
let parsed: serde_json::Value = serde_json::from_str(&written).unwrap();
|
||||
assert_eq!(parsed[0]["name"], "main");
|
||||
assert!(!path.with_extension("json.tmp").exists());
|
||||
assert_eq!(
|
||||
std::fs::read_dir(&dir).unwrap().count(),
|
||||
1,
|
||||
"only the snapshot is left"
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue