Stage 07 · Manifest 发布崩溃矩阵¶
目标¶
在 manifest rename 前后注入崩溃,钉死其线性化点。
动手任务¶
从stage-06开始,通过 CrashOnce 覆盖 DiskStorage.persist_bucket 的每个发布边界。 行为必须留在下列源码同构边界中;不要先复制补丁。
交付文件¶
tests/test_storage.py
自查¶
-
本阶段的可见性或状态迁移由谁负责?
答案
rename 前恢复到旧状态;rename 后看到完整新状态。
-
如果绕过新边界,哪个测试会最先失败?
答案
阅读
tests.txt,找出最窄的新节点,并说出它覆盖的公开调用。
通关命令¶
uv run pytest -q $(cat journey/stages/07-publication-crash-matrix/tests.txt)
对应真实 S3 的一课¶
rename 前恢复到旧状态;rename 后看到完整新状态。
教材¶
完成后可运行 git checkout stage-07 对照你的结果。
先做后看:stage.patch
diff --git a/tests/test_storage.py b/tests/test_storage.py
index c96a7a6..5faad97 100644
--- a/tests/test_storage.py
+++ b/tests/test_storage.py
@@ -30,3 +30,69 @@ def test_restart_restores_versions_bodies_and_counter(tmp_path: Path) -> None:
assert reopened.get_object("b", "k", version_id=first.version_id).body == b"one"
assert second.version_id != first.version_id
+
+
+def test_crash_before_manifest_publish_leaves_old_state(tmp_path: Path) -> None:
+ MiniS3(tmp_path).create_bucket("b")
+ crashing = MiniS3(
+ tmp_path,
+ counter=SequenceCounter(10),
+ crash_injector=CrashOnce("before_manifest_publish"),
+ )
+
+ with pytest.raises(InjectedCrash):
+ crashing.put_object("b", "new", b"value")
+
+ reopened = MiniS3(tmp_path)
+ with pytest.raises(NoSuchKey):
+ reopened.get_object("b", "new")
+ assert not list(tmp_path.rglob("*.tmp-*"))
+ assert not list(tmp_path.rglob("e00000010.*"))
+
+
+@pytest.mark.parametrize(
+ "crash_point",
+ [
+ "after_object_directory_create",
+ "after_object_directory_parent_fsync",
+ "before_manifest_publish",
+ ],
+)
+def test_crash_before_manifest_publication_matrix_leaves_old_state(
+ tmp_path: Path,
+ crash_point: str,
+) -> None:
+ MiniS3(tmp_path).create_bucket("b")
+ crashing = MiniS3(
+ tmp_path,
+ counter=SequenceCounter(10),
+ crash_injector=CrashOnce(crash_point),
+ )
+
+ with pytest.raises(InjectedCrash):
+ crashing.put_object("b", "new", b"value")
+
+ reopened = MiniS3(tmp_path)
+ with pytest.raises(NoSuchKey):
+ reopened.get_object("b", "new")
+ assert not list(tmp_path.rglob("*.tmp-*"))
+ assert not list(tmp_path.rglob("e00000010.*"))
+
+
+def test_crash_after_manifest_publish_exposes_complete_new_state(
+ tmp_path: Path,
+) -> None:
+ MiniS3(tmp_path).create_bucket("b")
+ crashing = MiniS3(
+ tmp_path,
+ counter=SequenceCounter(10),
+ crash_injector=CrashOnce("after_manifest_publish"),
+ )
+
+ with pytest.raises(InjectedCrash):
+ crashing.put_object("b", "new", b"value")
+
+ reopened = MiniS3(tmp_path)
+ visible = reopened.get_object("b", "new")
+ assert visible.body == b"value"
+ assert visible.etag == '"2063c1608d6e0baf80249c42e2be5804"'