Skip to content

feat(sandbox): dedicated short timeout for background status polls - #2765

Merged
bxyu-nvidia merged 2 commits into
mainfrom
fix/dedup-command-submit-on-retry
Aug 26, 2026
Merged

bxyu-nvidia merged 2 commits into
mainfrom
fix/dedup-command-submit-on-retry

Conversation

@hemildesai

@hemildesai hemildesai commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Background-command status polls are sub-second idempotent GETs, but they inherit the general request_timeout_s budget (tuned for long submits/creates, often minutes). Against a sandbox whose pod has become unreachable (a blackholed IP that swallows SYNs), each poll then hangs for a full TCP connect timeout before failing β€” retries Γ— minutes per dead sandbox before the typed unreachable error fires, tying up a rollout slot the whole time.

feat: dedicated short timeout for background status polls

  • New operations.status_poll_timeout_s: float | None = 10.0: per-request budget for background-command status polls only. A healthy poll answers in milliseconds and even the slow legitimate path (endpoint re-resolution plus a connect retry) is a few seconds. None restores the previous behavior; submits keep their timeout_s + 60 headroom and the logs fetch keeps the shared budget.
  • A per-call timeout surfaces as builtin TimeoutError, which the default retry classifier deliberately treats as terminal (a long timeout has already burned its budget β€” and TimeoutError subclasses OSError, which would otherwise be retried). With a 10s budget that would make a single slow poll rollout-fatal, so _await_sdk_operation gains a pluggable is_retryable predicate (default unchanged for all other callers) and the status-poll path passes one that retries timeouts β€” safe because a status poll is an idempotent GET, unlike a submit where a timeout must stay terminal.
  • With the retry budget, an unresponsive sandbox is detected in ~1 minute instead of ~25+.

fix: stop relabeling inner timeouts as the exec hard cap

Pre-existing bug the short poll budget makes frequent: since Python 3.11 asyncio.TimeoutError IS builtin TimeoutError, so the wait_for-based hard-cap wrapper around exec dispatch also caught any TimeoutError raised inside the dispatch (e.g. a status poll exhausting its budget and retries) and relabeled that minutes-scale failure as exceeded hard cap of <hours>s; the command wedged β€” factually wrong, corrupting failure taxonomy. Now uses asyncio.timeout and attaches the wedged message only when the cap itself expired (Timeout.expired()); inner timeouts propagate with their original message.

Note on the branch's original scope

This PR originally implemented duplicate-submission protection for background commands (adopting an already-scheduled execution after a lost submit response). Per-request tracking deployed at production scale (>115k requests across multiple 1024-way-parallel runs, with body-hash-level duplicate detection) observed zero duplicate submissions in practice, so that machinery was withdrawn to keep the provider simple; this PR now ships only the status-poll timeout work above.

Tests

51 passed on the provider suite: parametrized per-operation timeout routing (poll gets the short budget; submit and logs keep theirs), None fallback, poll-timeout retryability, config validation, inner-timeout propagation with original message (fails against the old wait_for code), and a genuinely wedged dispatch still receiving the hard-cap label.

πŸ€– Generated with Claude Code

@copy-pr-bot

copy-pr-bot Bot commented Aug 25, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Preview your docs: https://nvidia-preview-fix-dedup-command-submit-on-retry.docs.buildwithfern.com/nemo/gym

Here are the markdown pages you've updated:

hemildesai and others added 2 commits August 25, 2026 19:36
Background-command status polls are sub-second GETs, but they inherit
the general connection request timeout, which is tuned for long
submits and creates (often minutes). Against a sandbox whose backend
has become unreachable, every poll then hangs for that full budget per
retry, so a dead sandbox occupies a rollout slot for retries x minutes
before the typed unreachable error fires.

Add operations.status_poll_timeout_s (default 10s: a healthy poll
answers in milliseconds and even the slow legitimate path, endpoint
re-resolution plus a connect retry, is a few seconds) as the
per-request budget for status polls only; None falls back to the
shared budget, restoring the old behavior. The submit call and the
final logs fetch keep their existing timeouts (log payloads can be
large).

A per-call timeout surfaces as TimeoutError, which the shared retry
classifier deliberately treats as terminal so a timed-out submit is
never replayed into a double-run. With the short poll budget that
would let a single slow poll kill a running command, so
_await_sdk_operation now accepts an is_retryable predicate (default
unchanged for every other caller) and the status poll passes one that
treats TimeoutError as retryable: re-polling a status is an idempotent
GET, and the retry budget still bounds the total time against a truly
dead backend.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
Since Python 3.11 asyncio.TimeoutError IS builtin TimeoutError, so the
wait_for-based hard-cap wrapper around exec dispatch also caught any
TimeoutError raised inside the dispatch β€” for example a status poll
exhausting its per-request budget and retries β€” and relabeled that
minutes-scale failure as 'exceeded hard cap of <hours>s; the command
wedged', which is factually wrong and corrupts failure taxonomy.

Replace wait_for with asyncio.timeout and attach the wedged message
only when the cap itself expired (Timeout.expired()); a TimeoutError
raised inside the dispatch now propagates with its original message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hemil Desai <hemild@nvidia.com>
@hemildesai
hemildesai force-pushed the fix/dedup-command-submit-on-retry branch from f8de5b5 to cdcd0f7 Compare August 26, 2026 02:39
@hemildesai hemildesai changed the title fix(sandbox): adopt already-running background command when the submit response is lost feat(sandbox): dedicated short timeout for background status polls Aug 26, 2026
@bxyu-nvidia
bxyu-nvidia marked this pull request as ready for review August 26, 2026 15:44
@bxyu-nvidia

Copy link
Copy Markdown
Contributor

/ok to test cdcd0f7

@bxyu-nvidia
bxyu-nvidia merged commit 310d880 into main Aug 26, 2026
25 of 27 checks passed
@bxyu-nvidia
bxyu-nvidia deleted the fix/dedup-command-submit-on-retry branch August 26, 2026 15:58
khazic pushed a commit to khazic/Gym-lao that referenced this pull request Oct 7, 2026
…Mo#3993)

A timeout fetching a finished background command's logs currently aborts
the operation. Retry that idempotent GET for the same execution using
the existing retry budget, extending the status-read behavior from
NVIDIA-NeMo#2765. Command submission semantics and per-call timeouts stay
unchanged. Persistent log-read failures can now consume the configured
retry budget before surfacing.

The regression covers recovery, exhausted retries, and exactly one
command submission. A fresh-sandbox fault-injection probe recovered
after the first log-read timeout, with two read attempts and exactly one
submission; a file append also confirmed one execution. A subsequent
real-model terminal rollout completed with two accepted final
declarations and successful verification. The model smoke also included
NVIDIA-NeMo#3992's verifier-directory preparation.

The failure and focused regression are contained in this PR, so a
separate issue is unnecessary.

Signed-off-by: Frederic Lafrance <flafrance@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants