Skip to content

fix(runtime): rebind system status listener after accept failures - #12059

Merged
MatejKosec merged 33 commits into
ai-dynamo:mainfrom
glamr-agent:fix/system-health-server-rebind-after-criu--95e8e92a4325
Oct 6, 2026
Merged

MatejKosec merged 33 commits into
ai-dynamo:mainfrom
glamr-agent:fix/system-health-server-rebind-after-criu--95e8e92a4325

Conversation

@glamr-agent

@glamr-agent glamr-agent commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor

Overview:

Summary

After process restore, the system status server can retain a listener descriptor that no longer accepts connections. Axum normally retries the same descriptor indefinitely, leaving status endpoints unavailable while the runtime continues. This change recovers by rebinding the listener.

Details:

  • Retain the concrete address selected by the initial bind, including an assigned ephemeral port.
  • Retry dropped connections and Linux pending network errors immediately, retry resource shortages on the same socket after a delay, and drop/rebind after other accept failures. Try the first rebind immediately and rate-limit later attempts. Graceful shutdown can interrupt recovery.
  • Build the system status router separately; unmatched routes still return 404 and log at DEBUG.
  • Cover listener recovery, failed-rebind retries, and resource classification. A Linux subprocess test forces real EMFILE and verifies that the address remains bound and the queued connection survives until descriptors are released. The subprocess isolates descriptor limits and has a 15-second timeout.

Where should the reviewer start?

lib/runtime/src/system_status_server.rs: RebindingTcpListener::accept and test_rebinding_listener_preserves_backlog_during_resource_exhaustion.

Related Issues

Refs #10665 — system health server does not rebind after CRIU restore.

Validation

  • Rust 1.96.1 formatting and git diff --check passed. No local tests, builds, or benchmarks were run during maintenance.
  • Linux tests cover pending network error classification, cancellation during failed rebinds, and listener retention and retry delay under real EMFILE. The classifier test separately checks ENFILE and ENOBUFS.
  • Remote CI validation is required for the latest head; earlier-head results do not validate it.

Summary by CodeRabbit

  • Bug Fixes
    • Improved system status service recovery after listener interruptions while retaining dynamically assigned addresses, including ephemeral ports.
    • Added retries for temporary address contention and resource exhaustion, helping keep the service available and preserve queued connections.
    • Client connection errors no longer unnecessarily interrupt the service; certain temporary accept errors are retried without dropping the listener.

@glamr-agent
glamr-agent requested review from a team as code owners July 23, 2026 03:09
@copy-pr-bot

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

@glamr-agent
glamr-agent temporarily deployed to external_collaborator July 23, 2026 03:09 — with GitHub Actions Inactive
@glamr-agent
glamr-agent temporarily deployed to external_collaborator July 23, 2026 03:09 — with GitHub Actions Inactive
@github-actions

Copy link
Copy Markdown
Contributor

👋 Hi glamr-agent! 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 the external-contribution Pull request is from an external contributor label Jul 23, 2026
@glamr-agent

glamr-agent commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor Author
change.md
# Change — materialization of ai-dynamo/dynamo#12059 (no new authorship)

## This is somebody else's work

**Nothing in this diff was authored by this run.** The single commit on the
working branch is a verbatim materialization of the head diff of
[ai-dynamo/dynamo#12059](https://github.com/ai-dynamo/dynamo/pull/12059) —
*"fix(runtime): rebind system health server after CRIU listener loss"* — placed
on a local branch so that this work item's validator can compile and execute it
here. The plan's `Route: review` carries no `Review action: reproduce` line and
the caller asked only to *watch and review*, so per the `existing-request`
template's Printer focus I materialized the supplied head diff and wrote no fix,
no refactor, no test, and no drive-by cleanup of my own.

Attribution, taken from the PR's own commit list (`gh api .../pulls/12059/commits`):

| Role | Identity |
|---|---|
| PR author (opened the request) | `glamr-agent` |
| Original fix commit `b4d60c16d` (2026-07-28) | `svc-glamr@nvidia.com <svc-glamr@nvidia.com>` |
| Commits `32d582037`, `8cfdbe62b`, `bf2125f01` (merges from `main`) | Matej Kosec `<mkosec@nvidia.com>` |
| Commit `87a366567` (libc dev-dependency) | Matej Kosec `<mkosec@nvidia.com>` |
| Head commit `8325b4329` (socket2 instead of libc, 2026-08-12T17:22Z) | Matej Kosec `<mkosec@nvidia.com>` |

So the change is joint work: the original rebinding fix by `glamr-agent`, plus
later corrections by Matej Kosec. Both are recorded as `Co-authored-by:` trailers
on the local commit, and the commit body opens with `NOT AUTHORED HERE.` and
states explicitly that it "is not a fix, not a competing implementation, and must
not be published as one."

One naming trap worth flagging for the reviewer and publisher: this sandbox's own
git identity is also `svc-glamr@nvidia.com`, which happens to equal the author of
the *original* upstream commit `b4d60c16d`. The identity match is a coincidence of
the factory service account, **not** evidence that this run wrote the code. The
`Signed-off-by:` trailer attests only that I materialized and pass on the work.

## Commit

| | |
|---|---|
| Branch | `chore/review-pr-12059--cab24ee14044` |
| Branch point | `bb2dcc126ed833cf66997bc2545fd18c21f11d89` (`main`, unmoved) |
| Commit SHA | `41c6c3a3141de63ad64941d6fcf87c5846f01e07` |
| Files changed | 1 |
| Lines | +250 / −35 |
| Working tree after commit | clean |

`git log -1 --format='%ae | %(trailers:key=Signed-off-by,valueonly)'` returns
`svc-glamr@nvidia.com | svc-glamr@nvidia.com <svc-glamr@nvidia.com>` — author
email and DCO trailer match, and there is exactly one `Signed-off-by:` line (the
repo's `prepare-commit-msg` hook is idempotent against `--signoff`, as
`learnings/commits-dco.md` describes).

## Per-file summary

**`lib/runtime/src/system_status_server.rs`** (+250/−35) — the only file touched.
The upstream change extracts `build_system_status_router` (:174) and
`bind_system_status_listener` (:270) out of `spawn_system_status_server`; adds a
private `struct RebindingTcpListener` (:303) with an `impl Listener` (:322) whose
`accept` loop drops and rebinds a listening socket that has stopped accepting;
adds `is_transient_accept_error` (:372) and a one-second
`SYSTEM_STATUS_REBIND_BACKOFF` (:133); and adds three `#[tokio::test]`s at :871,
:917, and :935 (`test_rebinding_listener_serves_again_after_listener_shutdown`,
`..._reports_the_address_it_rebinds`, `..._still_shuts_down_gracefully`). That is
a description of what I received, not a review of it — judging the design is the
reviewer's job, and I deliberately did not touch a line of it.

## Was the apply clean? Yes — and byte-exact

Clean, with no conflict and no manual resolution of any kind.

- Source of the diff: `gh api -H "Accept: application/vnd.github.v3.diff"
  repos/ai-dynamo/dynamo/compare/ccc9cf8d3661b1440b13b0d47f4bb7db256abe23...8325b43295571b62e53af030233ad519808aa1b3`
  — pinned to the **immutable head SHA**, not to the mutable PR ref. I also ran
  `gh pr diff 12059` and the two outputs were byte-identical, which additionally
  confirms the head had not moved under me mid-run. Both are read-only calls.
- I re-checked the head SHA before applying: still
  `8325b43295571b62e53af030233ad519808aa1b3`, state `OPEN`, 1 file, +250/−35 —
  matching the plan's Discovery exactly.
- `git apply --check` passed, then `git apply` reported *"Applied patch
  lib/runtime/src/system_status_server.rs cleanly."*
- The diff's pre-image blob `e85d8d649814` is what local `main` already had, as
  the plan predicted.

The strongest check I ran: after applying, the working-tree file hashes to
`a7a9ac5d48cbce017e6352bfe35c10185fcb56a7` at 62431 bytes, and
`gh api ".../contents/lib/runtime/src/system_status_server.rs?ref=8325b432..."`
reports that same blob SHA and size. **The materialized file is byte-for-byte
identical to the file at the PR head.** There is no drift for the reviewer to
hunt.

`change.diff` (14527 bytes, `git diff bb2dcc126..HEAD`) re-applies cleanly to the
branch point — verified by checking out `bb2dcc126` detached and running
`git apply --check`. Compared against the upstream patch it differs on exactly
one line: `index e85d8d649..a7a9ac5d4` versus GitHub's
`index e85d8d649814..a7a9ac5d48cb`, a cosmetic `core.abbrev` width difference
over the *same* blobs. All 342 other lines are identical.

## What I ran, including what went wrong

I kept this deliberately narrow — recipes 00/02/05/07 belong to the validator and
I did not want to pre-empt or half-run them. What I did run:

- `git diff --check` — clean, no whitespace errors introduced.
- `cargo fmt -p dynamo-runtime --check` — **exit 0, passes.**

**The honest part: my first formatting check was a false alarm caused by my own
command, not by the code.** Following the per-file snippet in
`learnings/format-changed-only.md`, I ran
`cargo fmt -- --check lib/runtime/src/system_status_server.rs`, and it reported
four hunks of import-reordering "drift" (`use crate::config::HealthStatus;` and
`Router` in the `axum::{...}` group). Before touching anything, I stashed the
patch and ran the identical command against **unmodified `main`** — byte-identical
complaint, same four hunks. So it was not PR-introduced. Root cause: passing a
file path through to rustfmt bypasses cargo's edition detection, and rustfmt then
defaults to style edition 2015 while this workspace is `edition = "2024"`
(root `Cargo.toml:46`). `cargo fmt -- --check --style-edition 2024 <file>` exits
0, and the package-scoped `cargo fmt -p dynamo-runtime --check` exits 0. I
changed no code over this; per `learnings/format-changed-only.md` I would not
have "fixed" pre-existing drift anyway, but in this case there was no drift at
all. Recording it because a validator running the same per-file form would hit
the same false positive.

I did **not** run `cargo check`, `cargo clippy`, `cargo test`, or any smoke — no
compile of the materialized code has happened yet. That is recipe 02's and 07's
work and I left it whole rather than reporting a partial result.

**Environment mutation I made, disclosed:** `rustfmt` was not installed for the
active toolchain, so I ran `rustup component add rustfmt` once (succeeded). That
installed it for `1.93.1` only. Nothing else about the sandbox was changed, and
nothing outside the work-item root and the checkout was written.

## Handover notes for the validator

Two things I tripped over that will cost the validator time in recipe 02 if it
does not know them up front:

1. **The active Rust toolchain is not the one the repo pins.**
   `rust-toolchain.toml` pins `1.96.1`, but `RUSTUP_TOOLCHAIN=1.93.1` is set in
   the environment and overrides it (`rustup show` says *"active because:
   overridden by environment variable RUSTUP_TOOLCHAIN"*). Both toolchains are
   installed.
2. **`clippy` is missing from the active toolchain.** `cargo clippy --version` on
   `1.93.1` returns *"'cargo-clippy' is not installed for the toolchain
   '1.93.1-x86_64-unknown-linux-gnu'"*. The repo-pinned `1.96.1` already carries
   both `clippy` and `rustfmt`
   (`rustup component list --toolchain 1.96.1 --installed`). So recipe 02
   Section 3 will not run as written until the validator either adds the
   component to `1.93.1` or invokes the pinned toolchain.

I deliberately did not decide this for the validator. Both routes have a real
cost: `rustup component add clippy` keeps toolchain consistency with the
`dynamo._core` that `cli.ts` prebuilt under `1.93.1`, whereas switching to
`1.96.1` matches the repo's own pin and upstream CI but will recompile the
workspace from cold and may invalidate the prebuilt bindings that recipe 07's
worker loads. Whichever it picks, `change-validation.md` should say which
toolchain produced the numbers, because a clippy result from a different rustc
than the pinned one is weaker evidence than it looks.

Also carrying forward, unchanged, from the plan since it bears on recipe 07:
`lib/runtime/src/config.rs:21` sets `DEFAULT_SYSTEM_PORT: i16 = -1` and the
server only starts when `system_port >= 0`, so the smoke must export a positive
`DYN_SYSTEM_PORT` or it will exercise none of this code. And per the plan, recipe
00 must run *after* this materialization — the commit is now in place, so that
ordering constraint is satisfiable as of `41c6c3a31`.

## Constraints observed

No push, no PR, no comment, no CI trigger — every `gh` call was read-only
(`api ... --jq` reads, `pr diff`). No `mcp__compute-session__*`, `ssh`, or `srun`.
Branched from `main` only, under the exact name
`chore/review-pr-12059--cab24ee14044`. `plan.md` untouched; the only packets
written are `change.md` and `change.diff`.

MR URL: https://github.com/ai-dynamo/dynamo/pull/12059

@glamr-agent

glamr-agent commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor Author
change-validation.md
# Change validation — PR #12059, `fix(runtime): rebind system health server after CRIU listener loss`

Local, advisory validation of the printer-materialized head of ai-dynamo/dynamo#12059
(1 file, +250/−35, all in `lib/runtime/src/system_status_server.rs`). Checkout at
`/home/sandbox/workspace/wi-20260812T174023Z-12059/repo`, commit `41c6c3a31`
("chore(review): materialize PR #12059 head 8325b432 for local review") on top of
`bb2dcc126`. Four recipes were run, each through
`/workflow/agents/customs/evidence.ts record`: `00-dynamo-editable-install`,
`02-rust-cargo-check`, `05-code-inspection`, `07-agg-smoke`.

## Reading the registry honestly

Seventeen rows are recorded and every one carries `exit=0`. That is not seventeen
successes. My wrapper scripts capture substantive failures in echoed markers
(`workspace-check-exit=101`, `registered=0`, `HTTP 404`) and then exit 0 themselves, so
the registry's exit column is a statement about the wrapper, not about the work. Three
recipe-07 rows and one recipe-02 row are failed attempts that read green in that column.
The narrative below names each one. The latest row per recipe — 00 at 18:06:55, 02 at
18:17:09, 05 at 18:41:10, 07 at 18:37:26 — is the one that carries the evidence, and
customs scores the row verdict from the latest run, which is correct here only because I
re-ran each recipe to a genuinely green state before stopping.

## Toolchain: which compiler produced these numbers

`rust-toolchain.toml` pins `1.96.1`, but the environment sets `RUSTUP_TOOLCHAIN=1.93.1`,
which overrides the file. **Every recipe-02 number below was produced by
`rustc 1.93.1 (01f6ddf75 2026-02-11)`** — the same toolchain that built the `dynamo._core`
extension that recipe 07 then loads, which is the reason I let the override stand rather
than forcing 1.96.1 and desynchronizing the compiled artifact from the smoke test.

Clippy was initially absent for 1.93.1. That is a solvable tooling gap, not a
`missing-dep` disposition: `rustup component add clippy --toolchain 1.93.1` installed it,
and the recorded run reports `clippy 0.1.93 (01f6ddf758 2026-02-11)` followed by
`clippy-exit=0` for `cargo clippy --workspace --all-targets --no-deps -- -D warnings`.

For formatting I ran both toolchains. Under 1.93.1: `fmt-all-exit=0`, `fmt-pkg-exit=0`
(`cargo fmt -p dynamo-runtime --check`). Cross-checked under the repo-pinned toolchain:
`RUSTUP_TOOLCHAIN=1.96.1 cargo fmt --all --check` → `fmt-all-196-exit=0`. I also
reproduced the printer's reported false positive for the record: the file-path-scoped
invocation `cargo fmt -- --check lib/runtime/src/system_status_server.rs` exits 1 and
wants to move `use crate::config::HealthStatus;` down the import block, because that form
bypasses the manifest and defaults to edition 2015 import ordering. Re-running the same
argv with `--style-edition 2024` exits 0. The file is correctly formatted; only the
scoping was wrong.

## Recipe 00 — editable install, and the trap it hides

The plan warned that recipe 00 can short-circuit and leave the Python extension holding
pre-change bytes, in which case recipe 07's green result would be evidence of nothing.
That is exactly what happened on the first run: at 18:03:32 the recorded `BEFORE` probe
showed `_core.abi3.so` with mtime `Wed Aug 12 17:53:19 2026`, while the changed source
`lib/runtime/src/system_status_server.rs` has mtime `18:00:50` — the extension predated
the diff by seven minutes. The editable install did not rebuild it.

I therefore ran `maturin develop --uv` explicitly from `lib/bindings/python` through the
recorder (18:03:57). Its output names the crate under change:

```
   Compiling dynamo-runtime v1.4.0 (/home/sandbox/.../repo/lib/runtime)
   Compiling dynamo-py3 v1.4.0 (/home/sandbox/.../repo/lib/bindings/python)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 53s
📦 Built wheel for abi3 Python ≥ 3.10 ...
🛠 Installed ai-dynamo-runtime-1.4.0
```

The 18:06:55 verification run then established three things: the extension resolves inside
the checkout (`dynamo OK at ['.../components/src/dynamo', '.../lib/bindings/python/src/dynamo']`),
its mtime is now `Wed Aug 12 18:06:00 2026` (`extension newer than changed source: True`),
and — the decisive check — a string only introduced by this PR is present in the compiled
object: `strings _core.abi3.so | grep -c 'System status listener stopped accepting'` → `1`.
No `PYTHONPATH` substitution was used anywhere.

Recipe 00 also surfaced an environment failure that recurs below: the
`utoipa-swagger-ui v9.0.2` build script tries to download Swagger UI over the network and
fails with `curl ... exit status: 52` in this egress-less sandbox. Exporting
`SWAGGER_UI_DOWNLOAD_URL="file:///opt/dynamo/assets/swagger-ui-v5.17.14.zip"` resolves it.
This is a sandbox property, not a PR defect, and the export was applied to every
subsequent cargo invocation.

## Recipe 02 — cargo check, clippy, and the tests that carry the weight

Section 1 (format) is covered above. Section 2 (`cargo check`) failed on its first
recorded attempt with `workspace-check-exit=101` and `bindings-check-exit=101` — the
swagger download. With the local-file export, the re-run (18:09:29) gives
`workspace-check-exit=0` and `bindings-check-exit=0` for `cargo check --workspace
--all-targets` and `cargo check --manifest-path lib/bindings/python/Cargo.toml
--all-targets`. Section 3 (clippy) is `clippy-exit=0` as noted.

**Section 4 is the primary evidence, and it is real.** The full suite ran in the actual
crate binary against the actual changed source:

```
cargo test -p dynamo-runtime --lib
test result: ok. 578 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 111.49s
```

The three tests the PR adds each executed and each passed. Section 4b ran them together
with `--nocapture --test-threads=1`:

```
running 3 tests
test system_status_server::tests::test_rebinding_listener_reports_the_address_it_rebinds ... ok
test system_status_server::tests::test_rebinding_listener_serves_again_after_listener_shutdown ... ok
test system_status_server::tests::test_rebinding_listener_still_shuts_down_gracefully ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 577 filtered out; finished in 0.03s
```

`test_rebinding_listener_serves_again_after_listener_shutdown` is the only local evidence
that recovery happens at all. It stands in for CRIU by calling
`socket2::SockRef::from(&listener).shutdown(Shutdown::Both)` on the *listening* socket,
and it contains a deliberate early return for platforms that refuse that call:
`eprintln!("skipping: this platform refused shutdown on a listening socket: {error}"); return;`.
**On this host it did not take that path.** Section 4e greps the un-captured output for
that marker and reports `0`. The recovery assertions genuinely ran: the listener was
broken, the rebind fired, and the server served again.

I also ran the three tests as paired polarities, each as its own `--exact` invocation, so
the new harness is shown to distinguish the cases rather than merely to pass:

- Broken-listener polarity (4c) — `shutdown(2)` applied, rebind is required:
  `test_rebinding_listener_serves_again_after_listener_shutdown ... ok`,
  `test result: ok. 1 passed; 0 failed; ... 579 filtered out`.
- Healthy-listener polarity (4d) — accept works and no rebind should occur:
  `test_rebinding_listener_reports_the_address_it_rebinds ... ok` and
  `test_rebinding_listener_still_shuts_down_gracefully ... ok`, each
  `test result: ok. 1 passed; 0 failed; ... 579 filtered out`
  (`healthy-shutdown-exit=0`).

The graceful-shutdown test matters more than its name suggests: it drives
`axum::serve(listener, app).with_graceful_shutdown(cancel_token.cancelled_owned())`
against the new listener type, which is the interaction most likely to regress into a
hang, and it terminates.

## Recipe 05 — the five claims that cannot be executed here

Each claim is cited from source read in this sandbox, not from memory. The axum source is
the resolved one: root `Cargo.toml:154` pins `axum = { version = "=0.8.4", ... }`,
`Cargo.lock:922` carries `version = "0.8.4"`, and the vendored crate inspected below is
`/scratch/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/axum-0.8.4` whose own
`Cargo.toml` reports `version = "0.8.4"`.

**Claim 1 — `Listener::accept` returns no `Result`, so axum retries the same socket
forever.** `axum-0.8.4/src/serve/listener.rs:20` declares
`fn accept(&mut self) -> impl Future<Output = (Self::Io, Self::Addr)> + Send;` with the
doc "If the underlying accept call can return an error, this function must take care of
logging and retrying." axum's own `impl Listener for TcpListener` (`listener.rs:26-42`)
loops on `Self::accept(self)` and on error calls `handle_accept_error(e)`, which
(`listener.rs:140-158`) logs `error!("accept error: {e}")`, sleeps one second, and returns
— to the same file descriptor. This is precisely the failure mode the PR describes: after
CRIU restore the socket never recovers, and stock axum will spin on it for the life of the
process. The trait signature is also why the fix has to be a new `Listener` impl rather
than a wrapper around `TcpListener::accept`.

**Claim 2 — axum selects the shutdown future against accept.**
`axum-0.8.4/src/serve/mod.rs:284-291`:

```rust
let (io, remote_addr) = tokio::select! {
    conn = listener.accept() => conn,
    _ = signal_tx.closed() => { trace!("signal received, not accepting new connections"); break; }
};
```

The consequence is that the new `accept`'s internal `tokio::time::sleep` backoff cannot
delay shutdown: the select cancels the accept future outright. This is what makes the
1-second rebind backoff safe, and it is corroborated empirically by
`test_rebinding_listener_still_shuts_down_gracefully` and by recipe 07's SIGTERM result.

**Claim 3 — the transient `ErrorKind` set matches axum's own.**
`is_transient_accept_error` (`system_status_server.rs:371-379`) matches
`ConnectionRefused | ConnectionAborted | ConnectionReset`. axum's `is_connection_error`
(`listener.rs:160-167`) matches the identical triple. The classification is neither
narrower nor wider than upstream's, so a client that dies between SYN and accept is
retried immediately and does not trigger a spurious rebind.

**Claim 4 — drop-before-rebind avoids `EADDRINUSE`.** In the accept loop
(`system_status_server.rs:326-366`) the non-transient arm sets `self.listener = None`
*before* sleeping, and the rebind happens on the next iteration through the
`let Some(listener) = self.listener.as_ref() else { ... }` branch. The ordering is
load-bearing and the code says so: a socket that has stopped accepting is still bound, so
rebinding first would fail. The rebind targets `self.address` — the *resolved* address
from the initial bind, not the caller's request — which is what keeps a `port 0`
deployment on the same port after recovery, and is what
`test_rebinding_listener_reports_the_address_it_rebinds` asserts.

**Claim 5 — one production caller, behind a default-off gate.** Outside its own module,
`spawn_system_status_server` is called exactly once, at
`lib/runtime/src/distributed.rs:248`. It is reached only when `cancel_token` is `Some`,
which follows `system_server_enabled()` (`config.rs:339-344`, `self.system_port >= 0`),
and `config.rs:20-21` defaults `DEFAULT_SYSTEM_PORT: i16 = -1` (disabled). The blast
radius of this change is therefore one server, in deployments that have explicitly set
`DYN_SYSTEM_PORT`. It is also why recipe 07 had to set that variable to exercise anything
at all.

One out-of-scope observation, offered as context rather than as a finding against this PR.
The same raw-`TcpListener` pattern that motivated the fix is still present at nine other
`axum::serve` call sites — `lib/kv-router/src/services/{selection/service.rs:513,
selection/server.rs:360, slot_tracker/mod.rs:83, shared_cache/mod.rs:43,
indexer/mod.rs:243}`, `lib/llm/src/http/service/service_v2.rs:{877,930}`, and
`lib/llm/src/block_manager/metrics_kvbm.rs:197`. If CRIU listener loss is a real
production concern, the frontend HTTP service in particular would suffer the same way. The
tree contains exactly one custom `Listener` impl, the new one
(`system_status_server.rs:322`), so nothing else already covers those sites.

Upstream state, recorded as context only and never treated as local evidence:
PR #12059 is `OPEN`, `reviewDecision: REVIEW_REQUIRED`, 1 changed file, +250/−35, with
**0** `CHANGES_REQUESTED` reviews. One check fails (`lychee`, a link checker) and several
were pending at the time of reading.

## Recipe 07 — aggregate smoke on the in-sandbox A100

Four attempts. The first three failed for reasons that are all environmental or
recipe-staleness, and all three are recorded with `exit=0` wrappers, so I name them
explicitly.

1. **Stale flag.** The recipe's `--store-kv file` no longer exists in this checkout; the
   worker exited with `unrecognized arguments: --store-kv file`. The current option is
   `--discovery-backend` (`components/src/dynamo/common/configuration/groups/runtime_args.py:172`,
   `components/src/dynamo/frontend/frontend_args.py:393`; env `DYN_DISCOVERY_BACKEND`,
   choices `kubernetes|etcd|file|mem`). Substituted `--discovery-backend file`.
2. **Self-inflicted kill.** My own cleanup `pkill -f "dynamo.vllm"` matched the wrapper
   shell whose command line contained that literal, killing the run (exit 144). Fixed by
   never colocating the pattern with the launch, and by verifying a clean slate
   (`ps aux | grep -c "[d]ynamo\."` = 0, GPU at 1 MiB) before relaunching.
3. **Stale gate literal, then the proxy.** The recipe waits for `"endpoint_id"` in
   `/health`; this build emits `"instance_id"` inside an `"instances":[...]` array, so the
   gate never fired even with a healthy worker. Re-gated on the array and added a second
   gate on `/v1/models`. That exposed the real blocker: the chat completion returned
   HTTP 404 with an empty model list, and the frontend log read
   `Model materialization failed ... fetching http://10.200.0.2:9090/v1/metadata/dynamo/backend/generate/qwen_qwen3-0_6b/_base/config.json returned status 403 Forbidden`.
   The 403 did not come from the worker. The status router
   (`system_status_server.rs:174-266`) has no 403 path at all — its handlers return only
   200/404/500 and its fallback logs `[fallback handler] called`, and
   `grep -c "fallback handler"` on the worker log was `0`, so the request never arrived.
   The sandbox routes egress through a persistent internal proxy address with
   `no_proxy=127.0.0.1,localhost,::1`; the pod IP `10.200.0.2` was not exempt, so reqwest
   sent an intra-pod request to the egress proxy, which refused it. Fixed by appending the
   pod IP to `no_proxy`/`NO_PROXY`. This is a sandbox networking property with no bearing
   on the PR.

The fourth attempt (18:37:26) is clean. The worker ran with `DYN_SYSTEM_PORT=9090`
(mandatory — without a non-negative port the gate at `config.rs:343` never starts the
server and none of the changed code would execute), model `Qwen/Qwen3-0.6B` with
`--load-format dummy`, TP 1, on the single A100. The worker log confirms the server came
up on the new listener:

```
INFO dynamo_runtime::system_status_server: [spawn_system_status_server] binding to: 0.0.0.0:9090
INFO dynamo_runtime::system_status_server: [spawn_system_status_server] system status server bound to: 0.0.0.0:9090
INFO dynamo_runtime::distributed: System status server started successfully on 0.0.0.0:9090
```

Startup: endpoints registered 60s after launch, model card published in the same window,
`/v1/models` listing `Qwen/Qwen3-0.6B`. Section 2: the chat completion through the
frontend returned **HTTP 200** with a well-formed `chatcmpl-...` body and
`finish_reason: "length"` at `max_tokens: 5` (the text is gibberish, as expected from
`--load-format dummy` weights — that is correct behavior, not a defect). Direct hits on
the changed code path: `GET :9090/health` → **HTTP 200** `{"status":"ready",...}`,
`GET :9090/live` → **HTTP 200**, and a repeat `/health` → **HTTP 200**, showing the same
listener accepting repeatedly rather than serving once.

The strongest signal is indirect. Model-card materialization in Dynamo is a *pull*: the
frontend fetches `/v1/metadata/...` from the worker's own system status server. With the
proxy exemption in place, `frontend materialization failures: 0` — meaning the
`RebindingTcpListener` was on the critical path of the deployment coming up, not merely
poked by a side curl. The error path stayed quiet, as it should have with nothing breaking
the socket: `grep -c "System status listener stopped accepting"` → `0`.

Section 3 teardown: `SIGTERM-result=EXITED worker gone after 8s (no hang in the accept
loop)`, `frontend-wait-status=0`, `worker-wait-status=0`, `No errors` in the worker log,
the status port refusing connections afterwards, and GPU memory back to `0, 1 MiB`. The
new accept loop with its 1-second backoff does not delay or block shutdown — the empirical
counterpart to Claim 2.

## What was and was not demonstrated

Demonstrated locally: the change compiles clean under check and clippy; the full
`dynamo-runtime` lib suite passes with the change in place (578 passed, 0 failed); the
three new tests execute in the real crate binary, and the one that matters —
`test_rebinding_listener_serves_again_after_listener_shutdown` — took the real path, not
its platform early return; the new listener carries a live worker's health, metadata, and
model-card traffic on a GPU; and it shuts down promptly on SIGTERM.

Not demonstrated, and honestly outside what this sandbox can do: an actual CRIU
checkpoint/restore. The test's `shutdown(2)` on a listening socket is a stand-in for it,
and a good one for the accept-side failure, but it does not reproduce the network-namespace
change that the code comment cites as a second motivation for rebinding. The claim that
rebinding "lands the server in the restored network namespace" is reasoned, not verified
here. Likewise, the failure mode has no repeated-failure test: nothing exercises a rebind
that itself fails and retries under backoff, so the `bind_system_status_listener` error arm
at `system_status_server.rs:331-338` is unexecuted by the suite.

Every recipe reached a green final state; the environment failures (swagger download, the
egress proxy) were resolved rather than worked around, and none of them are attributable to
the change.

## Verdict: pass

@glamr-agent

Copy link
Copy Markdown
Contributor Author

Automated AI review — advisory. An AI agent's judgment of whether this change is logically sound on the evidence; not a merge authorization. CI, customs, and a human reviewer hold that.

Assessment: sound

Findings

No blocking findings remain.

Evidence audit

  • The prior blocking issue is addressed: /sandbox/job/artifacts/wi-20260723T020701Z-10665/change.md now records Commit: 21a7ccd509b832fb8e0a429c085b764e6080131b, and git -C /home/sandbox/workspace/wi-20260723T020701Z-10665/repo rev-parse fix/system-health-server-rebind-after-criu--95e8e92a4325 returns the same SHA.
  • The recorded commit exists in the clone: git -C /home/sandbox/workspace/wi-20260723T020701Z-10665/repo cat-file -e 21a7ccd509b832fb8e0a429c085b764e6080131b^{commit} exits successfully.
  • The target checkout is clean: git -C /home/sandbox/workspace/wi-20260723T020701Z-10665/repo status --short returns no entries.
  • /sandbox/job/artifacts/wi-20260723T020701Z-10665/change.diff exactly matches the current committed source diff from git -C /home/sandbox/workspace/wi-20260723T020701Z-10665/repo diff main..fix/system-health-server-rebind-after-criu--95e8e92a4325; both are 14,135 bytes with SHA-256 d628125b8e78df1746af9f7cb67627a18279591956a35c5c90d7e41a14919e17.
  • The committed source diff is focused on /home/sandbox/workspace/wi-20260723T020701Z-10665/repo/lib/runtime/src/system_status_server.rs, and git diff main..fix/system-health-server-rebind-after-criu--95e8e92a4325 --check exits 0.
  • /sandbox/job/artifacts/wi-20260723T020701Z-10665/change-validation.md appropriately scopes re-entry validation to packet/repo consistency and explicitly states that full cargo and vLLM validation were not rerun because the source diff did not change. It preserves the earlier recorded cargo/vLLM evidence without claiming it was rerun during this metadata-only repair.
  • The active template in /sandbox/job/artifacts/wi-20260723T020701Z-10665/plan.md is debug-investigation, and the required re-entry checks listed there are reflected in /sandbox/job/artifacts/wi-20260723T020701Z-10665/change-validation.md.

@glamr-agent

Copy link
Copy Markdown
Contributor Author

CI result: failed

Observed during bounded CI watch for #12059.

Failing checks/statuses observed:

@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 found 1 potential issue.

Open in Devin Review

Comment thread lib/runtime/src/system_status_server.rs Outdated
@coderabbitai

coderabbitai Bot commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor

Review in 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: ai-dynamo/dynamo/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Enterprise
  • Run ID: faec44ac-0032-44b8-b8f3-e36ce4ab7f18
📥 Commits

Reviewing files that changed from the base of the PR and between 006e5f3 and c56d94d.

📒 Files selected for processing (1)
  • lib/runtime/src/system_status_server.rs

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 10 remain after this review.


Walkthrough

The system status server retains its resolved address and retries failed listener binds with a one-second backoff. It classifies accept errors and serves with graceful cancellation. Tests cover listener recovery, address contention, error classification, shutdown during backoff, and descriptor exhaustion.

Changes

System status server

Layer / File(s) Summary
Startup and routing integration
lib/runtime/src/system_status_server.rs
Startup builds the router, binds through a helper, retains the resolved address, and serves with graceful cancellation. The router retains its routes and tracing; its fallback logs missing routes at debug level.
Listener recovery and error classification
lib/runtime/src/system_status_server.rs
RebindingTcpListener retries binds after fatal accept errors, retries resource-exhaustion errors on the same socket, and preserves the socket for recoverable connection errors.
Listener recovery and exhaustion tests
lib/runtime/src/system_status_server.rs
Tests cover rebinding, address contention, error classification, shutdown during rebind backoff, and accepting a queued connection after descriptor exhaustion.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to c56d9

The listener retains its chosen address and can retry recovery without the previously duplicated bind errors. No identified issue blocks merging after normal checks.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 15 functions across 1 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly and concisely describes the main change: rebinding the system status listener after accept failures.
Description check ✅ Passed The description covers the overview, implementation details, reviewer starting point, related issue (#10665), and validation status. It meets the repository template requirements.
  • Fix all pre-merge checks with AI

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

🧹 Nitpick comments (1)
lib/runtime/src/system_status_server.rs (1)

268-271: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fallback logs at info on every 404 — looks like a debug artifact.

[fallback handler] called fires at info level for every unmatched request with no path context, which is noisy in production. Consider debug and including the URI.

♻️ Suggested tweak
-    .fallback(|| async {
-        tracing::info!("[fallback handler] called");
-        (StatusCode::NOT_FOUND, "Route not found").into_response()
-    })
+    .fallback(|uri: axum::http::Uri| async move {
+        tracing::debug!(%uri, "system status route not found");
+        (StatusCode::NOT_FOUND, "Route not found").into_response()
+    })
🤖 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/system_status_server.rs` around lines 268 - 271, Update the
fallback handler’s tracing call to use debug-level logging instead of info, and
include the unmatched request URI in the message. Preserve the existing
NOT_FOUND response returned by the fallback closure.
🤖 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/system_status_server.rs`:
- Around line 294-303: Update serve_system_status_once to use a serving path
that surfaces unrecoverable TcpListener::accept failures instead of relying on
axum::serve’s retry loop. Preserve graceful shutdown via cancel_token, and
ensure listener errors propagate from this function so
supervise_rebinding_server can perform rebinding.

---

Nitpick comments:
In `@lib/runtime/src/system_status_server.rs`:
- Around line 268-271: Update the fallback handler’s tracing call to use
debug-level logging instead of info, and include the unmatched request URI in
the message. Preserve the existing NOT_FOUND response returned by the fallback
closure.
🪄 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: d39fbfb5-1814-4402-8fa6-9e9d60f6f4a3

📥 Commits

Reviewing files that changed from the base of the PR and between 0299856 and 21a7ccd.

📒 Files selected for processing (1)
  • lib/runtime/src/system_status_server.rs

Comment thread lib/runtime/src/system_status_server.rs Outdated
@datadog-official

datadog-official Bot commented Jul 23, 2026 •

Copy link
Copy Markdown

Tests

🔄 Datadog auto-retried 8 jobs - 8 passed on retry View in Datadog

🎯 Code Coverage (details)
• Patch Coverage: 100.00%
• Overall Coverage: 48.66% (-2.90%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 0e64178 | Docs | Datadog PR Page | Give us feedback!

@glamr-agent
glamr-agent requested a review from a team as a code owner July 24, 2026 20:43
@glamr-agent
glamr-agent temporarily deployed to external_collaborator July 24, 2026 20:43 — with GitHub Actions Inactive
@glamr-agent

glamr-agent commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor Author

Repair round 1 pushed commit 85d54b0ae9347b00c61d14c70b77c163cadb2653.

Addressed:

  • Devin and CodeRabbit accept-loop finding: confirmed from the pinned Axum 0.8.4 source that axum::serve does not return on TCP accept errors, then replaced the supervised status-server path with an explicit TcpListener::accept loop that propagates non-transient listener errors to the rebind supervisor.
  • CodeRabbit fallback-log nit: removed the debug-like info log on every 404 fallback.

Validation run:

  • cargo fmt --all
  • git diff --check
  • cargo test -p dynamo-runtime test_serve_system_status_once_returns_after_listener_shutdown --lib -- --nocapture
  • cargo test -p dynamo-runtime system_status_server::tests --lib
  • cargo check -p dynamo-runtime
  • cargo clippy -p dynamo-runtime --lib

Note: I could not fetch the previous failed GitHub Actions logs because GitHub returned Forbidden for the log zip URLs, so the repair was validated locally against the touched runtime crate and the confirmed review finding.

@glamr-agent
glamr-agent temporarily deployed to external_collaborator July 24, 2026 20:53 — with GitHub Actions Inactive
@glamr-agent

Copy link
Copy Markdown
Contributor Author

Repair round 2 pushed commit 2bb92781969ddf9bce765f62d36935dd0712f74f.

Addressed:

  • The rust-clippy (lib/runtime/examples) CI matrix failed before clippy because cargo metadata --locked in the nested lib/runtime/examples workspace found its Cargo.lock out of sync after round 1 added direct hyper-util usage to dynamo-runtime.
  • Updated lib/runtime/examples/Cargo.lock to match the dependency graph.

Validation run from lib/runtime/examples:

  • cargo metadata --locked --format-version 1
  • cargo clippy --no-deps --all-targets -- -D warnings

@MatejKosec MatejKosec changed the title [0/3 validated · 3 not run] fix(runtime): rebind system health server after CRIU listener loss fix(runtime): rebind system health server after CRIU listener loss Jul 24, 2026
@github-actions github-actions Bot added the fix label Jul 24, 2026
@MatejKosec
MatejKosec temporarily deployed to external_collaborator July 24, 2026 22:39 — with GitHub Actions Inactive
@MatejKosec
MatejKosec temporarily deployed to external_collaborator July 28, 2026 21:02 — with GitHub Actions Inactive
@MatejKosec
MatejKosec force-pushed the fix/system-health-server-rebind-after-criu--95e8e92a4325 branch from d6297d3 to 4da3e75 Compare July 28, 2026 21:34
@MatejKosec
MatejKosec temporarily deployed to external_collaborator July 28, 2026 21:34 — with GitHub Actions Inactive
@glamr-agent
glamr-agent deployed to external_collaborator October 5, 2026 20:08 — with GitHub Actions Active
@glamr-agent

Copy link
Copy Markdown
Contributor Author

nursery: Removed both redundant helper summaries in f11a1846f, addressing the repeated findings in dynamo-review-agent’s review. The comment above is_resource_exhaustion_error still explains why raw errno checks are needed.

@glamr-agent

Copy link
Copy Markdown
Contributor Author

nursery: Extended test_rebinding_listener_preserves_backlog_during_resource_exhaustion in f11a1846f, addressing the delay coverage item in dmitry-tokarev-nv’s review. The test keeps the same accept future alive while descriptors are released, then checks that it waits for the configured backoff before returning the queued connection.

@glamr-agent

Copy link
Copy Markdown
Contributor Author

nursery: Added test_rebinding_listener_shutdown_interrupts_failed_rebinds in f11a1846f, addressing the cancellation finding in CodeRabbit’s review comment. Another listener holds the recovery address, so rebinding fails. The test serves RebindingTcpListener with a 60-second backoff and checks that cancellation finishes within one second.

@glamr-agent

Copy link
Copy Markdown
Contributor Author

nursery: Changed nothing for the docstring percentage warning in CodeRabbit’s summary. The changed helpers are private. Their useful comments explain the Axum constraint and raw errno handling. Adding summaries just to raise the percentage would conflict with the specific review findings asking to remove redundant helper comments.

@glamr-agent

Copy link
Copy Markdown
Contributor Author

nursery: @coderabbitai full review

@glamr-agent

Copy link
Copy Markdown
Contributor Author

nursery: /devin review

@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

@glamr-agent I’ll run a full review of PR #12059, including the resource-exhaustion backoff test and cancellation during failed rebinds.

✅ Action performed

Full review finished.

@glamr-agent

Copy link
Copy Markdown
Contributor Author

nursery: @dynamo-ops please run full CI for f11a184

@dynamo-review-agent dynamo-review-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Previously reported defects still present:

  • Original discussion: The EMFILE entry is still present in test_resource_exhaustion_does_not_look_like_a_dead_listener. In the selected Linux Rust test stages, test_rebinding_listener_preserves_backlog_during_resource_exhaustion already drives real EMFILE through RebindingTcpListener::accept and fails if EMFILE is handled as either the rebind path or the no-backoff connection-error path, so the classifier parameter remains redundant there while ENFILE and ENOBUFS retain distinct raw-errno coverage.

Signed-off-by: GLAMR <svc-glamr@nvidia.com>
@glamr-agent
glamr-agent deployed to external_collaborator October 5, 2026 20:30 — with GitHub Actions Active
@glamr-agent

Copy link
Copy Markdown
Contributor Author

nursery: Removed the duplicate EMFILE case from test_resource_exhaustion_does_not_look_like_a_dead_listener in c56d94d02, addressing the repeated finding in dynamo-review-agent’s review. The Linux backlog test already drives real EMFILE through RebindingTcpListener::accept and checks that the listener stays bound and waits for the retry delay. The classifier test still checks ENFILE and ENOBUFS.

@glamr-agent

Copy link
Copy Markdown
Contributor Author

nursery: @coderabbitai full review

@glamr-agent

Copy link
Copy Markdown
Contributor Author

nursery: /devin review

@glamr-agent

Copy link
Copy Markdown
Contributor Author

nursery: @dynamo-ops please run full CI for c56d94d

@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

@glamr-agent I’ll run a full review of PR #12059, including the latest test changes in c56d94d02.

✅ Action performed

Full review finished.

@dmitry-tokarev-nv
dmitry-tokarev-nv dismissed their stale review October 5, 2026 21:13

Withdrawing my approval because new commits landed after it. I will review the new commits before I approve again.

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

Reviewed c56d94d. All four prior comments are addressed, and I found no additional issues. Source review only; I did not run tests or inspect CI.

@dynamo-ops

Copy link
Copy Markdown
Contributor

/ok to test c56d94d

@MatejKosec
MatejKosec enabled auto-merge (squash) October 5, 2026 23:08
Signed-off-by: GLAMR <svc-glamr@nvidia.com>
@glamr-agent
glamr-agent deployed to external_collaborator October 5, 2026 23:41 — with GitHub Actions Active
@glamr-agent

Copy link
Copy Markdown
Contributor Author

nursery: @dynamo-ops please run full CI for c8850ef

@dynamo-ops

Copy link
Copy Markdown
Contributor

/ok to test c8850ef

@devin-ai-integration

Copy link
Copy Markdown
Contributor

External PR triage

Blocker: approved and otherwise green, but BLOCKED by infrastructure. @jthomson04 approved head c8850efb87. In run 37389982152 two image builds (dynamo-runtime / image / Build multi-arch cuda13.0, planner / Build multi-arch cpu) hit the 1h job timeout in Build and Push Image, so backend-status-check and dynamo-status-check fail. The same builds pass on main. This is not the contributor's fault.

Next action, owned by @jthomson04 (or @dynamo-ops): re-run the failed jobs of run 37389982152. If the gates turn green, merge.
Owner on the contributor side: none needed.


Automated external-PR triage. This comment is edited in place on each run. Written by Devin

Signed-off-by: GLAMR <svc-glamr@nvidia.com>
@glamr-agent
glamr-agent deployed to external_collaborator October 6, 2026 19:27 — with GitHub Actions Active
@glamr-agent

Copy link
Copy Markdown
Contributor Author

nursery: @dynamo-ops please run full CI for 9a4d2c3

@dynamo-ops

Copy link
Copy Markdown
Contributor

/ok to test 9a4d2c3

This branch was successfully deployed

1 active deployment
external_collaborator — 9a4d2c39 Deployed Oct 6, 2026 by glamr-agent via ok-to-test #27146
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

actions container 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.

6 participants