sui_rpc_store/schema/
pruning_watermark.rs1use std::sync::atomic::Ordering;
18
19use sui_consistent_store::Protobuf;
20use sui_consistent_store::error::Error;
21use sui_consistent_store::reader::Reader;
22
23use crate::proto::PruningWatermarks;
24use crate::schema::primitives::UnitKey;
25
26pub const NAME: &str = "pruning_watermark";
27
28pub type Key = UnitKey;
29pub type Value = Protobuf<PruningWatermarks>;
30
31pub fn options(resolver: &sui_consistent_store::CfOptionsResolver) -> rocksdb::Options {
32 resolver.options(NAME)
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
45pub struct Watermarks {
46 pub tx_seq_lo: u64,
47 pub checkpoint_lo: u64,
48}
49
50pub fn store(watermarks: &Watermarks) -> (Key, Value) {
53 (
54 UnitKey,
55 Protobuf(PruningWatermarks {
56 tx_seq_lo: watermarks.tx_seq_lo,
57 checkpoint_lo: watermarks.checkpoint_lo,
58 }),
59 )
60}
61
62impl<R: Reader> super::RpcStoreSchema<R> {
63 pub fn get_pruning_watermarks(&self) -> Result<Option<Watermarks>, Error> {
65 let Some(stored) = self.pruning_watermark.get(&UnitKey)? else {
66 return Ok(None);
67 };
68 let stored = stored.into_inner();
69 Ok(Some(Watermarks {
70 tx_seq_lo: stored.tx_seq_lo,
71 checkpoint_lo: stored.checkpoint_lo,
72 }))
73 }
74}
75
76impl super::RpcStoreSchema {
77 pub fn set_pruning_floor(&self, tx_seq_lo: u64) {
85 self.tx_seq_pruning_floor
86 .store(tx_seq_lo, Ordering::Relaxed);
87 }
88
89 #[cfg(test)]
90 pub(crate) fn current_pruning_floor(&self) -> u64 {
91 self.tx_seq_pruning_floor.load(Ordering::Relaxed)
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use sui_consistent_store::Db;
98 use sui_consistent_store::DbOptions;
99
100 use super::*;
101 use crate::RpcStoreSchema;
102 use crate::schema::event_bitmap;
103 use crate::schema::transaction_bitmap;
104
105 fn fresh_db() -> (tempfile::TempDir, Db, RpcStoreSchema) {
106 let dir = tempfile::tempdir().unwrap();
107 let (db, schema) = Db::open::<RpcStoreSchema>(dir.path(), DbOptions::default()).unwrap();
108 (dir, db, schema)
109 }
110
111 fn put_materialized_bucket_zero(db: &Db, schema: &RpcStoreSchema, dimension: &[u8]) {
112 let (tx_key, tx_value) = transaction_bitmap::store_match(dimension.to_vec(), 5);
113 let (event_key, event_value) = event_bitmap::store_match(dimension.to_vec(), 5, 0);
114 let mut batch = db.batch();
115 batch
116 .put(&schema.transaction_bitmap, &tx_key, &tx_value)
117 .unwrap();
118 batch
119 .put(&schema.event_bitmap, &event_key, &event_value)
120 .unwrap();
121 batch.commit().unwrap();
122 db.flush().unwrap();
123 }
124
125 #[test]
126 fn fresh_empty_db_starts_with_zero_pruning_floor() {
127 let (_dir, _db, schema) = fresh_db();
128 assert!(schema.get_pruning_watermarks().unwrap().is_none());
129 assert_eq!(schema.current_pruning_floor(), 0);
130 }
131
132 #[test]
133 fn reopen_loads_persisted_pruning_floor() {
134 let dir = tempfile::tempdir().unwrap();
135 let (db, schema) = Db::open::<RpcStoreSchema>(dir.path(), DbOptions::default()).unwrap();
136 let floor = transaction_bitmap::TX_BUCKET_SIZE;
137 let (watermark_key, watermark_value) = store(&Watermarks {
138 tx_seq_lo: floor,
139 checkpoint_lo: 1,
140 });
141 let mut batch = db.batch();
142 batch
143 .put(&schema.pruning_watermark, &watermark_key, &watermark_value)
144 .unwrap();
145 batch.commit().unwrap();
146 put_materialized_bucket_zero(&db, &schema, b"reopen");
147 assert_eq!(schema.current_pruning_floor(), 0);
148
149 drop(schema);
150 drop(db);
151
152 let (db, schema) = Db::open::<RpcStoreSchema>(dir.path(), DbOptions::default()).unwrap();
153 assert_eq!(schema.current_pruning_floor(), floor);
154 db.compact_range_cf(transaction_bitmap::NAME, None, None)
155 .unwrap();
156 db.compact_range_cf(event_bitmap::NAME, None, None).unwrap();
157 assert!(
158 schema
159 .get_transaction_bitmap(b"reopen".to_vec(), 0)
160 .unwrap()
161 .is_none()
162 );
163 assert!(
164 schema
165 .get_event_bitmap(b"reopen".to_vec(), 0)
166 .unwrap()
167 .is_none()
168 );
169 }
170
171 #[test]
172 fn bitmap_pruning_floors_are_isolated_per_database() {
173 let (_dir_a, db_a, schema_a) = fresh_db();
174 let (_dir_b, db_b, schema_b) = fresh_db();
175 let floor = transaction_bitmap::TX_BUCKET_SIZE;
176
177 let (watermark_key, watermark_value) = store(&Watermarks {
178 tx_seq_lo: floor,
179 checkpoint_lo: 1,
180 });
181 let mut batch = db_a.batch();
182 batch
183 .put(
184 &schema_a.pruning_watermark,
185 &watermark_key,
186 &watermark_value,
187 )
188 .unwrap();
189 batch.commit().unwrap();
190 schema_a.set_pruning_floor(floor);
191
192 put_materialized_bucket_zero(&db_a, &schema_a, b"isolated");
193 put_materialized_bucket_zero(&db_b, &schema_b, b"isolated");
194
195 for db in [&db_a, &db_b] {
196 db.compact_range_cf(transaction_bitmap::NAME, None, None)
197 .unwrap();
198 db.compact_range_cf(event_bitmap::NAME, None, None).unwrap();
199 }
200
201 assert!(
202 schema_a
203 .get_transaction_bitmap(b"isolated".to_vec(), 0)
204 .unwrap()
205 .is_none()
206 );
207 assert!(
208 schema_a
209 .get_event_bitmap(b"isolated".to_vec(), 0)
210 .unwrap()
211 .is_none()
212 );
213 assert!(
214 schema_b
215 .get_transaction_bitmap(b"isolated".to_vec(), 0)
216 .unwrap()
217 .is_some()
218 );
219 assert!(
220 schema_b
221 .get_event_bitmap(b"isolated".to_vec(), 0)
222 .unwrap()
223 .is_some()
224 );
225 assert!(schema_b.get_pruning_watermarks().unwrap().is_none());
226 assert_eq!(schema_b.current_pruning_floor(), 0);
227 }
228}