fix(ansible): runs end cleanly: output read in chunks, the process group always ended, a cancelled task recorded (#5156)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 55s
CI / unit (push) Successful in 1m11s
CI / publish (push) Successful in 53s

- Output is read in 64 KiB chunks and split into lines here; a line past
  64 KiB is cut and marked instead of raising out of readline's limit.
- ansible-playbook runs in its own session; whatever ends the read (finish,
  an error, the task being cancelled) SIGTERMs then SIGKILLs its whole group,
  so no ssh or worker it forked outlives the run.
- A run whose task is cancelled (shutdown) records 'cancelled' with
  finished_at before re-raising, instead of staying 'running'.
- A cancel that lands after the process exited 0 no longer turns a
  successful run into 'cancelled'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 21:08:30 -04:00
co-authored by Claude Opus 5.5
parent 9fdd67a675
commit d2ade9c1c7
3 changed files with 272 additions and 10 deletions
+28
View File
@@ -28,6 +28,34 @@ def test_parse_recap_empty_without_recap():
assert parse_recap(["TASK [x] ***", "ok: [h]"]) == {}
class _Stream:
def __init__(self, chunks):
self.chunks = list(chunks)
async def read(self, _n):
return self.chunks.pop(0) if self.chunks else b""
def _read_lines(chunks, monkeypatch, limit=8):
import asyncio
from steward.ansible import executor
monkeypatch.setattr(executor, "_MAX_LINE_BYTES", limit)
async def _go():
return [line async for line in executor._lines(_Stream(chunks))]
return asyncio.run(_go())
def test_lines_split_across_chunks(monkeypatch):
assert _read_lines([b"ab", b"c\nde", b"f\n", b"tail"], monkeypatch) == [b"abc", b"def", b"tail"]
def test_a_line_past_the_limit_is_cut_and_the_rest_dropped(monkeypatch):
# #5156: a long line can't stall or break the reader; the next line is intact.
got = _read_lines([b"0123456789", b"abcdef", b"gh\nnext\n"], monkeypatch)
assert got == [b"0123456789"[:8] + b" [line cut at 8 bytes]", b"next"]
def test_cancel_run_flags_untracked_run():
rid = "nonexistent-run-id"
assert cancel_run(rid) is True