feat(llm): hold model-card discovery writes until validation releases
Third of five stacked PRs for the worker validation gates DEP (ai-dynamo/dynamo#15897). Based on #2. #4 is stacked on this PR so that #5's end-to-end test covers both.
Why
Under etcd discovery the frontend routes to endpoint records that have an admitted model card, whatever the pod's readiness. Holding /health alone doesn't keep traffic off a held worker, so the card's discovery write has to be held too.
What
- New
lib/llm/src/validation_hold.rs:- With
DYN_WORKER_VALIDATION_HOLD=card(the default) and the worker not yet released,register_model_cardparks the card in a per-process map keyed by(connection_id, namespace, component, endpoint, suffix), then returnsOk. - Each held card adds a release hook (from #2), so cards are written in registration order. Each write is retried up to 3 times; a final failure fails the
publishgate and the worker stays held. - While a card is held,
update_model_taintsedits the held spec, using the newDiscoverySpec::with_model_taintsso the rules matchDiscovery::update_model_taints(reserved topology prefix and so on).detach_from_endpointdrops the held card and skips the discovery unregister. - The map uses an async lock, so a release write and a concurrent taint update or unregister are ordered.
- With
register_model_cardstill callsrecord_engine_capacity, which sizes the process rather than routing. Without gates, the order is unchanged: register, then record.hold=healthwrites the card immediately.- Virtual planner:
_get_actual_worker_countcounts only instances whose model card is published, using the existingFpmEventSubscriber.get_model_cards()snapshot. When cards can't be tracked, it falls back to the endpoint-instance count with a warning.
Tests
Discovery-level tests on the in-memory store (--features integration):
- the card is held, then written on release;
hold=healthwrites it immediately;- a base card and a LoRA card are written in registration order;
- taints set during the hold are in the written card, and go straight to discovery after release;
- reserved taints are rejected during the hold;
- unregister during the hold drops the card;
- after release, cards are written directly;
- a failing gate never writes the card;
- without gates, behaviour is unchanged.
Planner unit tests: workers without a card aren't counted and are counted once the card is published; untracked cards fall back to the endpoint count.
cargo clippy -p dynamo-llm -p dynamo-runtime --all-targets -D warnings is clean, and all 12 virtual-connector tests pass.
Known differences
Discovery::register's different-model conflict check runs at write time. Under the hold, a conflicting registration returnsOkand fails at release as apublishgate failure. That's fail-closed, but later than today.- Cards written outside
register_model_cardaren't held. That covers the vLLM sidecar LoRA path (sidecar mode is rejected at startup) andlib/rl.
Stack
- #1 Split
/livefrom/health - #2 Runtime validation gates
- This PR: hold the model card until release
- #4 Loopback OpenAI endpoint
- #5 Reference check runner, docs and an example DGD
š¤ Generated with Claude Code