Skip to content

Fix/worker oom kill - #48

Merged
Josh-Uvi merged 6 commits into
mainfrom
fix/worker-oom-kill
Aug 4, 2026
Merged

Fix/worker oom kill#48
Josh-Uvi merged 6 commits into
mainfrom
fix/worker-oom-kill

Conversation

@Josh-Uvi

@Josh-Uvi Josh-Uvi commented Aug 4, 2026

Copy link
Copy Markdown
Owner

fix: stop infinite OOM retry loop and harden job recovery

The task_acks_late=True setting caused the Redis broker to re-queue
un-acked tasks when a worker process was SIGKILL'd, resulting in an
infinite retry loop (17+ attempts). autoretry_for's max_retries did
not limit these broker-level re-queues.

Changes:

  • Set task_acks_late=False: tasks are acknowledged immediately, so the
    broker does NOT re-queue them when the worker process dies. Task-level
    exceptions are still retried via autoretry_for (max_retries=1).
  • Remove task_reject_on_worker_lost (only relevant with task_acks_late=True).
  • Keep the task_failure signal handler as immediate back-stop: it fires
    in the main process when a worker is lost, marking the job as 'failed'
    in the DB within seconds instead of waiting for the 5-min stale sweeper.
  • Lower worker_max_memory_per_child from 2 GiB to 1 GiB.
  • Remove ONNX model preload at worker startup (lazy load via lru_cache).
  • Lower SEGMENTER_MODEL_INPUT_SIZE from 256 to 128 (75% less input tensor
    memory per page).
  • Lower task_time_limit from 600 to 120s and task_soft_time_limit from
    540 to 100s (pipeline completes in ~22s, 120s is generous ceiling).
  • Reduce max_retries from 3 to 1 (OOM retries just OOM again).
  • Add SEGMENTER_MODEL_INPUT_SIZE to .env.example and docs/operations.md.
  • Add task_failure to celery signals py.typed stub.

The Celery worker was being SIGKILL'd (signal 9) during pipeline execution
because the ONNX segmentation model + image processing exceeded the
container's memory limit. Since SIGKILL cannot be caught, the job's
exception handler never ran, leaving it stuck in 'processing' forever.

Changes:
- docker-compose.yml: Add mem_limit (4g) and memswap_limit (6g) to the
  worker service, with env overrides (WORKER_MEM_LIMIT/WORKER_MEMSWAP_LIMIT)
- celery_app.py: Add task_acks_late + task_reject_on_worker_lost so tasks
  are re-queued when a worker dies; add task_time_limit (600s) and
  task_soft_time_limit (540s); add worker_max_memory_per_child (3 GiB) to
  recycle worker processes before OOM
- segmenter.py: Process ONNX inference one page at a time instead of
  batching all pages, reducing peak memory from N*page to 1*page
- cleanup.py: Add cleanup_stale_jobs Beat task (every 5 min) that marks
  jobs stuck in 'processing' longer than JOB_STALE
The Celery worker was being SIGKILL'd (signal 9) during pipeline executs
-because the ONNX segmentation model + image processing exceeded the
contaxacontainer's memory limit. Since SIGKILL cannot be caught, the job'ERexception handler never ran, leaving it sment new env vars, stale-jo
Changes:
- docker-compose.yml: Add mem_limit (4g) and memswap_limit Upd- dockeke  worker service, with env overrides (WORKER_MEM_LIMIT/WORKER_MEMSWAPal- celery_app.py: Add task_a
- Add typings/onnxruntime/__init__.pyi stub for InferenceSession, get_inputs,
  and run methods to satisfy strict Pylance/pyright type checking
- Add ellipsis body to SemanticSegmenter Protocol method
- Cast HoughLinesP result to np.ndarray | None since cv2 stubs claim it
  never returns None but it can at runtime
- Cast _require_image_array return value to fully-typed np.ndarray
- Add pyright: ignore[reportPrivateUsage] comments to test file for
  accessing the private _cleanup_stale_jobs_async helper
The task_acks_late=True setting caused the Redis broker to re-queue
un-acked tasks when a worker process was SIGKILL'd, resulting in an
infinite retry loop (17+ attempts). autoretry_for's max_retries did
not limit these broker-level re-queues.

Changes:
- Set task_acks_late=False: tasks are acknowledged immediately, so the
  broker does NOT re-queue them when the worker process dies. Task-level
  exceptions are still retried via autoretry_for (max_retries=1).
- Remove task_reject_on_worker_lost (only relevant with task_acks_late=True).
- Keep the task_failure signal handler as immediate back-stop: it fires
  in the main process when a worker is lost, marking the job as 'failed'
  in the DB within seconds instead of waiting for the 5-min stale sweeper.
- Lower worker_max_memory_per_child from 2 GiB to 1 GiB.
- Remove ONNX model preload at worker startup (lazy load via lru_cache).
- Lower SEGMENTER_MODEL_INPUT_SIZE from 256 to 128 (75% less input tensor
  memory per page).
- Lower task_time_limit from 600 to 120s and task_soft_time_limit from
  540 to 100s (pipeline completes in ~22s, 120s is generous ceiling).
- Reduce max_retries from 3 to 1 (OOM retries just OOM again).
- Add SEGMENTER_MODEL_INPUT_SIZE to .env.example and docs/operations.md.
- Add task_failure to celery signals py.typed stub.
@Josh-Uvi Josh-Uvi self-assigned this Aug 4, 2026
@Josh-Uvi Josh-Uvi added bug Something isn't working area:backend Backend (FastAPI / Celery) code labels Aug 4, 2026
@Josh-Uvi
Josh-Uvi merged commit ada80ca into main Aug 4, 2026
9 checks passed
@Josh-Uvi
Josh-Uvi deleted the fix/worker-oom-kill branch August 4, 2026 22:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:backend Backend (FastAPI / Celery) code bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant