Skip to content

Commit 21d0ad0

Browse files
AlexStocksOmX
andcommitted
Make snapshot installation crash recoverable
Persist staged, paused, rename, reopen, Raft metadata, cleanup, and rollback boundaries so startup can select one authority before admission. Move target replacement out of checkpoint restore and bind recovery to manifest/logical digests and the configured instance count. Constraint: Keep live RocksDB handles until admission is paused and owners drain. Rejected: Delete the live target in PreparedCheckpointRestore or infer authority from directory presence alone. Confidence: High; restart/fault recovery and full Raft/server gates pass. Scope-risk: High; changes snapshot install, startup recovery, and storage replacement ordering. Directive: Execute WP8 Task 4 from the approved Option C plan. Tested: WSL make fmt-check; WSL make lint; WSL cargo test -p raft --all-features; WSL cargo test -p server --all-features. Not-tested: Physical power-loss injection and Windows CI filesystem behavior. Related: #356, #422 Co-authored-by: OmX <omx@oh-my-codex.dev> Signed-off-by: Xin.Zh <alexstocks@foxmail.com>
1 parent 3240f24 commit 21d0ad0

19 files changed

Lines changed: 2448 additions & 391 deletions

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/superpowers/plans/2026-08-07-vector-set-storage-recovery.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,18 +235,22 @@
235235
- Create: `src/raft/tests/snapshot_install_recovery_test.rs`
236236
- Modify: `src/raft/tests/snapshot_roundtrip_test.rs`
237237

238-
- [ ] **步骤 1:写入 restart/fault 失败测试**
238+
- [x] **步骤 1:写入 restart/fault 失败测试**
239239

240240
- `marker_rejects_absolute_or_non_basename_paths`
241241
- `restart_resumes_from_staged_validated`
242+
- `abandoning_partial_staged_install_removes_intent_before_cleanup`
242243
- `restart_resumes_after_storage_paused_before_marker_persisted`
243244
- `storage_paused_closes_live_handles_and_keeps_network_admission_closed`
244245
- `restart_resumes_from_marker_persisted`
245246
- `restart_restores_backup_after_old_renamed_before_new_promoted`
247+
- `rollback_pending_survives_restart_after_backup_restore`
248+
- `rollback_cleanup_pending_survives_partial_cleanup_restart`
246249
- `restart_resumes_after_new_renamed_to_target`
247250
- `restart_reopens_and_verifies_new_storage_before_publication`
248251
- `restart_persists_raft_metadata_before_cleanup`
249252
- `cleanup_pending_survives_restart_and_completes_idempotently`
253+
- `cleanup_pending_survives_partial_backup_deletion_restart`
250254
- `target_and_backup_both_present_with_digest_mismatch_fail_closed`
251255
- `marker_snapshot_metadata_digest_mismatch_fail_closed`
252256
- `marker_root_or_instance_manifest_digest_mismatch_fail_closed`
@@ -256,7 +260,7 @@
256260
cargo test -p raft --test snapshot_install_recovery_test -- --nocapture
257261
```
258262

259-
- [ ] **步骤 2:实现 marker 编排和恢复决策**
263+
- [x] **步骤 2:实现 marker 编排和恢复决策**
260264

261265
`snapshot_install.rs` 定义 `SnapshotInstallPhase``SnapshotInstallMarkerV2``SnapshotInstallLayout``SnapshotInstallRecoveryDecision``persist_phase``recover_snapshot_install``validate_install_layout_and_digests``complete_pending_cleanup`
262266

@@ -274,9 +278,22 @@
274278
-> Complete
275279
```
276280

281+
`MarkerPersisted``OldRenamedToBackup` 尚未完成新库 promotion 时,恢复分支固定为:
282+
283+
```text
284+
MarkerPersisted | OldRenamedToBackup
285+
-> RollbackPending
286+
-> RollbackCleanupPending
287+
-> marker removed
288+
```
289+
290+
**计划偏差:** 原计划只持久化正向安装阶段,但回滚包含 `backup -> target`、删除 stage/pending 文件和删除 marker 等多次独立落盘操作;若回滚本身在任一步骤后崩溃,旧 phase 无法唯一解释磁盘布局。为满足同一 fail-closed/restart-safe 要求,回滚也必须先持久化 `RollbackPending`,恢复旧库后持久化 `RollbackCleanupPending`,再执行可重入清理。
291+
292+
`StagedValidated` 放弃路径尚未触碰 live target,因此必须先持久删除 marker、再清理 disposable stage/pending 文件;清理中途崩溃只允许留下无权威性的孤儿文件,不能留下指向残缺 stage 的恢复意图。正向和回滚 cleanup phase 在进入前完成副本 identity 校验,phase 持久化后只复验权威 target 和待删路径类型,以允许递归删除中途崩溃后的可重入清理。server preflight 同时要求 marker 的 `db_instance_num` 与启动配置一致,任何配置漂移必须在 rename/cleanup 之前失败。
293+
277294
`StoragePaused` 必须在任何 target/backup rename 之前关闭全部 live RocksDB handle、停止 background task 并保持 network admission 关闭。在 `StoragePaused` 后、`MarkerPersisted` 前崩溃时,启动预检必须使用 snapshot/install intent 和未改名的 target 恢复到可重试状态,不得开放未复验的 storage。`PreparedCheckpointRestore::commit` 不再删除 target;所有 target/backup/stage 判定由 marker phase 和 digest 决定。无法确定唯一权威副本时 fail closed。
278295

279-
- [ ] **步骤 3:回归**
296+
- [x] **步骤 3:回归**
280297

281298
```powershell
282299
cargo test -p raft --test snapshot_install_recovery_test -- --nocapture

src/raft/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ prost = "0.14.3"
3737
tonic-prost = "0.14.5"
3838
tokio-stream = "0.1"
3939

40+
[target.'cfg(windows)'.dependencies]
41+
windows-sys.workspace = true
42+
4043
[build-dependencies]
4144
tonic-prost-build = "0.14.6"
4245

src/raft/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ pub mod log_store_rocksdb;
2828
pub mod network;
2929
pub mod node;
3030
pub mod snapshot_archive;
31+
pub mod snapshot_install;
3132
pub mod state_machine;
3233

3334
pub mod raft_proto {

src/raft/src/node.rs

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -449,14 +449,34 @@ mod tests {
449449
let db_path = temp_dir.path().join("data");
450450
let marker_path =
451451
snapshot_install_marker_path(&db_path).expect("test DB path should support a marker");
452+
let digest = "0".repeat(64);
452453
let marker = serde_json::json!({
453-
"version": 2,
454-
"id": "future-snapshot",
455-
"index": 8,
456-
"term": 3,
457-
"db": db_path,
458-
"workdir": temp_dir.path().join("snapshots"),
459-
"instances": 1
454+
"version": 3,
455+
"phase": "staged_validated",
456+
"snapshot_id": "future-snapshot",
457+
"last_log_index": 8,
458+
"last_log_term": 3,
459+
"db_instance_num": 1,
460+
"target_name": "data",
461+
"staged_name": ".restore_temp_future",
462+
"backup_name": ".data.snapshot-install-backup-future",
463+
"pending_snapshot_data_name": ".snapshot-install-future.tar",
464+
"pending_raft_meta_name": ".snapshot-install-future.raft-meta.json",
465+
"pending_checkpoint_meta_name": ".snapshot-install-future.checkpoint-meta.json",
466+
"snapshot_archive_digest": digest,
467+
"raft_metadata_digest": digest,
468+
"checkpoint_metadata_digest": digest,
469+
"old_storage": null,
470+
"new_storage": {
471+
"root_manifest_id": "future-root",
472+
"root_manifest_digest": digest,
473+
"instance_manifests": [{
474+
"instance_id": 0,
475+
"manifest_digest": digest,
476+
"storage_incarnation": 1
477+
}],
478+
"logical_instance_digests": [digest]
479+
}
460480
});
461481
fs::write(
462482
&marker_path,
@@ -483,7 +503,9 @@ mod tests {
483503
Err(error) => error,
484504
};
485505
assert!(
486-
error.to_string().contains("unsupported marker version 2"),
506+
error
507+
.to_string()
508+
.contains("unsupported snapshot install marker version 3, expected 2"),
487509
"unexpected startup refusal: {error}"
488510
);
489511
assert!(

0 commit comments

Comments
 (0)