Skip to content

fix(deployments): gate READY on workload reachability (AIRCORE-998) - #1139

Merged
maxdubrinsky merged 4 commits into
mainfrom
mdubrinsky/aircore-998-sandboxed-deployment-reports-ready-before-the-workload-can
Aug 7, 2026
Merged

fix(deployments): gate READY on workload reachability (AIRCORE-998)#1139
maxdubrinsky merged 4 commits into
mainfrom
mdubrinsky/aircore-998-sandboxed-deployment-reports-ready-before-the-workload-can

Conversation

@maxdubrinsky

@maxdubrinsky maxdubrinsky commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Deployments reported READY before the workload had bound its port. On the openshell backend a port was exposed (which reads as READY) as soon as the serve pid was alive; on docker a running container with no declared readinessProbe was treated as READY. A live pid or a running container has not necessarily called bind(), so a caller trusting READY hit repeated 502s for the roughly 14s until nat serve finished starting. This gates READY on the port actually accepting a connection.

Related Issue

AIRCORE-998.

Changes

  • OpenShell: probe the workload from inside the sandbox against 127.0.0.1:<port> (gateway-independent) before exposing a port. Docker: TCP-connect the published port in the no-declared-probe branch of check_readiness_probe. Both honour a declared readinessProbe (httpGet/tcpSocket/exec) and otherwise default to a TCP connect; portless workloads stay ready.
  • Not-yet-reachable reports STARTING, so the reconciler's existing starting_timeout_seconds is the progress deadline that eventually fails a never-binding workload. No new deadline, knob, or status; death detection unchanged. Exposing only once reachable keeps the openshell endpoints -> READY fast path sticky (no flapping).
  • Hardening: HTTPS probes skip cert verification (k8s httpGet semantics); UDP-only and unresolvable declared ports fall back to the first TCP port or running-implies-ready; the exec-probe RPC and the docker socket connect are both bounded by the probe timeout so a hung probe cannot stall the serial reconcile loop.

Type of Change

  • Code change (feature, bug fix, or refactor)
  • Code change with documentation updates
  • Documentation only
  • Contributor tooling or automation
  • CI, build, or test infrastructure

Quality Gates

  • Tests added or updated for changed behavior
  • Existing tests cover changed behavior — justification:
  • Tests not applicable — justification:
  • Documentation updated for user-visible behavior
  • Documentation not applicable — justification: no user-visible API or CLI surface change. The deploy-sandbox SKILL text that documents the 502-retry workaround is tracked to follow via AIRCORE-981.

Verification

  • Pull request title follows the repository's Conventional Commit format
  • Every commit includes an appropriate Signed-off-by: trailer
  • uv run pre-commit run -a passes, or any blocked checks are identified below
  • Targeted tests pass, or tests are marked not applicable above
  • No secrets, API keys, or credentials are included

Targeted validation:

  • pre-commit run --files <changed files>: ruff, ruff format, ty, copyright headers, plugin-import guard, and merge-conflict hooks all pass; the remaining hooks skip as not-applicable. Run scoped to the changed files rather than -a because a full workspace sync could not be built in this worktree (a transitive dependency needs Python.h); the applicable hooks are the ones exercised above.
  • pytest plugins/nemo-deployments/tests/unit/backends/{openshell,docker}: 101 passed.
  • Full plugins/nemo-deployments/tests/unit: passes except test_ports.py::test_find_available_port_excludes_pending_assignments, a pre-existing real-socket flake that reproduces identically on unmodified main (host port 9000 is occupied on the dev box), unrelated to this change.

Summary by CodeRabbit

  • New Features
    • Added automatic TCP readiness checks for deployments with published ports.
    • Added OpenShell readiness probing for TCP, HTTP/HTTPS, and command-based checks.
    • Services are exposed only after readiness checks succeed.
    • Portless workloads remain ready immediately, while unavailable services stay in a starting state.
  • Bug Fixes
    • Improved protocol handling so UDP-only ports are not checked with TCP probes.
    • Added bounded timeouts to prevent readiness checks from hanging.
    • Improved handling of missing ports and readiness probe failures.

@maxdubrinsky
maxdubrinsky marked this pull request as ready for review August 6, 2026 20:14
@maxdubrinsky
maxdubrinsky requested review from a team as code owners August 6, 2026 20:14
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Docker now performs bounded TCP readiness checks for published TCP ports. OpenShell now supports default and declared readiness probes, applies execution timeouts, and delays service exposure until readiness succeeds. Tests cover TCP, HTTP, UDP-only, portless, and failed-readiness cases.

Changes

Deployment readiness probing

Layer / File(s) Summary
Docker TCP readiness
plugins/nemo-deployments/src/nemo_deployments_plugin/backends/docker/backend.py, plugins/nemo-deployments/src/nemo_deployments_plugin/backends/docker/probes.py, plugins/nemo-deployments/tests/unit/backends/docker/*
Docker filters readiness checks to TCP mappings. Workloads without declared probes use a bounded TCP check when a published port exists. Portless and UDP-only workloads remain ready immediately.
OpenShell probe execution
plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/backend.py
OpenShell builds default or declared exec, HTTP, HTTPS, and TCP probes. It resolves ports, normalizes paths, applies timeouts, and runs probes through sandbox RPC.
OpenShell readiness validation
plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_backend_mocked.py
Tests cover failed and successful readiness, delayed service exposure, HTTP paths, TCP port resolution, UDP exclusion, missing ports, and interpreter fallback.

Sequence Diagram(s)

sequenceDiagram
  participant OpenShellBackend
  participant WorkloadProcess
  participant SandboxRPC
  participant ServiceExposure
  OpenShellBackend->>WorkloadProcess: Confirm live process
  OpenShellBackend->>SandboxRPC: Run readiness probe
  SandboxRPC->>WorkloadProcess: Check loopback HTTP or TCP endpoint
  SandboxRPC-->>OpenShellBackend: Return probe status
  OpenShellBackend->>ServiceExposure: Expose ports after success
Loading

Possibly related PRs

Suggested reviewers: svvarom, tylersbray

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% 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 The title clearly summarizes the main change: deployments now gate READY status on workload reachability.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mdubrinsky/aircore-998-sandboxed-deployment-reports-ready-before-the-workload-can

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.

Actionable comments posted: 2

🤖 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.

Inline comments:
In
`@plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/backend.py`:
- Around line 700-704: Update OpenShellBackend readiness handling at
plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/backend.py:700-704
to return ready only when _exec_detached yields exit code 0, keeping None in
STARTING; at
plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/backend.py:762-765,
ensure the probe uses a guaranteed runtime or returns a nonzero result when no
interpreter exists, rather than exiting 0.

In `@plugins/nemo-deployments/tests/unit/backends/docker/test_probes.py`:
- Around line 84-93: Keep each dynamically bound test socket open until its
probe completes: in
plugins/nemo-deployments/tests/unit/backends/docker/test_probes.py lines 84-93,
move check_readiness_probe inside the socket context; in
plugins/nemo-deployments/tests/unit/backends/docker/test_backend_mocked.py lines
667-670, move read_status inside the socket context. Do not call listen() before
the respective check.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 98cb5aa7-e6e0-4213-ad95-5eec7b016173

📥 Commits

Reviewing files that changed from the base of the PR and between c71ca67 and cb1ca52.

📒 Files selected for processing (6)
  • plugins/nemo-deployments/src/nemo_deployments_plugin/backends/docker/backend.py
  • plugins/nemo-deployments/src/nemo_deployments_plugin/backends/docker/probes.py
  • plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/backend.py
  • plugins/nemo-deployments/tests/unit/backends/docker/test_backend_mocked.py
  • plugins/nemo-deployments/tests/unit/backends/docker/test_probes.py
  • plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_backend_mocked.py

Comment thread plugins/nemo-deployments/tests/unit/backends/docker/test_probes.py Outdated

@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.

🧹 Nitpick comments (1)
plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_backend_mocked.py (1)

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

Cover the retry and liveness branches.

Line 462 uses _exec_events(0), which includes an exit event, so this test does not exercise fail-open liveness. The test calls read_status once, so it also does not verify the later-poll retry described by the test comment. Add an undecidable liveness stream, a complete second-poll event sequence, and assertions for READY and ExposeService.

Based on the PR objective, readiness must retry after a transient no-exit result while liveness remains fail-open.

🤖 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
`@plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_backend_mocked.py`
around lines 448 - 470, Update
test_read_status_starting_when_readiness_probe_yields_no_exit to use an
undecidable no-exit stream for the liveness probe instead of _exec_events(0),
then supply the complete ExecSandbox sequence needed for a second poll. Call
read_status again and assert the retry transitions to READY and invokes
ExposeService, while preserving the first-poll STARTING and no-exposure
assertions.
🤖 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.

Nitpick comments:
In
`@plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_backend_mocked.py`:
- Around line 448-470: Update
test_read_status_starting_when_readiness_probe_yields_no_exit to use an
undecidable no-exit stream for the liveness probe instead of _exec_events(0),
then supply the complete ExecSandbox sequence needed for a second poll. Call
read_status again and assert the retry transitions to READY and invokes
ExposeService, while preserving the first-poll STARTING and no-exposure
assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 452698bb-3a0b-46ee-aa4b-8743a63db03a

📥 Commits

Reviewing files that changed from the base of the PR and between cb1ca52 and a5466ed.

📒 Files selected for processing (4)
  • plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/backend.py
  • plugins/nemo-deployments/tests/unit/backends/docker/test_backend_mocked.py
  • plugins/nemo-deployments/tests/unit/backends/docker/test_probes.py
  • plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_backend_mocked.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • plugins/nemo-deployments/tests/unit/backends/docker/test_backend_mocked.py
  • plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/backend.py

@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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_backend_mocked.py (1)

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

Cover the undecidable liveness branch.

At Line 465, _exec_events(0) includes an exit event, so liveness receives a definitive success. The test does not exercise the no-exit (None) path that liveness must fail open on. (raw.githubusercontent.com)

Use [stdout_only] for the liveness entry, then keep the existing readiness and second-poll assertions.

Suggested test adjustment
         _exec_events(0),  # marker present
-        _exec_events(0),  # liveness: alive
+        [stdout_only],  # liveness: no exit event; fail open
         [stdout_only],  # readiness: no exit event
🤖 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
`@plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_backend_mocked.py`
at line 465, Update the liveness test entry using _exec_events so it supplies
stdout-only output without an exit event, exercising the undecidable None
liveness path and its fail-open behavior. Keep the existing readiness assertion
and second-poll assertions unchanged.

Source: MCP tools

🤖 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.

Inline comments:
In @.pi/tasks/session-83576-83576/b744c26ca.json:
- Line 4: Update the polling loop using latest and newinline so it breaks on any
newer issue comment or inline review comment, and also detect newer submitted
reviews during polling. Remove error suppression from all gh calls so polling
failures are visible, and make the command exit nonzero when all 20 polls
complete without detected activity.
- Line 4: Update the polling command around the `gh api` calls to propagate API
failures instead of converting them into empty results: enable `set -o
pipefail`, remove the `2>/dev/null` suppression, and add explicit status checks
for each API request so the loop exits or fails when polling cannot complete.
Ensure the final review-summary and unresolved-thread commands also preserve
failures rather than allowing `tail` to mask them.

---

Nitpick comments:
In
`@plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_backend_mocked.py`:
- Line 465: Update the liveness test entry using _exec_events so it supplies
stdout-only output without an exit event, exercising the undecidable None
liveness path and its fail-open behavior. Keep the existing readiness assertion
and second-poll assertions unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 1569399e-b805-4b55-8a57-db4020a40d43

📥 Commits

Reviewing files that changed from the base of the PR and between a5466ed and dd8d75b.

📒 Files selected for processing (5)
  • .pi/tasks/session-83576-83576/b744c26ca.json
  • .pi/tasks/session-83576-83576/b744c26ca.output
  • .pi/tasks/session-83576-83576/b8de4a85b.json
  • .pi/tasks/session-83576-83576/b8de4a85b.output
  • plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_backend_mocked.py

Comment thread .pi/tasks/session-83576-83576/b744c26ca.json Outdated
@maxdubrinsky
maxdubrinsky force-pushed the mdubrinsky/aircore-998-sandboxed-deployment-reports-ready-before-the-workload-can branch from dd8d75b to 58e96be Compare August 6, 2026 22:21
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@ironcommit ironcommit 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.

LGTM

@github-actions github-actions Bot added the fix label Aug 6, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 31701/40325 78.6% 63.3%
Integration Tests 18433/38256 48.2% 20.8%

@maxdubrinsky
maxdubrinsky force-pushed the mdubrinsky/aircore-998-sandboxed-deployment-reports-ready-before-the-workload-can branch 2 times, most recently from 548580c to c63e369 Compare August 7, 2026 18:54
@maxdubrinsky
maxdubrinsky enabled auto-merge August 7, 2026 18:55
Both the openshell and docker backends published READY on a signal that does
not prove the workload is serving. openshell exposed a port as soon as the
serve pid was alive; docker treated a running container with no declared
readinessProbe as ready. A live pid or a running container has not
necessarily bound its socket, so a caller trusting READY hit repeated 502s
for the roughly 14s until nat serve finished starting.

Gate READY on the port actually accepting a connection. Honour a declared
readinessProbe (httpGet/tcpSocket/exec); with none declared, default to a
loopback TCP connect on the primary container port. A not-yet-reachable
workload reports STARTING, so the reconciler's existing starting-timeout
acts as the k8s-style progress deadline that eventually fails a workload
that never binds. Death detection is unchanged.

openshell probes inside the sandbox against 127.0.0.1 so readiness does not
depend on the gateway route, and exposing only once reachable keeps the fast
path's READY sticky (no flapping). docker changes only the
no-declared-probe branch; portless and declared-probe paths are untouched.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
…-998)

The openshell readiness gate treated an undecidable probe (no exit event)
the same as success, so a None exit promoted the workload to sticky READY
and exposed its port. Readiness is the inverse of liveness: liveness fails
open so a flaky RPC never demotes a healthy deployment, but readiness must
require positive proof of reachability (exit 0) before exposing. A transient
no-exit-event now stays STARTING and self-heals on the next poll; timeouts
and RPC errors already surface as UNKNOWN upstream, not as a None exit.

Also harden the docker not-ready port tests: hold the probe port
bound-but-not-listening for the whole probe so nothing else can bind and
listen on it mid-test, while a connect still gets ECONNREFUSED.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
The no-exit readiness test claimed the gate self-heals on the next poll but
only asserted the first poll. Drive read_status twice (undecidable then
reachable) and assert the second poll exposes the port and reads READY, so the
retry path the fix relies on is actually exercised.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
Address review feedback on the openshell backend: reformat the readiness
fail-closed rationale as a bulleted exit-code table, and cut the
_exec_detached docstring's narration down to the non-obvious facts (the
None exit-code semantics and the timeout rationale) instead of restating
what the code does.

Also satisfy ty in the openshell readiness tests: construct Probe via
pydantic aliases (httpGet/tcpSocket), since ty synthesizes __init__ from
aliases and does not honor populate_by_name, and route
_readiness_probe_command unpacks through a helper that narrows its
tuple|None return. Test-only; no behavior change.

Rename test_openshell_backend_mocked.py -> test_backend.py to match the
k8s unit backend test; the unit/ vs integration/ tree already conveys
mocked vs live, so the _mocked suffix was redundant.

Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
@maxdubrinsky
maxdubrinsky force-pushed the mdubrinsky/aircore-998-sandboxed-deployment-reports-ready-before-the-workload-can branch from c63e369 to 7b252f7 Compare August 7, 2026 22:55
@maxdubrinsky
maxdubrinsky added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit 4ab152f Aug 7, 2026
54 checks passed
@maxdubrinsky
maxdubrinsky deleted the mdubrinsky/aircore-998-sandboxed-deployment-reports-ready-before-the-workload-can branch August 7, 2026 23:26
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.

3 participants