_DueTracker counted a task that had never run from 0 on the loop clock, which is host uptime, so every run_on_startup=False task (hourly data cleanup with the Docker log prune, the weekly report check, alert reconcile, agent age, Ansible schedules) fired on the first tick after each deploy, while agents were flushing their backlog. Agent ingest was slow or dropped for a few minutes after every deploy. start_scheduler now calls tracker.begin(): such a task is first due one interval after the loop starts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
76 lines
2.9 KiB
Python
76 lines
2.9 KiB
Python
"""Unit tests for the scheduler's poll-overlap guard (_DueTracker).
|
|
|
|
The tracker is pure (clock passed in) so we can assert the overlap policy
|
|
deterministically: a task whose prior run is still in flight is never re-fired,
|
|
and a skipped task isn't penalised — it runs on the next tick once it finishes.
|
|
This is the rail that stops a hung poll from stacking overlapping runs (which
|
|
would amplify any per-poll resource/fd use).
|
|
"""
|
|
from steward.core.scheduler import ScheduledTask, _DueTracker
|
|
|
|
|
|
def _task(name: str, interval: int) -> ScheduledTask:
|
|
return ScheduledTask(name=name, coro_factory=lambda: None, interval_seconds=interval)
|
|
|
|
|
|
def test_due_only_after_interval_elapses():
|
|
t = _task("a", 60)
|
|
tr = _DueTracker()
|
|
assert tr.due([t], now=59) == [] # 59 < 60
|
|
assert tr.due([t], now=60) == [t] # interval elapsed
|
|
|
|
|
|
def test_in_flight_task_is_skipped_even_when_due():
|
|
t = _task("a", 0) # due every tick
|
|
tr = _DueTracker()
|
|
tr.mark_started(t, now=0)
|
|
assert tr.due([t], now=100) == [] # still running → skipped
|
|
tr.mark_done("a")
|
|
assert tr.due([t], now=100) == [t] # finished → eligible again
|
|
|
|
|
|
def test_skip_does_not_advance_last_run_so_it_retries():
|
|
t = _task("a", 10)
|
|
tr = _DueTracker()
|
|
tr.mark_started(t, now=0)
|
|
assert tr.due([t], now=100) == [] # due but in flight
|
|
assert tr.last_run["a"] == 0 # not advanced by the skip
|
|
tr.mark_done("a")
|
|
assert tr.due([t], now=100) == [t] # retried promptly after completion
|
|
|
|
|
|
def test_mark_started_advances_last_run_and_marks_in_flight():
|
|
t = _task("a", 10)
|
|
tr = _DueTracker()
|
|
tr.mark_started(t, now=50)
|
|
assert tr.last_run["a"] == 50
|
|
assert tr.in_flight == {"a"}
|
|
assert tr.due([t], now=55) == [] # 5 < 10 (not yet due)
|
|
assert tr.due([t], now=61) == [] # due by interval, but still in flight
|
|
tr.mark_done("a")
|
|
assert tr.due([t], now=61) == [t]
|
|
|
|
|
|
def test_independent_tasks_do_not_block_each_other():
|
|
a, b = _task("a", 0), _task("b", 0)
|
|
tr = _DueTracker()
|
|
tr.mark_started(a, now=0) # a hangs
|
|
assert tr.due([a, b], now=10) == [b] # b still fires
|
|
|
|
|
|
def test_a_task_not_run_on_startup_waits_one_interval_after_boot():
|
|
"""#5431: the loop's clock is host uptime, far past every interval. A task
|
|
marked not to run on startup used to count from 0 and fire on the first
|
|
tick anyway; it now waits a whole interval from when the loop began."""
|
|
boot = 1_000_000.0
|
|
hourly = _task("cleanup", 3600)
|
|
eager = ScheduledTask(name="poll", coro_factory=lambda: None, interval_seconds=60,
|
|
run_on_startup=True)
|
|
tr = _DueTracker()
|
|
tr.begin([hourly, eager], now=boot)
|
|
assert tr.due([hourly], now=boot + 1) == []
|
|
assert tr.due([hourly], now=boot + 3599) == []
|
|
assert tr.due([hourly], now=boot + 3600) == [hourly]
|
|
assert "poll" not in tr.last_run # started by start_scheduler itself
|
|
|