db: compaction was silently dropping channel and jupe state
All checks were successful
CI / check (push) Successful in 3m52s
All checks were successful
CI / check (push) Successful in 3m52s
compact() rebuilds channels field-by-field, and it omitted several fields, so automatic log compaction (should_compact -> compact) silently deleted them: channel url, email, successor, noexpire, the restricted/noautoop/bot_greet/ nobot settings, and every jupe. Emits them all now (settings gated on != default via a new PartialEq). Adds a compaction round-trip property test (compact()+reopen == before) alongside the fold-parity one; both share the broad build/snapshot helpers.
This commit is contained in:
parent
8c55347d80
commit
289470ca0c
2 changed files with 88 additions and 47 deletions
|
|
@ -362,7 +362,7 @@ pub struct AjoinEntry {
|
||||||
|
|
||||||
// A channel's on/off options (ChanServ SET). Typed, not a bag of string flags,
|
// A channel's on/off options (ChanServ SET). Typed, not a bag of string flags,
|
||||||
// so a new option is one field and the compiler proves every place handles it.
|
// so a new option is one field and the compiler proves every place handles it.
|
||||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct ChanSettings {
|
pub struct ChanSettings {
|
||||||
// Append "(requested by <nick>)" to ChanServ KICK reasons.
|
// Append "(requested by <nick>)" to ChanServ KICK reasons.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
@ -1101,7 +1101,19 @@ impl Db {
|
||||||
if !c.entrymsg.is_empty() {
|
if !c.entrymsg.is_empty() {
|
||||||
snapshot.push(Event::ChannelEntryMsgSet { channel: c.name.clone(), msg: c.entrymsg.clone() });
|
snapshot.push(Event::ChannelEntryMsgSet { channel: c.name.clone(), msg: c.entrymsg.clone() });
|
||||||
}
|
}
|
||||||
if c.settings.signkick || c.settings.private || c.settings.peace || c.settings.secureops || c.settings.keeptopic || c.settings.topiclock {
|
if !c.url.is_empty() {
|
||||||
|
snapshot.push(Event::ChannelUrlSet { channel: c.name.clone(), url: c.url.clone() });
|
||||||
|
}
|
||||||
|
if !c.email.is_empty() {
|
||||||
|
snapshot.push(Event::ChannelEmailSet { channel: c.name.clone(), email: c.email.clone() });
|
||||||
|
}
|
||||||
|
if let Some(s) = &c.successor {
|
||||||
|
snapshot.push(Event::ChannelSuccessorSet { channel: c.name.clone(), successor: Some(s.clone()) });
|
||||||
|
}
|
||||||
|
if c.noexpire {
|
||||||
|
snapshot.push(Event::ChannelNoExpire { channel: c.name.clone(), on: true });
|
||||||
|
}
|
||||||
|
if c.settings != ChanSettings::default() {
|
||||||
snapshot.push(Event::ChannelSettingsSet { channel: c.name.clone(), settings: c.settings });
|
snapshot.push(Event::ChannelSettingsSet { channel: c.name.clone(), settings: c.settings });
|
||||||
}
|
}
|
||||||
if !c.topic.is_empty() {
|
if !c.topic.is_empty() {
|
||||||
|
|
@ -1182,6 +1194,9 @@ impl Db {
|
||||||
for e in &self.net.sess_exceptions {
|
for e in &self.net.sess_exceptions {
|
||||||
snapshot.push(Event::SessionExceptionAdded { mask: e.mask.clone(), limit: e.limit, reason: e.reason.clone() });
|
snapshot.push(Event::SessionExceptionAdded { mask: e.mask.clone(), limit: e.limit, reason: e.reason.clone() });
|
||||||
}
|
}
|
||||||
|
for j in &self.net.jupes {
|
||||||
|
snapshot.push(Event::JupeAdded { name: j.name.clone(), sid: j.sid.clone(), reason: j.reason.clone() });
|
||||||
|
}
|
||||||
self.log.compact(snapshot)?;
|
self.log.compact(snapshot)?;
|
||||||
tracing::info!(before, after = self.log.len(), "compacted event log");
|
tracing::info!(before, after = self.log.len(), "compacted event log");
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -480,10 +480,9 @@
|
||||||
// Fold parity: the live state after a broad sequence of writes must equal the
|
// Fold parity: the live state after a broad sequence of writes must equal the
|
||||||
// state rebuilt by replaying the same log — every event's manual live mutation
|
// state rebuilt by replaying the same log — every event's manual live mutation
|
||||||
// has to match its `apply()` arm, or a restart silently changes the data.
|
// has to match its `apply()` arm, or a restart silently changes the data.
|
||||||
#[test]
|
// A canonical Debug snapshot of every persisted collection, sorted so the
|
||||||
fn live_state_matches_replay_after_a_broad_sequence() {
|
// string is order-independent. Shared by the fold-parity and compaction tests.
|
||||||
let p = tmp("foldparity");
|
fn broad_snapshot(db: &Db) -> String {
|
||||||
let snapshot = |db: &Db| -> String {
|
|
||||||
let mut a: Vec<_> = db.accounts().cloned().collect();
|
let mut a: Vec<_> = db.accounts().cloned().collect();
|
||||||
a.sort_by(|x, y| x.name.cmp(&y.name));
|
a.sort_by(|x, y| x.name.cmp(&y.name));
|
||||||
let mut c: Vec<_> = db.channels().cloned().collect();
|
let mut c: Vec<_> = db.channels().cloned().collect();
|
||||||
|
|
@ -513,10 +512,10 @@
|
||||||
op.sort();
|
op.sort();
|
||||||
let vt = db.vhost_template().map(str::to_string);
|
let vt = db.vhost_template().map(str::to_string);
|
||||||
format!("{a:?}\n{c:?}\n{b:?}\n{gv:?}\n{ak:?}\n{fb:?}\n{of:?}\n{vf:?}\n{jp:?}\n{nw:?}\n{rp:?}\n{hp:?}\n{se:?}\n{op:?}\n{vt:?}")
|
format!("{a:?}\n{c:?}\n{b:?}\n{gv:?}\n{ak:?}\n{fb:?}\n{of:?}\n{vf:?}\n{jp:?}\n{nw:?}\n{rp:?}\n{hp:?}\n{se:?}\n{op:?}\n{vt:?}")
|
||||||
};
|
}
|
||||||
|
|
||||||
let live = {
|
// Exercise ~45 event types, ending with a drop that must purge references.
|
||||||
let mut db = Db::open(&p, "N1");
|
fn build_broad_state(db: &mut Db) {
|
||||||
db.scram_iterations = 4096;
|
db.scram_iterations = 4096;
|
||||||
db.register("alice", "pw", Some("a@x.io".into())).unwrap();
|
db.register("alice", "pw", Some("a@x.io".into())).unwrap();
|
||||||
db.register("bob", "pw", None).unwrap();
|
db.register("bob", "pw", None).unwrap();
|
||||||
|
|
@ -574,12 +573,39 @@
|
||||||
// and group standing — identically in the live path and on replay.
|
// and group standing — identically in the live path and on replay.
|
||||||
db.drop_account("alice").unwrap();
|
db.drop_account("alice").unwrap();
|
||||||
db.drop_channel("#other").unwrap();
|
db.drop_channel("#other").unwrap();
|
||||||
snapshot(&db)
|
}
|
||||||
|
|
||||||
|
// Fold parity: live state after the sequence == state replayed from the log.
|
||||||
|
#[test]
|
||||||
|
fn live_state_matches_replay_after_a_broad_sequence() {
|
||||||
|
let p = tmp("foldparity");
|
||||||
|
let live = {
|
||||||
|
let mut db = Db::open(&p, "N1");
|
||||||
|
build_broad_state(&mut db);
|
||||||
|
broad_snapshot(&db)
|
||||||
};
|
};
|
||||||
let replayed = snapshot(&Db::open(&p, "N1"));
|
let replayed = broad_snapshot(&Db::open(&p, "N1"));
|
||||||
assert_eq!(live, replayed, "live state must equal the state replayed from the log");
|
assert_eq!(live, replayed, "live state must equal the state replayed from the log");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compaction parity: rewriting the log to a snapshot must not lose anything —
|
||||||
|
// the state after compact()+reopen must equal the state before.
|
||||||
|
#[test]
|
||||||
|
fn compaction_preserves_all_state() {
|
||||||
|
let p = tmp("compactparity");
|
||||||
|
{
|
||||||
|
let mut db = Db::open(&p, "N1");
|
||||||
|
build_broad_state(&mut db);
|
||||||
|
}
|
||||||
|
let before = broad_snapshot(&Db::open(&p, "N1"));
|
||||||
|
{
|
||||||
|
let mut db = Db::open(&p, "N1");
|
||||||
|
db.compact().unwrap();
|
||||||
|
}
|
||||||
|
let after = broad_snapshot(&Db::open(&p, "N1"));
|
||||||
|
assert_eq!(before, after, "compaction must preserve every persisted field");
|
||||||
|
}
|
||||||
|
|
||||||
// Dropping an account erases every reference to it — channel access and
|
// Dropping an account erases every reference to it — channel access and
|
||||||
// successorship, group membership and foundership — so a later re-registration
|
// successorship, group membership and foundership — so a later re-registration
|
||||||
// of the same name can't inherit its standing. Holds live and after replay.
|
// of the same name can't inherit its standing. Holds live and after replay.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue