Merge pull request #405 from mcdonc/fix/run-batch-hang-on-dead-workers

fix: run_batch hangs forever when all workers die
This commit is contained in:
Yiorgis Gozadinos 2026-06-01 17:51:49 +03:00 committed by GitHub
commit 64f2b7b7d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 42 additions and 0 deletions

View file

@ -205,6 +205,15 @@ class IngesterApp:
counts = await self._jobs.counts_by_status()
if not counts.get("queued") and not counts.get("claimed"):
break
if self._pool.live_workers == 0:
outstanding = counts.get("queued", 0) + counts.get("claimed", 0)
logger.error(
"All workers have died with %d outstanding job(s) "
"— aborting batch; stranded jobs will be reaped "
"on next start",
outstanding,
)
break
await asyncio.sleep(0.1)
completed = await self._jobs.counts_by_status_since(started_at)
return BatchReport(

View file

@ -235,6 +235,39 @@ async def test_run_batch_empty_source_returns_immediately(tmp_path, use_client):
client.create_document_from_source.assert_not_awaited()
@pytest.mark.asyncio
async def test_run_batch_aborts_when_all_workers_die(
tmp_path, use_client, monkeypatch, caplog
):
"""If all workers crash with outstanding jobs, run_batch should break
out of the drain loop instead of spinning forever."""
(tmp_path / "a.md").write_text("hello")
client = _mock_client()
use_client(client)
config = _config(tmp_path)
config.ingester.workers.worker_count = 1
# Patch _process to raise an unhandled exception, simulating a hard
# worker crash. _process only catches CancelledError, PermanentError,
# and TransientError — anything else propagates and kills the task.
monkeypatch.setattr(
WorkerPool,
"_process",
AsyncMock(side_effect=Exception("worker crash")),
)
with caplog.at_level("ERROR", logger="haiku.rag.ingester.app"):
report = await asyncio.wait_for(
IngesterApp(config=config, db_path=tmp_path / "db.lancedb").run_batch(),
timeout=10.0,
)
assert report is not None
assert "All workers have died" in caplog.text
async def _wait_until(predicate, *, timeout: float = 5.0):
deadline = asyncio.get_running_loop().time() + timeout
while not predicate():