Preserve channel last_used and LEVELS overrides across compaction
Some checks failed
CI / check (push) Has been cancelled

This commit is contained in:
Jean Chevronnet 2026-07-19 04:19:22 +00:00
parent ad8a3fe065
commit 75aa8bc096
No known key found for this signature in database
2 changed files with 35 additions and 0 deletions

View file

@ -1216,12 +1216,23 @@ impl Db {
let mut snapshot: Vec<Event> = self.accounts.values().cloned().map(|a| Event::AccountRegistered(Box::new(a))).collect(); let mut snapshot: Vec<Event> = self.accounts.values().cloned().map(|a| Event::AccountRegistered(Box::new(a))).collect();
for c in self.channels.values() { for c in self.channels.values() {
snapshot.push(Event::ChannelRegistered { name: c.name.clone(), founder: c.founder.clone(), ts: c.ts }); snapshot.push(Event::ChannelRegistered { name: c.name.clone(), founder: c.founder.clone(), ts: c.ts });
// ChannelRegistered replays last_used back to the registration ts, so a
// channel active since registration would have its inactivity-expiry clock
// reset by every compaction (and could then be wrongly expired). Restore it.
if c.last_used > c.ts {
snapshot.push(Event::ChannelUsed { channel: c.name.clone(), ts: c.last_used });
}
if !c.lock_on.is_empty() || !c.lock_off.is_empty() { if !c.lock_on.is_empty() || !c.lock_off.is_empty() {
snapshot.push(Event::ChannelMlock { name: c.name.clone(), on: c.lock_on.clone(), off: c.lock_off.clone(), params: c.lock_params.clone() }); snapshot.push(Event::ChannelMlock { name: c.name.clone(), on: c.lock_on.clone(), off: c.lock_off.clone(), params: c.lock_params.clone() });
} }
for a in &c.access { for a in &c.access {
snapshot.push(Event::ChannelAccessAdd { channel: c.name.clone(), account: a.account.clone(), level: a.level.clone() }); snapshot.push(Event::ChannelAccessAdd { channel: c.name.clone(), account: a.account.clone(), level: a.level.clone() });
} }
// LEVELS overrides live only in c.levels; without this they reset to the
// tier defaults on every compaction.
for (cap, role) in &c.levels {
snapshot.push(Event::ChannelLevelSet { channel: c.name.clone(), cap: cap.clone(), role: role.clone() });
}
for k in &c.akick { for k in &c.akick {
snapshot.push(Event::ChannelAkickAdd { channel: c.name.clone(), mask: k.mask.clone(), reason: k.reason.clone() }); snapshot.push(Event::ChannelAkickAdd { channel: c.name.clone(), mask: k.mask.clone(), reason: k.reason.clone() });
} }

View file

@ -458,6 +458,30 @@
assert_eq!(db.certfps("bob"), &[kept][..], "kept cert survives"); assert_eq!(db.certfps("bob"), &[kept][..], "kept cert survives");
} }
#[test]
fn compaction_preserves_channel_last_used_and_levels() {
let p = tmp("compact-chan");
let mut db = Db::open(&p, "local");
db.scram_iterations = 4096;
db.register("alice", "pw", None).unwrap();
db.register_channel("#chan", "alice").unwrap();
let reg_ts = db.channel("#chan").unwrap().ts;
// A LEVELS override and recent activity, both held outside ChannelRegistered.
db.level_set("#chan", "SET", "AOP").unwrap();
let active = reg_ts + 100_000; // past the 1-day activity-coalesce threshold
db.mark_channel_used("#chan", active);
assert_eq!(db.channel("#chan").unwrap().last_used, active);
db.compact().unwrap();
drop(db);
let db = Db::open(&p, "local");
let c = db.channel("#chan").expect("channel survives compaction");
// Before the fix, the snapshot emitted only ChannelRegistered, so replay
// reset last_used to reg_ts (risking wrong expiry) and dropped the override.
assert_eq!(c.last_used, active, "inactivity clock survives compaction");
assert!(c.levels.iter().any(|(cap, role)| cap == "SET" && role == "AOP"), "LEVELS override survives: {:?}", c.levels);
}
// An account provisioned from a SCRAM verifier alone (external authority) // An account provisioned from a SCRAM verifier alone (external authority)
// must authenticate over the PLAIN / IDENTIFY path (db.authenticate), not // must authenticate over the PLAIN / IDENTIFY path (db.authenticate), not
// only over SCRAM — the verifier is the sole credential, so a backfilled // only over SCRAM — the verifier is the sole credential, so a backfilled