Repository navigation
feat(global-router): read pool models from registrations, not a file - #17332
Conversation
…ts share dynamo-global-common holds the types that more than one Global Router component needs, so each is defined once: - a pool's identity and location (PoolKey, PoolId and its deriver, PoolLocation), from the shared crate on the global-router-v2 test branch; - the JSON of the Global Discovery Service's HTTP API: the POST /pools body and its lease reply, and the pool and router lists. Today the service and its client each define these; - how a router names a PoolRelay on a trusted network (address_identity), which the service writes into each pool's relay_identity; - Global Router replicas, and list-and-watch events and snapshots, which the service and the client in #17208 both define today. The JSON does not change. PoolKey now reads and writes {"site_id", "namespace", "dgd_name"} and checks each field while reading. A lease reply parses only when the heartbeat interval is above zero and below the lease, as in the client in #17208, and the lease is at most a day. Co-authored-by: Sachal Malick <s@chal.ai> Co-authored-by: Jacob Ioffe <jioffe@nvidia.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
❌ Dynamo PR CI failed — run 41559139567 (attempt 1) on
|
| Framework | 1-GPU arm64 |
|---|---|
| vLLM | ❌ 1 |
Failure details
1 job failed: both CPU pytest stages of the vLLM arm64 test job died right after pytest started, with the self-hosted runner's container hook error and no test results. This looks like infrastructure; rerunning the failed job is likely enough.
❌ vllm-runtime / Test cuda13.0, arm64: runner container hook failed ("Executing the custom container implementation failed")
Job: vllm-runtime / Test cuda13.0, arm64 · Failed step: Run CPU-only tests (parallelized) · Logs: gh run view --job 125680383412 -R ai-dynamo/dynamo --log-failed
▶️ Executing (cwd=/workspace): python -m pytest -n auto ... -m pre_merge and vllm and gpu_0 and not e2e
[Datadog Test Optimization] WARNING ... rerunfailures is installed alongside Datadog Test Optimization ...
##[error]Executing the custom container implementation failed. Please contact your self hosted runner administrator.
No files were found with the provided path: test-results/allure-results/. No artifacts will be uploaded.
▶️ Executing (cwd=/workspace): python -m pytest -n 0 ... -m pre_merge and vllm and gpu_0 and e2e
##[error]Executing the custom container implementation failed. Please contact your self hosted runner administrator.
...
❌ pre_merge_cpu: step outcome 'failure'
❌ pre_merge_cpu_e2e: step outcome 'failure'
##[error]A pytest stage did not record a passing result. See this step's log for the stage.
Neither pre_merge_cpu nor pre_merge_cpu_e2e printed a pytest result or test failure. Both ended with the runner's Kubernetes container hook error, and neither wrote allure results. That is an infrastructure signature (the runner or container exec failed), not a test failure, so a rerun of failed jobs is likely enough.
For agents
{"pr": 17332, "run_id": 41559139567, "run_attempt": 1, "head_sha": "451d3530462c824f8558866a69fcc7131f926524",
"failures": [{"job": "vllm-runtime / Test cuda13.0, arm64", "job_id": 125680383412, "failed_step": "Run CPU-only tests (parallelized)", "signature": "Executing the custom container implementation failed (runner container hook, infra)", "tests": [], "log_cmd": "gh run view --job 125680383412 -R ai-dynamo/dynamo --log-failed"}]}Posted automatically by Devin for run 41559139567. Updated on every full-CI run of this PR.
Review feedback: the comments on `round_trip`, `PoolLocation` and `Snapshot` repeated the name or the fields right below them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
…c comments - A `POST /pools` body with an empty or blank `model` no longer parses, as with an invalid pool key, so the PoolRelay gets a 422 instead of registering a pool that no request can match. - The crate docs said the service writes the API's JSON and its clients read it; the registration body goes the other way. - `pool.rs` no longer refers to a source adapter that is not on main, or calls the V1 derivation the POC one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
fa49608 to
8405ca5
Compare
0ca36ea to
819368d
Compare
2487538 to
f9f2f28
Compare
8405ca5 to
364b09e
Compare
364b09e to
ee79333
Compare
f9f2f28 to
b693840
Compare
The one-day cap made the shared `Lease` stricter than the client in #17208: once that client reads the shared type, a reply with a longer lease would fail to parse. The cap only protects the service's own clock arithmetic, so the service now bounds the lease it grants (#17371), and `Lease` keeps the checks a client needs: the interval is above zero and below the lease. Also drops `LeaseError`'s doc comment, which restated its use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
ee79333 to
ba40a2f
Compare
b693840 to
57067a7
Compare
A pool's listing named its PoolRelay by one address. On a dual-stack cluster the PoolRelay's pod has two, and it connects to a router from whichever family the router's address uses, so a router matching one address could turn the right PoolRelay away. `relay_identities` now lists each of them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
round_trip fails unless the parsed value serializes back to the same JSON, so the field assertions after it could not catch anything more. A lease of u64::MAX seconds already shows that a lease has no upper bound, so the 48-hour case goes too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
dynamo-global-discovery-service starts as a library: which Global Router pools exist, and which Global Router replicas a PoolRelay connects to. Following the Global Router coding standards: - The contract comes first: the store traits in pool_discovery (PoolDirectory, PoolRegistrar, ReplicaDirectory), on the dynamo-global-common types. - The stores sit below it: in memory (registrations with leases), and MCS (pools and router replicas from the EndpointSlices that Karmada or a standard MCS implementation imports). The HTTP service that serves them is added separately. The code is the discovery work from #15933 and #15949, in the crate layout of the global-router-v2 test branch. It leaves out the router-side pieces those PRs had (relay admission, state connections, router discovery modes): routers now read pools from the service, and relay connections belong to the router. Co-authored-by: Sachal Malick <s@chal.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
The join of registrations and MCS imports matches pools by key, so a registration's cluster_id must be the name MCS gives the pool's cluster: the About API cluster ID with standard MCS, or the member cluster name with Karmada. Only the warning for an unmatched registration said so; the module docs now do too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
estroz
left a comment
There was a problem hiding this comment.
lgtm
couple of questions:
frontend_urlis alwayshttp://(hub.rs:200). is tls on the frontend out of scope for v1, or should the scheme come from the port's appProtocol?- where do poolrelays get the mcs cluster name that
cluster_idhas to match?
A pool runs active-active PoolRelay replicas: each reports to every Global Router, and routers dedupe the overlapping reports, as the operator's DynamoPoolRelay runs two pods and #17686 keeps their reports apart. The MCS backend bound only the Ready pod with the lowest address, so routers matching relay_identities would admit one replica and refuse the other. It now lists every Ready PoolRelay pod by each of its addresses, sorted, so every service instance lists the same identities. A pool stays live while any PoolRelay endpoint is Ready, and a pod that stops being Ready leaves the listing. An IPv4-mapped IPv6 address counts as its IPv4 address, so a pod is never listed twice under one identity. The pods were grouped by targetRef only to choose one, so the grouping is gone. PoolListing::relay_identities now says it names every replica, and the memory store says that replicas registering one pool should send the same registration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
…obal-discovery-http Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
…very-pool-registrations Brings in the listing of every Ready PoolRelay pod. In this branch's MCS backend, which also reads frontend slices, ready_addresses returns plain addresses for both kinds of slice, and rebuild lists every Ready PoolRelay address, canonicalized, next to the frontends. A test now checks that frontend addresses are not PoolRelay identities. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
A pool runs active-active PoolRelay replicas, so the join lists a pool while MCS imports a Ready PoolRelay endpoint and one of its PoolRelays has registered, and MCS gives every Ready PoolRelay's addresses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
Fixes from review of the MCS backend with registrations: - The join gave up for good when it could not watch a source: every watch stream ended, and /healthz stayed OK. It now watches both sources again with kube's default backoff (0.8 s doubling to 30 s), also when a source's watch ends, and joins on each read until both have listed again. A rebuild after both sources listed again now also drops a pool that neither lists any more. - The MCS backend warns when a pool goes live without a Ready frontend, or loses its last one. The warning names the frontend Service and says whether any of its slices are imported, so a frontend Service that is not exported shows up. It is logged once per change, not on every slice event. - The warning for a pool that MCS lists but that has no live registration gives the cause the module describes first: a lease that lapsed while MCS still shows a stale Ready endpoint. - Tests: a source relist that drops a joined pool removes it from the join and from its Resync; a failed subscription and an ended watch are watched again; deleting a frontend slice lists the pool without frontends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
|
Thanks for the review, Eric. The five inline comments are addressed in 2ba2b8b, with a reply in each thread. On your two questions: TLS to the frontends. Out of scope for V1, which assumes a trusted network, as the Where PoolRelays get the cluster name. From With Karmada the operator doesn't check it, since Karmada names clusters by their member names, so |
…arning once Follow-ups from an independent review of the previous commit: - The join's backoff starts over once the join follows both sources again, as kube's StreamBackoff does once its watch delivers. Before, a watch that kept ending every minute would have waited 30 s each time. - While the join watches the sources again, its watchers get the last view marked Estimated, as the MCS backend's watchers do while its watch fails, instead of keeping a view marked Exact. - The warning for a live pool without a frontend is recorded in the naming rules rather than the index, so the new index that each read builds before the first sync does not log it again. The log field is now frontend_slices_with_port, since a frontend slice without the named port does not count. - The retry test checks that the first attempt failed before it checks that each read joins the sources. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
POST /pools took bodies of up to axum's default of 2 MiB, and the backend keeps each registration for its lease. A registration is a few hundred bytes, so the routes now refuse a body over 64 KiB with 413 before it reaches the backend. A test sends one body at the limit and one a byte over it. The module doc now says that ready does not mean the pool list is complete: GET /pools says that in `complete`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
…very-pool-registrations Brings in the 64 KiB limit on request bodies and the readiness note in the module doc. The note sits after this branch's text on partial lists under GET /pools. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
#17331 is in main as one squashed commit, so its files came from both sides. Main has not changed them since, so this branch's versions, which add the HTTP service to them, stand. Cargo.lock is main's, plus dynamo-http-server and the service's new dependencies. CODEOWNERS regenerates unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
The example apply command named global-discovery-service.yaml, which works only from container/. It now names container/global-discovery-service.yaml, so it runs from the repository root, like the build command in the Dockerfile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
…very-pool-registrations Brings in main, which the base branch merged after #17331 landed, and the manifest's apply command from the repository root. In the manifest's header, this branch's text on what the service lists stays, after the new command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
|
| Layer / File(s) | Summary |
|---|---|
HTTP server crate and shared data types Cargo.toml, lib/http-server/* |
The workspace adds the HTTP server crate. The crate exposes load and request utilities and declares its public modules. |
Connection and request lifecycle lib/http-server/src/disconnect.rs, lib/http-server/src/lifecycle.rs |
Connection handles report closure status to a monitor. The service observer tracks readiness stages and inflight responses. |
HTTP server, shutdown, and write deadlines lib/http-server/src/server.rs, lib/http-server/src/write_timeout.rs |
The server handles TCP and TLS connections, transfer and write-stall deadlines, and graceful shutdown. Tests cover listener configuration and timeout behavior. |
Kubernetes pool and frontend discovery lib/global-discovery-service/src/backends/kubernetes/*, lib/global-discovery-service/src/backends/memory.rs |
The Kubernetes backend builds pool listings from Ready relay endpoints and frontend addresses in EndpointSlices. It validates service naming and tracks frontend availability warnings. The memory backend caps its sweep interval at one second or the heartbeat interval, whichever is shorter. |
Joined registration and MCS listings lib/global-discovery-service/src/backends/registered.rs, lib/global-discovery-service/src/backends.rs |
The registered pool directory includes only pools present in both source directories. It combines their listing data, tracks completeness, and retries failed or ended watches. |
Discovery HTTP API lib/global-discovery-service/src/service.rs, lib/global-discovery-service/src/lib.rs |
The service adds pool and router routes, optional pool registration, health checks, and error responses. Tests cover route behavior, request limits, unavailable backends, and shutdown. |
Discovery service configuration and startup lib/global-discovery-service/Cargo.toml, lib/global-discovery-service/src/main.rs |
The binary adds discovery modes, CLI validation, backend construction, HTTP startup, and signal-based shutdown. |
Container and deployment configuration container/Dockerfile.global-discovery-service, container/global-discovery-service.yaml, .github/codeowners/areas.yaml, .github/filters.yaml, CODEOWNERS |
The change adds a container image and Kubernetes resources for the service. Ownership rules cover the new paths, and the CI ignore filter includes the Dockerfile and manifest. |
Estimated code review effort: 4 (Complex) | ~60 minutes
Pre-merge checks: 
4 · 
1
-
Failed checks: 1 warning Passed checks: 4 passed-
Full details: Description check
-
Explanation
The description is detailed and covers the change overview, key behavior, validation results, trade-offs, ongoing limitations, and related issues. It does not use the exact template headings or include the checklist, but the required information is mostly present.
-
Full details: Docstring Coverage
-
Explanation
Docstring coverage is 46.25% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 80 functions across 17 files. (8 skipped: 8 unsupported.)
Resolution
Write docstrings for the functions missing them to satisfy the coverage threshold.
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Warning
Review coverage is incomplete: 1 file could not be fully reviewed. Findings from completed review steps are included; see review info for details.
Brings in main, which now has #17371 (squash-merged as b751c95). This branch already had all of #17371 through 7f61b37, so the four files that conflicted keep this branch's version: the manifest, backends/kubernetes.rs, main.rs and service.rs. Main has not changed them since #17371, so the merged tree is main plus this branch's own changes, and nothing else. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
Brings in main up to c80a17b. None of main's eleven new commits touch this branch's files or the crates it depends on, so the merge is clean, and the merged tree is main plus this branch's own changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
Brings in main, which now has #17332 (squash-merged as d1cf542). This branch already had #17332 through d8f5672, so the merge takes that commit as its base: the merged tree is main plus this branch's own changes, and nothing else. The files that a plain merge reports as add/add conflicts take that tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
Summary
Before this PR the
POST /poolsanswered with HTTP 501 "this service takes no registrations". This was ok for the original K8 design but the problem is that we also need to communicate the model from the pools to the Global Router.Note that model info cannot be communicated to the Global Discovery service through our custom mcs implementation either. The controller copies a Service's
dynamo.nvidia.com/mcs-*annotations to the other clusters, so a model annotation could ride along. But the annotations land on the ServiceImport, not on the EndpointSlices the discovery service reads. This would be a less robust approach because: the service would need a second watch; someone has to write the annotations manually. The PoolRelay already knows it.This PR is an alternative to #17276. Both make MCS pools routable by giving each pool a model and live frontend addresses. #17276 reads the model from a hand-written
--routing-configfile. This PR gets it from the PoolRelay itself, throughPOST /pools, as the design says.Stacked on #17371.
Part of #17600, the pool discovery DEP.
How it works
--pool-discovery karmadaormcs),POST /poolsnow works (it returned 501). Each call is the pool's heartbeat, as with--pool-discovery registrations.--pool-discoveryvalue takes registrations: the lease flags and--registration-storageapply to all of them, while the frontend flags work only withkarmadaormcs.<dgd>-frontendService, the same way as feat(global-router): resolve MCS pool models and live frontends #17276. Each frontend pod is listed once, at its IPv4 address on a dual-stack cluster. The registration gives the model, capabilities, capacity and location.--peer-service) moved to feat(global-router): share registrations between discovery instances #17637, stacked on this PR, since shared storage such as DynamoDB may replace it.Problems with the file in #17276
"capabilities": []and"capacity": []. In this PR, both come from the PoolRelay's registration. Today's round-robin router doesn't use either field yet. They matter for later work, such as load-aware routing (how big is each pool) or sending requests only to pools that can handle them.POST /poolsstill returns 501, but the design has PoolRelays register in every mode. A PoolRelay pointed at an MCS-mode service would get an error on every heartbeat.ready_addressesinhub.rs).What this PR adds
--pool-discoveryvalue.cluster_idisn't the pool's MCS cluster name. Today such a pool is silently missing.<dgd>-frontendisn't exported. Such a pool is listed withfrontends: [], so routers have nowhere to send its requests.Trade-offs
POST /pools, and no PoolRelay does yet. Until then, register a pool withcurl, as feat(global-router): route round robin over pools from discovery #17273 did.GET /poolssayscomplete: falsefor one lease after the service starts, while PoolRelays may not have registered yet, and while the MCS watch keeps failing. A caller should then merge rather than replace: keep known pools that are missing, and remove pools only on a complete list. TheGET /poolsdocs inservice.rssay this, and so do the library's contract docs under "Partial lists" (feat(global-router): add the Global Discovery Service library (contract and backends) #17331).GET /poolssayscomplete: falseuntil a lease has passed. feat(global-router): resolve MCS pool models and live frontends #17276 doesn't have this, because MCS alone keeps those pools listed. Registration storage that every region reads, such as a later--registration-storageon DynamoDB global tables, would remove it, and would replace feat(global-router): share registrations between discovery instances #17637 too.To deploy, each pool also exports
<dgd>-frontend, as in #17276.Validation
cargo test -p dynamo-global-discovery-service: 73 passed (61 library, 12 binary); the binary's flag tests also cover the frontend flags. They cover:Estimated, until both sides list again, and the backoff then starts over), and its completeness,Exactonly while both sides are;cargo clippy --all-targets -- -D warningsandcargo fmton the crate: clean.-goes after=, a suffix left without a value fails instead of taking the next flag, and no flag can be empty.Estimated, that a dual-stack frontend pod was listed twice, that a misnamed frontend port dropped a pool's frontends without a log line, and that the manifest's comments predated registrations. A third found that the join could then sayExactwhile another instance's registrations were missing, and that slices that do not name their pods still listed a dual-stack frontend twice. All are fixed.Exactwhile the join watched the sources again, and that each read before the first sync repeated the frontend warning. Those are fixed too. Breaking each of the eight new guards on purpose made its test fail.🤖 Generated with Claude Code
Summary by CodeRabbit