recovery_orchestration_acceptance.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. import asyncio
  2. import json
  3. import subprocess
  4. import sys
  5. from pathlib import Path
  6. from uuid import uuid4
  7. import psutil
  8. from retrieval_evaluation import publish_dataset
  9. from test_after_sales import scenario # noqa: F401
  10. from zhifu.modules.knowledge.vector import connection
  11. def execute(command):
  12. with Path(".work/sequential-recovery.log").open("wb") as log:
  13. subprocess.run(command, check=True, timeout=300, stdout=log, stderr=subprocess.STDOUT)
  14. async def test_sequential_recovery_of_real_sources(scenario): # noqa: F811
  15. collection = "zhifu_recovery_sequence_" + uuid4().hex
  16. output = Path(".work/sequential-recovery.json")
  17. try:
  18. await publish_dataset(scenario)
  19. await asyncio.to_thread(
  20. execute,
  21. [sys.executable, "scripts/recovery_exercise.py", "--target-collection", collection,
  22. "--tenant-id", str(scenario["tenant"]), "--maximum-documents", "12",
  23. "--maximum-chunks", "64", "--timeout-seconds", "240", "--output", str(output)],
  24. )
  25. report = json.loads(await asyncio.to_thread(output.read_text, encoding="utf-8"))
  26. assert report["status"] == "VERIFIED"
  27. assert set(report["phases"]) == {"mysql", "redis", "knowledge"}
  28. assert report["phases"]["knowledge"]["documents_restored"] == 12
  29. assert report["phases"]["knowledge"]["tenant_id"] == str(scenario["tenant"])
  30. assert report["temporary_containers_remaining"] == []
  31. assert report["generation_attempts"] == 0
  32. assert report["business_targets_switched"] is False
  33. assert all(row["checksum_equal"] for row in report["phases"]["mysql"]["databases"].values())
  34. assert report["phases"]["redis"]["sha256_verified"] is True
  35. finally:
  36. with connection() as (client, business):
  37. assert collection != business
  38. if await asyncio.to_thread(client.has_collection, collection):
  39. await asyncio.to_thread(client.drop_collection, collection)
  40. await asyncio.to_thread(
  41. client.delete, business, filter=f'tenant == "{scenario["tenant"].hex}"'
  42. )
  43. async def test_timeout_terminates_phase_process_tree_and_containers():
  44. output = Path(".work/sequential-recovery-timeout.json")
  45. result = await asyncio.to_thread(
  46. subprocess.run,
  47. [sys.executable, "scripts/recovery_exercise.py", "--target-collection",
  48. "zhifu_recovery_timeout_" + uuid4().hex, "--timeout-seconds", "60",
  49. "--phase-timeout-seconds", "10", "--output", str(output)],
  50. capture_output=True, timeout=90,
  51. )
  52. await asyncio.to_thread(
  53. Path(".work/sequential-recovery-timeout.log").write_bytes, result.stdout + result.stderr
  54. )
  55. assert result.returncode != 0
  56. report = json.loads(await asyncio.to_thread(output.read_text, encoding="utf-8"))
  57. assert report["status"] == "INCOMPLETE"
  58. assert report["phases"] == {}
  59. assert report["temporary_containers_cleaned"], "需要在真实恢复容器运行期间验证超时"
  60. assert report["temporary_containers_remaining"] == []
  61. identifiers = report["terminated_process_ids"]
  62. assert identifiers and all(not psutil.pid_exists(identifier) for identifier in identifiers)
  63. # 阶段进程终止后再次检查,不能出现父工具清理之后新创建的恢复容器。
  64. await asyncio.sleep(2)
  65. remaining = await asyncio.to_thread(
  66. subprocess.run,
  67. ["docker", "ps", "-aq", "--filter", "label=zhifu.recovery-run=" + report["run_id"]],
  68. check=True, capture_output=True, timeout=15,
  69. )
  70. assert not remaining.stdout.strip()