Skip to content

feat(global-router): read pool models from registrations, not a file - #17332

Open
atchernych wants to merge 22 commits into
atchernych/global-discovery-httpfrom
atchernych/discovery-pool-registrations
Open

atchernych wants to merge 22 commits into
atchernych/global-discovery-httpfrom
atchernych/discovery-pool-registrations

Conversation

@atchernych

@atchernych atchernych commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Before this PR the POST /pools answered 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-config file. This PR gets it from the PoolRelay itself, through POST /pools, as the design says.

Stacked on #17371.

Part of #17600, the pool discovery DEP.

How it works

  • With MCS pool discovery (--pool-discovery karmada or mcs), POST /pools now works (it returned 501). Each call is the pool's heartbeat, as with --pool-discovery registrations.
  • So every --pool-discovery value takes registrations: the lease flags and --registration-storage apply to all of them, while the frontend flags work only with karmada or mcs.
  • A pool is listed only when both sides agree: MCS shows a Ready PoolRelay, and the pool registered within the lease (15 s by default).
  • MCS gives the addresses: the Relay's, and the frontends' from the <dgd>-frontend Service, 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.
  • One discovery instance keeps the registrations it receives, so run one, and have every PoolRelay register with it. Sharing registrations between the instances in every cluster (--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

  1. The model is typed twice: in the pool's deployment and in the file. If they differ, requests go to a pool that doesn't serve that model.
  2. The same file goes to every cluster. Adding a pool or changing a model means editing it, applying it everywhere, and restarting every discovery service.
  3. MCS pools get no capacity or capabilities. So every MCS pool in feat(global-router): resolve MCS pool models and live frontends #17276 shows "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.
  4. In MCS mode, POST /pools still 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.
  5. A pool is live only by MCS readiness. If a cluster loses contact with the MCS implementation, its pools can stay Ready after they are gone, until routers notice that the PoolRelay's connection is gone (see the comment above ready_addresses in hub.rs).

What this PR adds

  • No routing file. New pools and model changes need no config change or restart.
  • Capacity and capabilities in MCS mode.
  • One PoolRelay contract for every --pool-discovery value.
  • Missed heartbeats drop a pool even when MCS still shows it Ready.
  • A warning when MCS and the registrations disagree for two lease durations, for example when cluster_id isn't the pool's MCS cluster name. Today such a pool is silently missing.
  • A warning when a live pool has no Ready frontend, for example when <dgd>-frontend isn't exported. Such a pool is listed with frontends: [], so routers have nowhere to send its requests.

Trade-offs

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:
    • the join: only pools both sides list, which side each field comes from, watch events from both sides, a lapsed registration dropping a pool that MCS still lists, one-sided pools (one warning each, timed from when the pool became one-sided, and forgotten after a relist), a relist that drops a joined pool, watching both sides again when a watch cannot start or ends (watchers keep the last view, marked Estimated, until both sides list again, and the backoff then starts over), and its completeness, Exact only while both sides are;
    • frontends from MCS: frontend-only pools stay hidden, IPv6, a dual-stack pod listed once at its IPv4 address (also when its slices do not name it), only the named TCP port (and not port 0), a deleted frontend slice leaving a live pool without frontends, one warning per such pool, also across the reads before the first sync, and the check that the two Service suffixes differ;
  • Before the split, breaking each of 32 guarded behaviors on purpose made a test fail, for example one address family for frontends and the join's completeness. The peer ones (the self-skip, the readiness gate, the peer client's timeout, missing copies) moved to feat(global-router): share registrations between discovery instances #17637 with their tests. One bound has no test: the sweep runs at least once a second.
  • cargo clippy --all-targets -- -D warnings and cargo fmt on the crate: clean.
  • feat(global-router): add the Global Discovery Service library (contract and backends) #17331's and feat(global-router): add the Global Discovery HTTP service #17371's latest changes are merged in, and the frontend flags get the same checks as feat(global-router): add the Global Discovery HTTP service #17371's router flags: a suffix that starts with - goes after =, a suffix left without a value fails instead of taking the next flag, and no flag can be empty.
  • A second independent review found that the join always said 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 say Exact while 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.
  • A later review found four smaller issues, all fixed: overlapping frontend and PoolRelay suffixes were refused only after the Kubernetes client started, with exit code 1; a dual-stack pool's frontends followed whichever address family was Ready first; a frontend slice without the named port was warned about on every event; and the join's sweep test passed with any sweep period.
  • A review on the PR found that a source that could not be watched at startup would stop the join for good, and asked for a warning when a live pool has no frontend, for a warning text that gives the expected cause first, and for tests of a relist that drops a joined pool and of a deleted frontend slice. All are done: the join now watches both sides again after a backoff. An independent review of that fix found that the backoff never started over, that watchers kept a view marked Exact while 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.
  • Not yet run on a cluster.

🤖 Generated with Claude Code

@devin-ai-integration

devin-ai-integration Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

✅ Dynamo PR CI passed — run 41146614737 (attempt 1) on d8f56723e9

Gate checks: ✅ backend-status-check · ✅ deploy-status-check · ✅ dynamo-status-check

Posted automatically by Devin for run 41146614737. Updated on every full-CI run of this PR.

@atchernych
atchernych marked this pull request as ready for review October 27, 2026 00:21
@atchernych
atchernych requested review from a team as code owners October 27, 2026 00:21
peer: &ReplicaEndpoint,
) -> anyhow::Result<(SharedRegistrations, Instant)> {
let url = peer.url.join("registrations")?;
let reply = client

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

@atchernych
atchernych force-pushed the atchernych/discovery-pool-registrations branch from fa49608 to 8405ca5 Compare October 27, 2026 01:00
@atchernych
atchernych requested review from a team as code owners October 27, 2026 01:00
@atchernych
atchernych force-pushed the atchernych/global-discovery-service branch from 0ca36ea to 819368d Compare October 27, 2026 01:00
@atchernych
atchernych changed the base branch from atchernych/global-discovery-service to atchernych/global-discovery-http October 27, 2026 01:01
@atchernych
atchernych force-pushed the atchernych/global-discovery-http branch from 2487538 to f9f2f28 Compare October 27, 2026 01:53
@atchernych
atchernych force-pushed the atchernych/discovery-pool-registrations branch from 8405ca5 to 364b09e Compare October 27, 2026 01:53
@atchernych
atchernych marked this pull request as draft October 27, 2026 02:32
@atchernych
atchernych force-pushed the atchernych/discovery-pool-registrations branch from 364b09e to ee79333 Compare October 27, 2026 02:55
@atchernych
atchernych force-pushed the atchernych/global-discovery-http branch from f9f2f28 to b693840 Compare October 27, 2026 02:55
@atchernych
atchernych force-pushed the atchernych/discovery-pool-registrations branch from ee79333 to ba40a2f Compare October 27, 2026 03:38
@atchernych
atchernych force-pushed the atchernych/global-discovery-http branch from b693840 to 57067a7 Compare October 27, 2026 03:38
atchernych and others added 4 commits October 27, 2026 09:38
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>
@atchernych
atchernych force-pushed the atchernych/discovery-pool-registrations branch from ba40a2f to 896e50d Compare October 27, 2026 16:48
@atchernych
atchernych force-pushed the atchernych/global-discovery-http branch from 57067a7 to 6db71b3 Compare October 27, 2026 16:48
…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>
atchernych and others added 5 commits October 28, 2026 18:48
…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>
atchernych and others added 4 commits October 29, 2026 16:46
…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>
@atchernych atchernych changed the title feat(global-router): take MCS pool models from registrations, not a file feat(global-router): take pool models from registrations, not a file Oct 30, 2026
@atchernych atchernych changed the title feat(global-router): take pool models from registrations, not a file feat(global-router): read pool models from registrations, not a file Oct 30, 2026
@atchernych
atchernych marked this pull request as ready for review October 30, 2026 00:56
atchernych and others added 3 commits October 29, 2026 19:48
…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 estroz 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.

lgtm

couple of questions:

  • frontend_url is always http:// (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_id has to match?

"cannot watch the pool sources; joining on each read"
);
// End watch streams instead of leaving them waiting for a Resync.
stop.cancel();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

once registrations come from ddb (#17644) a transient failure at startup would leave this instance watch-dead until restart while still reporting healthy

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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!(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: the warning should reflect that the module doc (16-19) says the expected cause is a stale Ready slice / lapsed lease.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

atchernych and others added 3 commits October 30, 2026 13:03
…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>
@atchernych

Copy link
Copy Markdown
Contributor Author

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 ip: relay identities do. When we add it, the scheme should come from appProtocol, as you suggest: the operator would set appProtocol: https on the frontend Service's port when the frontend serves TLS (it sets none today), Karmada copies ports into the slices it imports, and this service would build https:// URLs from it. Routers would also need to trust the frontends' certificates for the pod addresses they connect to, which is the larger part. I can open an issue for it.

Where PoolRelays get the cluster name. From DynamoPoolRelay.spec.clusterId, which the operator passes to the Relay container as --cluster-id. The user sets it; nothing derives it yet. With standard MCS, #17412 compares it with the About API's ClusterProperty cluster.clusterset.k8s.io and sets the DGD's GlobalRouterPoolExported condition to False, with reason ClusterIDMismatch, when they differ. With Karmada the operator doesn't check it, since Karmada names clusters by their member names, so clusterId has to be set to that name by hand, and this service's one-sided warning ("check that cluster_id is the pool's MCS cluster name") is what shows a mismatch. Whether the operator should fill it in is open question 4 in #17600. Also, no PoolRelay calls POST /pools yet; that comes next.

atchernych and others added 2 commits October 30, 2026 14:55
…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>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants