| 497 | } |
| 498 | |
| 499 | async fn save_workflow_checkpoint( |
| 500 | &self, |
| 501 | workflow_id: &str, |
| 502 | checkpoint: &WorkflowCheckpoint, |
| 503 | ) -> Result<()> { |
| 504 | let path = self.workflow_checkpoint_path(workflow_id); |
| 505 | if let Some(parent) = path.parent() { |
| 506 | fs::create_dir_all(parent).await.with_context(|| { |
| 507 | format!( |
| 508 | "Failed to create workflow checkpoint directory: {}", |
| 509 | parent.display() |
| 510 | ) |
| 511 | })?; |
| 512 | } |
| 513 | let json = serde_json::to_string_pretty(checkpoint).with_context(|| { |
| 514 | format!("Failed to serialize workflow checkpoint for {workflow_id}") |
| 515 | })?; |
| 516 | |
| 517 | // Crash-atomic write (temp + fsync + rename) — same rationale as |
| 518 | // loop checkpoints: a truncated checkpoint would fail to resume. |
| 519 | let unique_suffix = format!( |
| 520 | "{}.{}", |
| 521 | std::time::SystemTime::now() |
| 522 | .duration_since(std::time::UNIX_EPOCH) |
| 523 | .map(|d| d.as_nanos()) |
| 524 | .unwrap_or(0), |
| 525 | std::process::id() |
| 526 | ); |
| 527 | let temp_path = path.with_extension(format!("json.{}.tmp", unique_suffix)); |
| 528 | let mut file = fs::File::create(&temp_path).await.with_context(|| { |
| 529 | format!( |
| 530 | "Failed to create workflow checkpoint temp file: {}", |
| 531 | temp_path.display() |
| 532 | ) |
| 533 | })?; |
| 534 | file.write_all(json.as_bytes()) |
| 535 | .await |
| 536 | .with_context(|| format!("Failed to write workflow checkpoint for {workflow_id}"))?; |
| 537 | file.sync_all() |
| 538 | .await |
| 539 | .with_context(|| format!("Failed to fsync workflow checkpoint for {workflow_id}"))?; |
| 540 | fs::rename(&temp_path, &path).await.with_context(|| { |
| 541 | format!( |
| 542 | "Failed to rename workflow checkpoint into place: {}", |
| 543 | path.display() |
| 544 | ) |
| 545 | })?; |
| 546 | Ok(()) |
| 547 | } |
| 548 | |
| 549 | async fn load_workflow_checkpoint( |
| 550 | &self, |