Skip to content

fix(jobs): prevent Docker job scheduling races under queue pressure - #615

Merged
ironcommit merged 1 commit into
mainfrom
jobs-race/rsadler
Jul 9, 2026
Merged

fix(jobs): prevent Docker job scheduling races under queue pressure#615
ironcommit merged 1 commit into
mainfrom
jobs-race/rsadler

Conversation

@ironcommit

@ironcommit ironcommit commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes job scheduling reliability under queue pressure by introducing admission control to the Docker backend, moving TTL enforcement out of individual backends into the scheduler/reconciler, and adding comprehensive debug logging across the scheduling pipeline.

Changes

Admission control for Docker container starts

  • Added a BoundedSemaphore-based admission gate (_container_start_admission) that limits concurrent container start operations to DOCKER_CONTAINER_START_WORKERS (10)
  • When the admission gate is full, scheduling raises SchedulingDeferred instead of blocking — the step stays in PENDING and is retried on the next scheduler tick
  • Container argument preparation (_prepare_container_args_for_start) is extracted and runs inside the admission gate, so storage setup and image pulls don't block unrelated steps
  • The semaphore is released in a finally block in run_container, ensuring slots are always freed even on failure

TTL enforcement moved to scheduler/reconciler

  • Removed per-backend ttl_seconds_before_active checks from DockerJobBackend.schedule() and SubprocessJobBackend.schedule() — these were racy because the backend could time out a step that the scheduler would then re-queue
  • PENDING steps that already have a running/exited/dead container now route through sync_pending in the reconciler, preventing the TTL path from killing steps that are actually making progress

Scheduling order fix

  • Changed step fetch sort from -created_at (newest first) to created_at (oldest first), so older queued steps are scheduled before newer ones

Debug logging

  • Added structured logger.debug timing spans across the entire scheduling and reconciliation pipeline: fetch, backend sync, status updates, container create, image pull, storage setup, launcher lookup, cancellation checks, and cleanup
  • Added docker_state_debug_fields() helper that extracts Docker container state (exit code, OOM, timestamps, auto-remove config, cleanup TTL) into a dict for structured logging
  • Added parse_docker_timestamp() to safely handle Docker's zero-time sentinel and parse errors
  • Wrapped update_status calls in _update_step_status_with_timing helpers in both scheduler and reconciler for consistent timing and error logging

Cleanup improvements

  • Container cleanup now uses docker_state_debug_fields() for consistent state extraction and logs detailed context for each cleanup decision (immediate removal, TTL retention, skip due to non-terminal state)
  • parse_docker_timestamp handles Docker's 0001-01-01T00:00:00Z sentinel without crashing

Tests

  • Updated Docker backend tests for the new admission semaphore and _prepare_container_args_for_start extraction
  • Updated scheduler tests for SchedulingDeferred handling and sort order change
  • Updated subprocess backend tests to remove TTL enforcement expectations
  • Added SchedulingDeferred to Kubernetes and Volcano backend test mocks

Test plan

  • Unit tests updated for all changed backends and controllers
  • Stress test with concurrent job submissions to verify admission backpressure
  • Verify PENDING steps with existing containers transition correctly through reconciler

Summary by CodeRabbit

  • New Features

    • Added bounded concurrency control for Docker container starts, including queue-delay tracking.
    • Added deferred-scheduling behavior when backend capacity is full.
    • Improved end-to-end scheduling/reconciliation visibility with monotonic timing and richer structured logs.
  • Bug Fixes

    • Prevented premature “before-active” TTL failures before backends accept steps (Docker/Subprocess/Kubernetes/Volcano).
    • Tightened pending sync and updated TTL/cleanup logic using parsed Docker timing; improved handling of Docker kill race (409).
  • Tests

    • Updated and expanded unit/regression coverage for TTL, deferred scheduling, and reconciliation outcomes across backends.

@ironcommit
ironcommit requested review from a team as code owners July 8, 2026 23:05
@ironcommit ironcommit changed the title Fix Docker job scheduling under queue pressure fix(docker): docker job scheduling fails under queue pressure Jul 8, 2026
@github-actions github-actions Bot added the fix label Jul 8, 2026
@ironcommit
ironcommit requested a review from mckornfield July 8, 2026 23:06
@ironcommit ironcommit changed the title fix(docker): docker job scheduling fails under queue pressure fix(jobs): prevent Docker job scheduling races under queue pressure Jul 8, 2026
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title matches the main change: Docker job scheduling is hardened under queue pressure with admission control and deferred scheduling.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jobs-race/rsadler

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
services/core/jobs/src/nmp/core/jobs/controllers/scheduler.py (1)

126-169: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard error-path status writes. _update_step_status_with_timing() re-raises on failure, so the ResourceAllocationError and generic Exception branches can still escape the per-step try and stop the rest of the batch. Wrap those calls like the success path’s stale-409 handling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/core/jobs/src/nmp/core/jobs/controllers/scheduler.py` around lines
126 - 169, The error-path status updates in the scheduler can still raise and
break the per-step loop because `_update_step_status_with_timing()` is called
directly in both the `ResourceAllocationError` and generic `Exception` handlers.
Update the `Scheduler` logic to wrap those calls in the same stale-409 handling
pattern used on the success path, likely within the `except
ResourceAllocationError` and `except Exception` branches of the scheduling loop,
so failures to persist status do not stop processing remaining steps.
🧹 Nitpick comments (2)
services/core/jobs/src/nmp/core/jobs/controllers/backends/docker.py (1)

92-92: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hardcoded worker count, no env override.

DOCKER_CONTAINER_START_WORKERS = 10 is a fixed constant. Given this PR's goal is tuning concurrency under queue pressure, consider making it configurable via env var so it can be tuned per-deployment without a code change.

♻️ Proposed fix
-DOCKER_CONTAINER_START_WORKERS = 10
+DOCKER_CONTAINER_START_WORKERS = int(os.environ.get("DOCKER_CONTAINER_START_WORKERS", "10"))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/core/jobs/src/nmp/core/jobs/controllers/backends/docker.py` at line
92, The worker count in DOCKER_CONTAINER_START_WORKERS is hardcoded, so make it
configurable via an environment variable instead of a fixed 10. Update the
docker backend configuration in the module that defines
DOCKER_CONTAINER_START_WORKERS to read from env with a sensible default, and
ensure the code paths that use this constant continue to reference the same
symbol so deployments can tune concurrency without code changes.
services/core/jobs/src/nmp/core/jobs/controllers/reconciler.py (1)

121-221: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate status-update-with-timing helper across scheduler and reconciler.

_update_step_status_with_timing here mirrors the same-named helper added to scheduler.py (per graph context), differing only in the identifying kwargs (provider/profile vs phase) and both wrapping update_status with monotonic timing + success/failure debug/warning logs. Consider extracting a shared utility (e.g., a free function or mixin taking generic extra context) to avoid maintaining two near-identical implementations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/core/jobs/src/nmp/core/jobs/controllers/reconciler.py` around lines
121 - 221, The _update_step_status_with_timing helper in reconciler duplicates
the same timing-and-logging wrapper already present in scheduler, so consolidate
both into a shared utility or mixin that wraps update_status once and accepts
generic extra context. Refactor the reconciler’s _update_step_status_with_timing
and the scheduler counterpart to call the shared helper, preserving the existing
success/failure logging and timing behavior while only passing their specific
context fields (provider/profile or phase).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@services/core/jobs/src/nmp/core/jobs/controllers/scheduler.py`:
- Around line 126-169: The error-path status updates in the scheduler can still
raise and break the per-step loop because `_update_step_status_with_timing()` is
called directly in both the `ResourceAllocationError` and generic `Exception`
handlers. Update the `Scheduler` logic to wrap those calls in the same stale-409
handling pattern used on the success path, likely within the `except
ResourceAllocationError` and `except Exception` branches of the scheduling loop,
so failures to persist status do not stop processing remaining steps.

---

Nitpick comments:
In `@services/core/jobs/src/nmp/core/jobs/controllers/backends/docker.py`:
- Line 92: The worker count in DOCKER_CONTAINER_START_WORKERS is hardcoded, so
make it configurable via an environment variable instead of a fixed 10. Update
the docker backend configuration in the module that defines
DOCKER_CONTAINER_START_WORKERS to read from env with a sensible default, and
ensure the code paths that use this constant continue to reference the same
symbol so deployments can tune concurrency without code changes.

In `@services/core/jobs/src/nmp/core/jobs/controllers/reconciler.py`:
- Around line 121-221: The _update_step_status_with_timing helper in reconciler
duplicates the same timing-and-logging wrapper already present in scheduler, so
consolidate both into a shared utility or mixin that wraps update_status once
and accepts generic extra context. Refactor the reconciler’s
_update_step_status_with_timing and the scheduler counterpart to call the shared
helper, preserving the existing success/failure logging and timing behavior
while only passing their specific context fields (provider/profile or phase).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: d7a3109b-2bd4-429e-bfe2-e3d27cc2afe2

📥 Commits

Reviewing files that changed from the base of the PR and between ae7e9fb and ce8a399.

📒 Files selected for processing (10)
  • services/core/jobs/src/nmp/core/jobs/controllers/backends/docker.py
  • services/core/jobs/src/nmp/core/jobs/controllers/backends/exceptions.py
  • services/core/jobs/src/nmp/core/jobs/controllers/backends/subprocess.py
  • services/core/jobs/src/nmp/core/jobs/controllers/reconciler.py
  • services/core/jobs/src/nmp/core/jobs/controllers/scheduler.py
  • services/core/jobs/tests/controllers/test_docker_backend.py
  • services/core/jobs/tests/controllers/test_kubernetes_backend.py
  • services/core/jobs/tests/controllers/test_scheduler.py
  • services/core/jobs/tests/controllers/test_subprocess_backend.py
  • services/core/jobs/tests/controllers/test_volcano_backend.py
💤 Files with no reviewable changes (1)
  • services/core/jobs/src/nmp/core/jobs/controllers/backends/subprocess.py

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 23427/30608 76.5% 61.3%
Integration Tests 13692/29288 46.8% 19.8%

Comment thread services/core/jobs/src/nmp/core/jobs/controllers/backends/docker.py
Comment thread services/core/jobs/src/nmp/core/jobs/controllers/backends/docker.py
Comment thread services/core/jobs/src/nmp/core/jobs/controllers/backends/docker.py Outdated
Comment thread services/core/jobs/src/nmp/core/jobs/controllers/scheduler.py
@ironcommit
ironcommit force-pushed the jobs-race/rsadler branch from ce8a399 to 5e6487f Compare July 9, 2026 04:04
Signed-off-by: Ryan S <267728323+ironcommit@users.noreply.github.com>
@ironcommit
ironcommit force-pushed the jobs-race/rsadler branch from 5e6487f to ca85989 Compare July 9, 2026 17:27
@ironcommit
ironcommit enabled auto-merge July 9, 2026 17:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
services/core/jobs/src/nmp/core/jobs/controllers/backends/docker.py (1)

684-714: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clean up resources on cancellation exits.

Line 702 allocates task/config volumes before cancellation checks. Lines 867 and 1046 can return without cleanup; Line 1046 can also leave a just-created created container, which cleanup_steps() never scans.

Proposed direction
     def _run_container_in_thread(self, step: PlatformJobStepWithContext, container_args: dict):
         status_details = {}
         status = PlatformJobStatus.PENDING.value
+        task_id = container_args.get("labels", {}).get(JOB_TASK_ID_LABEL)
+        created_container = False
@@
         if self.cancel_scheduling(step):
+            if task_id:
+                self.cleanup_task_storage_volumes(step.workspace, step.job, task_id)
             logger.debug(
@@
-                container = self._client.containers.create(**container_args)
+                container = self._client.containers.create(**container_args)
+                created_container = True
@@
-                    container = self._client.containers.create(**container_args)
+                    container = self._client.containers.create(**container_args)
+                    created_container = True
@@
         if self.cancel_scheduling(step):
+            if created_container:
+                self.cleanup_container(container)
+            if task_id:
+                self.cleanup_task_storage_volumes(step.workspace, step.job, task_id)
             logger.debug(

Also applies to: 866-876, 1045-1056, 1503-1515

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/core/jobs/src/nmp/core/jobs/controllers/backends/docker.py` around
lines 684 - 714, Ensure cancellation/early-exit paths in the Docker backend
clean up any resources already created by _prepare_container_args_for_start and
related launch flow. In the container startup path, add cleanup before returning
on cancellation at the points that currently exit from the main start logic, and
make sure a just-created container is removed/destroyed even if it never reaches
the normal cleanup scan used by cleanup_steps(). Use the existing helpers around
_prepare_container_args_for_start, ensure_job_storage, and the created container
handling to centralize the cleanup so task/config volumes and orphaned
containers are not left behind.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@services/core/jobs/src/nmp/core/jobs/controllers/backends/docker.py`:
- Around line 684-714: Ensure cancellation/early-exit paths in the Docker
backend clean up any resources already created by
_prepare_container_args_for_start and related launch flow. In the container
startup path, add cleanup before returning on cancellation at the points that
currently exit from the main start logic, and make sure a just-created container
is removed/destroyed even if it never reaches the normal cleanup scan used by
cleanup_steps(). Use the existing helpers around
_prepare_container_args_for_start, ensure_job_storage, and the created container
handling to centralize the cleanup so task/config volumes and orphaned
containers are not left behind.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 653a9954-504b-4d5c-a115-454458e85a8f

📥 Commits

Reviewing files that changed from the base of the PR and between 5e6487f and ca85989.

📒 Files selected for processing (10)
  • services/core/jobs/src/nmp/core/jobs/controllers/backends/docker.py
  • services/core/jobs/src/nmp/core/jobs/controllers/backends/exceptions.py
  • services/core/jobs/src/nmp/core/jobs/controllers/backends/subprocess.py
  • services/core/jobs/src/nmp/core/jobs/controllers/reconciler.py
  • services/core/jobs/src/nmp/core/jobs/controllers/scheduler.py
  • services/core/jobs/tests/controllers/test_docker_backend.py
  • services/core/jobs/tests/controllers/test_kubernetes_backend.py
  • services/core/jobs/tests/controllers/test_scheduler.py
  • services/core/jobs/tests/controllers/test_subprocess_backend.py
  • services/core/jobs/tests/controllers/test_volcano_backend.py
💤 Files with no reviewable changes (1)
  • services/core/jobs/src/nmp/core/jobs/controllers/backends/subprocess.py
🚧 Files skipped from review as they are similar to previous changes (8)
  • services/core/jobs/src/nmp/core/jobs/controllers/backends/exceptions.py
  • services/core/jobs/tests/controllers/test_scheduler.py
  • services/core/jobs/tests/controllers/test_kubernetes_backend.py
  • services/core/jobs/tests/controllers/test_subprocess_backend.py
  • services/core/jobs/tests/controllers/test_volcano_backend.py
  • services/core/jobs/src/nmp/core/jobs/controllers/scheduler.py
  • services/core/jobs/src/nmp/core/jobs/controllers/reconciler.py
  • services/core/jobs/tests/controllers/test_docker_backend.py

@ironcommit
ironcommit added this pull request to the merge queue Jul 9, 2026
Merged via the queue into main with commit 5913400 Jul 9, 2026
54 of 55 checks passed
@ironcommit
ironcommit deleted the jobs-race/rsadler branch July 9, 2026 18:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants