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 ) ]
2727pub struct TombstoneSet {
28- entries : HashMap < ( u64 , String ) , u64 > ,
28+ entries : HashMap < ( u64 , u64 , String ) , u64 > ,
2929}
3030
3131impl 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}
0 commit comments