sched: extract typed preparer/dispatcher seams; test impl out of production (F6)
This commit is contained in:
@@ -4,12 +4,19 @@ from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from tests.scheduling.controlled import (
|
||||
DictDeployments,
|
||||
ScriptedDispatcher,
|
||||
fixture_environment,
|
||||
)
|
||||
from wf_artifacts.runs.models import StoredRunStatus
|
||||
from wf_artifacts.runs.store import FileRunStore
|
||||
from wf_scheduling.calendar import OneShotSource
|
||||
from wf_scheduling.models import Schedule
|
||||
from wf_scheduling.poll import SCAN_CAP, Scheduler
|
||||
from wf_scheduling.prepare import SchedulePreparer
|
||||
from wf_scheduling.store import FileScheduleStore
|
||||
|
||||
UTC_TZ = UTC
|
||||
@@ -41,21 +48,6 @@ class PeriodicSource:
|
||||
return self.start + n * self.period
|
||||
|
||||
|
||||
class OneShotSource:
|
||||
def __init__(self, at: datetime) -> None:
|
||||
self.at = at
|
||||
self.next_calls = 0
|
||||
self.prev_calls = 0
|
||||
|
||||
def next_after(self, instant: datetime) -> datetime | None:
|
||||
self.next_calls += 1
|
||||
return self.at if instant < self.at else None
|
||||
|
||||
def prev_before(self, instant: datetime) -> datetime | None:
|
||||
self.prev_calls += 1
|
||||
return self.at if instant > self.at else None
|
||||
|
||||
|
||||
def _sched_model(sid: str, start_hint: str = "cron", **kw: Any) -> Schedule:
|
||||
now = ts(2026, 9, 8, 12, 0)
|
||||
base: dict[str, Any] = {
|
||||
@@ -74,21 +66,27 @@ def _harness(
|
||||
tmp_path: Path,
|
||||
*,
|
||||
capacity: int = 4,
|
||||
outcomes: dict[str, str] | None = None,
|
||||
script: dict | None = None,
|
||||
deployments: dict[str, dict] | None = None,
|
||||
) -> tuple[Scheduler, FileScheduleStore, FileRunStore, dict]:
|
||||
sched_store = FileScheduleStore(tmp_path / "sched")
|
||||
run_store = FileRunStore(tmp_path / "runs")
|
||||
sources: dict = {}
|
||||
preparer = SchedulePreparer(
|
||||
DictDeployments(
|
||||
deployments
|
||||
if deployments is not None
|
||||
else {"dep-1": {"rev": 1, "required": []}}
|
||||
),
|
||||
fixture_environment,
|
||||
)
|
||||
sched = Scheduler(
|
||||
schedule_store=sched_store,
|
||||
run_store=run_store,
|
||||
sources=sources,
|
||||
capacity=capacity,
|
||||
outcomes=outcomes,
|
||||
deployments=deployments
|
||||
if deployments is not None
|
||||
else {"dep-1": {"rev": 1, "required": []}},
|
||||
preparer=preparer,
|
||||
dispatcher=ScriptedDispatcher(script),
|
||||
)
|
||||
return sched, sched_store, run_store, sources
|
||||
|
||||
@@ -119,7 +117,7 @@ def test_overlap_skip_blocks_and_late_drops() -> None:
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
sched, store, runs, sources = _harness(root, capacity=4, outcomes={"*": "hang"})
|
||||
sched, store, runs, sources = _harness(root, capacity=4, script={"*": "hang"})
|
||||
t0 = ts(2026, 9, 8, 12, 0)
|
||||
_add(
|
||||
sched,
|
||||
@@ -161,7 +159,7 @@ def test_latest_coalesces_to_one_candidate_and_no_double_admit() -> None:
|
||||
# Capacity returns at 13:00 while 13:00 is also due: exactly one
|
||||
# admission for 13:00, 12:00 superseded, never both.
|
||||
sched.capacity = 4
|
||||
sched.outcomes = {"*": "complete"}
|
||||
cast(ScriptedDispatcher, sched.dispatcher).script = {"*": "complete"}
|
||||
sched.poll(ts(2026, 9, 8, 13, 0))
|
||||
admitted = [r for r in _history(store, "h") if r["kind"] == "admitted"]
|
||||
assert len(admitted) == 1
|
||||
@@ -176,7 +174,7 @@ def test_parallel_limits_and_interrupted_slots() -> None:
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
sched, store, runs, sources = _harness(root, capacity=4, outcomes={"*": "hang"})
|
||||
sched, store, runs, sources = _harness(root, capacity=4, script={"*": "hang"})
|
||||
t0 = ts(2026, 9, 8, 12, 0)
|
||||
_add(
|
||||
sched,
|
||||
@@ -216,7 +214,7 @@ def test_pause_is_not_downtime() -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
sched, store, runs, sources = _harness(
|
||||
root, capacity=4, outcomes={"*": "complete"}
|
||||
root, capacity=4, script={"*": "complete"}
|
||||
)
|
||||
_add(
|
||||
sched,
|
||||
@@ -244,7 +242,7 @@ def test_pause_is_not_downtime() -> None:
|
||||
|
||||
def test_long_downtime_is_bounded(tmp_path: Path) -> None:
|
||||
sched, store, runs, sources = _harness(
|
||||
tmp_path, capacity=4, outcomes={"*": "complete"}
|
||||
tmp_path, capacity=4, script={"*": "complete"}
|
||||
)
|
||||
src = PeriodicSource(timedelta(minutes=1), ts(2023, 9, 8, 12, 0))
|
||||
_add(sched, store, sources, "m", src, ts(2023, 9, 8, 12, 0), misfire="latest")
|
||||
@@ -259,7 +257,7 @@ def test_long_downtime_is_bounded(tmp_path: Path) -> None:
|
||||
|
||||
|
||||
def test_fairness_slow_schedule_not_starved(tmp_path: Path) -> None:
|
||||
sched, store, runs, sources = _harness(tmp_path, capacity=1, outcomes={"*": "hang"})
|
||||
sched, store, runs, sources = _harness(tmp_path, capacity=1, script={"*": "hang"})
|
||||
t0 = ts(2026, 9, 8, 12, 0)
|
||||
_add(
|
||||
sched,
|
||||
@@ -288,6 +286,6 @@ def test_capacity_wait_then_expire_for_skip(tmp_path: Path) -> None:
|
||||
assert sched.poll(t0) == {"a": "admit:held-undecided"}
|
||||
assert store.get_candidate("a") is None
|
||||
sched.capacity = 1
|
||||
sched.outcomes = {"*": "complete"}
|
||||
cast(ScriptedDispatcher, sched.dispatcher).script = {"*": "complete"}
|
||||
sched.poll(t0 + timedelta(seconds=30))
|
||||
assert len([r for r in _history(store, "a") if r["kind"] == "admitted"]) == 1
|
||||
|
||||
Reference in New Issue
Block a user