test_refund_worker_reporting.py 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. from datetime import UTC, datetime
  2. from threading import Event
  3. from typing import Any
  4. from zbt.commands.worker_reporting import RefundWorkerReporter
  5. from zbt.domains.surrender.monitoring import InMemoryWorkerRegistry, WorkerInstance
  6. NOW = datetime(2026, 7, 29, 11, 0, tzinfo=UTC)
  7. def instance() -> WorkerInstance:
  8. return WorkerInstance(
  9. instance_id="worker-reporting-01",
  10. worker_name="refund",
  11. mode="parallel",
  12. host="node-a",
  13. pid=33001,
  14. status="RUNNING",
  15. started_at=NOW,
  16. last_heartbeat_at=NOW,
  17. last_poll_at=None,
  18. last_processed_at=None,
  19. total_processed=0,
  20. last_error=None,
  21. )
  22. def test_reporter_records_worker_poll_and_graceful_stop() -> None:
  23. registry = InMemoryWorkerRegistry()
  24. reporter = RefundWorkerReporter(
  25. registry,
  26. instance(),
  27. clock=lambda: NOW,
  28. heartbeat_interval_seconds=60,
  29. )
  30. class SuccessfulWorker:
  31. def run_once(self, limit: int = 20) -> dict[str, Any]:
  32. assert limit == 20
  33. return {"processed": 2, "items": []}
  34. worker = reporter.wrap(SuccessfulWorker())
  35. result = worker.run_once()
  36. reporter.stop()
  37. reported = registry.list_instances("refund")[0]
  38. assert result["processed"] == 2
  39. assert reported.total_processed == 2
  40. assert reported.last_poll_at == NOW
  41. assert reported.status == "STOPPED"
  42. def test_reporter_records_unhandled_worker_error() -> None:
  43. registry = InMemoryWorkerRegistry()
  44. reporter = RefundWorkerReporter(
  45. registry,
  46. instance(),
  47. clock=lambda: NOW,
  48. heartbeat_interval_seconds=60,
  49. )
  50. class FailedWorker:
  51. def run_once(self, limit: int = 20) -> dict[str, Any]:
  52. del limit
  53. raise RuntimeError("数据库连接中断")
  54. worker = reporter.wrap(FailedWorker())
  55. try:
  56. worker.run_once()
  57. except RuntimeError:
  58. pass
  59. else:
  60. raise AssertionError("工作器异常不应被监控层吞掉")
  61. reporter.stop(error="数据库连接中断")
  62. reported = registry.list_instances("refund")[0]
  63. assert reported.status == "ERROR"
  64. assert reported.last_error == "数据库连接中断"
  65. def test_reporter_refreshes_heartbeat_while_worker_is_idle() -> None:
  66. registry = InMemoryWorkerRegistry()
  67. heartbeat_seen = Event()
  68. clock_calls = 0
  69. def clock() -> datetime:
  70. nonlocal clock_calls
  71. clock_calls += 1
  72. if clock_calls >= 1:
  73. heartbeat_seen.set()
  74. return NOW
  75. reporter = RefundWorkerReporter(
  76. registry,
  77. instance(),
  78. clock=clock,
  79. heartbeat_interval_seconds=0.001,
  80. )
  81. assert heartbeat_seen.wait(0.5)
  82. reporter.stop()
  83. assert clock_calls >= 1