Skip to content

feat(global-router): add the Global Discovery Service library (contract and backends) - #17331

Merged
atchernych merged 31 commits into
mainfrom
atchernych/global-discovery-service
Oct 31, 2026
Merged

atchernych merged 31 commits into
mainfrom
atchernych/global-discovery-service

Conversation

@atchernych

@atchernych atchernych commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds lib/global-discovery-service as a library: the contract for which Global Router pools exist and which router replicas a PoolRelay connects to, and the backends behind it. The HTTP service and its binary are in #17371. The crate's name and place follow #17354, and its shared types come from lib/global-common (#17330, merged).

Part of #17600, the pool discovery DEP.

We have 2 knobs in discovery:
--pool-discovery = registrations or karmada or mcs: where the pool and router lists come from. The  registrations means the data comes from the API calls i.e. POST /pools
 --registration-storage = memory or DynamoDB or Kubernetes : where registrations are kept.

Layout (Global Router coding standards)

  • Contract, pool_discovery: PoolDirectory lists and watches pools, ReplicaDirectory lists and watches router replicas, and PoolRegistrar registers a pool. Each registration is also the pool's heartbeat.
  • Backends, backends:
    • Memory: keeps each registration for its lease. Each replica keeps only the registrations it receives, so run one. Its lists are complete once a lease has passed since it started.
    • MCS: pools and router replicas from the EndpointSlices that MCS imports, named the Karmada way or the standard way. A pool is live while one of its PoolRelay endpoints is Ready. Its relay_identities name every Ready PoolRelay pod by each of its addresses, sorted, so every service instance lists the same pods: a pool runs active-active PoolRelay replicas, each reports to every router, and routers dedupe the overlapping reports.
  • When the MCS watch fails: after a minute of errors, the backend keeps serving its last view but marks it incomplete and logs errors. Two minutes without errors count as recovery. Watchers get a Resync at both changes, and a relist counts only once it completes.
  • Timeouts: watches end after 50 s, so a server that stops answering shows up as errors. The client's connect and read timeouts must add up to under 90 s (feat(global-router): add the Global Discovery HTTP service #17371 uses 10 s and 75 s). An empty relist, and a router EndpointSlice without the named TCP port, are logged.
  • Karmada names: a Service name may have at most 55 characters, since Karmada imports it as derived-<name>, which must fit in 63. The router directory logs an error for a longer name, and the operator in feat(operator): export Global Router pools through the Multi-Cluster Services API #17412 does not export one.

Where the code comes from: #15933 and #15949, in the crate layout of the global-router-v2 test branch; this PR replaces both. Their router-side pieces (relay admission, state connections, router discovery modes) are left out, since relay connections belong with the router. #17355 already added the empty crate and its code-owner rule, so this PR adds no workspace member or code-owner line.

Main components

Component Contract (pool_discovery) Backends (backends)
Pools: list and watch PoolDirectory (list, list_and_watch) memory (MemoryPoolStore); MCS (McsPoolDirectory, Karmada or standard)
Pools: register (heartbeat) PoolRegistrar::register takes a PoolRegistration and returns its Lease memory
Router replicas: list and watch ReplicaDirectory static (StaticReplicaDirectory, set by --router in #17371); MCS (McsReplicaDirectory)
Shared types PoolKey, PoolLocation, PoolRegistration, PoolListing, Lease, WatchEvent, Snapshot, Completeness, RouterReplica lib/global-common (#17330)

Per mode

The HTTP service in #17371 picks the backend with --pool-discovery.

Function Memory backend (--pool-discovery registrations) MCS backend (--pool-discovery karmada or mcs)
Lists and watches the pools: PoolDirectory (discovery service) The registrations, in the discovery service's memory, not the router's. Run one replica Watches the imported EndpointSlices of each pool's PoolRelay Service (<DGD name>-pool-relay by default), on the Karmada control plane or in a cluster that imports them. A pool is live while one of its PoolRelay endpoints is Ready
Saves a pool when its PoolRelay registers: PoolRegistrar (discovery service) Keeps each registration until its lease runs out None: POST /pools returns 501 (#17371), so records have no model, frontends, or capacity. #17332 takes the model and capacity from registrations, and the frontends from MCS
Admits a PoolRelay's state stream (router, not in this stack) Not designed yet. Listings name no PoolRelay Trusted network: relay_identities are the addresses of every Ready PoolRelay pod, for the router to match against the caller's source IP
Lets PoolRelays find the router replicas: ReplicaDirectory (discovery service) GET /routers lists the replicas set with --router (#17371). Routers cannot register yet (#17333) GET /routers lists the Ready replicas in the router Service's imported EndpointSlices. The router's Service is exported once (admin setup)
Registers the pool (PoolRelay, not in this stack) Calls POST /pools on a timer, through lib/global-discovery-client (#17208). No PoolRelay calls it yet Its PoolRelay Service is exported through MCS. The operator creates the ServiceExport in draft #17412. With #17332 the PoolRelay also calls POST /pools
Gets the router replicas (PoolRelay, not in this stack) GET /routers, through the same client The same GET /routers. The PoolRelay reads no EndpointSlices

Validation

  • cargo test -p dynamo-global-discovery-service: 39 passed. They cover the memory backend's leases and completeness, the MCS pool and replica indexes (dual-stack PoolRelays and replicas, every Ready PoolRelay pod listed alike by every instance, a pod that stops being Ready leaving the listing, an IPv4-mapped address listed once), Karmada's name limit, a watcher that falls behind, and failing watches: going stale, recovering by silence, by an event or by a relist, a relist that fails partway, and backoff. Breaking each behavior the reviews fixed makes a test fail.
  • cargo clippy --all-targets -- -D warnings, cargo fmt, and cargo doc with -D warnings: clean.
  • Three adversarial reviews against the Global Router coding standards ran, and their verified findings are fixed. Reusing dynamo_runtime's Kubernetes watch code would need a refactor of dynamo-runtime, so it is not done here.
  • Ran the MCS backend against a stand-in API server. Lists come from the API server's watch cache (resourceVersion=0, unpaged), watches resume with bookmarks, and after the first sync GET /pools and GET /routers make no API calls.
  • CodeRabbit's docstring check stays under 80% because it counts test functions, which are named for what they check.
  • Not yet run on a cluster.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added shared discovery data formats and pool identity handling, with validation for registrations and lease timings.
    • Added discovery service support for listing and watching pools and router replicas, including in-memory and Kubernetes-backed discovery.
    • Added replica and pool updates with resynchronization after changes or interruptions.
    • Updated the discovery client to use the shared router replica format.

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

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

Copy link
Copy Markdown
Contributor

✅ Dynamo PR CI passed — run 41120860737 (attempt 2) on 73326260bc

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

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

atchernych and others added 2 commits October 26, 2026 16:40
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>
@atchernych
atchernych force-pushed the atchernych/global-discovery-service branch from 0ca36ea to 819368d Compare October 27, 2026 01:00
@atchernych atchernych changed the title feat(global-router): add the Global Discovery Service crate feat(global-router): add the pool discovery contract and its stores Oct 27, 2026
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>
@atchernych
atchernych force-pushed the atchernych/global-discovery-service branch from 388d5bb to 444711b Compare October 27, 2026 03:38

@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: Verified still present: SliceCache::apply clears failing_since before matching the event, so repeated relists that emit Init or InitApply but fail before InitDone continually reset the outage timer and can keep a stale snapshot classified as Exact indefinitely.
  • Original discussion: Verified still present: watch failures only update failing_since; crossing STALE_AFTER does not broadcast an Estimated resync, and recovery does not necessarily broadcast an Exact resync when membership is unchanged. Existing subscribers can therefore retain obsolete completeness indefinitely.

atchernych and others added 7 commits October 26, 2026 20:57
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>
- When the EndpointSlice watch keeps failing, for example after its RBAC is
  removed, the cache served its last view as complete indefinitely. It
  still serves that view, and the service stays ready, but after a minute
  of failures it reports the view as Estimated, so GET /pools says
  "complete": false, and it logs the failures as errors.
- A misnamed router port, Service or namespace left GET /routers empty with
  nothing in the log. A router EndpointSlice without the named port is now
  logged, and so is a relist that lists nothing, which also catches a
  cluster without the expected ServiceImports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
… list

Review feedback:
- list_and_watch returns a stream that yields nothing until the first
  complete list. That is deliberate, as dynamo_runtime's Kubernetes
  discovery also waits to be ready, but it was undocumented. The cancel
  token ends the wait, and the watch's failures are logged.
- The error logged after a minute of watch failures claimed to list the
  last view as incomplete even before there was a view.
- A test comment only restated the next line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
…ache

- The watcher's lists, including its relists after a watch error, were
  quorum reads from etcd (kube's default, ListSemantic::MostRecent), paged
  500 at a time. They now use any_semantic(): the API server answers from
  its watch cache, and the watch catches up from there. Discovery needs
  nothing stronger, and the pool watch covers every imported EndpointSlice
  in the cluster.
- The one list made before the cache syncs uses match_any() for the same
  reason.
- The pool and router indexes each had a copy of the Ready filter; both now
  use one ready_endpoints helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
… them

The previous commit asked the watcher for lists from the API server's
cache with any_semantic(), but kube sends resourceVersion=0 only when no
page size is set, and the watcher pages 500 at a time by default. Its
lists therefore stayed paged quorum reads from etcd. With paging off they
are served from the watch cache in one response, as client-go's reflector
lists. The cache keeps only what it derives from each slice, so the larger
response costs little.

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 17:15
Main now has the Global Discovery Service client from #17208 in
lib/global-discovery-client, which the next commits move onto this crate's
types, and the PoolRelay rename from #17215. No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
From Neelay Shah's review:
- PoolKey's first field is cluster_id, not site_id, in Rust and in JSON.
  It is the cluster ID the pool's PoolRelay runs with (--cluster-id,
  DYN_CLUSTER_ID), so a pool key and its relay share one name. Where MCS
  also reports the pool, that ID must be the cluster's name in the
  ClusterSet, and the doc says so. Pool IDs do not change: the deriver
  hashes the values, not the field names.
- PoolLocation drops cluster, which the key carries.
- ReplicaEndpoint, ReplicaId and ReplicaEvent become RouterReplica,
  RouterReplicaId and RouterReplicaEvent, since relays and workers have
  replicas too. Their JSON does not change.
- WatchEvent::Added becomes Upserted, Lease::lease_duration() becomes
  duration(), and Capacity.count becomes workers, in JSON too.

Lease is now #[must_use], as the client's copy was, so the client keeps
that check when it moves onto this type.

From the review threads: a test, from Dmitry Tokarev's review, that a
reader skips unknown fields and fills missing lists, and two duplicate
assertions removed. A pre-push review added a test that a reply without
its list does not parse, so an empty body never reads as "no routers".

Co-authored-by: Dmitry Tokarev <191198572+dmitry-tokarev-nv@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
Neelay Shah and Jacob Ioffe asked on #17208 that its client move onto
dynamo-global-common in this PR, so two PoolKey definitions never sit on
main together. lib/global-discovery-client drops its own copies of the
JSON types, router replicas and watch events. It re-exports the shared
types its calls send and return, with their error types; the watch types
come from dynamo-global-common. Its calls and errors do not change.

What it sends follows the shared types: the key's cluster_id, a location
without cluster, and capacity in workers. A PoolKey is built with
PoolKey::new, which checks its fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
…ery-service

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. No conflicts; the next commit follows the renames.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
The base renamed them after review: PoolKey::cluster_id (was site_id),
RouterReplica, RouterReplicaId and RouterReplicaEvent (were
ReplicaEndpoint, ReplicaId and ReplicaEvent), WatchEvent::Upserted (was
Added), Lease::duration (was lease_duration) and Capacity.workers (was
count), in JSON too. PoolLocation has no cluster field, so nothing sets
it. Lease is #[must_use], so tests that only register drop the lease
explicitly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
Base automatically changed from atchernych/global-common to main October 29, 2026 02:24
@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 8a5b7f2 and 69d703e.

Files ignored due to path filters: 1
  • Cargo.lock is excluded by !**/*.lock
Files selected for processing: 28
  • Cargo.toml
  • lib/global-common/Cargo.toml
  • lib/global-common/src/discovery.rs
  • lib/global-common/src/lib.rs
  • lib/global-common/src/pool.rs
  • lib/global-common/src/replicas.rs
  • lib/global-common/src/watch.rs
  • lib/global-discovery-client/Cargo.toml
  • lib/global-discovery-client/src/client.rs
  • lib/global-discovery-client/src/lib.rs
  • lib/global-discovery-client/src/replicas.rs
  • lib/global-discovery-client/src/wire.rs
  • lib/global-discovery-client/tests/relay_calls.rs
  • lib/global-discovery-service/Cargo.toml
  • lib/global-discovery-service/src/backends.rs
  • lib/global-discovery-service/src/backends/kubernetes.rs
  • lib/global-discovery-service/src/backends/kubernetes/cache.rs
  • lib/global-discovery-service/src/backends/kubernetes/endpoints.rs
  • lib/global-discovery-service/src/backends/kubernetes/hub.rs
  • lib/global-discovery-service/src/backends/kubernetes/mcs.rs
  • lib/global-discovery-service/src/backends/memory.rs
  • lib/global-discovery-service/src/backends/stream.rs
  • lib/global-discovery-service/src/lib.rs
  • lib/global-discovery-service/src/pool_discovery.rs
  • lib/global-discovery-service/src/pool_discovery/pools.rs
  • lib/global-discovery-service/src/pool_discovery/registrar.rs
  • lib/global-discovery-service/src/pool_discovery/replicas.rs
  • lib/global-discovery-service/src/pool_discovery/watch.rs
Files with no reviewable changes: 2
  • lib/global-discovery-client/src/replicas.rs
  • lib/global-discovery-client/src/wire.rs

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

Walkthrough

The pull request adds shared Global Discovery types and service contracts, updates the client to use those types, and introduces in-memory and Kubernetes-backed pool and router replica directories with listing and watch support.

Changes

Global Discovery

Layer / File(s) Summary
Shared identity and discovery types
Cargo.toml, lib/global-common/*
Adds shared pool identity and location types, discovery request and response types, replica types, and watch events. Adds validation, serialization, and derivation tests.
Discovery service contracts
lib/global-discovery-service/src/pool_discovery*, lib/global-discovery-service/src/lib.rs, lib/global-discovery-service/src/backends.rs, lib/global-discovery-service/Cargo.toml
Adds pool registration and directory contracts, watch-stream types, a discovery error type, and public service modules.
Client migration to shared types
lib/global-discovery-client/*
Moves client-facing wire and replica types to dynamo-global-common. Updates the router response type and relay-call test fixtures.
In-memory stores and watch streams
lib/global-discovery-service/src/backends/memory.rs, lib/global-discovery-service/src/backends/stream.rs
Adds lease-based pool registration and expiration, a static replica directory, and watch-stream helpers for initial snapshots and lag recovery.
Shared Kubernetes slice cache
lib/global-discovery-service/src/backends/kubernetes/cache.rs
Adds a shared EndpointSlice cache with relisting, watch-error completeness updates, retry handling, and directory listing and watch operations.
MCS replica discovery
lib/global-discovery-service/src/backends/kubernetes.rs, lib/global-discovery-service/src/backends/kubernetes/mcs.rs, lib/global-discovery-service/src/backends/kubernetes/endpoints.rs
Adds Standard and Karmada slice naming support and replica discovery from imported service endpoints.
MCS pool discovery
lib/global-discovery-service/src/backends/kubernetes/hub.rs
Adds pool listing and watch behavior derived from imported slices, including relay selection, liveness, and completeness updates.

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

Merge Risk: Minimal · up to 69d70

This change adds the pool discovery contracts, shared types, and in-memory and MCS-backed stores, and migrates the client to the shared types. No concrete merge-blocking defect was identified. Cluster-level validation of the Kubernetes backends is still pending and should be done in normal rollout.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 52.27% 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. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed 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 identifies the main change: adding the Global Discovery Service library, its contract, and its backends.
Description check Passed The description is detailed and covers the change overview, key components, implementation modes, validation results, known limitations, and related issues. It does not use every template heading exac…
Full details: Description check

Explanation

The description is detailed and covers the change overview, key components, implementation modes, validation results, known limitations, and related issues. It does not use every template heading exactly and omits the checklist and explicit Issues and Ongoing Work section, but the required technical information is mostly complete.

Full details: Docstring Coverage

Explanation

Docstring coverage is 52.27% 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 44 functions across 22 files. (4 skipped: 4 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

Comment @coderabbitai help to get the list of available commands.

atchernych and others added 2 commits October 29, 2026 07:48
#17330 merged as 8a5b7f2, so main has the shared types and the
discovery client built on them. No conflicts. Against main, this PR is
again only lib/global-discovery-service.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
…asked

The review bot found two test comments that only repeat the code after
them ("The same slice again changes nothing."), so they are removed.

CodeRabbit's docstring check counts functions that branch. The four
outside tests that had no doc comment now have one: SliceCache::subscribe,
the replica index's replace, and both list_and_watch in the memory
backend.

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): add the pool discovery contract and its stores feat(global-router): add the Global Discovery Service library (contract and stores) Oct 29, 2026
atchernych and others added 2 commits October 29, 2026 08:03
Karmada v1.19 imports a Service as derived-<name>, and puts that name in
each imported slice's kubernetes.io/service-name label. A Service name
and a label value may have at most 63 characters, so Karmada imports
nothing for a name over 55: neither the derived Service nor its slices
appear. The store used 57, the length at which Kubernetes starts to
shorten slice names, but Karmada already fails at 56.

KARMADA_MAX_SERVICE_NAME is now 63 minus the "derived-" prefix, and a
test pins it at 55. McsReplicaDirectory logs an error when Karmada
cannot import the router Service, since none of its replicas would be
listed. The warning for a slice that does not say its cluster no longer
blames the Service name's length.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
…vice

McsReplicaDirectory reads any exported Service, not only the router's;
the PR stacked on this one also reads the service's peers through it.
Its error for a name Karmada cannot import, and the module doc that
points to it, no longer say "router". The doc also names the default
suffix behind the 44-character DGD limit, -pool-relay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
atchernych added a commit that referenced this pull request Oct 29, 2026
…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>
atchernych added a commit that referenced this pull request Oct 29, 2026
…very-register-router

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. 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>
… stores

What implements the pool discovery contract is a backend: MCS is read,
not stored into. The crate, contract and MCS docs now say "backend". The
memory backend's docs still call MemoryPoolStore a store, since it does
keep registrations.

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): add the Global Discovery Service library (contract and stores) feat(global-router): add the Global Discovery Service library (contract and backends) Oct 29, 2026

@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 on initial pass, may have some follow-ups

The stream's docs said it waits until the source has "a complete view",
which reads like `Completeness::Exact`. It means the source has listed
everything once: for MCS, kube's `InitDone`. Whether a list holds every
item is its `completeness`, which each `Resync` carries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
@atchernych
atchernych enabled auto-merge (squash) October 29, 2026 22:59
atchernych and others added 2 commits October 29, 2026 16:45
A list's completeness says whether it may be missing items, and
`GET /pools` passes it on as `complete`. The contract now says what a
caller that keeps its own pool list does with each: 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. The design link points at the DEP issue,
#17600, since enhancements #103 is closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anna Tchernych <atchernych@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Anna Tchernych <atchernych@nvidia.com>

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

LGTM. Built at 06b4f63: cargo test -p dynamo-global-discovery-service passes (37/37) and clippy is clean. Later commits only touch docs in this crate. Haven't run it against a real API server or Karmada.

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>

@dmitry-tokarev-nv dmitry-tokarev-nv 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.

No P0 or P1 found, and this review posts no findings. It covers head 73326260bcecf6edf2b4502039bd564304a1a8df, and a local merge of that head into main at ba9caeac492cdeef00fbb6096ae6efda40f81064, which merges without conflicts.

Open items: none. All earlier threads are resolved.

Tests, lock file and deliberate breaks, on the head and on merges with main.
  • cargo test --locked -p dynamo-global-discovery-service: 39 passed on the head, and on merges with main at 3057891245 and at ba9caeac49.
  • cargo tree --locked passes on the head and on both merges. The lock adds no package. Only the dependency list of dynamo-global-discovery-service changes, and every crate in it is already locked on main at the same version.
  • Removing registered.expires_at = expires_at; at memory.rs:209 fails registering_again_only_renews.
  • Changing >= to > at memory.rs:122 fails lists_are_exact_once_a_lease_has_passed.
  • Changing >= to > at cache.rs:167 fails 4 tests in hub.rs.
  • CI at this head, at the time of this review: the PR run on pull-request/17331 passed. In Pre Merge, rust-tests (.) was still running, and the other jobs passed.
Memory backend, measured with paused tokio time and with 8 worker threads.
  • The lease uses tokio::time::Instant, a monotonic clock.
  • With a 5 s / 15 s lease, a pool is listed at 14.999 s and gone at 15.000 s. A second registration at 10 s moves that to 24.999 s and 25.000 s. The sweep sends Removed at 15 s, with the Resync that marks the list Exact.
  • 32 tasks sent 16,000 registrations of one pool. With one payload, the store keeps one listing and sends no event after the first. With two alternating payloads, it sends more events than the 1,024-event buffer holds, and the watcher gets a fresh Resync, as the docs say.
  • The store has no bound on the number of pools: 100,000 distinct pools registered in 638 ms, and list() returned them in 61 ms.
  • Memory: 1,224 heap bytes per pool for a 291-byte registration, plus 661 bytes per pool for each list() copy. For a registration with 1,000 capabilities of 100 characters, each list() copy takes about 125 KB per pool, and the store keeps about the same.
MCS backend, against an in-process fake of the EndpointSlice API on 127.0.0.1, with the 10 s / 75 s client timeouts from #17371.
  • Pools, Karmada names: Ready endpoints and endpoints without ready are listed. NotReady endpoints are not, and a pool with only NotReady relays is not listed. A pool appears, grows and leaves through Upserted and Removed as its endpoints change.
  • Pools, standard multicluster.kubernetes.io labels: listed from the same fake, and slices named the other way are skipped.
  • Router replicas: both naming styles list Ready endpoints only. A 55-character Karmada Service is listed. A 56-character one logs the error from McsReplicaDirectory::new. Its list then fails, because the fake refuses a label value over 63 characters, as the API server does.
  • An empty first list, and an empty relist after a 410, both send Resync with no items and log the warning.
  • With the API server failing every request, the Resync marked Estimated came 80.4 s after the first error. Requests came at 0.8, 1.6, 3.2, 6.4, 12.8 and 25.6 s intervals, then every 30 s. The check runs only at an error. The first error past one minute came at 80.4 s, and until then list() stayed Exact.
  • Recovery: a changed slice brought Upserted and an Exact Resync 30.0 s after the API server recovered. With no change, the Exact Resync came 120.0 s after the last error.
  • Every request went to the fake. The client had no other API server address.

@dmitry-tokarev-nv dmitry-tokarev-nv 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 at 73326260bcecf6edf2b4502039bd564304a1a8df, and on a local merge with main at ba9caeac492cdeef00fbb6096ae6efda40f81064, which has no conflicts.

  • Tests: 39 of 39 pass on both trees. Three deliberate one-line breaks each fail a test: the lease renewal, the completeness boundary of the memory backend, and the staleness boundary of the MCS cache.
  • Lock file: no new crate, and cargo tree --locked passes on both trees.
  • Memory backend: lease expiry and renewal are exact on the monotonic tokio clock. 16,000 concurrent registrations of one pool keep one listing. The number of pools has no bound (100,000 registered in 638 ms), at about 1.2 KB per pool.
  • MCS backend, against an in-process fake of the EndpointSlice API: Ready and NotReady endpoints, both naming styles, the 55-character limit, empty relists, and the Estimated and Exact resyncs all match the description.

Open items: none. #17371 stacks on this PR.

@atchernych
atchernych merged commit 2cb12a1 into main Oct 31, 2026
213 of 220 checks passed
@atchernych
atchernych deleted the atchernych/global-discovery-service branch October 31, 2026 04:19
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.

4 participants