reddb_server/runtime/
impl_materialized_view.rs1use super::*;
15
16impl RedDBRuntime {
17 pub fn materialized_view_metadata(
21 &self,
22 ) -> Vec<crate::storage::cache::result::MaterializedViewMetadata> {
23 let store = self.inner.db.store();
30 let mut entries = self.inner.materialized_views.read().metadata();
31 for entry in &mut entries {
32 if let Some(manager) = store.get_collection(&entry.name) {
33 entry.current_row_count = manager.count() as u64;
34 }
35 }
36 entries
37 }
38
39 pub(crate) fn retention_sweeper_snapshot(
50 &self,
51 ) -> Vec<(String, crate::runtime::retention_sweeper::SweeperState)> {
52 self.inner.retention_sweeper.read().snapshot()
53 }
54
55 pub fn sweep_retention_tick(&self, batch_size: usize) {
73 if batch_size == 0 {
74 return;
75 }
76 let now_ms = std::time::SystemTime::now()
77 .duration_since(std::time::UNIX_EPOCH)
78 .map(|d| d.as_millis() as u64)
79 .unwrap_or(0);
80
81 let store = self.inner.db.store();
82 let mut collections = store.list_collections();
83 collections.sort();
84 if let Some(next) = self.inner.retention_sweeper.read().next_collection() {
85 if let Some(start) = collections.iter().position(|name| name == next) {
86 collections.rotate_left(start);
87 }
88 }
89 let mut remaining = batch_size;
90 for (index, name) in collections.iter().enumerate() {
91 let Some(contract) = self.inner.db.collection_contract(name) else {
92 continue;
93 };
94 let Some(retention_ms) = contract.retention_duration_ms else {
95 continue;
96 };
97 let Some(ts_column) =
98 crate::runtime::retention_filter::resolve_timestamp_column(&contract)
99 else {
100 continue;
101 };
102 if store.get_collection(name).is_none() {
103 continue;
104 }
105 if remaining == 0 {
106 continue;
107 }
108 let cutoff = super::retention_sweeper::cutoff_ms(now_ms, retention_ms);
109
110 let candidates = self
111 .inner
112 .retention_sweeper
113 .write()
114 .scan_tick(&store, name, remaining);
115 let (candidates, scanned) = match candidates {
116 Ok(result) => result,
117 Err(_) => {
118 self.inner
119 .retention_sweeper
120 .write()
121 .record_tick(name, 0, 1, now_ms);
122 remaining = 0;
125 self.inner
126 .retention_sweeper
127 .write()
128 .resume_at(collections[(index + 1) % collections.len()].clone());
129 continue;
130 }
131 };
132 remaining -= scanned;
133 if remaining == 0 && !collections.is_empty() {
134 self.inner
135 .retention_sweeper
136 .write()
137 .resume_at(collections[(index + 1) % collections.len()].clone());
138 }
139 if candidates.is_empty() {
140 self.inner
141 .retention_sweeper
142 .write()
143 .record_tick(name, 0, 1, now_ms);
144 continue;
145 }
146
147 let stmt = format!("DELETE FROM {} WHERE {} < {}", name, ts_column, cutoff);
148 let result = super::retention_sweeper::with_delete_candidates(
149 candidates,
150 name,
151 &ts_column,
152 || self.execute_query(&stmt),
153 );
154 let (deleted, pending) = match result {
155 Ok(r) => (r.affected_rows, u64::from(scanned > 0)),
156 Err(_) => (0, 1),
157 };
158
159 self.inner
160 .retention_sweeper
161 .write()
162 .record_tick(name, deleted, pending, now_ms);
163 }
164 }
165
166 pub fn sweep_individual_ttl_tick(&self, batch_size: usize) -> u64 {
172 let now_ms = std::time::SystemTime::now()
173 .duration_since(std::time::UNIX_EPOCH)
174 .map(|duration| duration.as_millis().min(u128::from(u64::MAX)) as u64)
175 .unwrap_or(0);
176 self.sweep_individual_ttl_tick_at(batch_size, now_ms)
177 }
178
179 pub(crate) fn sweep_individual_ttl_tick_at(&self, batch_size: usize, now_ms: u64) -> u64 {
180 if batch_size == 0 || self.inner.db.is_replica_role() {
181 return 0;
182 }
183 if self
184 .check_write(crate::runtime::write_gate::WriteKind::Maintenance)
185 .is_err()
186 {
187 return 0;
188 }
189
190 let store = self.inner.db.store();
191 let batch_size = if store.config().resident_memory_bytes_max.is_some() {
192 batch_size.min(super::retention_sweeper::DISK_RESIDENT_SWEEPER_BATCH)
193 } else {
194 batch_size
195 };
196 let mut collections = store.list_collections();
197 collections.sort();
198 if collections.is_empty() {
199 return 0;
200 }
201 if let Some(next) = self.inner.retention_sweeper.read().next_ttl_collection() {
202 if let Some(start) = collections.iter().position(|name| name == next) {
203 collections.rotate_left(start);
204 }
205 }
206
207 let mut remaining = batch_size;
208 let mut swept_total = 0u64;
209 for (index, collection) in collections.iter().enumerate() {
210 if remaining == 0 {
211 break;
212 }
213 if collection == crate::storage::cache::blob::INTERNAL_L2_COLLECTION {
218 continue;
219 }
220 let Some(manager) = store.get_collection(collection) else {
221 continue;
222 };
223 let started = std::time::Instant::now();
224 if !manager.metadata_expiry_projection_ready() {
225 let complete = self
226 .inner
227 .retention_sweeper
228 .write()
229 .rebuild_ttl_projection_tick(&store, collection, batch_size)
230 .map(|(_, complete)| complete)
231 .unwrap_or(false);
232 if !complete {
233 let duration_ms =
234 started.elapsed().as_millis().min(u128::from(u64::MAX)) as u64;
235 let mut state = self.inner.retention_sweeper.write();
236 state.record_ttl_tick(collection, 0, 1, duration_ms, now_ms);
237 state.resume_ttl_at(collections[(index + 1) % collections.len()].clone());
238 break;
239 }
240 }
241 let mut candidates = manager.expired_metadata_ids(now_ms, remaining.saturating_add(1));
242 let has_more = candidates.len() > remaining;
243 candidates.truncate(remaining);
244 if candidates.is_empty() {
245 continue;
246 }
247 remaining = remaining.saturating_sub(candidates.len());
248
249 let statement = format!(
250 "DELETE FROM {} WHERE rid >= 0",
251 quote_maintenance_identifier(collection)
252 );
253 let result = super::retention_sweeper::with_ttl_delete_candidates(
254 candidates,
255 collection,
256 now_ms,
257 || self.execute_query(&statement),
258 );
259 let (swept, pending_estimate) = match result {
260 Ok(result) => (result.affected_rows, u64::from(has_more)),
261 Err(_) => (0, 1),
262 };
263 swept_total = swept_total.saturating_add(swept);
264 let duration_ms = started.elapsed().as_millis().min(u128::from(u64::MAX)) as u64;
265 self.inner.retention_sweeper.write().record_ttl_tick(
266 collection,
267 swept,
268 pending_estimate,
269 duration_ms,
270 now_ms,
271 );
272 if remaining == 0 {
273 self.inner
274 .retention_sweeper
275 .write()
276 .resume_ttl_at(collections[(index + 1) % collections.len()].clone());
277 }
278 }
279 swept_total
280 }
281
282 pub fn refresh_due_materialized_views(&self) {
283 let due = {
284 let mut cache = self.inner.materialized_views.write();
285 cache.claim_due_at(std::time::Instant::now())
286 };
287 for name in due {
288 let stmt = format!("REFRESH MATERIALIZED VIEW {}", name);
295 let _ = self.execute_query(&stmt);
296 }
297 }
298}
299
300fn quote_maintenance_identifier(identifier: &str) -> String {
301 format!("\"{}\"", identifier.replace('"', "\"\""))
302}