// SPDX-FileCopyrightText: Copyright (c) 2026 The SGLang Authors // SPDX-License-Identifier: Apache-2.0 use axum::{routing::get, Json, Router}; use serde_json::{json, Value}; use sgl_router::discovery::{DiscoveryEvent, ModelId, WorkerId, WorkerMode, WorkerSpec}; use sgl_router::workers::{manager, WireProtocol, WorkerRegistry}; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::net::TcpListener; use tokio::sync::{mpsc, oneshot, Barrier}; /// Spin up a tiny fake worker that returns `body` on both introspection /// endpoints. Returns the worker base URL and a shutdown channel. /// /// One body for both because a real engine reports `served_model_name` on /// each. async fn spawn_fake_worker(body: Value) -> (String, oneshot::Sender<()>) { spawn_worker_serving(body, true).await } /// A worker that answers `/server_info` only — an SGLang predating /// `served_model_name` on `/model_info`, and the shape the kind e2e fleet /// still has (`tests/e2e/k8s_integration/fake_worker.py` defines no /// `/model_info`). Registration must resolve the model name from the /// `/server_info` fallback for this worker. async fn spawn_server_info_only_worker(body: Value) -> (String, oneshot::Sender<()>) { spawn_worker_serving(body, false).await } async fn spawn_worker_serving(body: Value, with_model_info: bool) -> (String, oneshot::Sender<()>) { let body = Arc::new(body); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let port = listener.local_addr().unwrap().port(); let serve_body = move || { let body = body.clone(); async move { Json((*body).clone()) } }; let mut app = Router::new().route("/server_info", get(serve_body.clone())); if with_model_info { app = app.route("/model_info", get(serve_body)); } let (tx, rx) = oneshot::channel::<()>(); tokio::spawn(async move { let _ = axum::serve(listener, app) .with_graceful_shutdown(async move { let _ = rx.await; }) .await; }); (format!("http://127.0.0.1:{port}"), tx) } fn spec_for(id: &str, url: &str, mode: WorkerMode) -> WorkerSpec { // model_ids are intentionally empty: the manager resolves them via // /server_info introspection. Pre-populating here would lie about // what discovery backends actually emit. WorkerSpec { id: WorkerId(id.into()), url: url.into(), mode, model_ids: Vec::new(), bootstrap_port: None, } } async fn wait_until(condition: impl Fn() -> bool, description: &str) { tokio::time::timeout(Duration::from_secs(2), async { loop { if condition() { return; } tokio::time::sleep(Duration::from_millis(10)).await; } }) .await .unwrap_or_else(|_| panic!("timed out waiting for {description}")); } #[tokio::test] async fn manager_processes_added_then_removed() { let (url_a, _s_a) = spawn_fake_worker(json!({"served_model_name": "m"})).await; let (url_b, _s_b) = spawn_fake_worker(json!({"served_model_name": "m"})).await; let (tx, rx) = mpsc::channel(16); let registry = Arc::new(WorkerRegistry::default()); let h = tokio::spawn(manager::run(rx, registry.clone())); tx.send(DiscoveryEvent::Added(spec_for( "w1", &url_a, WorkerMode::Plain, ))) .await .unwrap(); tx.send(DiscoveryEvent::Added(spec_for( "w2", &url_b, WorkerMode::Plain, ))) .await .unwrap(); wait_until( || registry.workers_for(&ModelId("m".into())).len() == 2, "both workers to register", ) .await; assert_eq!(registry.workers_for(&ModelId("m".into())).len(), 2); tx.send(DiscoveryEvent::Removed { id: WorkerId("w1".into()), }) .await .unwrap(); wait_until( || registry.workers_for(&ModelId("m".into())).len() == 1, "removed worker to leave the registry", ) .await; assert_eq!(registry.workers_for(&ModelId("m".into())).len(), 1); drop(tx); h.await.unwrap(); } #[tokio::test] async fn manager_handles_mode_changed() { let (url, _s) = spawn_fake_worker(json!({"served_model_name": "m"})).await; let (tx, rx) = mpsc::channel(16); let registry = Arc::new(WorkerRegistry::default()); let h = tokio::spawn(manager::run(rx, registry.clone())); tx.send(DiscoveryEvent::Added(spec_for( "w1", &url, WorkerMode::Prefill, ))) .await .unwrap(); wait_until( || { registry .workers_for_mode(&ModelId("m".into()), WorkerMode::Prefill) .len() == 1 }, "prefill worker to register", ) .await; assert_eq!( registry .workers_for_mode(&ModelId("m".into()), WorkerMode::Prefill) .len(), 1 ); tx.send(DiscoveryEvent::ModeChanged { id: WorkerId("w1".into()), mode: WorkerMode::Decode, }) .await .unwrap(); wait_until( || { registry .workers_for_mode(&ModelId("m".into()), WorkerMode::Prefill) .is_empty() && registry .workers_for_mode(&ModelId("m".into()), WorkerMode::Decode) .len() == 1 }, "worker mode to change to decode", ) .await; assert_eq!( registry .workers_for_mode(&ModelId("m".into()), WorkerMode::Prefill) .len(), 0 ); assert_eq!( registry .workers_for_mode(&ModelId("m".into()), WorkerMode::Decode) .len(), 1 ); drop(tx); h.await.unwrap(); } /// A worker that reports `enable_http2: true` is registered with /// [`WireProtocol::H2c`], so the proxy forwards to it over cleartext h2c. #[tokio::test] async fn manager_resolves_h2c_protocol_from_introspection() { let (url, _s) = spawn_fake_worker(json!({"served_model_name": "m", "enable_http2": true})).await; let (tx, rx) = mpsc::channel(16); let registry = Arc::new(WorkerRegistry::default()); let h = tokio::spawn(manager::run(rx, registry.clone())); tx.send(DiscoveryEvent::Added(spec_for( "w1", &url, WorkerMode::Plain, ))) .await .unwrap(); let resolved = wait_for_protocol(®istry, "w1", WireProtocol::H2c).await; assert!( resolved, "worker reporting enable_http2 must resolve to h2c, got {:?}", registry.get(&WorkerId("w1".into())).map(|w| w.protocol()), ); drop(tx); h.await.unwrap(); } /// A worker that omits `enable_http2` (older SGLang) keeps the safe HTTP/1.1 /// default on its registry entry. #[tokio::test] async fn manager_defaults_http1_when_enable_http2_absent() { let (url, _s) = spawn_fake_worker(json!({"served_model_name": "m"})).await; let (tx, rx) = mpsc::channel(16); let registry = Arc::new(WorkerRegistry::default()); let h = tokio::spawn(manager::run(rx, registry.clone())); tx.send(DiscoveryEvent::Added(spec_for( "w1", &url, WorkerMode::Plain, ))) .await .unwrap(); // Non-empty `model_ids` is what proves introspection ran: `spawn_fake_worker` // answers from one body, so a resolved model id means `/server_info` // answered too and its silence on `enable_http2` is the engine's, not the // fixture's. The positive direction — that `enable_http2` is actually read // — is pinned by `manager_resolves_protocol_per_worker_no_fleet_lock` // below. let registered = wait_for(Duration::from_secs(2), || { registry .get(&WorkerId("w1".into())) .is_some_and(|w| !w.model_ids.is_empty()) }) .await; assert!(registered, "worker registered"); let w = registry.get(&WorkerId("w1".into())).unwrap(); assert_eq!(w.protocol(), WireProtocol::Http1); drop(tx); h.await.unwrap(); } /// A worker with no `/model_info` at all still registers, resolving its model /// name from the `/server_info` fallback and its protocol from the same /// response. /// /// Every other test here uses a fixture that answers both endpoints, so without /// this one the fallback chain in `introspect::fetch` has no component-level /// coverage — while the kind e2e fleet runs exactly this shape. #[tokio::test] async fn manager_registers_worker_that_serves_server_info_only() { let (url, _s) = spawn_server_info_only_worker(json!({"served_model_name": "m", "enable_http2": true})) .await; let (tx, rx) = mpsc::channel(16); let registry = Arc::new(WorkerRegistry::default()); let h = tokio::spawn(manager::run(rx, registry.clone())); tx.send(DiscoveryEvent::Added(spec_for( "w-no-model-info", &url, WorkerMode::Plain, ))) .await .unwrap(); assert!( wait_for_protocol(®istry, "w-no-model-info", WireProtocol::H2c).await, "a worker without /model_info must still resolve its model name and \ protocol from /server_info; got {:?}", registry .get(&WorkerId("w-no-model-info".into())) .map(|w| (w.model_ids.clone(), w.protocol())), ); drop(tx); h.await.unwrap(); } /// The load-bearing regression for the production h2c bug: per-worker protocol /// means one worker that resolved HTTP/1.1 first does NOT lock the rest of the /// fleet off h2c. A mixed fleet (one h2c-capable worker registered AFTER a /// plain HTTP/1.1 worker) ends with each worker on its own protocol — the old /// single-client first-write-wins design forced both to HTTP/1.1. #[tokio::test] async fn manager_resolves_protocol_per_worker_no_fleet_lock() { // w-http1 registers first and reports no enable_http2 (resolves Http1); // w-h2c registers second and reports enable_http2: true (must still get // h2c — it is not dragged down by w-http1's earlier Http1 resolution). let (url_http1, _s1) = spawn_fake_worker(json!({"served_model_name": "m"})).await; let (url_h2c, _s2) = spawn_fake_worker(json!({"served_model_name": "m", "enable_http2": true})).await; let (tx, rx) = mpsc::channel(16); let registry = Arc::new(WorkerRegistry::default()); let h = tokio::spawn(manager::run(rx, registry.clone())); tx.send(DiscoveryEvent::Added(spec_for( "w-http1", &url_http1, WorkerMode::Plain, ))) .await .unwrap(); // Let the HTTP/1.1 worker resolve first so it is the one that would have // "won" the old fleet-wide client. assert!( wait_for_protocol(®istry, "w-http1", WireProtocol::Http1).await, "first worker should resolve Http1", ); tx.send(DiscoveryEvent::Added(spec_for( "w-h2c", &url_h2c, WorkerMode::Plain, ))) .await .unwrap(); assert!( wait_for_protocol(®istry, "w-h2c", WireProtocol::H2c).await, "an h2c-capable worker registered after an Http1 worker must still resolve h2c \ (per-worker protocol, no fleet-wide lock); got {:?}", registry .get(&WorkerId("w-h2c".into())) .map(|w| w.protocol()), ); // The earlier worker is untouched. let first = registry.get(&WorkerId("w-http1".into())).unwrap(); assert_eq!( first.protocol(), WireProtocol::Http1, "the Http1 worker must stay Http1", ); drop(tx); h.await.unwrap(); } /// Block until `worker_id`'s registry entry reports `expected`, or 2 s elapse. /// Returns whether it converged. /// /// The `model_ids` check is half the predicate on purpose. `H2c` proves itself /// — it is reachable only through a successful `/server_info` read — but /// `Http1` is also the default for a worker built without a resolved protocol, /// so waiting on that value alone would be satisfied by an implementation that /// never reads one. Requiring a resolved model id as well means the worker at /// least completed introspection against a fixture that answers both /// endpoints. async fn wait_for_protocol( registry: &Arc, worker_id: &str, expected: WireProtocol, ) -> bool { let id = WorkerId(worker_id.into()); wait_for(Duration::from_secs(2), || { registry .get(&id) .is_some_and(|w| !w.model_ids.is_empty() && w.protocol() == expected) }) .await } /// Poll `cond` every 20 ms until it returns true or `budget` elapses. async fn wait_for(budget: Duration, mut cond: impl FnMut() -> bool) -> bool { let deadline = Instant::now() + budget; while Instant::now() < deadline { if cond() { return true; } tokio::time::sleep(Duration::from_millis(20)).await; } cond() } #[tokio::test] async fn mode_changed_preserves_active_requests_and_breaker() { let (url, _s) = spawn_fake_worker(json!({"served_model_name": "m"})).await; let (tx, rx) = mpsc::channel(16); let registry = Arc::new(WorkerRegistry::default()); let h = tokio::spawn(manager::run(rx, registry.clone())); tx.send(DiscoveryEvent::Added(spec_for( "w1", &url, WorkerMode::Prefill, ))) .await .unwrap(); tokio::time::sleep(Duration::from_millis(200)).await; // Grab a handle, bump active_requests, and open the breaker. let w = registry.get(&WorkerId("w1".into())).unwrap(); w.active_requests.fetch_add(5, Ordering::Relaxed); // Default threshold is 3 — record 10 failures to guarantee Open state. for _ in 0..10 { w.breaker.record_failure(); } let breaker_open_before = !w.breaker.allow(); assert!( breaker_open_before, "breaker should be open after 10 failures" ); // Flip mode via ModeChanged. tx.send(DiscoveryEvent::ModeChanged { id: WorkerId("w1".into()), mode: WorkerMode::Decode, }) .await .unwrap(); tokio::time::sleep(Duration::from_millis(50)).await; // Re-fetch the Worker handle from the registry. let w_after = registry.get(&WorkerId("w1".into())).unwrap(); assert_eq!( w_after.mode(), WorkerMode::Decode, "mode should have flipped to Decode" ); assert_eq!( w_after.active_requests.load(Ordering::Relaxed), 5, "active_requests should be preserved across mode change" ); assert!( !w_after.breaker.allow(), "breaker open state should be preserved across mode change" ); // Critical: the Arc identity must be the same — mutation in place. assert!( Arc::ptr_eq(&w, &w_after), "Worker handle should be the SAME Arc, not a fresh replacement" ); drop(tx); h.await.unwrap(); } /// An out-of-order `ModeChanged` for a worker the registry does not know /// about (e.g. a buggy discovery backend reordered `Removed` and /// `ModeChanged`) must not panic, must not silently log INFO claiming the /// mode flip happened, and must leave the registry untouched. #[tokio::test] async fn manager_handles_orphan_mode_changed_without_panic() { let (tx, rx) = mpsc::channel(16); let registry = Arc::new(WorkerRegistry::default()); let h = tokio::spawn(manager::run(rx, registry.clone())); tx.send(DiscoveryEvent::ModeChanged { id: WorkerId("ghost".into()), mode: WorkerMode::Decode, }) .await .unwrap(); tokio::time::sleep(Duration::from_millis(50)).await; assert!( registry.get(&WorkerId("ghost".into())).is_none(), "an orphan ModeChanged must not create a phantom worker", ); assert_eq!( registry.workers_for(&ModelId("m".into())).len(), 0, "registry must be empty after an orphan event", ); drop(tx); h.await.unwrap(); } /// A `Removed` for an unknown id is a no-op — registry stays empty, manager /// keeps running. #[tokio::test] async fn manager_handles_orphan_removed_without_panic() { let (tx, rx) = mpsc::channel(16); let registry = Arc::new(WorkerRegistry::default()); let h = tokio::spawn(manager::run(rx, registry.clone())); tx.send(DiscoveryEvent::Removed { id: WorkerId("ghost".into()), }) .await .unwrap(); tokio::time::sleep(Duration::from_millis(50)).await; assert!(registry.is_empty()); drop(tx); h.await.unwrap(); } /// Duplicate `Added` for the same id is an upsert — the registry ends up /// with exactly one worker. The model resolved by /server_info wins on /// re-add (a different worker may advertise a different served model). #[tokio::test] async fn manager_handles_duplicate_added_as_upsert() { let (url_first, _s_first) = spawn_fake_worker(json!({"served_model_name": "m1"})).await; let (url_second, _s_second) = spawn_fake_worker(json!({"served_model_name": "m1"})).await; let (tx, rx) = mpsc::channel(16); let registry = Arc::new(WorkerRegistry::default()); let h = tokio::spawn(manager::run(rx, registry.clone())); tx.send(DiscoveryEvent::Added(spec_for( "w1", &url_first, WorkerMode::Plain, ))) .await .unwrap(); tx.send(DiscoveryEvent::Added(spec_for( "w1", &url_second, WorkerMode::Plain, ))) .await .unwrap(); wait_until( || { registry .get(&WorkerId("w1".into())) .is_some_and(|worker| worker.url == url_second) }, "replacement Added event to update the worker", ) .await; assert_eq!( registry.workers_for(&ModelId("m1".into())).len(), 1, "w1 still serves m1 after the second Added", ); drop(tx); h.await.unwrap(); } /// Spawn a fake worker whose `/server_info` returns `body` only after /// sleeping for `delay`. Returns the worker URL and a shutdown channel. async fn spawn_slow_worker(body: Value, delay: Duration) -> (String, oneshot::Sender<()>) { let body = Arc::new(body); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let port = listener.local_addr().unwrap().port(); let app = Router::new().route( "/server_info", get(move || { let body = body.clone(); async move { tokio::time::sleep(delay).await; Json((*body).clone()) } }), ); let (tx, rx) = oneshot::channel::<()>(); tokio::spawn(async move { let _ = axum::serve(listener, app) .with_graceful_shutdown(async move { let _ = rx.await; }) .await; }); (format!("http://127.0.0.1:{port}"), tx) } async fn spawn_gated_worker(body: Value, gate: Arc) -> (String, oneshot::Sender<()>) { let body = Arc::new(body); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let port = listener.local_addr().unwrap().port(); let app = Router::new().route( "/server_info", get(move || { let body = body.clone(); let gate = Arc::clone(&gate); async move { gate.wait().await; Json((*body).clone()) } }), ); let (tx, rx) = oneshot::channel::<()>(); tokio::spawn(async move { let _ = axum::serve(listener, app) .with_graceful_shutdown(async move { let _ = rx.await; }) .await; }); (format!("http://127.0.0.1:{port}"), tx) } /// Spawn a fake worker that counts each `GET /server_info` hit in the /// returned `AtomicUsize`. Used to assert the manager makes exactly /// one round-trip per worker. async fn spawn_counting_worker(body: Value) -> (String, Arc, oneshot::Sender<()>) { let body = Arc::new(body); let counter = Arc::new(AtomicUsize::new(0)); let counter_clone = counter.clone(); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let port = listener.local_addr().unwrap().port(); let app = Router::new().route( "/server_info", get(move || { let body = body.clone(); let counter = counter_clone.clone(); async move { counter.fetch_add(1, Ordering::SeqCst); Json((*body).clone()) } }), ); let (tx, rx) = oneshot::channel::<()>(); tokio::spawn(async move { let _ = axum::serve(listener, app) .with_graceful_shutdown(async move { let _ = rx.await; }) .await; }); (format!("http://127.0.0.1:{port}"), counter, tx) } /// Registration must start every `/server_info` fetch before any response is /// released. A sequential manager stalls at the first worker's barrier. #[tokio::test] async fn added_events_run_in_parallel() { let n = 5; let gate = Arc::new(Barrier::new(n + 1)); let mut workers = Vec::new(); for _ in 0..n { workers .push(spawn_gated_worker(json!({"served_model_name": "m"}), Arc::clone(&gate)).await); } let (tx, rx) = mpsc::channel(16); let registry = Arc::new(WorkerRegistry::default()); let h = tokio::spawn(manager::run(rx, registry.clone())); for (i, (url, _s)) in workers.iter().enumerate() { tx.send(DiscoveryEvent::Added(spec_for( &format!("w{i}"), url, WorkerMode::Plain, ))) .await .unwrap(); } assert!( tokio::time::timeout(Duration::from_secs(2), gate.wait()) .await .is_ok(), "manager did not start all {n} /server_info requests concurrently" ); let registered = tokio::time::timeout(Duration::from_secs(2), async { loop { if registry.workers_for(&ModelId("m".into())).len() == n { return; } tokio::time::sleep(Duration::from_millis(10)).await; } }) .await; assert!(registered.is_ok(), "manager failed to register {n} workers"); drop(tx); h.await.unwrap(); } /// A `Removed` issued while the matching `Added` is still mid-fetch /// must await the in-flight registration handle before removing. /// Without that ordering the removal runs first (registry has nothing /// to remove), then the Added's deferred registry write leaks the /// worker. #[tokio::test] async fn removed_awaits_pending_added() { let (url, _s) = spawn_slow_worker( json!({"served_model_name": "m"}), Duration::from_millis(300), ) .await; let (tx, rx) = mpsc::channel(16); let registry = Arc::new(WorkerRegistry::default()); let h = tokio::spawn(manager::run(rx, registry.clone())); tx.send(DiscoveryEvent::Added(spec_for( "w-slow", &url, WorkerMode::Plain, ))) .await .unwrap(); tx.send(DiscoveryEvent::Removed { id: WorkerId("w-slow".into()), }) .await .unwrap(); // Wait long enough for the Added's /server_info to complete (300ms), // then assert the worker is gone. If Removed ran before Added's // registry write, the post-fetch write would leak the entry. tokio::time::sleep(Duration::from_millis(600)).await; assert!( registry.get(&WorkerId("w-slow".into())).is_none(), "Removed must await the in-flight Added; otherwise the deferred \ registry write leaks the worker" ); drop(tx); h.await.unwrap(); } /// The manager must make exactly ONE `/server_info` request per worker. /// Before this fix the worker manager fetched `served_model_name` and /// `KvEventIndex::add_worker` fetched the `kv_events` block /// independently — 2N round-trips for N workers. #[tokio::test] async fn manager_emits_single_server_info_fetch_per_worker() { use sgl_router::state::kv_events::KvEventIndex; let body = json!({ "served_model_name": "m", "kv_events": { "publisher": "zmq", "endpoint_host": "127.0.0.1", "endpoint_port_base": 60100, "topic": "", "block_size": 64, "dp_size": 1, } }); let (url, counter, _s) = spawn_counting_worker(body).await; let (tx, rx) = mpsc::channel(16); let registry = Arc::new(WorkerRegistry::default()); let kv_index = KvEventIndex::new(); let h = tokio::spawn(manager::run_with_config( rx, registry.clone(), None, Some(kv_index.clone()), None, )); tx.send(DiscoveryEvent::Added(spec_for( "w1", &url, WorkerMode::Plain, ))) .await .unwrap(); // Wait for both the registry and kv-events index to reflect the worker. let ready = tokio::time::timeout(Duration::from_secs(2), async { loop { if registry.get(&WorkerId("w1".into())).is_some() && kv_index.known_worker_count() == 1 { return true; } tokio::time::sleep(Duration::from_millis(20)).await; } }) .await; assert!( ready.is_ok(), "manager did not finish onboarding the worker" ); let hits = counter.load(Ordering::SeqCst); assert_eq!( hits, 1, "manager must fetch /server_info exactly once per worker (got {hits})" ); drop(tx); h.await.unwrap(); kv_index.shutdown().await; } /// Public-API integration for the reconcile loop: a worker that registers /// with empty `model_ids` (its `/server_info` was failing when discovery /// first reported it — the "EndpointSlice ready before the engine can /// answer /server_info" race) must be recovered by the manager's /// reconcile loop once `/server_info` starts answering. Exercised through /// the public `run_with_introspector_and_reconcile` entrypoint with no /// new discovery event after the initial `Added`. #[tokio::test] async fn reconcile_recovers_worker_with_unresolved_model_ids() { use axum::http::StatusCode; use axum::response::IntoResponse; use sgl_router::workers::WorkerIntrospector; use std::sync::atomic::AtomicBool; // Switchable fake engine: 503 on /server_info until `ready` flips // true, then serves a body advertising model "m". let ready = Arc::new(AtomicBool::new(false)); let ready_handler = ready.clone(); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let port = listener.local_addr().unwrap().port(); let app = Router::new().route( "/server_info", get(move || { let ready = ready_handler.clone(); async move { if ready.load(Ordering::SeqCst) { Json(json!({"served_model_name": "m"})).into_response() } else { StatusCode::SERVICE_UNAVAILABLE.into_response() } } }), ); let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); tokio::spawn(async move { let _ = axum::serve(listener, app) .with_graceful_shutdown(async move { let _ = shutdown_rx.await; }) .await; }); let url = format!("http://127.0.0.1:{port}"); let (tx, rx) = mpsc::channel(16); let registry = Arc::new(WorkerRegistry::default()); let h = tokio::spawn(manager::run_with_introspector_and_reconcile( rx, registry.clone(), None, None, None, Arc::new(WorkerIntrospector::new(Duration::from_millis(300))), Duration::from_millis(150), )); tx.send(DiscoveryEvent::Added(spec_for( "w-slow", &url, WorkerMode::Plain, ))) .await .unwrap(); // Phase 1: the worker registers but stays out of the model pool while // /server_info keeps failing. let stuck = tokio::time::timeout(Duration::from_secs(2), async { loop { if let Some(w) = registry.get(&WorkerId("w-slow".into())) { if w.model_ids.is_empty() && registry.workers_for(&ModelId("m".into())).is_empty() { return true; } } tokio::time::sleep(Duration::from_millis(20)).await; } }) .await; assert!( stuck.is_ok(), "worker should register with empty model_ids (invisible to routing) while /server_info fails", ); // Engine finishes coming up. ready.store(true, Ordering::SeqCst); // Phase 2: the reconcile loop re-introspects and the worker joins the // model pool — no new discovery event was sent. let recovered = tokio::time::timeout(Duration::from_secs(3), async { loop { if registry.workers_for(&ModelId("m".into())).len() == 1 { return true; } tokio::time::sleep(Duration::from_millis(20)).await; } }) .await; assert!( recovered.is_ok(), "reconcile loop must add the worker to the model pool once /server_info recovers", ); drop(tx); h.await.unwrap(); let _ = shutdown_tx.send(()); }