Merge pull request #409 from mcdonc/fix/sync-state-write-crash
fix: sync_state write failure after mark_succeeded should not crash worker
This commit is contained in:
commit
49cb6e7591
2 changed files with 64 additions and 16 deletions
|
|
@ -244,25 +244,37 @@ class WorkerPool:
|
||||||
self._breaker.record_success()
|
self._breaker.record_success()
|
||||||
if was_open:
|
if was_open:
|
||||||
logger.info("Worker pool breaker closed after successful probe")
|
logger.info("Worker pool breaker closed after successful probe")
|
||||||
if job.op is JobOp.DELETE:
|
try:
|
||||||
await self._sync.delete(job.source_id, job.uri)
|
if job.op is JobOp.DELETE:
|
||||||
# A successful DELETE resolves any earlier UPSERT failures for the
|
await self._sync.delete(job.source_id, job.uri)
|
||||||
# same (source_id, uri): the document is gone, the original error
|
# A successful DELETE resolves any earlier UPSERT failures for
|
||||||
# is no longer actionable, the DLQ entry is just visual noise.
|
# the same (source_id, uri): the document is gone, the original
|
||||||
pruned = await self._jobs.prune_dead(job.source_id, job.uri)
|
# error is no longer actionable, the DLQ entry is visual noise.
|
||||||
if pruned:
|
pruned = await self._jobs.prune_dead(job.source_id, job.uri)
|
||||||
logger.info(
|
if pruned:
|
||||||
"Pruned %d dead job(s) for %s after successful DELETE",
|
logger.info(
|
||||||
pruned,
|
"Pruned %d dead job(s) for %s after successful DELETE",
|
||||||
|
pruned,
|
||||||
|
job.uri,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await self._sync.upsert(
|
||||||
|
job.source_id,
|
||||||
job.uri,
|
job.uri,
|
||||||
|
revision=result.revision,
|
||||||
|
content_hash=result.content_hash,
|
||||||
|
ingested=True,
|
||||||
)
|
)
|
||||||
else:
|
except Exception:
|
||||||
await self._sync.upsert(
|
# The job is already marked succeeded — the document was ingested
|
||||||
job.source_id,
|
# correctly. A sync_state write failure means the next sweep may
|
||||||
|
# redundantly re-ingest this URI, but that's better than crashing
|
||||||
|
# the worker and blocking the rest of the queue.
|
||||||
|
logger.exception(
|
||||||
|
"Job %s succeeded but sync_state write failed for %s; "
|
||||||
|
"next sweep may re-ingest",
|
||||||
|
job.id,
|
||||||
job.uri,
|
job.uri,
|
||||||
revision=result.revision,
|
|
||||||
content_hash=result.content_hash,
|
|
||||||
ingested=True,
|
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
"Job %s succeeded in %.2fs: %s", job.id, time.monotonic() - started, job.uri
|
"Job %s succeeded in %.2fs: %s", job.id, time.monotonic() - started, job.uri
|
||||||
|
|
|
||||||
|
|
@ -645,6 +645,42 @@ async def test_breaker_ignores_permanent_errors(client, jobs, sync):
|
||||||
assert pool.breaker_consecutive_failures == 0
|
assert pool.breaker_consecutive_failures == 0
|
||||||
|
|
||||||
|
|
||||||
|
# --- sync_state write resilience ---
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_sync_state_write_failure_does_not_crash_worker(
|
||||||
|
client, jobs, sync, monkeypatch
|
||||||
|
):
|
||||||
|
"""If the sync_state write fails after mark_succeeded, the worker should
|
||||||
|
log the error and continue rather than crashing. The job is already
|
||||||
|
marked succeeded — a stale sync_state just means a redundant re-ingest
|
||||||
|
on the next sweep."""
|
||||||
|
client.create_document_from_source.return_value = Document(
|
||||||
|
id="d", content="x", uri="u", metadata={"md5": "m", "source_revision": "r"}
|
||||||
|
)
|
||||||
|
await jobs.enqueue("src", "u", JobOp.UPSERT)
|
||||||
|
|
||||||
|
original_upsert = sync.upsert
|
||||||
|
|
||||||
|
async def _failing_upsert(*args, **kwargs):
|
||||||
|
# Only fail for ingested=True (the post-success write)
|
||||||
|
if kwargs.get("ingested"):
|
||||||
|
raise OSError("disk full")
|
||||||
|
return await original_upsert(*args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr(sync, "upsert", _failing_upsert)
|
||||||
|
|
||||||
|
pool = _pool(client, jobs, sync)
|
||||||
|
# drain_once should complete without raising
|
||||||
|
processed = await pool.drain_once()
|
||||||
|
assert processed == 1
|
||||||
|
|
||||||
|
# Job should still be marked succeeded
|
||||||
|
listed = await jobs.list_jobs(status=JobStatus.SUCCEEDED)
|
||||||
|
assert len(listed) == 1
|
||||||
|
|
||||||
|
|
||||||
# --- reaper ---
|
# --- reaper ---
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue