sched: require proven ownership at poll/admin/recovery entries (F5)

This commit is contained in:
lda
2026-09-08 11:24:40 +07:00 Verified
parent 15bd472bdc
commit 993ed07fd3
7 changed files with 293 additions and 22 deletions
+49 -22
View File
@@ -15,6 +15,7 @@ from wf_artifacts.runs.store import FileRunStore
from wf_scheduling import recovery as sched_recovery
from wf_scheduling.calendar import OneShotSource
from wf_scheduling.models import Schedule
from wf_scheduling.ownership import SchedulerOwnership
from wf_scheduling.poll import Scheduler
from wf_scheduling.prepare import SchedulePreparer
from wf_scheduling.store import FileScheduleStore
@@ -57,9 +58,16 @@ def test_recovery_materializes_missing_view_as_pending(tmp_path: Path) -> None:
resolved_input={},
max_steps=None,
)
diags = sched_recovery.recover(
schedule_store=sched_store, run_store=run_store, now=ts(2026, 9, 8, 12, 0)
)
ownership = SchedulerOwnership(tmp_path / "sched", owner="test").acquire()
try:
diags = sched_recovery.recover(
schedule_store=sched_store,
run_store=run_store,
now=ts(2026, 9, 8, 12, 0),
ownership=ownership,
)
finally:
ownership.release()
assert any("pending-dispatch" in d for d in diags)
assert run_store.get_run(admission.id).status.value == "admitted"
assert sched_recovery._is_pending(run_store, admission.id)
@@ -88,9 +96,16 @@ def test_recovery_fails_abandoned_admitted_without_replay(tmp_path: Path) -> Non
schedule_revision=1,
)
materialize_admitted_view(store=run_store, admission=admission)
diags = sched_recovery.recover(
schedule_store=sched_store, run_store=run_store, now=ts(2026, 9, 8, 12, 0)
)
ownership = SchedulerOwnership(tmp_path / "sched", owner="test").acquire()
try:
diags = sched_recovery.recover(
schedule_store=sched_store,
run_store=run_store,
now=ts(2026, 9, 8, 12, 0),
ownership=ownership,
)
finally:
ownership.release()
assert any("failed-closed" in d for d in diags)
assert run_store.get_run(admission.id).status.value == "failed"
@@ -118,25 +133,37 @@ def test_recovery_never_executes_pending_until_poll(tmp_path: Path) -> None:
schedule_id="a",
schedule_revision=1,
)
diags = sched_recovery.recover(
schedule_store=sched_store, run_store=run_store, now=ts(2026, 9, 8, 12, 0)
)
ownership = SchedulerOwnership(tmp_path / "sched", owner="test").acquire()
try:
diags = sched_recovery.recover(
schedule_store=sched_store,
run_store=run_store,
now=ts(2026, 9, 8, 12, 0),
ownership=ownership,
)
finally:
ownership.release()
assert any("pending-dispatch" in d for d in diags)
# Recovery itself produced no terminal history; the poll sweep dispatches.
assert sched_store.list_occurrences("a", limit=100)["total"] == 0
sources = {"a": OneShotSource(ts(2026, 9, 8, 12, 0))}
sched = Scheduler(
schedule_store=sched_store,
run_store=run_store,
sources=sources, # type: ignore[arg-type]
capacity=4,
preparer=SchedulePreparer(
DictDeployments({"dep-1": {"rev": 1, "required": []}}),
fixture_environment,
),
dispatcher=ScriptedDispatcher({"*": "complete"}),
)
sched_store.save_consumed("a", ts(2026, 9, 8, 12, 0))
sched.poll(ts(2026, 9, 8, 12, 1))
ownership = SchedulerOwnership(tmp_path / "sched", owner="test").acquire()
try:
sched = Scheduler(
schedule_store=sched_store,
run_store=run_store,
sources=sources, # type: ignore[arg-type]
capacity=4,
preparer=SchedulePreparer(
DictDeployments({"dep-1": {"rev": 1, "required": []}}),
fixture_environment,
),
dispatcher=ScriptedDispatcher({"*": "complete"}),
ownership=ownership,
)
sched_store.save_consumed("a", ts(2026, 9, 8, 12, 0))
sched.poll(ts(2026, 9, 8, 12, 1))
finally:
ownership.release()
assert run_store.get_run(admission.id).status.value == "completed"
assert not sched_recovery._is_pending(run_store, admission.id)