1use std::{
5 ops::Bound,
6 process,
7 sync::{
8 Arc,
9 atomic::{AtomicBool, Ordering},
10 },
11};
12
13use reifydb_codec::key::encoded::EncodedKey;
14use reifydb_core::{
15 actors::cdc::CdcPollMessage,
16 common::CommitVersion,
17 interface::{
18 catalog::config::{ConfigKey, GetConfig},
19 cdc::{Cdc, CdcConsumerId, SystemChange},
20 },
21 key::{EncodableKey, Key, cdc_consumer::CdcConsumerKey, kind::KeyKind},
22};
23use reifydb_runtime::actor::{
24 context::Context,
25 system::ActorConfig,
26 traits::{Actor, Directive},
27};
28use reifydb_transaction::transaction::Transaction;
29use reifydb_value::{Result, error::Error, reifydb_assertions, value::duration::Duration};
30use tracing::{debug, error};
31
32use super::{checkpoint::CdcCheckpoint, consumer::CdcConsume, host::CdcHost, watermark::CdcConsumerWatermark};
33use crate::storage::CdcStore;
34
35#[derive(Debug, Clone)]
36pub struct PollActorConfig {
37 pub consumer_id: CdcConsumerId,
38
39 pub poll_interval: Duration,
40
41 pub max_batch_size: Option<u64>,
42}
43
44pub struct PollActor<H: CdcHost, C: CdcConsume> {
45 config: PollActorConfig,
46 host: H,
47 consumer: Box<C>,
48 store: CdcStore,
49 consumer_key: EncodedKey,
50 consumer_watermark: Option<CdcConsumerWatermark>,
51 wake_armed: Arc<AtomicBool>,
52}
53
54impl<H: CdcHost, C: CdcConsume> PollActor<H, C> {
55 pub fn new(
56 config: PollActorConfig,
57 host: H,
58 consumer: C,
59 store: CdcStore,
60 consumer_watermark: Option<CdcConsumerWatermark>,
61 wake_armed: Arc<AtomicBool>,
62 ) -> Self {
63 let consumer_key = CdcConsumerKey {
64 consumer: config.consumer_id.clone(),
65 }
66 .encode();
67
68 Self {
69 config,
70 host,
71 consumer: Box::new(consumer),
72 store,
73 consumer_key,
74 consumer_watermark,
75 wake_armed,
76 }
77 }
78
79 #[inline]
80 fn publish_watermark(&self, version: CommitVersion) {
81 if let Some(wm) = &self.consumer_watermark {
82 wm.store(version);
83 }
84 }
85
86 #[inline]
87 fn consume_wait_timeout(&self) -> Duration {
88 self.host.catalog().get_config_duration(ConfigKey::CdcConsumeWaitTimeout)
89 }
90}
91
92pub enum Phase {
93 Ready,
94
95 WaitingForWatermark,
96
97 WaitingForConsume {
98 latest_version: CommitVersion,
99
100 count: usize,
101
102 generation: u64,
103 },
104}
105
106pub struct PollState {
107 phase: Phase,
108
109 cached_checkpoint: Option<CommitVersion>,
110
111 consume_generation: u64,
112
113 consume_stall_ticks: u32,
114}
115
116impl<H: CdcHost, C: CdcConsume + Send + Sync + 'static> Actor for PollActor<H, C> {
117 type State = PollState;
118 type Message = CdcPollMessage;
119
120 fn init(&self, ctx: &Context<Self::Message>) -> Self::State {
121 debug!(
122 "[Consumer {:?}] Started polling with interval {:?}",
123 self.config.consumer_id, self.config.poll_interval
124 );
125
126 let _ = ctx.self_ref().send(CdcPollMessage::Poll);
127 let _ = ctx.schedule_once(self.config.poll_interval, || CdcPollMessage::Tick);
128
129 PollState {
130 phase: Phase::Ready,
131 cached_checkpoint: None,
132 consume_generation: 0,
133 consume_stall_ticks: 0,
134 }
135 }
136
137 fn handle(&self, state: &mut Self::State, msg: Self::Message, ctx: &Context<Self::Message>) -> Directive {
138 match msg {
139 CdcPollMessage::Poll => self.on_poll(state, ctx),
140 CdcPollMessage::CheckWatermark => self.on_check_watermark(state, ctx),
141 CdcPollMessage::ConsumeResponse {
142 generation,
143 result,
144 } => self.on_consume_response(state, ctx, generation, result),
145 CdcPollMessage::Tick => self.on_tick(state, ctx),
146 CdcPollMessage::Shutdown => {
147 debug!("[Consumer {:?}] Shutdown", self.config.consumer_id);
148 Directive::Stop
149 }
150 }
151 }
152
153 fn config(&self) -> ActorConfig {
154 ActorConfig::new()
155 }
156}
157
158impl<H: CdcHost, C: CdcConsume> PollActor<H, C> {
159 #[inline]
160 fn on_poll(&self, state: &mut PollState, ctx: &Context<CdcPollMessage>) -> Directive {
161 if !matches!(state.phase, Phase::Ready) {
162 return Directive::Continue;
163 }
164 if ctx.is_cancelled() {
165 debug!("[Consumer {:?}] Stopped", self.config.consumer_id);
166 return Directive::Stop;
167 }
168 let current_version = match self.host.current_version() {
169 Ok(v) => v,
170 Err(e) => {
171 error!("[Consumer {:?}] Error getting current version: {}", self.config.consumer_id, e);
172 return Directive::Continue;
173 }
174 };
175 if self.host.done_until() >= current_version {
176 self.start_consume(state, ctx);
177 } else {
178 state.phase = Phase::WaitingForWatermark;
179 let self_ref = ctx.self_ref();
180 self.host.notify_on_mark(
181 current_version,
182 Box::new(move || {
183 let _ = self_ref.send(CdcPollMessage::CheckWatermark);
184 }),
185 );
186 }
187 Directive::Continue
188 }
189
190 #[inline]
191 fn on_check_watermark(&self, state: &mut PollState, ctx: &Context<CdcPollMessage>) -> Directive {
192 if !matches!(state.phase, Phase::WaitingForWatermark) {
193 return Directive::Continue;
194 }
195 if ctx.is_cancelled() {
196 debug!("[Consumer {:?}] Stopped", self.config.consumer_id);
197 return Directive::Stop;
198 }
199 state.phase = Phase::Ready;
200 self.start_consume(state, ctx);
201 Directive::Continue
202 }
203
204 #[inline]
205 fn on_consume_response(
206 &self,
207 state: &mut PollState,
208 ctx: &Context<CdcPollMessage>,
209 generation: u64,
210 result: Result<()>,
211 ) -> Directive {
212 if let Phase::WaitingForConsume {
213 latest_version,
214 count,
215 generation: pending,
216 } = state.phase
217 {
218 if pending != generation {
219 return Directive::Continue;
220 }
221 state.phase = Phase::Ready;
222 self.finish_consume(state, ctx, latest_version, count, result);
223 }
224 Directive::Continue
225 }
226
227 #[inline]
228 fn on_tick(&self, state: &mut PollState, ctx: &Context<CdcPollMessage>) -> Directive {
229 if ctx.is_cancelled() {
230 debug!("[Consumer {:?}] Stopped", self.config.consumer_id);
231 return Directive::Stop;
232 }
233
234 let flow = if matches!(state.phase, Phase::Ready) {
235 self.on_poll(state, ctx)
236 } else if matches!(state.phase, Phase::WaitingForWatermark) {
237 self.on_check_watermark(state, ctx)
238 } else {
239 self.check_consume_stall(state, ctx)
240 };
241 if matches!(flow, Directive::Stop) {
242 return Directive::Stop;
243 }
244
245 ctx.schedule_once(self.config.poll_interval, || CdcPollMessage::Tick);
246 Directive::Continue
247 }
248
249 #[inline]
250 fn check_consume_stall(&self, state: &mut PollState, ctx: &Context<CdcPollMessage>) -> Directive {
251 if !matches!(state.phase, Phase::WaitingForConsume { .. }) {
252 return Directive::Continue;
253 }
254 state.consume_stall_ticks = state.consume_stall_ticks.saturating_add(1);
255 if state.consume_stall_ticks < self.stall_tick_threshold() {
256 return Directive::Continue;
257 }
258 error!(
259 "[Consumer {:?}] consume reply not received within {:?}; re-dispatching batch",
260 self.config.consumer_id,
261 self.consume_wait_timeout()
262 );
263 state.phase = Phase::Ready;
264 state.consume_stall_ticks = 0;
265 let _ = ctx.self_ref().send(CdcPollMessage::Poll);
266 Directive::Continue
267 }
268
269 #[inline]
270 fn stall_tick_threshold(&self) -> u32 {
271 let consume_ms = self.consume_wait_timeout().to_std().as_millis().max(1);
272 let poll_ms = self.config.poll_interval.to_std().as_millis().max(1);
273 consume_ms.div_ceil(poll_ms).max(1) as u32
274 }
275
276 fn start_consume(&self, state: &mut PollState, ctx: &Context<CdcPollMessage>) {
277 state.phase = Phase::Ready;
278 self.wake_armed.store(false, Ordering::Release);
279 let safe_version = self.host.cdc_producer_watermark();
280 if safe_version > self.host.done_until() {
281 return;
282 }
283
284 let Some(checkpoint) = self.resolve_checkpoint(state) else {
285 return;
286 };
287 if safe_version <= checkpoint {
288 return;
289 }
290
291 let Some(transactions) = self.fetch_cdcs(checkpoint, safe_version) else {
292 return;
293 };
294 if transactions.is_empty() {
295 self.advance_checkpoint_skip_ahead(state, ctx, safe_version);
296 return;
297 }
298
299 let (count, latest_version) = summarize_batch(checkpoint, &transactions);
300 let relevant_cdcs: Vec<Cdc> = transactions.into_iter().filter(is_relevant_cdc).collect();
301
302 if relevant_cdcs.is_empty() {
303 self.advance_checkpoint_skip_ahead(state, ctx, latest_version);
304 return;
305 }
306
307 state.consume_generation = state.consume_generation.wrapping_add(1);
308 let generation = state.consume_generation;
309 state.phase = Phase::WaitingForConsume {
310 latest_version,
311 count,
312 generation,
313 };
314 state.consume_stall_ticks = 0;
315 self.dispatch_to_consumer(relevant_cdcs, generation, ctx);
316 }
317
318 #[inline]
319 fn advance_checkpoint_skip_ahead(
320 &self,
321 state: &mut PollState,
322 ctx: &Context<CdcPollMessage>,
323 latest_version: CommitVersion,
324 ) {
325 reifydb_assertions! {
326 if let Some(prev) = state.cached_checkpoint {
327 assert!(
328 latest_version >= prev,
329 "the consumer checkpoint moved backwards, so CDC that was already consumed would be \
330 re-delivered (cached checkpoint prev={}, new latest={})",
331 prev.0,
332 latest_version.0
333 );
334 }
335 }
336 state.cached_checkpoint = Some(latest_version);
337 self.publish_watermark(latest_version);
338 let _ = ctx.self_ref().send(CdcPollMessage::Poll);
339 }
340
341 #[inline]
342 fn resolve_checkpoint(&self, state: &mut PollState) -> Option<CommitVersion> {
343 if let Some(v) = state.cached_checkpoint {
344 return Some(v);
345 }
346 let v = self.seed_checkpoint_from_durable()?;
347 state.cached_checkpoint = Some(v);
348 self.publish_watermark(v);
349 Some(v)
350 }
351
352 #[inline]
353 fn seed_checkpoint_from_durable(&self) -> Option<CommitVersion> {
354 let mut query = match self.host.begin_query() {
355 Ok(q) => q,
356 Err(e) => {
357 error!("[Consumer {:?}] Error beginning query: {}", self.config.consumer_id, e);
358 return None;
359 }
360 };
361 let v = match CdcCheckpoint::fetch(&mut Transaction::Query(&mut query), &self.consumer_key) {
362 Ok(c) => c,
363 Err(e) => {
364 error!("[Consumer {:?}] Error fetching checkpoint: {}", self.config.consumer_id, e);
365 return None;
366 }
367 };
368 drop(query);
369 Some(v)
370 }
371
372 #[inline]
373 fn fetch_cdcs(&self, checkpoint: CommitVersion, safe_version: CommitVersion) -> Option<Vec<Cdc>> {
374 match self.fetch_cdcs_until(checkpoint, safe_version) {
375 Ok(t) => Some(t),
376 Err(e) => {
377 error!("[Consumer {:?}] Error fetching CDCs: {}", self.config.consumer_id, e);
378 None
379 }
380 }
381 }
382
383 #[inline]
384 fn dispatch_to_consumer(&self, cdcs: Vec<Cdc>, generation: u64, ctx: &Context<CdcPollMessage>) {
385 let self_ref = ctx.self_ref().clone();
386 let reply: Box<dyn FnOnce(Result<()>) + Send> = Box::new(move |result| {
387 let _ = self_ref.send(CdcPollMessage::ConsumeResponse {
388 generation,
389 result,
390 });
391 });
392 self.consumer.consume(cdcs, reply);
393 }
394
395 fn finish_consume(
396 &self,
397 state: &mut PollState,
398 ctx: &Context<CdcPollMessage>,
399 latest_version: CommitVersion,
400 count: usize,
401 result: Result<()>,
402 ) {
403 state.phase = Phase::Ready;
404 match result {
405 Ok(()) => self.advance_after_success(state, ctx, latest_version, count),
406 Err(e) => self.abort_on_error(e),
407 }
408 }
409
410 #[inline]
411 fn advance_after_success(
412 &self,
413 state: &mut PollState,
414 ctx: &Context<CdcPollMessage>,
415 latest_version: CommitVersion,
416 count: usize,
417 ) {
418 reifydb_assertions! {
419 if let Some(prev) = state.cached_checkpoint {
420 assert!(
421 latest_version >= prev,
422 "the consumer checkpoint moved backwards, so CDC that was already consumed would be \
423 re-delivered (cached checkpoint prev={}, new latest={})",
424 prev.0,
425 latest_version.0
426 );
427 }
428 }
429 state.cached_checkpoint = Some(latest_version);
430 self.publish_watermark(latest_version);
431 if count > 0 {
432 let _ = ctx.self_ref().send(CdcPollMessage::Poll);
433 }
434 }
435
436 #[inline]
437 fn abort_on_error(&self, err: Error) -> ! {
438 error!(
439 "[Consumer {:?}] fatal error consuming events, aborting application: {}",
440 self.config.consumer_id, err
441 );
442 process::abort();
443 }
444
445 fn fetch_cdcs_until(&self, since_version: CommitVersion, until_version: CommitVersion) -> Result<Vec<Cdc>> {
446 let batch_size = self.config.max_batch_size.unwrap_or(1024);
447 let batch = self.store.read_range(
448 Bound::Excluded(since_version),
449 Bound::Included(until_version),
450 batch_size,
451 )?;
452 Ok(batch.items)
453 }
454}
455
456#[inline]
457fn summarize_batch(checkpoint: CommitVersion, transactions: &[Cdc]) -> (usize, CommitVersion) {
458 let count = transactions.len();
459 let latest_version = transactions.iter().map(|tx| tx.version).max().unwrap_or(checkpoint);
460 (count, latest_version)
461}
462
463fn is_relevant_cdc(cdc: &Cdc) -> bool {
464 !cdc.changes.is_empty() || cdc.system_changes.iter().any(is_relevant_system_change)
465}
466
467fn is_relevant_system_change(change: &SystemChange) -> bool {
468 let key = match change {
469 SystemChange::Insert {
470 key,
471 ..
472 }
473 | SystemChange::Update {
474 key,
475 ..
476 }
477 | SystemChange::Delete {
478 key,
479 ..
480 } => key,
481 };
482 Key::kind(key)
483 .map(|kind| {
484 matches!(
485 kind,
486 KeyKind::Row
487 | KeyKind::PartitionedRow | KeyKind::Flow
488 | KeyKind::FlowNode | KeyKind::FlowNodeByFlow
489 | KeyKind::FlowEdge | KeyKind::FlowEdgeByFlow
490 | KeyKind::NamespaceFlow
491 )
492 })
493 .unwrap_or(false)
494}