Skip to content

fix(runtime): bound TCP accept failures under file-descriptor pressure - #12282

Closed
MatejKosec wants to merge 2 commits into
ai-dynamo:mainfrom
glamr-agent:fix/tcp-accept-backoff-emfile--b95d3083a558
Closed

fix(runtime): bound TCP accept failures under file-descriptor pressure#12282
MatejKosec wants to merge 2 commits into
ai-dynamo:mainfrom
glamr-agent:fix/tcp-accept-backoff-emfile--b95d3083a558

Conversation

@MatejKosec

@MatejKosec MatejKosec commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Overview:

The TCP response server retried listener.accept() immediately after any error. Once
the process reached its file-descriptor ceiling (EMFILE/ENFILE), that became a busy
loop: accept() fails deterministically while the descriptor table is full, so the loop
spun at microsecond cadence, accepted nothing, and wrote the same warning on every
iteration. The error arm also logged twice — tracing::warn! plus an unconditional
eprintln! — so rate-limiting only the tracing sink would have left half the flood in
place.

This bounds the spin and the logging without changing behaviour for any other accept
error.

Details:

AcceptBackoff is a private, pure, synchronous policy in
lib/runtime/src/pipeline/network/tcp/server.rs. It classifies an accept() error via
raw_os_error(), computes a bounded exponential delay (5 ms doubling to a 1 s ceiling),
decides whether a summary is due against a caller-supplied Instant, and resets on a
successful accept. It reads no clock, performs no I/O, and never sleeps — the caller does
both. That is what makes the retry schedule and the log-rate decision testable without
reproducing the host's file-descriptor limit.

Only exhaustion errors take the new path. Every other accept error keeps the pre-existing
warn-and-retry-immediately behaviour, so genuinely unexpected failures are neither hidden
nor slowed. The eprintln! is now gated behind #[cfg(debug_assertions)], matching its
sibling in handle_connection.

Operator visibility: at most one structured summary per 5 s, carrying the error, the
current delay, and the number of failures suppressed since the last emission; one line on
recovery; and a new dynamo_transport_tcp_accept_backoff_total counter registered in
ensure_transport_metrics_registered_prometheus.

Five new tests: bounded growth and saturation, reset after recovery, rate-limited logging
against an injected clock, a deterministic socket recovery test using an injected EMFILE
(no host FD limit is touched), and a negative control proving ConnectionAborted stays on
the ordinary path with no delay and no perturbation of the backoff schedule.

Complementary to the RLIMIT_NOFILE change in #11802, which raises the ceiling but does
not alter accept-loop behaviour at it. Follow-up to #11801.

Where should the reviewer start?

  • lib/runtime/src/pipeline/network/tcp/server.rs (+412/−4) — the AcceptBackoff policy,
    its integration into the accept loop, and the five tests. The policy type and its
    classification of raw_os_error() are the parts worth the closest reading.
  • lib/runtime/src/metrics/transport_metrics.rs (+13/−0) and
    lib/runtime/src/metrics/prometheus_names.rs (+2/−0) — registration and naming of the
    new counter.

Two notes for the reviewer, both about what this PR does not do:

  • A parallel instance of the same tight-retry pattern remains at
    lib/runtime/src/pipeline/network/tcp/shared_tcp_endpoint.rs:389. It is deliberately out
    of scope here.
  • The full cargo test -p dynamo-runtime --lib run does not complete on this branch, but
    the cause is pre-existing and unrelated:
    pipeline::network::egress::push_router::tests::transport_resolution_falls_back_when_selected_instance_disappears
    hangs, and it hangs identically on a clean main checkout with none of this change
    present.

Related Issues

🔗 This PR is linked to an issue:


Open in Devin Review

Summary by CodeRabbit

  • Bug Fixes
    • Improved TCP listener recovery when the system runs out of file descriptors.
    • Added bounded retry delays to prevent excessive resource usage during repeated failures.
    • Preserved immediate retries for ordinary connection errors.
    • Ensured the listener resumes accepting connections after resources become available.
  • Monitoring
    • Added a Prometheus counter for TCP accept backoff events.
    • Enhanced warning messages with summarized failure information.

The TCP response server retried `listener.accept()` immediately after any
error. Once the process hit its file-descriptor ceiling (`EMFILE`/`ENFILE`)
that became a busy loop: `accept()` fails deterministically while the table
is full, so the loop spun at microsecond cadence, accepted nothing, and wrote
the same warning line on every iteration. The error arm also logged twice --
`tracing::warn!` plus an unconditional `eprintln!` -- so rate-limiting only
the tracing sink would have left half the flood in place.

Introduce `AcceptBackoff`, a private, pure, synchronous policy: it classifies
an `accept()` error via `raw_os_error()`, computes a bounded exponential
delay (5ms doubling to a 1s ceiling), decides whether a summary is due against
a caller-supplied `Instant`, and resets on a successful accept. It reads no
clock, performs no I/O and never sleeps -- the caller does both -- which is
what lets the retry schedule and the log-rate decision be tested without
reproducing the host's file-descriptor limit.

Only exhaustion errors take the new path. Every other accept error keeps the
pre-existing warn-and-retry-immediately behavior, so genuinely unexpected
failures are neither hidden nor slowed. The `eprintln!` is now gated behind
`#[cfg(debug_assertions)]`, matching its sibling in `handle_connection`.

Operator visibility: at most one structured summary per 5s carrying the error,
the current delay and the number of failures suppressed since the last
emission, one line on recovery, and a new `dynamo_transport_tcp_accept_backoff_total`
counter registered in `ensure_transport_metrics_registered_prometheus`.

Adds five tests: bounded growth-and-saturation, reset after recovery,
rate-limited logging against an injected clock, a deterministic socket
recovery test using an injected `EMFILE` (no host FD limit is touched), and a
negative control proving `ConnectionAborted` stays on the ordinary path with
no delay and no perturbation of the backoff schedule.

Follow-up to ai-dynamo#11801; complementary to the RLIMIT_NOFILE change in ai-dynamo#11802,
which raises the ceiling but does not alter accept-loop behavior at it.

Closes ai-dynamo#11822

Signed-off-by: svc-glamr@nvidia.com <svc-glamr@nvidia.com>
@MatejKosec
MatejKosec requested review from a team as code owners July 28, 2026 21:01
@copy-pr-bot

copy-pr-bot Bot commented Jul 28, 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.

@MatejKosec
MatejKosec temporarily deployed to external_collaborator July 28, 2026 21:02 — with GitHub Actions Inactive
@MatejKosec
MatejKosec temporarily deployed to external_collaborator July 28, 2026 21:02 — with GitHub Actions Inactive
@github-actions

Copy link
Copy Markdown
Contributor

👋 Hi MatejKosec! Thank you for contributing to ai-dynamo/dynamo.

Just a reminder: The NVIDIA Test Github Validation CI runs an essential subset of the testing framework to quickly catch errors.Your PR reviewers may elect to test the changes comprehensively before approving your changes.

🚀

@github-actions github-actions Bot added external-contribution Pull request is from an external contributor fix labels Jul 28, 2026

@devin-ai-integration devin-ai-integration 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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

TCP accept backoff

Layer / File(s) Summary
Backoff metric contract
lib/runtime/src/metrics/prometheus_names.rs, lib/runtime/src/metrics/transport_metrics.rs
Adds and registers the tcp_accept_backoff_total Prometheus counter.
Accept-loop backoff policy
lib/runtime/src/pipeline/network/tcp/server.rs
Adds bounded exponential backoff for EMFILE/ENFILE, rate-limited warnings, recovery reset behavior, and immediate retries for ordinary errors.
Backoff behavior validation
lib/runtime/src/pipeline/network/tcp/server.rs
Adds deterministic unit and integration tests for delay growth, reset, logging, ordinary errors, metrics, and continued connections.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement the linked issue goals: backoff for EMFILE/ENFILE, reset on success, rate-limited warnings, and tests.
Out of Scope Changes check ✅ Passed No unrelated code changes are evident beyond the requested TCP accept-loop backoff and supporting metrics/tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly and concisely describes the main change: bounding TCP accept failures under file-descriptor pressure.
Description check ✅ Passed The description covers the required sections, implementation details, reviewer focus, testing status, scope, and linked issue.

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: 3

🧹 Nitpick comments (1)
lib/runtime/src/pipeline/network/tcp/server.rs (1)

2344-2346: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

These two tests don't need the unix gate.

accept_backoff_delay_grows_and_saturates and accept_backoff_rate_limits_summary_and_reports_suppressed_count exercise only the clock-free policy — no libc, no emfile_error. Dropping #[cfg(unix)] keeps the schedule/rate-limit coverage on non-unix targets. The classify-based and emfile_error-based tests legitimately stay gated.

Also applies to: 2423-2425

🤖 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 `@lib/runtime/src/pipeline/network/tcp/server.rs` around lines 2344 - 2346,
Remove the #[cfg(unix)] attributes from accept_backoff_delay_grows_and_saturates
and accept_backoff_rate_limits_summary_and_reports_suppressed_count so these
clock-free policy tests run on all targets. Leave the classify- and
emfile_error-based tests’ Unix gates unchanged.
🤖 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 `@lib/runtime/src/metrics/transport_metrics.rs`:
- Around line 42-45: Update the doc comments for the accept-backoff metric in
lib/runtime/src/metrics/transport_metrics.rs lines 42-45 and
lib/runtime/src/metrics/prometheus_names.rs lines 739-740 to describe one
increment per EMFILE/ENFILE accept failure that triggers a backoff sleep, rather
than per entry into a backoff episode; leave the metric implementation and help
text unchanged.

In `@lib/runtime/src/pipeline/network/tcp/server.rs`:
- Around line 2495-2501: Remove the wall-clock assertion comparing
started.elapsed() with next_if_untouched from the test, since slept ==
Duration::ZERO already verifies no backoff delay. Also remove the now-unused
started binding and any related timing setup, leaving the existing zero-duration
assertion intact.
- Around line 749-754: Update the recovery path around
accept_backoff.record_success() so the tcp recovery warning uses the same
5-second rate-limit window as the failure warning. Pass the required rate-limit
argument at both production and test record_success() call sites, while
preserving suppressed_failures reporting and recovery behavior.

---

Nitpick comments:
In `@lib/runtime/src/pipeline/network/tcp/server.rs`:
- Around line 2344-2346: Remove the #[cfg(unix)] attributes from
accept_backoff_delay_grows_and_saturates and
accept_backoff_rate_limits_summary_and_reports_suppressed_count so these
clock-free policy tests run on all targets. Leave the classify- and
emfile_error-based tests’ Unix gates unchanged.
🪄 Autofix (Beta)

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: c65ed9f4-4226-4465-9ea6-19cdf0113734

📥 Commits

Reviewing files that changed from the base of the PR and between 8e8ce2c and 211676f.

📒 Files selected for processing (3)
  • lib/runtime/src/metrics/prometheus_names.rs
  • lib/runtime/src/metrics/transport_metrics.rs
  • lib/runtime/src/pipeline/network/tcp/server.rs

Comment thread lib/runtime/src/metrics/transport_metrics.rs Outdated
Comment thread lib/runtime/src/pipeline/network/tcp/server.rs Outdated
Comment thread lib/runtime/src/pipeline/network/tcp/server.rs Outdated
@MatejKosec

Copy link
Copy Markdown
Contributor Author

All three CodeRabbit findings are valid. I verified each against the code rather than
taking them at face value, and prepared the fix — but this PR was opened with
maintainer_can_modify: false and its head branch lives on the glamr-agent fork, so
I cannot push to it. Posting the patch here so it isn't lost; whoever has write on that
fork can apply it, or it can land as a follow-up.

1. Recovery log is unbounded (server.rs) — real, and the most substantive.
record_success returns Some(..) whenever an episode ends and the caller warns
unconditionally, while the failure side is capped at one line per interval. At the
descriptor ceiling the loop does not fail in one long run — it alternates failure and
success as descriptors are freed and reclaimed — so every successful accept ends an
episode and logs, reinstating the storm this PR exists to bound. Fixed by having
record_success take the caller's Instant and consult the same window as
record_exhaustion, which keeps the policy clock-free and testable. Suppressed counts
roll into the next emitted line rather than being discarded.

2. Metric doc comments are wrong — real. TCP_ACCEPT_BACKOFF_TOTAL.inc() fires once
per failed accept, not once per episode. The Counter::new help text was already
correct; only the two doc comments were wrong, and an alert written from them would
under-estimate the rate.

3. Timing assertion can flake — real. started.elapsed() < next_if_untouched is a
10 ms upper bound on a path that emits a tracing::warn! and, under debug_assertions,
an eprintln!. assert_eq!(slept, Duration::ZERO) already establishes the property, so
the wall-clock check is redundant as well as fragile. Removed. The sibling lower-bound
assertion in accept_backoff_socket_recovers_after_injected_exhaustion is kept — extra
elapsed time cannot make that one fail.

Also adds accept_backoff_rate_limits_recovery_notice covering the new window behaviour.

Not compiled locally (this workspace is not built on my machine); CI is the verification.
For what it is worth, the current head is green across all 29 checks.

Patch (3 files, +87/-19)
diff --git a/lib/runtime/src/metrics/prometheus_names.rs b/lib/runtime/src/metrics/prometheus_names.rs
index 5cf591dc05..5fdab3edbb 100644
--- a/lib/runtime/src/metrics/prometheus_names.rs
+++ b/lib/runtime/src/metrics/prometheus_names.rs
@@ -736,7 +736,8 @@ pub mod transport {
         pub const BYTES_RECEIVED_TOTAL: &str = "tcp_bytes_received_total";
         pub const ERRORS_TOTAL: &str = "tcp_errors_total";
         pub const SERVER_QUEUE_DEPTH: &str = "tcp_server_queue_depth";
-        /// Times the response-server accept loop entered file-descriptor-exhaustion backoff
+        /// Response-server accept failures that triggered a file-descriptor-exhaustion
+        /// backoff sleep; counts per failed accept, not per backoff episode
         pub const ACCEPT_BACKOFF_TOTAL: &str = "tcp_accept_backoff_total";
     }
     pub mod nats {
diff --git a/lib/runtime/src/metrics/transport_metrics.rs b/lib/runtime/src/metrics/transport_metrics.rs
index 6de997c661..591653f154 100644
--- a/lib/runtime/src/metrics/transport_metrics.rs
+++ b/lib/runtime/src/metrics/transport_metrics.rs
@@ -39,10 +39,11 @@ pub static TCP_ERRORS_TOTAL: Lazy<Counter> = Lazy::new(|| {
     .expect("tcp_errors_total counter")
 });
 
-/// Incremented once per transition into the accept-loop backoff state, i.e. each time
-/// `listener.accept()` fails with `EMFILE`/`ENFILE` while the process is at its
-/// file-descriptor ceiling. Alertable signal for the condition described in
-/// <https://github.com/ai-dynamo/dynamo/issues/11822>.
+/// Incremented once per `listener.accept()` failure with `EMFILE`/`ENFILE` — that is,
+/// per failed accept while the process is at its file-descriptor ceiling, not once per
+/// backoff episode. A single episode of exhaustion therefore advances this counter many
+/// times, which is what makes it usable as a rate. Alertable signal for the condition
+/// described in <https://github.com/ai-dynamo/dynamo/issues/11822>.
 pub static TCP_ACCEPT_BACKOFF_TOTAL: Lazy<Counter> = Lazy::new(|| {
     Counter::new(
         transport_metric_name(transport::tcp::ACCEPT_BACKOFF_TOTAL),
diff --git a/lib/runtime/src/pipeline/network/tcp/server.rs b/lib/runtime/src/pipeline/network/tcp/server.rs
index f15d6eaa0b..f912779be4 100644
--- a/lib/runtime/src/pipeline/network/tcp/server.rs
+++ b/lib/runtime/src/pipeline/network/tcp/server.rs
@@ -659,14 +659,33 @@ impl AcceptBackoff {
     }
 
     /// Record a successful accept. Returns `Some(suppressed)` when this success
-    /// ended an exhaustion episode, so the caller can note the recovery once;
-    /// `None` during ordinary steady-state accepts.
-    fn record_success(&mut self) -> Option<u64> {
+    /// ended an exhaustion episode *and* the same rate-limit window that governs
+    /// the failure warning allows another line; `None` during ordinary
+    /// steady-state accepts, and `None` when a recovery line was emitted too
+    /// recently.
+    ///
+    /// The window matters because at the descriptor ceiling the loop does not
+    /// fail in one long run — it alternates failure, success, failure as
+    /// descriptors are freed and immediately reclaimed. Every one of those
+    /// successes ends an episode, so an unthrottled recovery log would reinstate
+    /// the very storm the failure-side limit exists to bound.
+    fn record_success(&mut self, now: std::time::Instant) -> Option<u64> {
         self.current_delay = self.initial_delay;
         if !self.in_backoff {
             return None;
         }
         self.in_backoff = false;
+
+        let due = match self.last_log_at {
+            None => true,
+            Some(prev) => now.saturating_duration_since(prev) >= self.log_interval,
+        };
+        if !due {
+            // Keep the tally rolling into the next line that does get emitted
+            // rather than discarding it.
+            return None;
+        }
+        self.last_log_at = Some(now);
         Some(std::mem::take(&mut self.suppressed))
     }
 }
@@ -746,7 +765,8 @@ async fn tcp_listener(
         // todo - add counter for outgoing bytes
         let (stream, _addr) = match listener.accept().await {
             Ok((stream, _addr)) => {
-                if let Some(suppressed) = accept_backoff.record_success() {
+                let now = std::time::Instant::now();
+                if let Some(suppressed) = accept_backoff.record_success(now) {
                     tracing::warn!(
                         suppressed_failures = suppressed,
                         "tcp accept recovered from file-descriptor exhaustion"
@@ -2407,7 +2427,7 @@ mod tests {
             "precondition: delay should have grown, {grown:?} vs {first:?}"
         );
 
-        backoff.record_success();
+        backoff.record_success(now);
 
         let after_recovery = backoff.record_exhaustion(now).delay;
         assert_eq!(
@@ -2416,6 +2436,55 @@ mod tests {
         );
     }
 
+    /// Recovery lines obey the same rate-limit window as failure lines. At the
+    /// descriptor ceiling the loop alternates failure and success as descriptors
+    /// are freed and immediately reclaimed, so every success ends an episode; an
+    /// unthrottled recovery log would reinstate the storm the failure-side limit
+    /// exists to bound. Deleting the window check makes the second assertion fail.
+    #[cfg(unix)]
+    #[test]
+    fn accept_backoff_rate_limits_recovery_notice() {
+        let mut backoff = AcceptBackoff::default();
+        let t0 = std::time::Instant::now();
+
+        // One failure opens the episode and claims this interval's single line.
+        assert_eq!(
+            backoff.record_exhaustion(t0).log_suppressed,
+            Some(0),
+            "precondition: the first failure emits and claims the window"
+        );
+
+        // A success inside that same window ends the episode but stays silent.
+        assert_eq!(
+            backoff.record_success(t0),
+            None,
+            "a recovery inside the log window must not emit"
+        );
+        // The schedule still reset, silence notwithstanding.
+        assert_eq!(
+            backoff.record_exhaustion(t0).delay,
+            ACCEPT_BACKOFF_INITIAL_DELAY,
+            "recovery must reset the schedule whether or not it logged"
+        );
+
+        // Once the window elapses a recovery is authorized again, and carries
+        // the failures suppressed behind it rather than discarding them.
+        let t1 = t0 + ACCEPT_BACKOFF_LOG_INTERVAL + Duration::from_millis(1);
+        assert_eq!(
+            backoff.record_success(t1),
+            Some(1),
+            "a recovery after the window elapses must emit and report suppressions"
+        );
+
+        // A success outside an episode never emits, window or not.
+        let t2 = t1 + ACCEPT_BACKOFF_LOG_INTERVAL + Duration::from_millis(1);
+        assert_eq!(
+            backoff.record_success(t2),
+            None,
+            "steady-state accepts must never emit a recovery line"
+        );
+    }
+
     /// The summary is rate-limited against a caller-supplied clock: exactly one
     /// emission per interval, carrying the count of failures suppressed since
     /// the previous one, and a fresh emission once the interval elapses.
@@ -2485,20 +2554,17 @@ mod tests {
         backoff.record_exhaustion(now);
 
         // An ordinary error in the middle of an episode sleeps for nothing...
-        let started = std::time::Instant::now();
+        // `handle_accept_error` returns the duration it actually slept, so that
+        // return value is the whole property; a wall-clock bound on the
+        // surrounding call would only add flakiness, because the measured path
+        // also emits a `tracing::warn!` and, under `debug_assertions`, an
+        // `eprintln!`.
         let slept = handle_accept_error(&ordinary, &mut backoff).await;
         assert_eq!(
             slept,
             Duration::ZERO,
             "ordinary accept errors must retry immediately"
         );
-        // Had it been routed through the backoff path it would have slept for
-        // the schedule's current delay; anything below that proves it did not.
-        assert!(
-            started.elapsed() < next_if_untouched,
-            "ordinary accept errors must not sleep at all, slept {:?}",
-            started.elapsed()
-        );
 
         // ...and leaves the exhaustion schedule exactly where it was: the next
         // exhaustion failure continues the sequence rather than restarting it.
@@ -2561,7 +2627,7 @@ mod tests {
         drop(accepted);
 
         // Recovery resets the schedule, as the loop's Ok(..) arm does.
-        backoff.record_success();
+        backoff.record_success(std::time::Instant::now());
         assert_eq!(
             backoff.record_exhaustion(std::time::Instant::now()).delay,
             ACCEPT_BACKOFF_INITIAL_DELAY,

…ic docs

Addresses the three review findings on ai-dynamo#12282.

Recovery warning is now rate-limited. `AcceptBackoff::record_success` takes
the caller's clock and gates its `Some(suppressed)` return on the same
`log_interval` window that `record_exhaustion` already uses. Under sustained
descriptor pressure the loop alternates success -> EMFILE -> success, so every
accepted connection ended an episode and emitted an unthrottled `warn!` while
the failure side was capped at one per 5s -- reinstating the log storm this
policy exists to bound. A withheld recovery keeps its suppressed count rather
than discarding it, so the next emitted summary on either path still accounts
for every failure observed. The schedule reset is unconditional, as before.

Metric doc comments corrected. `TCP_ACCEPT_BACKOFF_TOTAL` is `.inc()`d per
failed EMFILE/ENFILE accept, not once per backoff episode; both doc comments
claimed the latter, so an alert written from them would under-estimate the
rate. The counter implementation and its help text are unchanged.

Dropped a flaky wall-clock assertion. The `started.elapsed() <
next_if_untouched` upper bound in
`accept_backoff_leaves_ordinary_errors_undelayed_and_stateless` measures a path
that emits a `tracing::warn!` plus a debug-build `eprintln!`, so a loaded runner
can exceed it. The `assert_eq!(slept, Duration::ZERO)` above already establishes
the property.

Adds `accept_backoff_rate_limits_recovery_notice`, which drives the
success/EMFILE alternation and asserts silence inside the window plus a
rolled-forward count after it. Verified non-tautological: deleting the window
check from `record_success` makes it fail with Some(2) vs None.

Builds on 211676f by @glamr-agent; history is
not rewritten.

Validation: cargo check -p dynamo-runtime --lib --all-targets (pass);
cargo clippy -p dynamo-runtime --lib --all-targets -- -D warnings (clean);
cargo test -p dynamo-runtime --lib accept_backoff (6 passed, 0 failed);
cargo fmt and pre-commit on the changed files (clean).

Signed-off-by: svc-glamr@nvidia.com <svc-glamr@nvidia.com>
Signed-off-by: glamr-agent <svc-glamr@nvidia.com>
@glamr-agent
glamr-agent temporarily deployed to external_collaborator July 28, 2026 22:25 — with GitHub Actions Inactive
@glamr-agent

Copy link
Copy Markdown
Contributor

Babysitter round 1 — all three review findings addressed

Pushed f36c26d3e as a fast-forward on top of 211676fd2171. No history was rewritten.

Note the prior comment on this PR parked a patch because it believed it could not push. That was wrong: the head branch lives on glamr-agent/dynamo, which this identity owns. This round supersedes that comment, and — unlike it — the change was compiled and tested locally rather than left to CI.

Findings addressed

  1. server.rs:754 — recovery warning not rate-limited. record_success now takes the caller's clock and gates its Some(suppressed) return on the same log_interval window record_exhaustion already uses. A withheld recovery keeps its suppressed count rather than discarding it, so the next emitted summary on either path still accounts for every failure observed. The schedule reset stays unconditional. New test accept_backoff_rate_limits_recovery_notice drives the success → EMFILE → success alternation.
  2. transport_metrics.rs:42-45 / prometheus_names.rs:739-740 — doc comments. Both reworded to per-failed-accept semantics, matching the counter's help text and the .inc() site. Implementation and help text unchanged.
  3. server.rs:2495-2501 — flaky wall-clock bound. Removed, along with the now-unused started binding. assert_eq!(slept, Duration::ZERO) above already establishes the property.

The nitpick about #[cfg(unix)] on the clock-free policy tests was left alone: AcceptBackoff::classify is #[cfg(unix)]-gated at the libc::EMFILE match, so the gate on its tests is load-bearing, not incidental.

Validation — run locally in this checkout, not delegated to CI:

Check Result
cargo check -p dynamo-runtime --lib --all-targets pass
cargo clippy -p dynamo-runtime --lib --all-targets -- -D warnings clean
cargo test -p dynamo-runtime --lib accept_backoff 6 passed, 0 failed
cargo fmt + pre-commit on the 3 changed files clean

The test scope is deliberately the accept_backoff filter: a full --lib run hangs in push_router::tests::transport_resolution_falls_back_when_selected_instance_disappears on clean main too, and that pre-existing hang is unrelated to this diff.

Mutation-checked the new test rather than assuming it bites — deleting the window gate from record_success makes it fail with Some(2) vs None.

Validation status: incomplete for merge, for reasons outside this branch. Every check on the previous head was already green; nothing was failing. Merge is still gated on a maintainer posting /ok to test f36c26d3e (the full NVIDIA-runner CI in pr.yaml has never run for this PR — glamr-agent is not in APPROVED_EXTERNAL_CONTRIBUTORS, so the approval workflow's Comment /ok to test step skipped) plus a CODEOWNERS review. Neither is something this identity can supply. The commits are also unsigned, which is an additional gate on that same approval path.

@glamr-agent

Copy link
Copy Markdown
Contributor

CI result — green on f36c26d3e0094eee9457a2998702df3086f83289

All checks on the current head are terminal-success. 21 SUCCESS, 4 SKIPPED, 0 failed, 0 pending.

The Rust checks that actually exercise this diff are green: rust-tests (.), rust-tests (lib/bindings/kvbm), rust-tests (lib/bindings/python), rust-tests (lib/runtime/examples), all four rust-clippy shards, plus pre-commit, copyright-checks, DCO, codeowners, and pre-merge-status-check. Skipped: Fern Broken Links Check, Fern Configuration Check, operator, snapshot — path filters, none applicable to a lib/runtime change.

All three CodeRabbit review threads are resolved. Held green with no actionable unresolved discussion across three consecutive polls (22:51Z → 23:02Z).

Merge is still gated on two things this identity cannot supply, and neither is a defect in the branch:

  • The full NVIDIA-runner CI (pr.yaml) has never run for this PR. It triggers only on a push to pull-request/12282, which copy-pr-bot creates after a maintainer comments /ok to test f36c26d3e. The ok-to-test check reads SUCCESS but is a no-op — its Validate approved contributor step returned false (glamr-agent is not in APPROVED_EXTERNAL_CONTRIBUTORS), so Comment /ok to test skipped.
  • A CODEOWNERS approval. The existing dynamo-review-agent approval is authorAssociation: NONE and does not satisfy it.

Separately, the commits on this branch are unsigned. The approval workflow verifies commit signatures before posting /ok to test, so that is an additional gate on the same path — this environment has no signing key available.

@MatejKosec
MatejKosec requested a review from richardhuo-nv July 29, 2026 23:11
Comment thread lib/runtime/src/pipeline/network/tcp/server.rs
@MatejKosec
MatejKosec requested a review from richardhuo-nv July 30, 2026 00:23
@zhongdaor-nv

Copy link
Copy Markdown
Contributor

@MatejKosec can we merge this?

@MatejKosec

Copy link
Copy Markdown
Contributor Author

/ok-to-test 211676f

@copy-pr-bot

copy-pr-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown

/ok-to-test 211676f

@MatejKosec, there was an error processing your request: E2

See the following link for more information: https://docs.gha-runners.nvidia.com/cpr/e/2/

@MatejKosec

Copy link
Copy Markdown
Contributor Author

Superseded by #13146. Closing this one.

@zhongdaor-nv — answering your question: this PR could not be merged, and not because
anything on the branch was failing. It was opened from a fork, which routed it through
copy-pr-bot, and the full NVIDIA-runner CI never ran on it. It saw 25 checks where an
in-repo PR sees 76 — no container builds, no CodeQL, no deploy tests. The ok-to-test
check here reads green but did nothing: its last four steps, including the one that posts
/ok to test, were skipped.

Since then #10921 rewrote this same accept loop to
spawn per-connection for the TLS handshake, so this branch now conflicts with main.
Rather than hand-resolve that, #13146 is a fresh
branch off main with AcceptBackoff re-integrated against the new loop shape. It also
restores libc to lib/runtime/Cargo.toml, which
#12620 removed after this branch was cut — a rebase
would have produced no conflict there and simply failed to compile.

All review feedback from this thread carries over: the three CodeRabbit findings, and
@richardhuo-nv's question about per-request cost, which the closure-based clock in
record_success answers. That last fix was written in
#12282 (comment) but never landed
here; it is in the new PR.

@richardhuo-nv — sorry to ask twice. Your approval here was against the pre-TLS loop, so
the re-integration needs fresh eyes regardless.

The underlying bug is still live on main: the accept loop there still does
warn! + eprintln! + continue with no delay. #11822 stays open until
#13146 lands.

@MatejKosec MatejKosec closed this Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external-contribution Pull request is from an external contributor fix size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[CONTRIBUTION]: Bound TCP accept failures under file-descriptor pressure

4 participants