Skip to content

Commit 8c1d7ca

Browse files
committed
scope collection ownership and replay by database
1 parent 083cc23 commit 8c1d7ca

193 files changed

Lines changed: 2098 additions & 850 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

nodedb-wal/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ pub use record::{
5858
SpatialDeletePayload, SpatialPutPayload, WalRecord, WalRecordArgs,
5959
};
6060
pub use recovery::{RecoveryInfo, recover};
61-
pub use replay::{TombstoneSet, extract_tombstones};
61+
pub use replay::{DatabaseTombstones, TombstoneSet, extract_tombstones};
6262
pub use secure_mem::SecureKey;
6363
pub use segmented::{SegmentedWal, SegmentedWalConfig};
6464
pub use temporal_purge::{TemporalPurgeEngine, TemporalPurgePayload};

nodedb-wal/src/replay/filter.rs

Lines changed: 91 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
//! Collection-tombstone replay filter.
44
//!
5-
//! A [`TombstoneSet`] records, per `(tenant_id, collection)` pair, the
6-
//! highest `purge_lsn` observed in any `RecordType::CollectionTombstoned`
5+
//! A [`TombstoneSet`] records, per `(database_id, tenant_id, collection)` tuple,
6+
//! the highest `purge_lsn` observed in any `RecordType::CollectionTombstoned`
77
//! record. Replay consumers query [`TombstoneSet::is_tombstoned`] after
88
//! decoding the collection field from their payload; any record with
99
//! `lsn < purge_lsn` for a tombstoned pair MUST be skipped.
@@ -20,24 +20,33 @@ use crate::tombstone::CollectionTombstonePayload;
2020

2121
/// In-memory index of active collection tombstones.
2222
///
23-
/// Keyed by `(tenant_id, collection)`; value is the `purge_lsn` written
24-
/// at tombstone time. If the same pair is tombstoned more than once
23+
/// Keyed by `(database_id, tenant_id, collection)`; value is the `purge_lsn`
24+
/// written at tombstone time. If the same object is tombstoned more than once
2525
/// (re-create → re-drop in the same log), the highest `purge_lsn` wins.
2626
#[derive(Debug, Default, Clone)]
2727
pub struct TombstoneSet {
28-
entries: HashMap<(u64, String), u64>,
28+
entries: HashMap<(u64, u64, String), u64>,
2929
}
3030

3131
impl TombstoneSet {
3232
pub fn new() -> Self {
3333
Self::default()
3434
}
3535

36-
/// Record a tombstone. If the pair already has a higher `purge_lsn`,
36+
/// Bind replay checks to one database while preserving the concise
37+
/// `(tenant, collection, lsn)` predicate used inside engine replay loops.
38+
pub fn for_database(&self, database_id: u64) -> DatabaseTombstones<'_> {
39+
DatabaseTombstones {
40+
set: self,
41+
database_id,
42+
}
43+
}
44+
45+
/// Record a tombstone. If the object already has a higher `purge_lsn`,
3746
/// the existing value is kept (idempotent, order-independent).
38-
pub fn insert(&mut self, tenant_id: u64, collection: String, purge_lsn: u64) {
47+
pub fn insert(&mut self, database_id: u64, tenant_id: u64, collection: String, purge_lsn: u64) {
3948
self.entries
40-
.entry((tenant_id, collection))
49+
.entry((database_id, tenant_id, collection))
4150
.and_modify(|existing| {
4251
if purge_lsn > *existing {
4352
*existing = purge_lsn;
@@ -46,20 +55,25 @@ impl TombstoneSet {
4655
.or_insert(purge_lsn);
4756
}
4857

49-
/// Return `true` iff a write at `lsn` for `(tenant_id, collection)`
50-
/// is shadowed by a later tombstone and therefore must be skipped
51-
/// during replay.
52-
pub fn is_tombstoned(&self, tenant_id: u64, collection: &str, lsn: u64) -> bool {
58+
/// Return `true` iff a write at `lsn` for the database-scoped object
59+
/// is shadowed by a later tombstone and therefore must be skipped.
60+
pub fn is_tombstoned(
61+
&self,
62+
database_id: u64,
63+
tenant_id: u64,
64+
collection: &str,
65+
lsn: u64,
66+
) -> bool {
5367
self.entries
54-
.get(&(tenant_id, collection.to_string()))
68+
.get(&(database_id, tenant_id, collection.to_string()))
5569
.is_some_and(|&purge_lsn| lsn < purge_lsn)
5670
}
5771

58-
/// Return the `purge_lsn` for a pair, if any. Primarily used by redb
72+
/// Return the `purge_lsn` for an object, if any. Primarily used by redb
5973
/// persistence to serialize the current set after a replay pass.
60-
pub fn purge_lsn(&self, tenant_id: u64, collection: &str) -> Option<u64> {
74+
pub fn purge_lsn(&self, database_id: u64, tenant_id: u64, collection: &str) -> Option<u64> {
6175
self.entries
62-
.get(&(tenant_id, collection.to_string()))
76+
.get(&(database_id, tenant_id, collection.to_string()))
6377
.copied()
6478
}
6579

@@ -71,22 +85,43 @@ impl TombstoneSet {
7185
self.entries.is_empty()
7286
}
7387

74-
/// Iterate over every `(tenant_id, collection, purge_lsn)` in the set.
75-
pub fn iter(&self) -> impl Iterator<Item = (u64, &str, u64)> + '_ {
88+
/// Iterate over every `(database_id, tenant_id, collection, purge_lsn)`.
89+
pub fn iter(&self) -> impl Iterator<Item = (u64, u64, &str, u64)> + '_ {
7690
self.entries
7791
.iter()
78-
.map(|((tid, name), lsn)| (*tid, name.as_str(), *lsn))
92+
.map(|((db, tid, name), lsn)| (*db, *tid, name.as_str(), *lsn))
7993
}
8094

8195
/// Merge another tombstone set into this one. Used when loading
8296
/// persisted tombstones from redb at startup before a fresh WAL pass.
8397
pub fn extend(&mut self, other: TombstoneSet) {
84-
for ((tid, name), lsn) in other.entries {
85-
self.insert(tid, name, lsn);
98+
for ((db, tid, name), lsn) in other.entries {
99+
self.insert(db, tid, name, lsn);
86100
}
87101
}
88102
}
89103

104+
/// Database-bound view over a [`TombstoneSet`].
105+
pub struct DatabaseTombstones<'a> {
106+
set: &'a TombstoneSet,
107+
database_id: u64,
108+
}
109+
110+
impl DatabaseTombstones<'_> {
111+
pub fn is_tombstoned(&self, tenant_id: u64, collection: &str, lsn: u64) -> bool {
112+
self.set
113+
.is_tombstoned(self.database_id, tenant_id, collection, lsn)
114+
}
115+
}
116+
117+
impl std::ops::Deref for DatabaseTombstones<'_> {
118+
type Target = TombstoneSet;
119+
120+
fn deref(&self) -> &Self::Target {
121+
self.set
122+
}
123+
}
124+
90125
/// Single-pass extraction: walk `records`, decode every
91126
/// `CollectionTombstoned` entry, and return the resulting set.
92127
///
@@ -109,6 +144,7 @@ pub fn extract_tombstones(records: &[WalRecord]) -> TombstoneSet {
109144
// this call site changes to `decrypt_payload_ring` first.
110145
match CollectionTombstonePayload::from_bytes(&record.payload) {
111146
Ok(payload) => set.insert(
147+
record.header.database_id,
112148
record.header.tenant_id,
113149
payload.collection,
114150
payload.purge_lsn,
@@ -129,7 +165,13 @@ mod tests {
129165
use super::*;
130166
use crate::record::{WalRecord, WalRecordArgs};
131167

132-
fn tombstone_record(tenant: u64, name: &str, purge_lsn: u64, record_lsn: u64) -> WalRecord {
168+
fn tombstone_record(
169+
database: u64,
170+
tenant: u64,
171+
name: &str,
172+
purge_lsn: u64,
173+
record_lsn: u64,
174+
) -> WalRecord {
133175
let payload = CollectionTombstonePayload::new(name, purge_lsn)
134176
.to_bytes()
135177
.unwrap();
@@ -138,7 +180,7 @@ mod tests {
138180
lsn: record_lsn,
139181
tenant_id: tenant,
140182
vshard_id: 0,
141-
database_id: 0,
183+
database_id: database,
142184
payload,
143185
encryption_key: None,
144186
preamble_bytes: None,
@@ -149,42 +191,43 @@ mod tests {
149191
#[test]
150192
fn is_tombstoned_shadows_older_writes() {
151193
let mut set = TombstoneSet::new();
152-
set.insert(1, "users".into(), 100);
153-
assert!(set.is_tombstoned(1, "users", 50));
154-
assert!(!set.is_tombstoned(1, "users", 100));
155-
assert!(!set.is_tombstoned(1, "users", 200));
156-
assert!(!set.is_tombstoned(1, "other", 50));
157-
assert!(!set.is_tombstoned(2, "users", 50));
194+
set.insert(7, 1, "users".into(), 100);
195+
assert!(set.is_tombstoned(7, 1, "users", 50));
196+
assert!(!set.is_tombstoned(7, 1, "users", 100));
197+
assert!(!set.is_tombstoned(7, 1, "users", 200));
198+
assert!(!set.is_tombstoned(7, 1, "other", 50));
199+
assert!(!set.is_tombstoned(7, 2, "users", 50));
200+
assert!(!set.is_tombstoned(8, 1, "users", 50));
158201
}
159202

160203
#[test]
161204
fn insert_keeps_highest_purge_lsn() {
162205
let mut set = TombstoneSet::new();
163-
set.insert(1, "users".into(), 100);
164-
set.insert(1, "users".into(), 50);
165-
assert_eq!(set.purge_lsn(1, "users"), Some(100));
166-
set.insert(1, "users".into(), 200);
167-
assert_eq!(set.purge_lsn(1, "users"), Some(200));
206+
set.insert(7, 1, "users".into(), 100);
207+
set.insert(7, 1, "users".into(), 50);
208+
assert_eq!(set.purge_lsn(7, 1, "users"), Some(100));
209+
set.insert(7, 1, "users".into(), 200);
210+
assert_eq!(set.purge_lsn(7, 1, "users"), Some(200));
168211
}
169212

170213
#[test]
171214
fn extract_from_record_stream() {
172215
let records = vec![
173-
tombstone_record(1, "users", 100, 10),
174-
tombstone_record(1, "orders", 150, 11),
175-
tombstone_record(2, "users", 200, 12),
216+
tombstone_record(7, 1, "users", 100, 10),
217+
tombstone_record(7, 1, "orders", 150, 11),
218+
tombstone_record(8, 1, "users", 200, 12),
176219
];
177220
let set = extract_tombstones(&records);
178221
assert_eq!(set.len(), 3);
179-
assert_eq!(set.purge_lsn(1, "users"), Some(100));
180-
assert_eq!(set.purge_lsn(1, "orders"), Some(150));
181-
assert_eq!(set.purge_lsn(2, "users"), Some(200));
222+
assert_eq!(set.purge_lsn(7, 1, "users"), Some(100));
223+
assert_eq!(set.purge_lsn(7, 1, "orders"), Some(150));
224+
assert_eq!(set.purge_lsn(8, 1, "users"), Some(200));
182225
}
183226

184227
#[test]
185228
fn extract_ignores_non_tombstone_records() {
186229
let records = vec![
187-
tombstone_record(1, "users", 100, 10),
230+
tombstone_record(0, 1, "users", 100, 10),
188231
WalRecord::new(WalRecordArgs {
189232
record_type: RecordType::Put as u32,
190233
lsn: 11,
@@ -226,25 +269,25 @@ mod tests {
226269
preamble_bytes: None,
227270
})
228271
.unwrap();
229-
let records = vec![bogus, tombstone_record(1, "users", 100, 10)];
272+
let records = vec![bogus, tombstone_record(0, 1, "users", 100, 10)];
230273
let set = extract_tombstones(&records);
231274
assert_eq!(
232275
set.len(),
233276
1,
234277
"valid tombstone still captured despite peer corruption"
235278
);
236-
assert_eq!(set.purge_lsn(1, "users"), Some(100));
279+
assert_eq!(set.purge_lsn(0, 1, "users"), Some(100));
237280
}
238281

239282
#[test]
240283
fn extend_merges_sets() {
241284
let mut a = TombstoneSet::new();
242-
a.insert(1, "users".into(), 100);
285+
a.insert(7, 1, "users".into(), 100);
243286
let mut b = TombstoneSet::new();
244-
b.insert(1, "users".into(), 150);
245-
b.insert(1, "orders".into(), 200);
287+
b.insert(7, 1, "users".into(), 150);
288+
b.insert(8, 1, "orders".into(), 200);
246289
a.extend(b);
247-
assert_eq!(a.purge_lsn(1, "users"), Some(150));
248-
assert_eq!(a.purge_lsn(1, "orders"), Some(200));
290+
assert_eq!(a.purge_lsn(7, 1, "users"), Some(150));
291+
assert_eq!(a.purge_lsn(8, 1, "orders"), Some(200));
249292
}
250293
}

nodedb-wal/src/replay/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@
99
1010
pub mod filter;
1111

12-
pub use filter::{TombstoneSet, extract_tombstones};
12+
pub use filter::{DatabaseTombstones, TombstoneSet, extract_tombstones};

nodedb-wal/tests/wal_collection_tombstone.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -98,24 +98,24 @@ fn extract_and_shadow_writes_before_purge_lsn() {
9898
let set: TombstoneSet = extract_tombstones(&records);
9999

100100
assert_eq!(set.len(), 1);
101-
assert_eq!(set.purge_lsn(1, "users"), Some(purge_lsn));
101+
assert_eq!(set.purge_lsn(0, 1, "users"), Some(purge_lsn));
102102

103103
for lsn in &put_lsns {
104104
assert!(
105-
set.is_tombstoned(1, "users", *lsn),
105+
set.is_tombstoned(0, 1, "users", *lsn),
106106
"pre-purge write at lsn {lsn} must be shadowed"
107107
);
108108
}
109109
assert!(
110-
!set.is_tombstoned(1, "orders", orders_lsn),
110+
!set.is_tombstoned(0, 1, "orders", orders_lsn),
111111
"different collection must not be shadowed"
112112
);
113113
assert!(
114-
!set.is_tombstoned(2, "users", other_tenant_lsn),
114+
!set.is_tombstoned(0, 2, "users", other_tenant_lsn),
115115
"different tenant must not be shadowed"
116116
);
117117
assert!(
118-
!set.is_tombstoned(1, "users", post_purge_lsn),
118+
!set.is_tombstoned(0, 1, "users", post_purge_lsn),
119119
"post-purge write (lsn >= purge_lsn) must not be shadowed"
120120
);
121121
}
@@ -160,9 +160,9 @@ fn multiple_tombstones_keep_highest_purge_lsn() {
160160

161161
let records = read_all(&path);
162162
let set = extract_tombstones(&records);
163-
assert_eq!(set.purge_lsn(1, "users"), Some(500));
164-
assert!(set.is_tombstoned(1, "users", 499));
165-
assert!(!set.is_tombstoned(1, "users", 500));
163+
assert_eq!(set.purge_lsn(0, 1, "users"), Some(500));
164+
assert!(set.is_tombstoned(0, 1, "users", 499));
165+
assert!(!set.is_tombstoned(0, 1, "users", 500));
166166
}
167167

168168
#[test]

0 commit comments

Comments
 (0)