1use std::path::{Path, PathBuf};
2use std::time::{Duration, Instant, SystemTime};
3
4use linkrs_core::types::Timestamp;
5use linkrs_core::{StorageError, StorageResult};
6use linkrs_metrics::CheckpointTriggerReason;
7use linkrs_sync::checkpoint_manifest::{
8 CheckpointManifest, CheckpointManifestManager, IndexManifestRef,
9};
10use linkrs_transaction::wal::Lsn;
11
12use crate::engine::snapshot_manager::SnapshotOptions;
13use crate::persistence::dirty_page::IncrementalCheckpointMeta;
14
15use super::fs_util::{sync_directory, sync_tree};
16
17pub use super::checkpoint_meta::{
19 CheckpointInfo, CHECKPOINT_FORMAT_VERSION, INCREMENTAL_CHECKPOINT_FORMAT_VERSION,
20};
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct CheckpointStats {
24 pub checkpoint_id: u64,
25 pub data_flushed: u64,
26 pub wal_truncated: u64,
27 pub duration: Duration,
28 pub snapshot_created: bool,
29 pub checkpoint_seq: u64,
30 pub data_files_created: usize,
31 pub bytes_flushed: u64,
32 pub wal_files_truncated: usize,
33 pub trigger_reason: CheckpointTriggerReason,
34 pub snapshot_timestamp: Timestamp,
38}
39
40#[derive(Debug, Clone)]
41pub struct CheckpointData {
42 pub vertex_count: u64,
45 pub edge_count: u64,
47 pub data_size: u64,
48}
49
50impl crate::engine::persistence_coordinator::PersistenceCoordinator {
51 pub(crate) fn cleanup_temporary_checkpoints(checkpoint_dir: &Path) -> StorageResult<()> {
52 for entry in std::fs::read_dir(checkpoint_dir)? {
53 let entry = entry?;
54 let path = entry.path();
55 let is_temporary = path
56 .file_name()
57 .and_then(|name| name.to_str())
58 .is_some_and(|name| name.starts_with("checkpoint_") && name.ends_with(".tmp"));
59 if is_temporary {
60 if path.is_dir() {
61 std::fs::remove_dir_all(path)?;
62 } else {
63 std::fs::remove_file(path)?;
64 }
65 }
66 }
67 Ok(())
68 }
69
70 pub(crate) fn latest_published_sequence(
71 manifest_manager: &CheckpointManifestManager,
72 ) -> StorageResult<u64> {
73 manifest_manager
74 .load_latest()
75 .map_err(StorageError::db_error)
76 .map(|manifest| manifest.map_or(0, |manifest| manifest.checkpoint_id))
77 }
78
79 pub(crate) fn cleanup_unpublished_checkpoints(
83 checkpoint_dir: &Path,
84 latest_published_sequence: u64,
85 ) -> StorageResult<()> {
86 for entry in std::fs::read_dir(checkpoint_dir)? {
87 let path = entry?.path();
88 if !path.is_dir() {
89 continue;
90 }
91 let Some(sequence) = path
92 .file_name()
93 .and_then(|name| name.to_str())
94 .and_then(|name| name.strip_prefix("checkpoint_"))
95 .and_then(|value| value.parse::<u64>().ok())
96 else {
97 continue;
98 };
99 if sequence > latest_published_sequence {
100 std::fs::remove_dir_all(path)?;
101 }
102 }
103 Ok(())
104 }
105
106 pub(crate) fn current_lsn(&self) -> Lsn {
107 match &self.wal_manager {
108 Some(wal) => wal.read().current_lsn(),
109 None => Lsn::ZERO,
110 }
111 }
112
113 pub(crate) fn wal_bytes_since(&self, base_lsn: Lsn) -> u64 {
114 self.current_lsn().offset_in_file(base_lsn)
115 }
116
117 pub fn should_flush(&self) -> bool {
118 let last_flush_lsn = *self.last_flush_lsn.read();
119 let last_flush = *self.last_flush_time.read();
120
121 self.wal_bytes_since(last_flush_lsn) >= self.config.checkpoint_threshold
122 || last_flush.elapsed() >= self.config.auto_flush_interval
123 }
124
125 pub fn should_checkpoint(&self) -> bool {
126 let last_checkpoint_lsn = *self.last_checkpoint_lsn.read();
127 let last_checkpoint = *self.last_checkpoint_time.read();
128 let wal_bytes_since_checkpoint = self.wal_bytes_since(last_checkpoint_lsn);
129
130 wal_bytes_since_checkpoint >= self.config.checkpoint_threshold
131 || wal_bytes_since_checkpoint >= self.config.max_wal_size
132 || last_checkpoint.elapsed() >= self.config.auto_checkpoint_interval
133 }
134
135 pub fn checkpoint_trigger_reason(&self) -> Option<CheckpointTriggerReason> {
136 let last_checkpoint_lsn = *self.last_checkpoint_lsn.read();
137 let last_checkpoint = *self.last_checkpoint_time.read();
138 let wal_bytes_since_checkpoint = self.wal_bytes_since(last_checkpoint_lsn);
139 if wal_bytes_since_checkpoint >= self.config.max_wal_size
140 || wal_bytes_since_checkpoint >= self.config.checkpoint_threshold
141 {
142 Some(CheckpointTriggerReason::WalSizeExceeded)
143 } else if last_checkpoint.elapsed() >= self.config.auto_checkpoint_interval {
144 Some(CheckpointTriggerReason::TimeSinceLastCheckpoint)
145 } else {
146 None
147 }
148 }
149
150 pub fn should_snapshot(&self) -> bool {
151 if !self.config.enable_snapshots {
152 return false;
153 }
154
155 if let Some(last_snapshot) = *self.last_snapshot_time.read() {
156 if let Ok(elapsed) = last_snapshot.elapsed() {
157 return elapsed >= self.config.snapshot_interval;
158 }
159 }
160
161 true
162 }
163
164 #[cfg(test)]
165 pub(crate) fn create_checkpoint(
166 &self,
167 flush_data: impl FnOnce(&Path, Timestamp) -> StorageResult<CheckpointData>,
168 timestamp: Timestamp,
169 ) -> StorageResult<CheckpointStats> {
170 self.create_checkpoint_with_reason(flush_data, timestamp, CheckpointTriggerReason::Explicit)
171 }
172
173 pub fn create_checkpoint_with_reason(
174 &self,
175 flush_data: impl FnOnce(&Path, Timestamp) -> StorageResult<CheckpointData>,
176 timestamp: Timestamp,
177 reason: CheckpointTriggerReason,
178 ) -> StorageResult<CheckpointStats> {
179 let result = self.create_checkpoint_inner(flush_data, timestamp, reason);
180 match &result {
181 Ok(stats) => {
182 *self.last_checkpoint_error.write() = None;
183 self.emit_storage_event(
184 crate::engine::storage_events::StorageEvent::CheckpointCompleted {
185 sequence: stats.checkpoint_seq,
186 duration_ms: stats.duration.as_millis() as u64,
187 data_flushed: stats.data_flushed,
188 wal_truncated: stats.wal_truncated,
189 snapshot_created: stats.snapshot_created,
190 },
191 );
192 }
193 Err(error) => {
194 *self.last_checkpoint_error.write() = Some(error.to_string());
195 self.emit_storage_event(
196 crate::engine::storage_events::StorageEvent::CheckpointFailed {
197 sequence: None,
198 error: error.to_string(),
199 },
200 );
201 }
202 }
203 result
204 }
205
206 pub(crate) fn create_checkpoint_with_guard(
207 &self,
208 guard: crate::engine::persistence_coordinator::PersistenceStateGuard,
209 flush_data: impl FnOnce(&Path, Timestamp) -> StorageResult<CheckpointData>,
210 timestamp: Timestamp,
211 reason: CheckpointTriggerReason,
212 ) -> StorageResult<CheckpointStats> {
213 let result = self.create_checkpoint_inner_with_guard(guard, flush_data, timestamp, reason);
214 match &result {
215 Ok(stats) => {
216 *self.last_checkpoint_error.write() = None;
217 self.emit_storage_event(
218 crate::engine::storage_events::StorageEvent::CheckpointCompleted {
219 sequence: stats.checkpoint_seq,
220 duration_ms: stats.duration.as_millis() as u64,
221 data_flushed: stats.data_flushed,
222 wal_truncated: stats.wal_truncated,
223 snapshot_created: stats.snapshot_created,
224 },
225 );
226 }
227 Err(error) => {
228 *self.last_checkpoint_error.write() = Some(error.to_string());
229 self.emit_storage_event(
230 crate::engine::storage_events::StorageEvent::CheckpointFailed {
231 sequence: None,
232 error: error.to_string(),
233 },
234 );
235 }
236 }
237 result
238 }
239
240 fn create_checkpoint_inner(
241 &self,
242 flush_data: impl FnOnce(&Path, Timestamp) -> StorageResult<CheckpointData>,
243 timestamp: Timestamp,
244 trigger_reason: CheckpointTriggerReason,
245 ) -> StorageResult<CheckpointStats> {
246 let guard = self
247 .enter_state(crate::engine::persistence_coordinator::PersistenceState::Checkpointing)?;
248 self.create_checkpoint_inner_with_guard(guard, flush_data, timestamp, trigger_reason)
249 }
250
251 fn create_checkpoint_inner_with_guard(
252 &self,
253 _guard: crate::engine::persistence_coordinator::PersistenceStateGuard,
254 flush_data: impl FnOnce(&Path, Timestamp) -> StorageResult<CheckpointData>,
255 timestamp: Timestamp,
256 trigger_reason: CheckpointTriggerReason,
257 ) -> StorageResult<CheckpointStats> {
258 let start = Instant::now();
259
260 let wal_lsn = {
261 match &self.wal_manager {
262 Some(wal) => {
263 wal.read().sync()?;
264 wal.read().durable_lsn()
265 }
266 None => Lsn::ZERO,
267 }
268 };
269
270 log::info!(
271 "Creating checkpoint at timestamp {}, LSN {}",
272 timestamp,
273 wal_lsn
274 );
275
276 let checkpoint = {
277 let published_sequence = Self::latest_published_sequence(&self.manifest_manager)?;
278 let mut cm = self.checkpoint_manager.write();
279 cm.adopt_published_sequence(published_sequence)
280 .map_err(|error| {
281 StorageError::db_error(format!(
282 "Failed to reconcile published checkpoints: {}",
283 error
284 ))
285 })?;
286 cm.prepare_checkpoint(timestamp, wal_lsn).map_err(|e| {
287 linkrs_core::StorageError::db_error(format!("Failed to create checkpoint: {}", e))
288 })?
289 };
290
291 self.emit_storage_event(
292 crate::engine::storage_events::StorageEvent::CheckpointStarted {
293 sequence: checkpoint.seq,
294 },
295 );
296
297 let checkpoint_dir = self
298 .config
299 .checkpoint_dir
300 .join(format!("checkpoint_{}", checkpoint.seq));
301 let temporary_dir = self
302 .config
303 .checkpoint_dir
304 .join(format!("checkpoint_{}.tmp", checkpoint.seq));
305 if temporary_dir.exists() {
306 std::fs::remove_dir_all(&temporary_dir)?;
307 }
308 if checkpoint_dir.exists() {
309 let is_published = self
310 .manifest_manager
311 .load(checkpoint.seq)
312 .map_err(|error| {
313 StorageError::db_error(format!(
314 "Failed to inspect existing checkpoint {}: {}",
315 checkpoint.seq, error
316 ))
317 })?
318 .is_some();
319 if is_published {
320 return Err(StorageError::invalid_operation(format!(
321 "checkpoint {} already exists",
322 checkpoint.seq
323 )));
324 }
325
326 std::fs::remove_dir_all(&checkpoint_dir)?;
327 sync_directory(&self.config.checkpoint_dir)?;
328 }
329 std::fs::create_dir(&temporary_dir)?;
330
331 self.fail_if_injected(
332 crate::engine::persistence_coordinator::PersistenceFaultPoint::CheckpointRedoBefore,
333 )?;
334 let data = flush_data(&temporary_dir, timestamp)?;
335 self.fail_if_injected(
336 crate::engine::persistence_coordinator::PersistenceFaultPoint::CheckpointIntentMid,
337 )?;
338 let files = Self::collect_checkpoint_files(&temporary_dir)?;
339 let incremental_meta_path = temporary_dir.join("incremental.meta");
341 if incremental_meta_path.exists() {
342 if let Ok(bytes) = std::fs::read(&incremental_meta_path) {
343 if let Ok(meta) = serde_json::from_slice::<IncrementalCheckpointMeta>(&bytes) {
344 self.save_checkpoint_metadata_extended(
345 &temporary_dir,
346 &checkpoint,
347 &data,
348 &files,
349 Some(&meta),
350 )?;
351 } else {
353 self.save_checkpoint_metadata(&temporary_dir, &checkpoint, &data, &files)?;
354 }
355 } else {
356 self.save_checkpoint_metadata(&temporary_dir, &checkpoint, &data, &files)?;
357 }
358 } else {
359 self.save_checkpoint_metadata(&temporary_dir, &checkpoint, &data, &files)?;
360 }
361 self.fail_if_injected(
362 crate::engine::persistence_coordinator::PersistenceFaultPoint::CheckpointCommitMid,
363 )?;
364 sync_tree(&temporary_dir)?;
365 std::fs::rename(&temporary_dir, &checkpoint_dir)?;
366 sync_directory(&self.config.checkpoint_dir)?;
367 self.fail_if_injected(
368 crate::engine::persistence_coordinator::PersistenceFaultPoint::CheckpointFsyncAfter,
369 )?;
370
371 let snapshot_created = if self.should_snapshot() {
372 *self.state.write() =
373 crate::engine::persistence_coordinator::PersistenceState::Snapshotting;
374 if let Some(ref snapshot_manager) = self.snapshot_manager {
375 let snapshot_options = SnapshotOptions::default();
376 match snapshot_manager.create_snapshot(
377 crate::engine::snapshot_manager::CreateSnapshotParams {
378 source_dir: checkpoint_dir.clone(),
379 snapshot_id: checkpoint.seq,
380 vertex_count: data.vertex_count,
381 edge_count: data.edge_count,
382 checkpoint_seq: checkpoint.seq,
383 wal_lsn: wal_lsn.into(),
384 options: snapshot_options,
385 },
386 ) {
387 Ok(_) => {
388 *self.last_snapshot_time.write() = Some(SystemTime::now());
389 *self.last_snapshot_error.write() = None;
390 true
391 }
392 Err(e) => {
393 log::error!("Failed to create snapshot: {}", e);
394 *self.last_snapshot_error.write() = Some(e.to_string());
395 false
396 }
397 }
398 } else {
399 false
400 }
401 } else {
402 false
403 };
404
405 self.fail_if_injected(
406 crate::engine::persistence_coordinator::PersistenceFaultPoint::CheckpointVisibilityPublish,
407 )?;
408 match self.publish_checkpoint_manifest(&checkpoint, &data, &checkpoint_dir, wal_lsn) {
409 Ok((snapshot_ts, safe_lsn)) => {
410 self.refresh_checkpoint_watermark(snapshot_ts, safe_lsn);
415 }
416 Err(error) => return Err(error),
417 }
418
419 {
420 let mut cm = self.checkpoint_manager.write();
421 cm.publish_checkpoint(&checkpoint).map_err(|e| {
422 StorageError::db_error(format!("Failed to publish checkpoint: {}", e))
423 })?;
424 }
425
426 if let Some(ref wal) = self.wal_manager {
427 wal.read().set_checkpoint_seq(checkpoint.seq)?;
428 }
429
430 self.mark_checkpointed(wal_lsn);
431
432 let safe_wal_lsn = if let Some(ref wal) = self.wal_manager {
433 let manifest_safe_lsn = self.manifest_manager.latest_safe_lsn().map_err(|error| {
434 StorageError::db_error(format!("Failed to get safe LSN: {}", error))
435 })?;
436 let outbox_safe_lsn = self
437 .outbox_frontier_provider
438 .read()
439 .as_ref()
440 .map(|provider| provider())
441 .transpose()?
442 .flatten()
443 .map(|lsn| lsn.get());
444 let safe_lsn = outbox_safe_lsn.map_or(manifest_safe_lsn.get(), |outbox| {
445 manifest_safe_lsn.get().min(outbox)
446 });
447 let safe_wal_lsn = Lsn::new(safe_lsn);
448 wal.read().truncate(safe_wal_lsn)?;
449 if safe_wal_lsn != Lsn::ZERO {
450 self.notify_wal_truncated(safe_wal_lsn.into());
451 }
452 safe_wal_lsn
453 } else {
454 wal_lsn
455 };
456
457 if snapshot_created {
458 self.emit_storage_event(
459 crate::engine::storage_events::StorageEvent::SnapshotCreated {
460 sequence: checkpoint.seq,
461 },
462 );
463 }
464
465 let stats = CheckpointStats {
466 checkpoint_id: checkpoint.seq,
467 data_flushed: data.data_size,
468 wal_truncated: safe_wal_lsn.into(),
469 duration: start.elapsed(),
470 snapshot_created,
471 checkpoint_seq: checkpoint.seq,
472 data_files_created: files.len(),
473 bytes_flushed: data.data_size,
474 wal_files_truncated: if safe_wal_lsn != Lsn::ZERO { 1 } else { 0 },
475 trigger_reason,
476 snapshot_timestamp: timestamp,
477 };
478 *self.last_checkpoint_stats.write() = Some(stats.clone());
479
480 log::info!(
481 "Checkpoint {} completed in {:?}",
482 checkpoint.seq,
483 stats.duration
484 );
485
486 Ok(stats)
487 }
488
489 pub fn load_latest_checkpoint(
490 &self,
491 load_data: impl FnOnce(&Path) -> StorageResult<()>,
492 ) -> StorageResult<Option<CheckpointInfo>> {
493 self.fail_if_injected(
494 crate::engine::persistence_coordinator::PersistenceFaultPoint::RecoveryScan,
495 )?;
496 let checkpoints_dir = &self.config.checkpoint_dir;
497
498 if !checkpoints_dir.exists() {
499 return Ok(None);
500 }
501
502 let published_manifest = self
503 .manifest_manager
504 .load_latest()
505 .map_err(StorageError::db_error)?;
506 let Some(published_manifest) = published_manifest else {
507 let manifest_dir = self.config.checkpoint_dir.join("manifests");
508 if manifest_dir.exists() {
509 if let Ok(mut entries) = std::fs::read_dir(&manifest_dir) {
510 if entries.any(|e| e.is_ok()) {
511 return Err(StorageError::deserialize_error(
512 "Published manifest exists but failed validation. \
513 Checkpoint files or manifests may be corrupted."
514 .to_string(),
515 ));
516 }
517 }
518 }
519 return Ok(None);
520 };
521 let published_checkpoint = published_manifest.storage_snapshot.checkpoint_seq;
522 let mut checkpoints: Vec<(u64, PathBuf)> = std::fs::read_dir(checkpoints_dir)?
523 .filter_map(|entry| entry.ok())
524 .filter_map(|entry| {
525 let path = entry.path();
526 if path.is_dir() && path.join("checkpoint.meta").is_file() {
527 let name = path.file_name()?.to_string_lossy();
528 if name.starts_with("checkpoint_") {
529 let id: u64 = name.trim_start_matches("checkpoint_").parse().ok()?;
530 (id == published_checkpoint).then_some((id, path))
531 } else {
532 None
533 }
534 } else {
535 None
536 }
537 })
538 .collect();
539
540 checkpoints.sort_by_key(|(id, _)| std::cmp::Reverse(*id));
541
542 if let Some((_, checkpoint_path)) = checkpoints.first() {
543 let (info, files) = self.load_checkpoint_metadata(checkpoint_path)?;
544 Self::verify_checkpoint_files(checkpoint_path, &files)?;
545
546 load_data(checkpoint_path)?;
547
548 if let Some(ref wal) = self.wal_manager {
549 wal.read().set_checkpoint_seq(info.checkpoint_id)?;
550 wal.read().set_recovery_baseline_lsn(info.lsn)?;
551 }
552
553 return Ok(Some(info));
554 }
555
556 Ok(None)
557 }
558
559 fn publish_checkpoint_manifest(
569 &self,
570 checkpoint: &linkrs_transaction::wal::Checkpoint,
571 data: &CheckpointData,
572 checkpoint_dir: &Path,
573 wal_lsn: Lsn,
574 ) -> StorageResult<(Timestamp, linkrs_core::types::CommitLsn)> {
575 let storage_snapshot_ref = CheckpointManifest::storage_snapshot_from_directory(
576 checkpoint_dir,
577 checkpoint.seq,
578 data.vertex_count,
579 data.edge_count,
580 )
581 .map_err(|error| {
582 StorageError::db_error(format!(
583 "Failed to build storage snapshot reference: {}",
584 error
585 ))
586 })?;
587
588 let storage_lsn = linkrs_core::types::CommitLsn::new(wal_lsn.into());
589 let work_dir = self
590 .config
591 .data_dir
592 .parent()
593 .unwrap_or(&self.config.data_dir);
594 let outbox_snapshot = linkrs_sync::find_latest_snapshot_at_or_before(
595 &work_dir.join("outbox_snapshots"),
596 storage_lsn.get(),
597 )
598 .map(|snapshot| CheckpointManifest::outbox_snapshot_from(&snapshot));
599 let index_manifests = Self::collect_index_manifest_refs(checkpoint_dir)?;
600
601 let outbox_enabled = work_dir.join("outbox/outbox.sqlite").exists()
602 || work_dir.join("outbox_snapshots").is_dir();
603
604 let manifest = if outbox_enabled {
605 CheckpointManifest::new_with_outbox(
606 checkpoint.seq,
607 storage_lsn,
608 storage_snapshot_ref,
609 outbox_snapshot,
610 index_manifests,
611 )
612 .map_err(StorageError::db_error)?
613 } else {
614 CheckpointManifest::new(
615 checkpoint.seq,
616 storage_lsn,
617 storage_snapshot_ref,
618 outbox_snapshot,
619 index_manifests,
620 )
621 .map_err(StorageError::db_error)?
622 };
623 let manifest = manifest
624 .with_snapshot_timestamp(checkpoint.timestamp)
625 .map_err(StorageError::db_error)?;
626
627 self.manifest_manager.publish(&manifest).map_err(|error| {
628 StorageError::db_error(format!("Failed to publish manifest: {}", error))
629 })?;
630
631 log::info!(
632 "Published checkpoint manifest {} with safe LSN {}",
633 checkpoint.seq,
634 manifest.safe_lsn
635 );
636
637 Ok((checkpoint.timestamp, manifest.safe_lsn))
638 }
639
640 fn collect_index_manifest_refs(root: &Path) -> StorageResult<Vec<IndexManifestRef>> {
641 fn visit(directory: &Path, refs: &mut Vec<IndexManifestRef>) -> StorageResult<()> {
642 for entry in std::fs::read_dir(directory)? {
643 let path = entry?.path();
644 if path.is_dir() {
645 visit(&path, refs)?;
646 continue;
647 }
648 if path
649 .parent()
650 .and_then(Path::file_name)
651 .and_then(|name| name.to_str())
652 != Some("native_index_manifests")
653 || path.extension().and_then(|extension| extension.to_str()) != Some("json")
654 {
655 continue;
656 }
657 let manifest = crate::index::manifest::IndexManifest::load(&path)?;
658 let bytes = std::fs::read(&path)?;
659 refs.push(IndexManifestRef {
660 space_id: manifest.space_id,
661 index_id: manifest.index_id,
662 generation: manifest.generation,
663 path,
664 size_bytes: bytes.len() as u64,
665 checksum: crc32fast::hash(&bytes),
666 });
667 }
668 Ok(())
669 }
670
671 let mut refs = Vec::new();
672 visit(root, &mut refs)?;
673 refs.sort_by_key(|reference| reference.index_id);
674 Ok(refs)
675 }
676}