1use std::collections::{HashMap, HashSet, VecDeque};
4use std::sync::Arc;
5
6use crate::pipeline::{
7 run_source_runtime_with, ApplyOpts, CheckpointPolicy, Pipeline, PositionedEvent, SourceDriver,
8 SourceRuntimeOpts,
9};
10use anyhow::Result;
11use async_trait::async_trait;
12use surreal_sync_core::SurrealSink;
13use surreal_sync_core::{Change, Row};
14use surreal_sync_core::{InterleavedSnapshotCheckpoint, SnapshotTableProgress};
15use tracing::info;
16use uuid::Uuid;
17
18use super::checkpointer::SnapshotCheckpointer;
19use super::source::WatermarkSource;
20use super::types::{PkTuple, ReconciliationPos, TableSpec, WatermarkKind};
21
22pub const DEFAULT_CHUNK_SIZE: usize = 1024;
24
25#[derive(Debug, Clone)]
27pub struct InterleavedSnapshotConfig {
28 pub chunk_size: usize,
31}
32
33impl Default for InterleavedSnapshotConfig {
34 fn default() -> Self {
35 Self {
36 chunk_size: DEFAULT_CHUNK_SIZE,
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
46pub struct SnapshotTransforms {
47 pub pipeline: Pipeline,
48 pub apply_opts: ApplyOpts,
49}
50
51impl Default for SnapshotTransforms {
52 fn default() -> Self {
53 Self::identity()
54 }
55}
56
57impl SnapshotTransforms {
58 pub fn identity() -> Self {
60 Self {
61 pipeline: Pipeline::new(),
62 apply_opts: ApplyOpts::identity(),
63 }
64 }
65
66 pub fn from_refs(pipeline: &Pipeline, apply_opts: &ApplyOpts) -> Self {
68 Self {
69 pipeline: pipeline.clone(),
70 apply_opts: apply_opts.clone(),
71 }
72 }
73}
74
75#[derive(Debug, Clone)]
77pub struct InterleavedSnapshotResult<P: ReconciliationPos> {
78 pub final_position: P,
81 pub peak_buffered_rows: usize,
86}
87
88struct TableState {
90 name: String,
91 last_pk: Option<PkTuple>,
92 done: bool,
93}
94
95fn build_checkpoint<P: ReconciliationPos>(
96 position: &P,
97 progress: &[TableState],
98) -> Result<InterleavedSnapshotCheckpoint> {
99 let tables = progress
100 .iter()
101 .map(|t| {
102 Ok(SnapshotTableProgress {
103 name: t.name.clone(),
104 last_pk: t.last_pk.as_ref().map(serde_json::to_value).transpose()?,
105 done: t.done,
106 })
107 })
108 .collect::<Result<Vec<_>>>()?;
109 Ok(InterleavedSnapshotCheckpoint {
110 reconciliation_pos: serde_json::to_value(position)?,
111 tables,
112 })
113}
114
115fn table_state_from_progress(t: &SnapshotTableProgress) -> Result<TableState> {
116 let last_pk = match &t.last_pk {
117 None => None,
118 Some(v) => Some(serde_json::from_value(v.clone())?),
119 };
120 Ok(TableState {
121 name: t.name.clone(),
122 last_pk,
123 done: t.done,
124 })
125}
126
127fn init_snapshot_state(
128 all_tables: Vec<TableSpec>,
129 resume: Option<&InterleavedSnapshotCheckpoint>,
130) -> Result<(VecDeque<TableSpec>, HashSet<String>, Vec<TableState>)> {
131 let progress_by_name: HashMap<String, &SnapshotTableProgress> = resume
132 .map(|cp| cp.tables.iter().map(|t| (t.name.clone(), t)).collect())
133 .unwrap_or_default();
134
135 let progress: Vec<TableState> = all_tables
136 .iter()
137 .map(|spec| {
138 if let Some(saved) = progress_by_name.get(&spec.table) {
139 table_state_from_progress(saved)
140 } else {
141 Ok(TableState {
142 name: spec.table.clone(),
143 last_pk: None,
144 done: false,
145 })
146 }
147 })
148 .collect::<Result<Vec<_>>>()?;
149
150 let queue: VecDeque<TableSpec> = all_tables
151 .into_iter()
152 .filter(|spec| {
153 progress_by_name
154 .get(&spec.table)
155 .map(|t| !t.done)
156 .unwrap_or(true)
157 })
158 .collect();
159
160 let seen: HashSet<String> = progress.iter().map(|t| t.name.clone()).collect();
161 Ok((queue, seen, progress))
162}
163
164pub async fn run_interleaved_snapshot<S, K, C>(
171 source: &mut S,
172 sink: &K,
173 config: &InterleavedSnapshotConfig,
174 checkpointer: &mut C,
175) -> Result<InterleavedSnapshotResult<S::Position>>
176where
177 S: WatermarkSource,
178 K: SurrealSink,
179 C: SnapshotCheckpointer,
180{
181 let transforms = SnapshotTransforms::identity();
182 run_interleaved_snapshot_with_resume_and_transforms(
183 source,
184 sink,
185 config,
186 checkpointer,
187 None,
188 &transforms,
189 )
190 .await
191}
192
193pub async fn run_interleaved_snapshot_with_resume<S, K, C>(
196 source: &mut S,
197 sink: &K,
198 config: &InterleavedSnapshotConfig,
199 checkpointer: &mut C,
200 resume: Option<&InterleavedSnapshotCheckpoint>,
201) -> Result<InterleavedSnapshotResult<S::Position>>
202where
203 S: WatermarkSource,
204 K: SurrealSink,
205 C: SnapshotCheckpointer,
206{
207 let transforms = SnapshotTransforms::identity();
208 run_interleaved_snapshot_with_resume_and_transforms(
209 source,
210 sink,
211 config,
212 checkpointer,
213 resume,
214 &transforms,
215 )
216 .await
217}
218
219pub async fn run_interleaved_snapshot_with_transforms<S, K, C>(
221 source: &mut S,
222 sink: &K,
223 config: &InterleavedSnapshotConfig,
224 checkpointer: &mut C,
225 transforms: &SnapshotTransforms,
226) -> Result<InterleavedSnapshotResult<S::Position>>
227where
228 S: WatermarkSource,
229 K: SurrealSink,
230 C: SnapshotCheckpointer,
231{
232 run_interleaved_snapshot_with_resume_and_transforms(
233 source,
234 sink,
235 config,
236 checkpointer,
237 None,
238 transforms,
239 )
240 .await
241}
242
243pub async fn run_interleaved_snapshot_with_resume_and_transforms<S, K, C>(
248 source: &mut S,
249 sink: &K,
250 config: &InterleavedSnapshotConfig,
251 checkpointer: &mut C,
252 resume: Option<&InterleavedSnapshotCheckpoint>,
253 transforms: &SnapshotTransforms,
254) -> Result<InterleavedSnapshotResult<S::Position>>
255where
256 S: WatermarkSource,
257 K: SurrealSink,
258 C: SnapshotCheckpointer,
259{
260 let all_tables = source.snapshot_tables().await?;
261 let (mut queue, mut seen, mut progress) = init_snapshot_state(all_tables, resume)?;
262
263 let mut peak_buffered_rows = 0usize;
264
265 loop {
266 for signal in source.read_signals().await? {
270 let specs = source.resolve_tables(&signal.tables).await?;
271 for spec in specs {
272 if seen.insert(spec.table.clone()) {
273 info!(
274 "ad-hoc snapshot requested for table '{}' (signal {})",
275 spec.table, signal.id
276 );
277 progress.push(TableState {
278 name: spec.table.clone(),
279 last_pk: None,
280 done: false,
281 });
282 queue.push_back(spec);
283 }
284 }
285 }
286
287 let Some(spec) = queue.pop_front() else {
288 break;
289 };
290 let table_index = progress
291 .iter()
292 .position(|t| t.name == spec.table)
293 .expect("progress entry for queued table");
294
295 snapshot_one_table(
296 source,
297 sink,
298 config,
299 checkpointer,
300 &spec,
301 table_index,
302 &mut progress,
303 &mut peak_buffered_rows,
304 transforms,
305 )
306 .await?;
307 }
308
309 let final_position = source.current_position().await?;
310 Ok(InterleavedSnapshotResult {
311 final_position,
312 peak_buffered_rows,
313 })
314}
315
316pub async fn run_adhoc_snapshot_tables<S, K, C>(
320 source: &mut S,
321 sink: &K,
322 tables: Vec<TableSpec>,
323 config: &InterleavedSnapshotConfig,
324 checkpointer: &mut C,
325) -> Result<()>
326where
327 S: WatermarkSource,
328 K: SurrealSink,
329 C: SnapshotCheckpointer,
330{
331 let transforms = SnapshotTransforms::identity();
332 run_adhoc_snapshot_tables_with_transforms(
333 source,
334 sink,
335 tables,
336 config,
337 checkpointer,
338 &transforms,
339 )
340 .await
341}
342
343pub async fn run_adhoc_snapshot_tables_with_transforms<S, K, C>(
345 source: &mut S,
346 sink: &K,
347 tables: Vec<TableSpec>,
348 config: &InterleavedSnapshotConfig,
349 checkpointer: &mut C,
350 transforms: &SnapshotTransforms,
351) -> Result<()>
352where
353 S: WatermarkSource,
354 K: SurrealSink,
355 C: SnapshotCheckpointer,
356{
357 let mut progress: Vec<TableState> = tables
358 .iter()
359 .map(|t| TableState {
360 name: t.table.clone(),
361 last_pk: None,
362 done: false,
363 })
364 .collect();
365 let mut peak_buffered_rows = 0usize;
366 for (table_index, spec) in tables.iter().enumerate() {
367 snapshot_one_table(
368 source,
369 sink,
370 config,
371 checkpointer,
372 spec,
373 table_index,
374 &mut progress,
375 &mut peak_buffered_rows,
376 transforms,
377 )
378 .await?;
379 }
380 Ok(())
381}
382
383#[allow(clippy::too_many_arguments)]
391async fn snapshot_one_table<S, K, C>(
392 source: &mut S,
393 sink: &K,
394 config: &InterleavedSnapshotConfig,
395 checkpointer: &mut C,
396 spec: &TableSpec,
397 table_index: usize,
398 progress: &mut [TableState],
399 peak_buffered_rows: &mut usize,
400 transforms: &SnapshotTransforms,
401) -> Result<()>
402where
403 S: WatermarkSource,
404 K: SurrealSink,
405 C: SnapshotCheckpointer,
406{
407 let after = progress[table_index].last_pk.clone();
408 let mut driver = SnapshotTableDriver {
409 source,
410 checkpointer,
411 spec,
412 table_index,
413 progress,
414 peak_buffered_rows,
415 chunk_size: config.chunk_size,
416 after,
417 phase: SnapshotPhase::NeedChunk,
418 low_id: Uuid::nil(),
419 high_id: Uuid::nil(),
420 in_window: false,
421 buffer: HashMap::new(),
422 last_pk: None,
423 chunk_len: 0,
424 pending: VecDeque::new(),
425 next_pos: 0,
426 events_emitted: 0,
427 events_sunk: 0,
428 chunk_poll_complete: false,
429 finished: false,
430 };
431
432 let transformer = Arc::new(transforms.pipeline.clone());
433 let runtime_opts = SourceRuntimeOpts::new();
434 run_source_runtime_with(
435 &mut driver,
436 sink,
437 transformer,
438 &transforms.apply_opts,
439 &runtime_opts,
440 )
441 .await?;
442 Ok(())
443}
444
445enum SnapshotPhase {
447 NeedChunk,
449 Reconciling,
453 AwaitingChunkSink,
455}
456
457struct SnapshotTableDriver<'a, S, C>
460where
461 S: WatermarkSource,
462 C: SnapshotCheckpointer,
463{
464 source: &'a mut S,
465 checkpointer: &'a mut C,
466 spec: &'a TableSpec,
467 table_index: usize,
468 progress: &'a mut [TableState],
469 peak_buffered_rows: &'a mut usize,
470 chunk_size: usize,
471 after: Option<PkTuple>,
472 phase: SnapshotPhase,
473 low_id: Uuid,
474 high_id: Uuid,
475 in_window: bool,
476 buffer: HashMap<String, Row>,
477 last_pk: Option<PkTuple>,
478 chunk_len: usize,
479 pending: VecDeque<PositionedEvent<u64>>,
480 next_pos: u64,
481 events_emitted: u64,
482 events_sunk: u64,
483 chunk_poll_complete: bool,
484 finished: bool,
485}
486
487impl<'a, S, C> SnapshotTableDriver<'a, S, C>
488where
489 S: WatermarkSource,
490 C: SnapshotCheckpointer,
491{
492 fn push_change(&mut self, change: Change) {
493 let pos = self.next_pos;
494 self.next_pos = self.next_pos.saturating_add(1);
495 self.events_emitted = self.events_emitted.saturating_add(1);
496 self.pending.push_back(PositionedEvent::change(change, pos));
497 }
498
499 async fn open_chunk(&mut self) -> Result<()> {
500 let low_id = Uuid::new_v4();
501 self.source
502 .write_watermark(WatermarkKind::Low, low_id)
503 .await?;
504
505 let rows = self
506 .source
507 .read_chunk(self.spec, self.after.as_ref(), self.chunk_size)
508 .await?;
509
510 if rows.is_empty() {
511 self.progress[self.table_index].done = true;
512 self.source
513 .on_table_snapshot_complete(&self.spec.table)
514 .await?;
515 let position = self.source.current_position().await?;
516 self.checkpointer
517 .save_progress(&build_checkpoint(&position, self.progress)?)
518 .await?;
519 self.source.commit_reconciled(position).await?;
520 self.finished = true;
521 return Ok(());
522 }
523
524 self.chunk_len = rows.len();
525 self.buffer = HashMap::with_capacity(self.chunk_len);
526 self.last_pk = None;
527 for row in rows {
528 let pk = PkTuple::from_row(&row, &self.spec.pk_columns)?;
529 self.last_pk = Some(pk.clone());
530 self.buffer.insert(pk.key(), row);
531 if self.buffer.len() > *self.peak_buffered_rows {
532 *self.peak_buffered_rows = self.buffer.len();
533 }
534 }
535
536 let high_id = Uuid::new_v4();
537 self.source
538 .write_watermark(WatermarkKind::High, high_id)
539 .await?;
540
541 self.low_id = low_id;
542 self.high_id = high_id;
543 self.in_window = false;
544 self.events_emitted = 0;
545 self.events_sunk = 0;
546 self.chunk_poll_complete = false;
547 self.phase = SnapshotPhase::Reconciling;
548 Ok(())
549 }
550
551 fn queue_buffer_rows(&mut self) {
552 let rows: Vec<Row> = self.buffer.drain().map(|(_, row)| row).collect();
553 for row in rows {
554 let change = Change::update(row.table, row.id, row.fields);
555 self.push_change(change);
556 }
557 self.chunk_poll_complete = true;
558 self.phase = SnapshotPhase::AwaitingChunkSink;
559 }
560
561 async fn try_finish_chunk_after_sink(&mut self) -> Result<()> {
562 if !self.chunk_poll_complete || self.events_sunk < self.events_emitted {
563 return Ok(());
564 }
565
566 self.after = self.last_pk.clone();
567 self.progress[self.table_index].last_pk = self.last_pk.clone();
568
569 let table_done = self.chunk_len < self.chunk_size;
570 if table_done {
571 self.progress[self.table_index].done = true;
572 self.source
573 .on_table_snapshot_complete(&self.spec.table)
574 .await?;
575 }
576
577 let position = self.source.current_position().await?;
578 self.checkpointer
579 .save_progress(&build_checkpoint(&position, self.progress)?)
580 .await?;
581 self.source.commit_reconciled(position).await?;
582
583 if table_done {
584 self.finished = true;
585 } else {
586 self.phase = SnapshotPhase::NeedChunk;
589 self.chunk_poll_complete = false;
590 }
591 Ok(())
592 }
593}
594
595#[async_trait]
596impl<S, C> SourceDriver for SnapshotTableDriver<'_, S, C>
597where
598 S: WatermarkSource,
599 C: SnapshotCheckpointer,
600{
601 type Position = u64;
602
603 async fn poll_work(&mut self) -> Result<Vec<PositionedEvent<Self::Position>>> {
604 loop {
605 if self.finished {
606 return Ok(Vec::new());
607 }
608
609 if !self.pending.is_empty() {
611 let mut out = Vec::with_capacity(self.pending.len().min(64));
612 while out.len() < 64 {
613 match self.pending.pop_front() {
614 Some(pe) => out.push(pe),
615 None => break,
616 }
617 }
618 return Ok(out);
619 }
620
621 match self.phase {
622 SnapshotPhase::NeedChunk => {
623 self.open_chunk().await?;
624 if self.finished {
625 return Ok(Vec::new());
626 }
627 continue;
629 }
630 SnapshotPhase::Reconciling => {
631 let events = self.source.next_reconciliation_events().await?;
632 let mut saw_high = false;
633 for event in events {
634 if let Some(watermark_id) = event.pk.single_uuid() {
635 if watermark_id == self.low_id {
636 self.in_window = true;
637 continue;
638 }
639 if watermark_id == self.high_id {
640 self.in_window = false;
641 self.queue_buffer_rows();
646 saw_high = true;
647 continue;
648 }
649 }
650
651 if self.in_window {
652 self.buffer.remove(&event.pk.key());
653 }
654 self.push_change(event.change);
655 }
656
657 if saw_high {
658 if self.events_emitted == 0 {
659 self.try_finish_chunk_after_sink().await?;
661 continue;
662 }
663 let mut out = Vec::with_capacity(self.pending.len().min(64));
664 while out.len() < 64 {
665 match self.pending.pop_front() {
666 Some(pe) => out.push(pe),
667 None => break,
668 }
669 }
670 return Ok(out);
671 }
672
673 let mut out = Vec::with_capacity(self.pending.len().min(64));
674 while out.len() < 64 {
675 match self.pending.pop_front() {
676 Some(pe) => out.push(pe),
677 None => break,
678 }
679 }
680 return Ok(out);
681 }
682 SnapshotPhase::AwaitingChunkSink => {
683 return Ok(Vec::new());
685 }
686 }
687 }
688 }
689
690 async fn advance_watermark(&mut self, _position: Self::Position) -> Result<()> {
691 self.try_finish_chunk_after_sink().await
695 }
696
697 fn is_finished(&self) -> bool {
698 self.finished
699 }
700
701 fn checkpoint_policy(&self) -> CheckpointPolicy {
702 CheckpointPolicy::AdvanceOnly
705 }
706
707 fn note_sunk_events(&mut self, count: u64) {
708 self.events_sunk = self.events_sunk.saturating_add(count);
709 }
710}