| 417 | } |
| 418 | |
| 419 | async fn save_loop_checkpoint(&self, run_id: &str, checkpoint: &LoopCheckpoint) -> Result<()> { |
| 420 | let path = self.loop_checkpoint_path(run_id); |
| 421 | if let Some(parent) = path.parent() { |
| 422 | fs::create_dir_all(parent).await.with_context(|| { |
| 423 | format!( |
| 424 | "Failed to create loop checkpoint directory: {}", |
| 425 | parent.display() |
| 426 | ) |
| 427 | })?; |
| 428 | } |
| 429 | let json = serde_json::to_string_pretty(checkpoint) |
| 430 | .with_context(|| format!("Failed to serialize loop checkpoint for run {run_id}"))?; |
| 431 | |
| 432 | // Crash-atomic write: a checkpoint exists precisely to survive a |
| 433 | // process crash, so the write itself must be crash-safe. A plain |
| 434 | // `fs::write` can leave a truncated JSON file if the process dies |
| 435 | // mid-write — which `resume_run` would then fail to parse, |
| 436 | // defeating the whole point. Write to a unique temp file, fsync, |
| 437 | // then atomically rename over the target. |
| 438 | let unique_suffix = format!( |
| 439 | "{}.{}", |
| 440 | std::time::SystemTime::now() |
| 441 | .duration_since(std::time::UNIX_EPOCH) |
| 442 | .map(|d| d.as_nanos()) |
| 443 | .unwrap_or(0), |
| 444 | std::process::id() |
| 445 | ); |
| 446 | let temp_path = path.with_extension(format!("json.{}.tmp", unique_suffix)); |
| 447 | let mut file = fs::File::create(&temp_path).await.with_context(|| { |
| 448 | format!( |
| 449 | "Failed to create checkpoint temp file: {}", |
| 450 | temp_path.display() |
| 451 | ) |
| 452 | })?; |
| 453 | file.write_all(json.as_bytes()) |
| 454 | .await |
| 455 | .with_context(|| format!("Failed to write loop checkpoint for run {run_id}"))?; |
| 456 | file.sync_all() |
| 457 | .await |
| 458 | .with_context(|| format!("Failed to fsync loop checkpoint for run {run_id}"))?; |
| 459 | fs::rename(&temp_path, &path).await.with_context(|| { |
| 460 | format!( |
| 461 | "Failed to rename loop checkpoint into place: {}", |
| 462 | path.display() |
| 463 | ) |
| 464 | })?; |
| 465 | Ok(()) |
| 466 | } |
| 467 | |
| 468 | async fn load_loop_checkpoint(&self, run_id: &str) -> Result<Option<LoopCheckpoint>> { |
| 469 | let path = self.loop_checkpoint_path(run_id); |