Skip to main content

reddb_server/runtime/
impl_cdc.rs

1use super::*;
2use crate::application::entity::metadata_to_json;
3use crate::replication::cdc::{server_json_to_wire_json, ChangeRecord};
4use crate::runtime::impl_core::current_connection_id;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7impl RedDBRuntime {
8    fn latest_metadata_for(
9        &self,
10        collection: &str,
11        entity_id: u64,
12    ) -> Option<reddb_wire::replication::ChangeRecordJsonValue> {
13        self.inner
14            .db
15            .store()
16            .get_metadata(collection, EntityId::new(entity_id))
17            .map(|metadata| server_json_to_wire_json(metadata_to_json(&metadata)))
18    }
19
20    /// Emit a CDC record without invalidating the result cache.
21    ///
22    /// Used by `MutationEngine::append_batch` which calls
23    /// `invalidate_result_cache` once for the whole batch before this
24    /// loop, avoiding N write-lock acquisitions.
25    pub(crate) fn cdc_emit_no_cache_invalidate(
26        &self,
27        operation: crate::replication::cdc::ChangeOperation,
28        collection: &str,
29        entity_id: u64,
30        entity_kind: &str,
31    ) -> RedDBResult<u64> {
32        let lsn = self
33            .inner
34            .cdc
35            .emit(operation, collection, entity_id, entity_kind);
36
37        // Append to logical WAL replication buffer (if primary mode)
38        if self.inner.db.replication.is_some() {
39            let store = self.inner.db.store();
40            let entity = if operation == crate::replication::cdc::ChangeOperation::Delete {
41                None
42            } else {
43                store.get(collection, EntityId::new(entity_id))
44            };
45            let record = ChangeRecord {
46                term: self.current_replication_term(),
47                lsn,
48                timestamp: SystemTime::now()
49                    .duration_since(UNIX_EPOCH)
50                    .unwrap_or_default()
51                    .as_millis() as u64,
52                operation,
53                collection: collection.to_string(),
54                entity_id,
55                entity_kind: entity_kind.to_string(),
56                entity_bytes: entity
57                    .as_ref()
58                    .map(|e| UnifiedStore::serialize_entity(e, store.format_version())),
59                metadata: self.latest_metadata_for(collection, entity_id),
60                refresh_records: None,
61                range_id: None,
62                ownership_epoch: None,
63            };
64            self.stage_or_publish_logical_record(record)?;
65        }
66        Ok(lsn)
67    }
68
69    pub(crate) fn cdc_emit_insert_batch_no_cache_invalidate(
70        &self,
71        collection: &str,
72        ids: &[EntityId],
73        entity_kind: &str,
74    ) -> RedDBResult<Vec<u64>> {
75        if ids.is_empty() {
76            return Ok(Vec::new());
77        }
78
79        // Without logical replication, CDC only needs the in-memory event
80        // ring. Reserve all LSNs and push the batch under one mutex instead
81        // of taking the ring lock once per inserted row.
82        if self.inner.db.replication.is_none() {
83            return Ok(self.inner.cdc.emit_batch_same_collection(
84                crate::replication::cdc::ChangeOperation::Insert,
85                collection,
86                entity_kind,
87                ids.iter().map(|id| id.raw()),
88            ));
89        }
90
91        // Replication needs one logical-WAL record per entity with the
92        // serialized entity bytes, so keep the existing per-row path.
93        ids.iter()
94            .map(|id| {
95                self.cdc_emit_no_cache_invalidate(
96                    crate::replication::cdc::ChangeOperation::Insert,
97                    collection,
98                    id.raw(),
99                    entity_kind,
100                )
101            })
102            .collect()
103    }
104
105    pub fn cdc_emit(
106        &self,
107        operation: crate::replication::cdc::ChangeOperation,
108        collection: &str,
109        entity_id: u64,
110        entity_kind: &str,
111    ) -> RedDBResult<u64> {
112        let lsn = self
113            .inner
114            .cdc
115            .emit(operation, collection, entity_id, entity_kind);
116        // Perf: prior to this we called `invalidate_result_cache()`
117        // which wipes EVERY cached query, across every table, under
118        // a write lock — turning each INSERT into a serialisation
119        // point for all readers. Swap to the per-table variant so
120        // unrelated query caches survive.
121        self.invalidate_result_cache_for_table(collection);
122
123        // Append to logical WAL replication buffer (if primary mode)
124        if self.inner.db.replication.is_some() {
125            let store = self.inner.db.store();
126            let entity = if operation == crate::replication::cdc::ChangeOperation::Delete {
127                None
128            } else {
129                store.get(collection, EntityId::new(entity_id))
130            };
131            let record = ChangeRecord {
132                term: self.current_replication_term(),
133                lsn,
134                timestamp: SystemTime::now()
135                    .duration_since(UNIX_EPOCH)
136                    .unwrap_or_default()
137                    .as_millis() as u64,
138                operation,
139                collection: collection.to_string(),
140                entity_id,
141                entity_kind: entity_kind.to_string(),
142                entity_bytes: entity
143                    .as_ref()
144                    .map(|entity| UnifiedStore::serialize_entity(entity, store.format_version())),
145                metadata: self.latest_metadata_for(collection, entity_id),
146                refresh_records: None,
147                range_id: None,
148                ownership_epoch: None,
149            };
150            self.stage_or_publish_logical_record(record)?;
151        }
152        Ok(lsn)
153    }
154
155    pub(crate) fn cdc_emit_kv(
156        &self,
157        operation: crate::replication::cdc::ChangeOperation,
158        collection: &str,
159        key: &str,
160        entity_id: u64,
161        before: Option<crate::json::Value>,
162        after: Option<crate::json::Value>,
163    ) -> u64 {
164        let lsn = self
165            .inner
166            .cdc
167            .emit_kv(operation, collection, key, entity_id, before, after);
168        self.inner.kv_stats.incr_watch_events_emitted();
169        self.invalidate_result_cache_for_table(collection);
170        lsn
171    }
172
173    pub(crate) fn record_kv_watch_event(
174        &self,
175        operation: crate::replication::cdc::ChangeOperation,
176        collection: &str,
177        key: &str,
178        entity_id: u64,
179        before: Option<crate::json::Value>,
180        after: Option<crate::json::Value>,
181    ) {
182        if let Some(writer_xid) = self.current_xid() {
183            let conn_id = current_connection_id();
184            let event = crate::replication::cdc::KvWatchEvent {
185                collection: collection.to_string(),
186                key: key.to_string(),
187                op: operation,
188                before,
189                after,
190                lsn: 0,
191                committed_at: 0,
192                dropped_event_count: 0,
193            };
194            self.inner
195                .pending_kv_watch_events
196                .write()
197                .entry(conn_id)
198                .or_default()
199                .push((writer_xid, event));
200            return;
201        }
202
203        self.cdc_emit_kv(operation, collection, key, entity_id, before, after);
204    }
205
206    pub(crate) fn cdc_emit_prebuilt(
207        &self,
208        operation: crate::replication::cdc::ChangeOperation,
209        collection: &str,
210        entity: &UnifiedEntity,
211        entity_kind: &str,
212        metadata: Option<&crate::storage::Metadata>,
213        invalidate_cache: bool,
214    ) -> RedDBResult<u64> {
215        self.cdc_emit_prebuilt_with_columns(
216            operation,
217            collection,
218            entity,
219            entity_kind,
220            metadata,
221            invalidate_cache,
222            None,
223        )
224    }
225
226    /// `cdc_emit_prebuilt` plus the list of column names whose values
227    /// changed on this update. Callers that have already computed a
228    /// `RowDamageVector` pass it here so downstream CDC consumers can
229    /// filter events by touched column without re-diffing.
230    /// `changed_columns` is only meaningful for `Update` operations —
231    /// insert and delete events ignore it.
232    pub(crate) fn cdc_emit_prebuilt_with_columns(
233        &self,
234        operation: crate::replication::cdc::ChangeOperation,
235        collection: &str,
236        entity: &UnifiedEntity,
237        entity_kind: &str,
238        metadata: Option<&crate::storage::Metadata>,
239        invalidate_cache: bool,
240        changed_columns: Option<Vec<String>>,
241    ) -> RedDBResult<u64> {
242        if invalidate_cache {
243            self.invalidate_result_cache();
244        }
245
246        let public_id = entity.logical_id().raw();
247        let lsn = self.inner.cdc.emit_with_columns(
248            operation,
249            collection,
250            public_id,
251            entity_kind,
252            changed_columns,
253        );
254
255        if self.inner.db.replication.is_some() {
256            let store = self.inner.db.store();
257            let record = ChangeRecord {
258                term: self.current_replication_term(),
259                lsn,
260                timestamp: SystemTime::now()
261                    .duration_since(UNIX_EPOCH)
262                    .unwrap_or_default()
263                    .as_millis() as u64,
264                operation,
265                collection: collection.to_string(),
266                entity_id: entity.id.raw(),
267                entity_kind: entity_kind.to_string(),
268                entity_bytes: Some(UnifiedStore::serialize_entity(
269                    entity,
270                    store.format_version(),
271                )),
272                metadata: metadata
273                    .map(metadata_to_json)
274                    .map(server_json_to_wire_json)
275                    .or_else(|| self.latest_metadata_for(collection, entity.id.raw())),
276                refresh_records: None,
277                range_id: None,
278                ownership_epoch: None,
279            };
280            self.stage_or_publish_logical_record(record)?;
281        }
282
283        Ok(lsn)
284    }
285
286    pub(crate) fn current_replication_term(&self) -> u64 {
287        self.inner.db.options().replication.term
288    }
289
290    pub(crate) fn cdc_emit_prebuilt_batch<'a, I>(
291        &self,
292        operation: crate::replication::cdc::ChangeOperation,
293        entity_kind: &str,
294        items: I,
295        invalidate_cache: bool,
296    ) -> RedDBResult<()>
297    where
298        I: IntoIterator<
299            Item = (
300                &'a str,
301                &'a UnifiedEntity,
302                Option<&'a crate::storage::Metadata>,
303            ),
304        >,
305    {
306        let items: Vec<(&str, &UnifiedEntity, Option<&crate::storage::Metadata>)> =
307            items.into_iter().collect();
308        if items.is_empty() {
309            return Ok(());
310        }
311
312        if invalidate_cache {
313            self.invalidate_result_cache();
314        }
315
316        for (collection, entity, metadata) in items {
317            self.cdc_emit_prebuilt(operation, collection, entity, entity_kind, metadata, false)?;
318        }
319        Ok(())
320    }
321
322    /// Poll CDC events since a given LSN.
323    pub fn cdc_poll(
324        &self,
325        since_lsn: u64,
326        max_count: usize,
327    ) -> Vec<crate::replication::cdc::ChangeEvent> {
328        self.inner.cdc.poll(since_lsn, max_count)
329    }
330
331    /// Current event cursor for CDC polling. A committed replication batch
332    /// can contain several events, so this is not a replica ACK position.
333    pub fn cdc_current_lsn(&self) -> u64 {
334        self.inner.cdc.current_lsn()
335    }
336
337    /// Published logical commit frontier; standalone instances retain their
338    /// local event cursor. Request-specific receipts are handled separately.
339    pub fn replication_commit_lsn(&self) -> u64 {
340        if self.inner.db.replication.is_some() {
341            self.primary_logical_head_lsn()
342        } else {
343            self.cdc_current_lsn()
344        }
345    }
346
347    pub fn kv_watch_events_since(
348        &self,
349        collection: &str,
350        key: &str,
351        since_lsn: u64,
352        max_count: usize,
353    ) -> Vec<crate::replication::cdc::KvWatchEvent> {
354        self.kv_watch_batch_since(collection, key, since_lsn, max_count)
355            .0
356    }
357
358    pub(crate) fn kv_watch_batch_since(
359        &self,
360        collection: &str,
361        key: &str,
362        since_lsn: u64,
363        max_count: usize,
364    ) -> (Vec<crate::replication::cdc::KvWatchEvent>, u64) {
365        let events = self.inner.cdc.poll(since_lsn, max_count);
366        let scanned_through = events.last().map(|event| event.lsn).unwrap_or(since_lsn);
367        let visible = events
368            .into_iter()
369            .filter_map(|event| event.kv)
370            .filter(|event| event.collection == collection && event.key == key)
371            .filter(|event| crate::runtime::rls_injection::kv_watch_event_passes_rls(self, event))
372            .collect();
373        (visible, scanned_through)
374    }
375
376    pub fn kv_watch_events_since_prefix(
377        &self,
378        collection: &str,
379        prefix: &str,
380        since_lsn: u64,
381        max_count: usize,
382    ) -> Vec<crate::replication::cdc::KvWatchEvent> {
383        self.kv_watch_batch_since_prefix(collection, prefix, since_lsn, max_count)
384            .0
385    }
386
387    pub(crate) fn kv_watch_batch_since_prefix(
388        &self,
389        collection: &str,
390        prefix: &str,
391        since_lsn: u64,
392        max_count: usize,
393    ) -> (Vec<crate::replication::cdc::KvWatchEvent>, u64) {
394        let events = self.inner.cdc.poll(since_lsn, max_count);
395        let scanned_through = events.last().map(|event| event.lsn).unwrap_or(since_lsn);
396        let visible = events
397            .into_iter()
398            .filter_map(|event| event.kv)
399            .filter(|event| event.collection == collection && event.key.starts_with(prefix))
400            .filter(|event| crate::runtime::rls_injection::kv_watch_event_passes_rls(self, event))
401            .collect();
402        (visible, scanned_through)
403    }
404
405    pub(crate) fn kv_watch_subscribe<'a>(
406        &'a self,
407        collection: impl Into<String>,
408        key: impl Into<String>,
409        from_lsn: Option<u64>,
410    ) -> crate::runtime::kv_watch::KvWatchStream<'a> {
411        crate::runtime::kv_watch::KvWatchStream::subscribe(
412            &self.inner.cdc,
413            &self.inner.kv_stats,
414            collection,
415            key,
416            from_lsn,
417            self.kv_watch_idle_timeout_ms(),
418        )
419    }
420
421    pub(crate) fn kv_watch_subscribe_prefix<'a>(
422        &'a self,
423        collection: impl Into<String>,
424        prefix: impl Into<String>,
425        from_lsn: Option<u64>,
426    ) -> crate::runtime::kv_watch::KvWatchStream<'a> {
427        crate::runtime::kv_watch::KvWatchStream::subscribe_prefix(
428            &self.inner.cdc,
429            &self.inner.kv_stats,
430            collection,
431            prefix,
432            from_lsn,
433            self.kv_watch_idle_timeout_ms(),
434        )
435    }
436
437    pub(crate) fn kv_watch_idle_timeout_ms(&self) -> u64 {
438        self.config_u64("kv.watch.idle_timeout_ms", 60_000)
439    }
440}