sched: bind ownership to store composition; stable recovery failure; schema-checked prepare; guarded settle (R4 wave 2)

This commit is contained in:
lda
2026-09-08 18:36:09 +07:00 Verified
parent b9d8eb9d16
commit 3f1d5d158b
16 changed files with 1299 additions and 60 deletions
+23 -4
View File
@@ -139,13 +139,20 @@ class Scheduler:
def _require_ownership(self) -> None:
"""Reject schedule mutation/dispatch without proven live ownership.
Runs before any store write or dispatcher side effect: without a
held lock this process cannot prove exclusive ownership, so polling
Runs before any store write or dispatcher side effect: the held
lock must cover the actual schedule and run store composition, not
merely be held on some unrelated directory. Without covering
ownership this process cannot prove exclusive ownership, so polling
or administering schedules would risk double admission.
"""
if self.ownership is None or not self.ownership.held:
ownership = self.ownership
if ownership is None or not ownership.covers(
getattr(self.schedule_store, "root", None),
getattr(self.run_store, "root", None),
):
raise SecondOwnerError(
"scheduler ownership is required before polling or mutating schedules"
"scheduler ownership must cover the schedule and run stores "
"before polling or mutating schedules"
)
# -- helpers ------------------------------------------------------
@@ -431,6 +438,12 @@ class Scheduler:
:class:`wf_core.RunState`. Persists through the shared lifecycle
boundary, clears the executing mark, and records terminal history.
Never re-invokes the dispatcher.
An admitted status alone is not enough: pending, never-dispatched
work is also admitted. Settlement requires the durable executing
transition and refuses contradictory pending/executing state (a
crash between the transition writes owns that run now, not this
caller); it also refuses runs that already stopped.
"""
self._require_ownership()
from wf_api.run_lifecycle import persist_stopped_run
@@ -448,6 +461,12 @@ class Scheduler:
raise BlockedSchedule(f"settle missing run view: {run_id!r}") from exc
if self._status_value(record) != "admitted":
raise BlockedSchedule(f"settle non-admitted run: {run_id!r}")
if self.run_store.is_pending_dispatch(run_id):
raise BlockedSchedule(
f"settle contradictory pending/executing run: {run_id!r}"
)
if not self.run_store.is_executing(run_id):
raise BlockedSchedule(f"settle without executing transition: {run_id!r}")
stopped = persist_stopped_run(
store=self.run_store,
environment=admission.environment,