sched: add occurrence identity and UTC helpers (T02)
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"""Occurrence identity and UTC persistence helpers.
|
||||
|
||||
Occurrence identity is ``(schedule_id, resolved UTC instant)``. The
|
||||
``occurrence_id`` string is derived deterministically from that pair as
|
||||
``f"{schedule_id}|{resolved_utc.isoformat()}"`` with the instant normalized
|
||||
to UTC; the definition's cron expression, time-zone name, and admission
|
||||
snapshots are retained separately, but no finer intended-wall-time
|
||||
provenance is claimed beyond what the calendar library supplies.
|
||||
|
||||
Clock use is split: a monotonic clock drives sleeping, and a wall clock
|
||||
drives calendar eligibility. Clock rollback cannot admit an already-consumed
|
||||
instant again: any resolved instant ``<= consumed_through`` is never
|
||||
re-admitted.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
|
||||
UTC = timezone.utc
|
||||
|
||||
|
||||
def _require_aware(instant: datetime, *, op: str) -> datetime:
|
||||
if instant.tzinfo is None or instant.utcoffset() is None:
|
||||
raise ValueError(f"{op} requires an aware datetime with an offset")
|
||||
return instant
|
||||
|
||||
|
||||
def occurrence_id(schedule_id: str, resolved_utc: datetime) -> str:
|
||||
"""Derive a deterministic occurrence id from schedule id and UTC instant."""
|
||||
_require_aware(resolved_utc, op="occurrence_id")
|
||||
normalized = resolved_utc.astimezone(UTC)
|
||||
return f"{schedule_id}|{normalized.isoformat()}"
|
||||
|
||||
|
||||
def scheduled_at_str(resolved: datetime) -> str:
|
||||
"""Serialize a resolved instant as a UTC RFC 3339 string."""
|
||||
_require_aware(resolved, op="scheduled_at")
|
||||
return resolved.astimezone(UTC).isoformat()
|
||||
|
||||
|
||||
def is_already_consumed(resolved_utc: datetime, consumed_through: datetime) -> bool:
|
||||
"""Return whether ``resolved_utc`` was already decided (rollback guard)."""
|
||||
_require_aware(resolved_utc, op="is_already_consumed")
|
||||
_require_aware(consumed_through, op="is_already_consumed")
|
||||
return resolved_utc.astimezone(UTC) <= consumed_through.astimezone(UTC)
|
||||
|
||||
|
||||
def wall_now_utc() -> datetime:
|
||||
"""Return the current wall-clock time in UTC for calendar eligibility."""
|
||||
return datetime.now(UTC)
|
||||
|
||||
|
||||
def monotonic_ns() -> int:
|
||||
"""Return a monotonic timestamp for sleeping between polls."""
|
||||
return time.monotonic_ns()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Occurrence:
|
||||
"""One resolved schedule occurrence with its durable identity."""
|
||||
|
||||
schedule_id: str
|
||||
scheduled_at: datetime
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_require_aware(self.scheduled_at, op="Occurrence")
|
||||
object.__setattr__(self, "scheduled_at", self.scheduled_at.astimezone(UTC))
|
||||
|
||||
@property
|
||||
def occurrence_id(self) -> str:
|
||||
"""Return the deterministic ``(schedule_id, resolved UTC)`` identity."""
|
||||
return occurrence_id(self.schedule_id, self.scheduled_at)
|
||||
|
||||
def to_binding_dict(self) -> dict[str, str]:
|
||||
"""Return the schedule environment exposed to occurrence expressions."""
|
||||
return {
|
||||
"schedule_id": self.schedule_id,
|
||||
"occurrence_id": self.occurrence_id,
|
||||
"scheduled_at": scheduled_at_str(self.scheduled_at),
|
||||
}
|
||||
Reference in New Issue
Block a user