sched: server scheduler wiring, config section, transport lifespan, CLI flag (T12)
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
"""Server scheduler composition (T12): config mapping and store wiring.
|
||||
|
||||
Scheduling stays off unless the config section or the CLI flag enables
|
||||
it. When enabled, the service runs over the server's own stores behind
|
||||
one composition lock, and the transport lifespan owns start/stop.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from wf_config import WorkflowConfigFile
|
||||
from wf_scheduling.lifecycle import DrainReport, SchedulerServiceConfig
|
||||
from wf_scheduling.ownership import SchedulerOwnership
|
||||
from wf_server.context import build_local_static_workflow_server
|
||||
from wf_server.scheduling import (
|
||||
build_scheduler_service,
|
||||
scheduler_lifespan,
|
||||
server_scheduler_config,
|
||||
)
|
||||
|
||||
|
||||
def test_server_scheduler_config_disabled_by_default() -> None:
|
||||
assert server_scheduler_config(None, False) is None
|
||||
|
||||
bare = WorkflowConfigFile.model_validate({"version": 1})
|
||||
assert server_scheduler_config(bare, False) is None
|
||||
|
||||
file_disabled = WorkflowConfigFile.model_validate(
|
||||
{"version": 1, "server": {"scheduler": {"enabled": False}}}
|
||||
)
|
||||
assert server_scheduler_config(file_disabled, False) is None
|
||||
|
||||
|
||||
def test_server_scheduler_config_flag_enables_defaults_without_config() -> None:
|
||||
resolved = server_scheduler_config(None, True)
|
||||
|
||||
assert resolved is not None
|
||||
assert resolved.poll_interval_s == 1.0
|
||||
assert resolved.capacity == 4
|
||||
assert resolved.drain_grace_s == 30.0
|
||||
assert resolved.auto_tick is True
|
||||
|
||||
|
||||
def test_server_scheduler_config_maps_file_values() -> None:
|
||||
config = WorkflowConfigFile.model_validate(
|
||||
{
|
||||
"version": 1,
|
||||
"server": {
|
||||
"scheduler": {
|
||||
"enabled": True,
|
||||
"poll_interval_s": 2.5,
|
||||
"max_concurrent_runs": 8,
|
||||
"drain_grace_s": 60.0,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
resolved = server_scheduler_config(config, False)
|
||||
|
||||
assert resolved is not None
|
||||
assert resolved.poll_interval_s == 2.5
|
||||
assert resolved.capacity == 8
|
||||
assert resolved.drain_grace_s == 60.0
|
||||
assert resolved.auto_tick is True
|
||||
|
||||
|
||||
def test_server_scheduler_config_flag_overrides_disabled_section() -> None:
|
||||
config = WorkflowConfigFile.model_validate(
|
||||
{
|
||||
"version": 1,
|
||||
"server": {
|
||||
"scheduler": {"enabled": False, "max_concurrent_runs": 2},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
resolved = server_scheduler_config(config, True)
|
||||
|
||||
assert resolved is not None
|
||||
assert resolved.capacity == 2
|
||||
assert resolved.auto_tick is True
|
||||
|
||||
|
||||
def test_build_scheduler_service_wires_server_stores(tmp_path: Path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path)
|
||||
service = build_scheduler_service(server, SchedulerServiceConfig(auto_tick=False))
|
||||
|
||||
assert service.schedule_store.root == server.config.store_root
|
||||
assert (
|
||||
service.schedule_store.schedules_dir == server.config.store_root / "schedules"
|
||||
)
|
||||
assert service.run_store is server.stores.run_store
|
||||
assert service.artifact_store is server.stores.artifact_store
|
||||
assert service.runtime is server.context.runtime
|
||||
assert service.ownership.lock_path == server.config.store_root / "scheduler.lock"
|
||||
|
||||
|
||||
async def test_scheduler_service_start_stop_on_server_stores(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path)
|
||||
service = build_scheduler_service(server, SchedulerServiceConfig(auto_tick=False))
|
||||
|
||||
await service.start()
|
||||
try:
|
||||
assert service.running is True
|
||||
assert service.ownership.covers(
|
||||
service.schedule_store.root, service.run_store.root
|
||||
)
|
||||
finally:
|
||||
report = await service.stop()
|
||||
|
||||
assert isinstance(report, DrainReport)
|
||||
assert service.running is False
|
||||
# The lock is released: a fresh owner can acquire the same composition.
|
||||
probe = SchedulerOwnership(tmp_path, owner="probe")
|
||||
probe.acquire()
|
||||
try:
|
||||
assert probe.held is True
|
||||
finally:
|
||||
probe.release()
|
||||
|
||||
|
||||
async def test_scheduler_lifespan_releases_lock_on_exit(tmp_path: Path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path)
|
||||
resolved = server_scheduler_config(None, True)
|
||||
assert resolved is not None
|
||||
|
||||
async with scheduler_lifespan(server, resolved) as service:
|
||||
assert service.running is True
|
||||
|
||||
probe = SchedulerOwnership(tmp_path, owner="probe")
|
||||
probe.acquire()
|
||||
try:
|
||||
assert probe.held is True
|
||||
finally:
|
||||
probe.release()
|
||||
Reference in New Issue
Block a user