fix: reconcile live scheduled resume cancellation
This commit is contained in:
@@ -1408,6 +1408,87 @@ async def test_shutdown_timeout_fences_inflight_resume_and_restart_keeps_decisio
|
||||
await service.stop()
|
||||
|
||||
|
||||
async def test_caller_cancellation_reconciles_and_frees_capacity(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Caller cancellation fails one ambiguous resume without fencing capacity."""
|
||||
_gate_open.clear()
|
||||
root = tmp_path / "store"
|
||||
server = build_local_static_workflow_server(root, extra_sources=_gate_sources())
|
||||
await _seed(
|
||||
server,
|
||||
"ask_gate",
|
||||
"ask_gate.default",
|
||||
_interrupt_then_gate_plan("ask_gate"),
|
||||
["submitted"],
|
||||
)
|
||||
await _seed(server, "const", "const.default", _constant_plan("const"), ["ok"])
|
||||
store = FileScheduleStore(root)
|
||||
store.create_schedule(_one_shot("asker", "ask_gate.default", datetime.now(UTC)))
|
||||
service = _scheduler(server, capacity=1, auto_tick=False)
|
||||
try:
|
||||
await service.start()
|
||||
async with asyncio.timeout(20):
|
||||
while not _run_ids(root):
|
||||
await service.poll_once(datetime.now(UTC))
|
||||
await asyncio.sleep(0.02)
|
||||
ask_id = _run_ids(root)[0]
|
||||
await _wait_for(
|
||||
lambda: FileRunStore(root).get_run(ask_id).status.value == "interrupted"
|
||||
)
|
||||
|
||||
resume_task = asyncio.create_task(
|
||||
server.api.resume_run(
|
||||
run_id=ask_id, resume_payload={}, resume_outcome="submitted"
|
||||
)
|
||||
)
|
||||
await _wait_for(lambda: ask_id in service._live_resumes)
|
||||
await _wait_for(lambda: FileRunStore(root).is_executing(ask_id))
|
||||
await _wait_for(
|
||||
lambda: (
|
||||
(attempt := FileRunStore(root).get_resume_attempt(ask_id)) is not None
|
||||
and attempt.state == "ACTIVE"
|
||||
)
|
||||
)
|
||||
resume_task.cancel()
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await resume_task
|
||||
|
||||
assert service.running
|
||||
assert not service._live_resumes
|
||||
cancelled = FileRunStore(root).get_run(ask_id)
|
||||
assert cancelled.status.value == "failed"
|
||||
assert not FileRunStore(root).is_executing(ask_id)
|
||||
attempt = FileRunStore(root).get_resume_attempt(ask_id)
|
||||
assert attempt is not None and attempt.state == "ACTIVE"
|
||||
assert len(_entries(root, "asker", "failed")) == 1
|
||||
|
||||
with pytest.raises(ValueError, match="ambiguous active resume attempt"):
|
||||
await server.api.resume_run(
|
||||
run_id=ask_id, resume_payload={}, resume_outcome="submitted"
|
||||
)
|
||||
assert _entries(root, "asker", "completed") == []
|
||||
|
||||
store.create_schedule(_one_shot("after", "const.default", datetime.now(UTC)))
|
||||
async with asyncio.timeout(20):
|
||||
while True:
|
||||
await service.poll_once(datetime.now(UTC))
|
||||
if len(_run_ids(root)) == 2:
|
||||
after_id = [rid for rid in _run_ids(root) if rid != ask_id][0]
|
||||
if FileRunStore(root).get_run(after_id).status.value == "completed":
|
||||
break
|
||||
await asyncio.sleep(0.02)
|
||||
assert await _kinds(root, "asker") == [
|
||||
"admitted",
|
||||
"interrupted",
|
||||
"failed",
|
||||
]
|
||||
assert await _kinds(root, "after") == ["admitted", "completed"]
|
||||
finally:
|
||||
_gate_open.set()
|
||||
await service.stop()
|
||||
|
||||
|
||||
async def test_scheduled_resume_requested_during_drain_rejected(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user