health.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. from __future__ import annotations
  2. from datetime import datetime, timedelta
  3. from typing import Any
  4. from supply_infra.config import InfraSettings, get_infra_settings
  5. from supply_infra.db.repositories.pipeline_lock_repo import PipelineLockRepository
  6. from supply_infra.db.repositories.pipeline_run_repo import PipelineRunRepository
  7. from supply_infra.db.session import get_session
  8. from supply_infra.pipeline.dates import china_now
  9. SCHEDULER_HEARTBEAT_KEY = "pipeline:scheduler"
  10. _ACTIVE_STATUSES = {
  11. "queued",
  12. "running",
  13. "cancelling",
  14. "blocked_previous_run",
  15. }
  16. def touch_scheduler_heartbeat(
  17. *,
  18. owner: str,
  19. settings: InfraSettings | None = None,
  20. ) -> None:
  21. active = settings or get_infra_settings()
  22. with get_session() as session:
  23. PipelineLockRepository(session).touch(
  24. lock_key=SCHEDULER_HEARTBEAT_KEY,
  25. owner=owner,
  26. lease_seconds=max(
  27. active.pipeline_lease_seconds,
  28. active.pipeline_heartbeat_seconds * 3,
  29. ),
  30. now=china_now(),
  31. )
  32. def run_alert_level(
  33. *,
  34. status: str,
  35. started_at: datetime | None,
  36. created_at: datetime,
  37. deadline_at: datetime | None,
  38. now: datetime,
  39. settings: InfraSettings,
  40. ) -> str | None:
  41. if status not in _ACTIVE_STATUSES:
  42. return None
  43. if deadline_at is not None:
  44. remaining = deadline_at - now
  45. if remaining <= timedelta(minutes=settings.pipeline_final_warn_before_next_minutes):
  46. return "final_warning"
  47. if remaining <= timedelta(minutes=settings.pipeline_critical_before_next_minutes):
  48. return "critical"
  49. baseline = started_at or created_at
  50. if now - baseline >= timedelta(hours=settings.pipeline_warn_after_hours):
  51. return "warning"
  52. return None
  53. def pipeline_health_snapshot(
  54. settings: InfraSettings | None = None,
  55. ) -> dict[str, Any]:
  56. active = settings or get_infra_settings()
  57. now = china_now()
  58. with get_session() as session:
  59. scheduler_lock = PipelineLockRepository(session).get(SCHEDULER_HEARTBEAT_KEY)
  60. latest = PipelineRunRepository(session).list_recent(limit=1)
  61. latest_run = latest[0] if latest else None
  62. scheduler_heartbeat_at = (
  63. scheduler_lock.heartbeat_at if scheduler_lock is not None else None
  64. )
  65. scheduler_lease_until = (
  66. scheduler_lock.lease_until if scheduler_lock is not None else None
  67. )
  68. latest_snapshot = (
  69. {
  70. "run_id": latest_run.run_id,
  71. "status": latest_run.status,
  72. "started_at": latest_run.started_at,
  73. "created_at": latest_run.created_at,
  74. "deadline_at": latest_run.deadline_at,
  75. }
  76. if latest_run is not None
  77. else None
  78. )
  79. scheduler_heartbeat = (
  80. scheduler_heartbeat_at.isoformat()
  81. if scheduler_heartbeat_at is not None
  82. else None
  83. )
  84. scheduler_running = bool(
  85. active.scheduler_enabled
  86. and scheduler_lease_until is not None
  87. and scheduler_lease_until > now
  88. )
  89. alert_level = None
  90. if latest_snapshot is not None:
  91. alert_level = run_alert_level(
  92. status=latest_snapshot["status"],
  93. started_at=latest_snapshot["started_at"],
  94. created_at=latest_snapshot["created_at"],
  95. deadline_at=latest_snapshot["deadline_at"],
  96. now=now,
  97. settings=active,
  98. )
  99. return {
  100. "scheduler_enabled": active.scheduler_enabled,
  101. "scheduler_running": scheduler_running,
  102. "scheduler_heartbeat_at": scheduler_heartbeat,
  103. "latest_run_id": latest_snapshot["run_id"] if latest_snapshot else None,
  104. "latest_run_status": latest_snapshot["status"] if latest_snapshot else None,
  105. "latest_run_alert_level": alert_level,
  106. "checked_at": now.isoformat(),
  107. }