Repository navigation
feat(global-router): add GET /pools and a pool cache to the Global Discovery client - #17702
Conversation
A PoolRelay keeps the Global Router replicas in a HashSet, the directory PoolViewBroadcaster::run takes, so RouterReplica needs Hash. Signed-off-by: Jacob Ioffe <jioffe@nvidia.com>
RouterCache reads GET /routers now and then on an interval, and publishes the Global Router replica set on a watch channel. A refresh that lists the same routers publishes nothing. A failed refresh keeps the last set and reports the error through Freshness::Stale, so a discovery outage does not drop routers a caller already knows. The cache logs each new error once, the recovery once, and the router IDs added or removed. The refresh task stops when its cancel token fires or the cache is dropped. A zero interval panics at spawn instead of inside the task. The cache lives in the client so the PoolRelay and the Global Frontend share one refresh loop. GET /pools with merge-on-partial rules can follow the same way. Refs LLM-213. Signed-off-by: Jacob Ioffe <jioffe@nvidia.com>
A PoolRelay sends its state to every Global Router replica, so it needs the current replica list. With --global-discovery-url or DYN_RELAY_GLOBAL_DISCOVERY_URL set, PoolRelay::start builds a GlobalDiscoveryClient and starts a RouterCache that reads GET /routers every 5 s. subscribe_global_routers() returns the cache's watch channel of HashSet<RouterReplica>, the input PoolViewBroadcaster::run takes. Nothing reads it yet; LLM-228 hands it to the broadcaster. The cache starts after every fallible step, so a failed start leaves no task behind, and it stops with the Relay. health() reports global_discovery_enabled and global_routers_stale_reason. A stale list does not change healthy, so probes do not restart the Relay during a discovery outage. Closes LLM-213. Signed-off-by: Jacob Ioffe <jioffe@nvidia.com>
…rent RouterCache now publishes a changed set before it marks the list current, so a reader never sees Current next to the previous set. Its change log names routers as <id>=<url>, so a URL change under the same ID shows up. A unit test covers the error-cause chain that goes into the stale reason. Refs LLM-213. Signed-off-by: Jacob Ioffe <jioffe@nvidia.com>
DiscoveryClientError::Status kept the whole response body. RouterCache now puts that error in logs and in PoolRelay health, so a proxy's error page would fill both. The client keeps the first 1 KiB, cut on a character boundary. Refs LLM-213. Signed-off-by: Jacob Ioffe <jioffe@nvidia.com>
The test dialed 127.0.0.1:9, which can be in use or filtered. It now binds 127.0.0.1:0 for an unused port and checks that the stale reason carries the connection error's cause. Refs LLM-213. Signed-off-by: Jacob Ioffe <jioffe@nvidia.com>
GlobalDiscoveryClient::pools() calls GET /pools and returns the service's PoolsResponse: the pools it lists and whether the list is complete. A partial list can miss pools that still exist, so a caller that keeps a list removes pools only on a complete one. If any entry does not parse, the whole call fails. The crate now re-exports PoolListing and PoolsResponse from dynamo-global-common. Refs LLM-233. Signed-off-by: Jacob Ioffe <jioffe@nvidia.com>
Move RouterCache's tick setup into ticks() and its Freshness updates into mark_stale and mark_current, so a second cache refreshes and reports outages the same way: a zero interval panics at spawn, an overrunning refresh delays the next tick, each new failure reason and the recovery are logged once, and Current is set after the new list is published. Rename refresh to refresh_routers to say which list it reads. No behavior change. Refs LLM-233. Signed-off-by: Jacob Ioffe <jioffe@nvidia.com>
A PoolKey now displays as <cluster_id>/<namespace>/<dgd_name>, the name the Global Router already gives a pool in metrics. The Global Discovery client's pool cache logs pools by it. Refs LLM-233. Signed-off-by: Jacob Ioffe <jioffe@nvidia.com>
PoolCache reads GET /pools immediately, then every interval, and publishes the pools by PoolKey on a watch channel. It applies the service's rules for partial lists: - A complete list replaces the known pools. - A partial list adds new pools and updates changed ones, but keeps known pools that are missing. Pools are removed only on a complete list. - A cache that knows no pools takes a partial list too. A failed refresh keeps the last pools and reports Freshness::Stale, as RouterCache does. A refresh that changes nothing publishes nothing. The cache logs each change with the pools added, updated and removed, and logs once when the service starts or stops listing every pool. The published value leaves out `complete`. The cache already applies the only rule that depends on it, and a Global Router routes on a partial view, so no consumer needs it yet. A getter can add it later without changing the published type. Refs LLM-233. Signed-off-by: Jacob Ioffe <jioffe@nvidia.com>
Each refresh collected the listed pools into a new map, cloning every key, even when nothing changed. Look each pool up in the known map instead, so only a new pool's key is cloned, and borrow the listed keys for the removal pass. Signed-off-by: Jacob Ioffe <jioffe@nvidia.com>
…st, and a partial recovery Add tests that dropping a PoolCache stops its task, that a complete empty list removes every pool, and that a partial list after an outage keeps the missing pools. Note in the docs that Freshness keeps its last value after a cache stops, and that the complete flag starts true. Signed-off-by: Jacob Ioffe <jioffe@nvidia.com>
✅ Dynamo PR CI passed — run 41697079349 (attempt 1) on
|
…lobal-routers Signed-off-by: Jacob Ioffe <jioffe@nvidia.com> # Conflicts: # lib/bindings/python/Cargo.lock
The client cut an error body to 1 KiB only after reading all of it, so a large or endless error page was buffered whole on every refresh. Read chunks until the limit instead. The test serves an error body that never ends; the old read waits for the timeout. Signed-off-by: Jacob Ioffe <jioffe@nvidia.com>
…ing comments error_chain's test now nests two sources, so a version that appends only the first cause fails. Remove comments that repeat a constant's name or the assertions that follow them. Signed-off-by: Jacob Ioffe <jioffe@nvidia.com>
…pool-cache Signed-off-by: Jacob Ioffe <jioffe@nvidia.com> # Conflicts: # lib/global-discovery-client/tests/relay_calls.rs
The same restating comments came out of the router tests in #17688. Signed-off-by: Jacob Ioffe <jioffe@nvidia.com>
lib/bindings/kvbm resolves dynamo-llm by path, so its lockfile now lists dynamo-global-discovery-client and dynamo-global-common, and `cargo metadata --locked` passes there. Drop the zero-interval test, which only guarded a constant, and the HTTP RouterCache test, which repeated the unit and host tests. One test now serves an endless error body with a multibyte character at the cut. Remove a needless clone and a comment about start order: RouterCache aborts its task on drop, so the order does not matter. Signed-off-by: Jacob Ioffe <jioffe@nvidia.com>
…pool-cache Takes the review fixes from #17688. Like its RouterCache twin there, the HTTP PoolCache test goes too: the PoolCache unit tests cover the merge rules and the outage, and lists_pools_and_whether_the_list_is_complete covers decoding over HTTP. The fake service drops the fields only that test used. Signed-off-by: Jacob Ioffe <jioffe@nvidia.com>
Signed-off-by: Hannah Zhang <hannahz@nvidia.com>
|
No actionable comments were generated in the recent review. WalkthroughPool Relay can optionally read Global Router replicas from Global Discovery. The Global Discovery client adds pool listing and periodic router and pool caches. Pool Relay exposes router subscriptions and discovery freshness through its health data. ChangesGlobal Discovery client and caches
Pool Relay integration
Estimated code review effort: 3 (Moderate) | ~30 minutes Merge Risk:
|
…ache Signed-off-by: Jacob Ioffe <jioffe@nvidia.com> # Conflicts: # lib/global-discovery-client/src/cache.rs # lib/global-discovery-client/src/lib.rs
…ache Signed-off-by: Jacob Ioffe <jioffe@nvidia.com>
Summary
Add
GlobalDiscoveryClient::pools()forGET /pools, andPoolCache, which keeps the pool list current and merges partial lists the way the Global Discovery Service asks. The cache publishes the pools as awatch::Receiver<HashMap<PoolKey, PoolListing>>. Nothing reads it yet: the Global Frontend still takes its pools from--poolflags, and switching it over is a follow-up.About 150 lines of core logic; tests, docs and comments excluded.
Stacked on #17688, which adds
RouterCacheto the same file. The Files tab shows only this PR's changes.Closes LLM-233.
Design
GET /poolscan return a partial list (complete: false), for example while the service's memory store refills after a restart, or while its Kubernetes watch keeps failing. The service's contract documents how a caller that keeps its own list should handle one (lib/global-discovery-service/src/pool_discovery.rs, from #17331), andPoolCacheapplies those rules:PoolCachehas the same shape asRouterCache.spawnreads immediately, then every interval, until its cancel token fires or the cache is dropped. A failed read keeps the last pools and reportsFreshness::Stalewith the error. A partial list counts as a successful read. A refresh that adds, updates, and removes nothing publishes nothing. The cache logs each change with the pools added, updated, and removed, and logs once when the service starts or stops listing every pool.The published value leaves out
complete. The cache already applies the only rule that depends on it, and a Global Router routes on a partial view, so no consumer needs it yet. A getter can add it later without changing the published type.RouterCacheandPoolCacheshareticks(),mark_stale(), andmark_current(). So both handle a zero interval, a slow refresh, an outage, and a recovery the same way. Both publish a list before marking itCurrent. Moving these out ofRouterCacheis its own commit, with one behavior change.spawnnow builds the timer before it spawns the task, so a runtime without timers panics in the caller instead of in the task.PoolKeynow implementsDisplayas<cluster_id>/<namespace>/<dgd_name>, the name the Global Router already gives a pool in metrics. The cache logs pools by it.Review
lib/global-discovery-client/src/client.rs:pools().lib/global-discovery-client/src/cache.rs:PoolCache, the merge inrefresh_pools(), the shared helpers, and the tests.lib/global-discovery-client/tests/relay_calls.rs:pools()against an HTTP fake.lib/global-common/src/pool.rs:DisplayforPoolKey.Validation
cargo test -p dynamo-global-common -p dynamo-global-discovery-client --locked: 32 passed, 11 of them new.cargo clippy -p dynamo-global-common -p dynamo-global-discovery-client -p dynamo-global-router -p dynamo-global-frontend --all-targets --locked -- -D warningscargo fmt --all -- --checkEnd to end against the Global Discovery Service binary from feat(global-router): add the Global Discovery HTTP service #17371, with
--pool-discovery registrations --registration-storage memory, a 1 s heartbeat, and a 4 s lease. Poolsaanddregister withregister_pool()every 1 s and 3 s, andbstops after 2 s. The cache reads every 250 ms. The service was killed at 9 s and restarted empty at 11 s.GET /pools[d], partial[d]while empty[a, b, d], partialaandb[a, b, d], complete[a, d], complete, afterb's lease ran outbStale, keepsaandd[a], partial, after the restartCurrent, keepsd[a, d], partial[a, d], completeCI can't run this until the service is on
main.The new tests catch:
a_complete_list_replaces_the_known_pools: a complete list that doesn't remove or update a pool.a_partial_list_removes_nothing_until_a_complete_one: a partial list that removes a missing pool, a partial list with nothing new that still publishes, and a complete list that doesn't remove.an_empty_cache_takes_a_partial_list: a cache that waits for a complete list.the_last_listing_of_a_key_wins: a key listed twice.keeps_pools_through_an_outage_and_reports_them_stale: an outage that drops pools, an unchanged list that still publishes, and a channel that stays open after cancel.a_partial_list_after_an_outage_keeps_the_missing_pools: a restarted service's partial list that removes pools.a_complete_empty_list_removes_every_pool: a cache that ignores an empty complete list.dropping_the_pool_cache_stops_it: a refresh task that outlives its cache.lists_pools_and_whether_the_list_is_completeandone_bad_pool_fails_the_whole_list: the call against an HTTP service.a_key_displays_as_its_name_for_people: thePoolKeyformat.Summary by CodeRabbit