fix(proxy): address review findings on admission control (#64)
CI / CI (pull_request) Successful in 33m12s
CI / CI (pull_request) Successful in 33m12s
- tracing: responseWriter now forwards Flush and exposes Unwrap. Without it w.(http.Flusher) failed for every request passing the tracing middleware, so the parked-stream keep-alive never started: a parked streaming request wrote nothing for up to ADMISSION_MAX_PARK_SECONDS, which is exactly the client timeout the keep-alive was added to prevent. - proxy: once the SSE status line is spent, ModifyResponse frames an upstream response >= 400 as an event: error instead of letting a raw error body land inside the stream, where a streaming client reads it as an empty completion and reports success. net/http keeps the first status, so the upstream status cannot be forwarded after the park has written one. - engine_state: new Snapshot() returns the state and its tick channel under one lock. Get and Tick taken separately can miss the poll that lands between the two calls, parking a request for an extra interval. - engine_state: observer_degraded now tracks current parsability rather than being raised once on the first poll and never cleared, which pinned it at 1 after a single truncated response and never reported drift that appeared later. markDegraded only touches the exporter when the value changes. - deployment: ADMISSION_MAX_INFLIGHT is set explicitly to 4, documenting the coupling to the engine's --max-num-seqs, instead of silently defaulting to VLLM_MAX_NUM_SEQS.
This commit is contained in:
@@ -127,6 +127,30 @@ func (o *EngineObserver) Degraded() bool {
|
||||
return o.degraded
|
||||
}
|
||||
|
||||
// Snapshot returns the latest state together with the tick channel that the
|
||||
// next successful poll will close. Both are read under one lock on purpose:
|
||||
// calling Get and Tick separately can miss the wakeup that lands between the
|
||||
// two calls, parking a request for an extra poll interval.
|
||||
func (o *EngineObserver) Snapshot() (EngineState, <-chan struct{}) {
|
||||
o.mu.Lock()
|
||||
defer o.mu.Unlock()
|
||||
return o.state, o.tick
|
||||
}
|
||||
|
||||
// markDegraded records whether the metrics feed is currently unusable and
|
||||
// mirrors that into observer_degraded. It is cleared on the next parseable
|
||||
// scrape, so one truncated response cannot pin the gauge for the process
|
||||
// lifetime while metric-name drift after the first poll is still reported.
|
||||
func (o *EngineObserver) markDegraded(v bool) {
|
||||
o.mu.Lock()
|
||||
changed := o.degraded != v
|
||||
o.degraded = v
|
||||
o.mu.Unlock()
|
||||
if changed {
|
||||
observability.Global().SetObserverDegraded(v)
|
||||
}
|
||||
}
|
||||
|
||||
// poll scrapes /metrics once. startup marks the first poll, whose failure is a
|
||||
// self-diagnosis signal rather than a transient blip.
|
||||
func (o *EngineObserver) poll(ctx context.Context, startup bool) {
|
||||
@@ -161,13 +185,11 @@ func (o *EngineObserver) poll(ctx context.Context, startup bool) {
|
||||
|
||||
running, waiting, kvUsage, err := ParseEngineMetrics(resp.Body)
|
||||
if err != nil {
|
||||
// Metric-name drift must be loud, never a silent fall-back to fail-open
|
||||
// admission (decision Q21). The gauge tracks current state, so it clears
|
||||
// on the next parseable scrape instead of pinning for the pod's life.
|
||||
o.markDegraded(true)
|
||||
if startup {
|
||||
// Startup self-check (decision Q21): metric-name drift must be
|
||||
// loud, never a silent fall-back to fail-open admission.
|
||||
o.mu.Lock()
|
||||
o.degraded = true
|
||||
o.mu.Unlock()
|
||||
observability.Global().SetObserverDegraded(true)
|
||||
logging.Errorf("engine metrics unusable at startup", err, map[string]string{
|
||||
"address": o.addr,
|
||||
})
|
||||
@@ -181,6 +203,7 @@ func (o *EngineObserver) poll(ctx context.Context, startup bool) {
|
||||
return
|
||||
}
|
||||
|
||||
o.markDegraded(false)
|
||||
o.recordSuccess(running, waiting, kvUsage)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user