Repository navigation
feat(global-router): read pool models from registrations, not a file - #17332
atchernych wants to merge 22 commits into
Conversation
✅ Dynamo PR CI passed — run 41146614737 (attempt 1) on
|
| peer: &ReplicaEndpoint, | ||
| ) -> anyhow::Result<(SharedRegistrations, Instant)> { | ||
| let url = peer.url.join("registrations")?; | ||
| let reply = client |
There was a problem hiding this comment.
expires_in_ms is computed by the remote handler before response serialization and network transit, but fetch timestamps the reply only after the body has arrived. Consequently absorb subtracts only the later wait for other peers, allowing a slow response to extend a copied registration beyond the source registration by up to the request timeout. Account for the full request/response elapsed time so copies cannot outlive their originals.
🤖 AI Fix
Capture an Instant before sending the request and subtract its full elapsed duration from every returned registration before adopting it.
fa49608 to
8405ca5
Compare
0ca36ea to
819368d
Compare
2487538 to
f9f2f28
Compare
8405ca5 to
364b09e
Compare
364b09e to
ee79333
Compare
f9f2f28 to
b693840
Compare
ee79333 to
ba40a2f
Compare
b693840 to
57067a7
Compare
In MCS mode the Global Discovery Service now accepts POST /pools, and lists a pool only while MCS shows a Ready PoolRelay for it and the pool's registration is live. The registration gives the model, capabilities, capacity and location. MCS gives the Relay and frontend addresses; the frontends come from the `<dgd>-frontend` Service, as in #17276. Instances share registrations: each polls the others' GET /registrations twice per heartbeat interval, finding them through the service's own MCS export (--peer-service), and keeps copies for the time they have left. Only registrations a PoolRelay made locally are shared, so copies are never passed on. Sharing goes through a RegistrationExchange contract, which the memory store implements. A new instance is ready only after it has asked the others once, and with --peer-service the lease must be more than two heartbeat intervals. Instances ask each other over plain HTTP, so --peer-service is refused with --tls-cert-path for now. A pool that only one side lists for two lease durations is logged once. This replaces #17282, which targeted the global-router-v2 test branch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
--frontend-service-suffix had the same problem as --pool-service-suffix:
its values start with a hyphen ("-frontend"), so one given as a separate
argument did not parse. It now does, and an empty one is refused when the
flags are read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
- The join's lists are Exact while both sources' latest lists are, and Estimated otherwise, before and after it syncs. - List each frontend pod once, at its IPv4 address on a dual-stack cluster, and warn when a frontend slice has no TCP port of the configured name. The port check is the one router replicas use. - Give RegisteredPoolDirectory, PeerSync and Sharing Debug impls, and show sharing in DiscoveryStore's. - Describe registrations, frontends and --peer-service in the manifest. - The watch test heartbeats through the registration store's first lease, which now ends with a Resync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
- The registration store stays Estimated while registrations made with another instance may be missing: one has not answered within a lease, or the list of instances may be incomplete. PeerSync reports this after each round, so the join no longer says Exact while another cluster's pools are missing. - Frontends are listed in one address family, IPv4 when any has one, so a dual-stack frontend is listed once even when its slices do not name its pod. A PoolRelay in such slices is bound by its lowest address alone, as documented. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
ba40a2f to
896e50d
Compare
57067a7 to
6db71b3
Compare
…very-pool-registrations The base now has main, with #17355's Global Router crates. No conflicts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
…very-pool-registrations The base renamed the shared types after review (PoolKey's cluster_id, RouterReplica, WatchEvent::Upserted, Lease::duration, Capacity.workers, no PoolLocation.cluster), moved the discovery client onto them, and merged main. The two conflicts, in memory.rs and pool_discovery.rs, are lines this branch changed and the base renamed: keep this branch's lines with the new names. This commit also renames this branch's own uses, its test JSON included, and has tests that drop a lease say so, since Lease is now #[must_use]. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
…very-pool-registrations The base took #17331's changes: main (with #17330's shared types), doc comments on the stores' watch streams, and Karmada's Service name limit, 55, not 57. McsReplicaDirectory, which also reads the peer Service here, now logs an error for a Service name Karmada cannot import. No conflicts. CODEOWNERS still matches areas.yaml. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
…very-pool-registrations The base split --store into --pool-discovery (registrations, karmada or mcs) and --registration-storage (memory), and renamed DiscoveryStore to DiscoveryBackend. Resolved for this PR: - main.rs: the base's flags, with this PR's additions. Every --pool-discovery value takes registrations here, so the lease flags apply to all of them. The frontend and peer flags work only with karmada or mcs, so --help lists them there, and --peer-service with registrations fails at startup, which replaces this PR's own check. - service.rs: GET /registrations and the peer readiness check use DiscoveryBackend. The 501 for a backend without registrations no longer says its pools come from MCS, since MCS takes registrations here. - The peers tests and the MCS example use DiscoveryBackend. - The manifest names the default --registration-storage=memory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
…very-pool-registrations The base renamed DiscoveryBackend to PoolDiscoveryBackend and tightened the new flags after an adversarial review. Resolved for this PR: - main.rs: the base's changes, with this PR's flags kept. clap now requires --peer-service for --peer-namespace and --peer-port-name, which were ignored without it, and a test covers that. --registration-storage and the lease flags stay without a heading here, since every --pool-discovery value takes registrations. - service.rs, peers.rs and the MCS example use PoolDiscoveryBackend. The peer readiness check in /healthz is kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
…very-pool-registrations The base now calls the discovery implementations backends, not stores. memory.rs conflicted in its module doc, which this PR extends: kept this PR's text with the new word. This PR's other mentions of "the MCS store" now say "the MCS backend"; "registration store" stays, since it does keep registrations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
…reat it The `GET /pools` docs now list when `complete` is false: for one lease after the service starts, and with MCS, while another discovery instance has not answered for a lease and while the MCS watch keeps failing. They also say what a caller that keeps its own list does: replace on a complete list, merge a partial one and keep missing pools, and still use a partial list when it knows no pools yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
…very-pool-registrations Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
…lags are The same fixes #17371 made to its flags, for the flags this PR adds: - A `--frontend-service-suffix` left without a value no longer takes the next flag as the suffix. A suffix that starts with `-` goes after `=`. - `--frontend-port-name` and the `--peer-*` flags cannot be empty. - With `karmada`, a `--peer-service` longer than Karmada can import fails at startup, as `--router-service` does. - `--peer-service` with `--tls-cert-path`, and a lease of at most two heartbeats with `--peer-service`, get clap's usage and exit code 2 instead of exiting 1 from `main`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
…to its own PR `--peer-service`, `GET /registrations` and the copies the memory store kept for other instances move to a follow-up PR stacked on this one. Shared registration storage, such as DynamoDB, may replace them, so this PR keeps the join of registrations with MCS and leaves the choice to that PR. Without sharing, one instance keeps the registrations it receives, so run one and have every PoolRelay register with it. The memory store's faster sweep stays, since the join drops a pool when the sweep removes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
…in one family Fixes from a review of this PR: - `--frontend-service-suffix` and `--pool-service-suffix` that overlap are refused by `parse_args`, with clap's usage and exit code 2. Before, the check ran only after the Kubernetes client was created, so it exited 1, or was hidden behind a failure to create the client. - On a dual-stack cluster, a pool's frontends stay IPv4 while the pool has an IPv4 frontend slice, even one with no Ready endpoint yet. Before, the family followed the Ready addresses, so the IPv6 slice being imported or Ready first gave IPv6 frontends that IPv4-only routers cannot reach. - A frontend slice without the named port is warned about once, not on every slice event. Any imported Service whose name ends with the suffix, such as another app's `shop-frontend`, could repeat it. - The join's test now checks that a pool goes within one sweep of its lease's end. It passed with any sweep period before, since `advance` ran every pending sweep after the lease had already ended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
…very-pool-registrations 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?
| "cannot watch the pool sources; joining on each read" | ||
| ); | ||
| // End watch streams instead of leaving them waiting for a Resync. | ||
| stop.cancel(); |
There was a problem hiding this comment.
once registrations come from ddb (#17644) a transient failure at startup would leave this instance watch-dead until restart while still reporting healthy
There was a problem hiding this comment.
Fixed in 2ba2b8b. join_sources no longer gives up. When it can't watch a source, or a source's watch ends, it unsyncs the join, waits with kube's DefaultBackoff (0.8 s doubling to 30 s), and watches both sources again. Until both have sent a Resync, list() joins the sources on each read and watch streams wait. rebuild now also visits pools already in the join, since after both sides relist a joined pool may be in neither. Tests: sources_that_cannot_be_watched_yet_are_watched_later and the_join_watches_again_when_a_watch_ends.
On #17644 itself: DynamoDbPoolStore::list_and_watch doesn't call DynamoDB. It starts the refresh task and subscribes, and a failed first read leaves the stream waiting while the refresh retries every second, so a table outage couldn't reach this branch. If the table is unreadable at startup, /healthz returns 503 until the first read succeeds, because list() reads the table until then. In the old give-up state GET /pools also stayed correct, since list() joined the sources on each read, but the join's watch streams and its one-sided warnings stopped for good.
There was a problem hiding this comment.
Follow-up in 892659d, from a second review of this fix: the backoff now starts over once the join follows both sources again, as kube's StreamBackoff does once its watch delivers, so a watch that keeps ending isn't retried every 30 s. And while the join watches the sources again, its watchers get the last view marked Estimated instead of keeping one marked Exact.
| model: None, | ||
| capabilities: Vec::new(), | ||
| frontends: Vec::new(), | ||
| frontends, |
There was a problem hiding this comment.
can you add a warn log here? a live pool whose <dgd>-frontend slice is missing or has no ready endpoint gets listed with frontends: []. that would make the usual misconfig (forgot to export the frontend service) obvious.
There was a problem hiding this comment.
Added in 2ba2b8b. PoolIndex::rebuild warns when a pool goes live without a Ready frontend, or loses its last one. The log has the pool key, the frontend Service it expects (<dgd>-frontend), the port name, and frontend_slices_imported: false means MCS imports no slice of that Service with that port, usually because the Service isn't exported. It's logged once per change, not on every slice event. A frontend slice without the named port already had its own warning.
There was a problem hiding this comment.
Follow-up in 892659d: warned pools are now recorded in the naming rules rather than the index. Before the first sync, list() builds a new index on each read, so the warning would have repeated on every /healthz until then. The field is now frontend_slices_with_port, since a frontend slice without the named port doesn't count.
| } | ||
|
|
||
| #[tokio::test(start_paused = true)] | ||
| async fn tracks_pools_only_one_source_lists() { |
There was a problem hiding this comment.
test gap: tracks_pools_only_one_source_lists checks one_sided but nothing asserts that a source Resync which drops a joined pool removes it from pools and from the published Resync.
There was a problem hiding this comment.
Added a_relist_that_drops_a_joined_pool_removes_it in 2ba2b8b. For each source, a Resync without a joined pool removes it from pools and from the published Resync, with no per-pool event, and the pool becomes one-sided. It also covers both sources relisting, the case the new retry adds: a pool that neither lists any more leaves the join. That needed the rebuild change from the startup-failure thread.
| } | ||
|
|
||
| #[test] | ||
| fn frontends_come_from_the_frontend_service() { |
There was a problem hiding this comment.
test gap: every Event::Delete test deletes a poolrelay slice; deleting a frontend slice through upsert isn't covered, and that's the case that should flip frontends to empty (or warn, per the comment above).
There was a problem hiding this comment.
Added a_pool_that_loses_its_frontend_slice_is_listed_without_frontends in 2ba2b8b. An Event::Delete of the frontend slice goes through upsert with None, and the pool stays live, since its PoolRelay is Ready, with frontends: []. It also checks when the new warning fires: on losing the last frontend or going live without one, and not again while the pool has none.
| } | ||
| one_sided.warned = true; | ||
| if mcs.listings.contains_key(key) { | ||
| tracing::warn!( |
There was a problem hiding this comment.
nit: the warning should reflect that the module doc (16-19) says the expected cause is a stale Ready slice / lapsed lease.
There was a problem hiding this comment.
Reworded in 2ba2b8b to give that cause first: "Either its lease lapsed while MCS still shows a stale Ready endpoint, as when the pool's cluster loses contact with the MCS implementation, or its PoolRelays do not call POST /pools on this service with this key." In #17637 the second half also says to check that this instance shares registrations with the discovery instance in the pool's cluster.
…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 |
…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>
…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>
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