Skip to content

Commit d34b7d9

Browse files
committed
fix(flowcontrol): apply effective defaults for DefaultRequestTTL and per-band MaxRequests
Neither field had an effective default despite documentation claiming otherwise: an unset defaultRequestTTL applied no TTL at all, and the per-band MaxRequests doc referenced a defaultPriorityBandMaxRequests constant that did not exist. A default configuration could therefore queue requests with no time or count bound until the per-band byte limit (1 GB). - Default DefaultRequestTTL to 60s in controller.NewConfig. The TTL is a queue-wait budget: a request that cannot dispatch within it is shed with a retryable backpressure error rather than served with severely degraded time-to-first-token. It is the only bound on queue wait when neither the client nor the gateway enforces a request deadline (the well-lit guides configure no gateway request timeout); deadlines that fire sooner evict via context cancellation instead. An explicit 0s disables the TTL (disconnect-only), distinguishable from unset via the pointer-typed API field. - Default per-band MaxRequests to 5000 in applyDefaults, mirroring the MaxBytes handling: band-level 0 is treated as unset and receives the default. The pair is self-consistent: under a dispatch halt, occupancy self-limits at arrival rate x TTL, so the count cap only binds above ~83 req/s per band. - Global limits remain unlimited by default (omitted or explicit 0); band count is bounded by the distinct priorities defined in InferenceObjectives. - Fix stale docs: FlowGCTimeout default (5 minutes, not 1 hour), the nonexistent maxBytes-0-drops-immediately claim on DefaultNegativePriorityBand (band-level 0 has never meant zero capacity), and the InitialEffectiveTTL adapter docstring. Fixes #2095 Part of #1187 Signed-off-by: Luke Van Drie <lukevandrie@google.com>
1 parent 04cc371 commit d34b7d9

7 files changed

Lines changed: 131 additions & 56 deletions

File tree

apix/config/v1alpha1/endpointpickerconfig_types.go

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -351,23 +351,25 @@ type FlowControlConfig struct {
351351
// levels. If this limit is exceeded, new requests will be rejected even if their specific
352352
// priority band has capacity.
353353
// Accepts standard Kubernetes resource quantities (e.g., "1Gi", "500M").
354-
// If omitted, no global byte limit is enforced.
354+
// If omitted or "0", no global byte limit is enforced.
355355
MaxBytes *resource.Quantity `json:"maxBytes,omitempty"`
356356

357357
// +optional
358358
// MaxRequests defines the global maximum number of concurrent requests across all priority
359359
// levels. If this limit is exceeded, new requests will be rejected even if their specific
360360
// priority band has capacity.
361361
// Accepts standard Kubernetes resource quantities (e.g., "100", "1k").
362-
// If omitted, no global request limit is enforced.
362+
// If omitted or "0", no global request limit is enforced.
363363
MaxRequests *resource.Quantity `json:"maxRequests,omitempty"`
364364

365365
// +optional
366-
// DefaultRequestTTL serves as a fallback timeout for requests that do not specify their own
367-
// deadline.
368-
// It ensures that requests do not hang indefinitely in the queue.
369-
// If 0 or omitted, it defaults to the client context deadline, meaning requests may wait
370-
// indefinitely unless cancelled by the client.
366+
// DefaultRequestTTL bounds how long a request may wait in the queue before it is evicted.
367+
// If omitted, it defaults to 60s. This is a queue-wait budget: a request that cannot dispatch
368+
// within it is shed with a retryable backpressure error rather than served late, and it is the
369+
// only bound on queue wait when neither the client nor the gateway enforces a request deadline.
370+
// Where such deadlines exist and fire sooner, they evict the request first (client disconnect).
371+
// An explicit "0s" disables the TTL: requests then wait until client disconnect or controller
372+
// shutdown.
371373
DefaultRequestTTL *metav1.Duration `json:"defaultRequestTTL,omitempty"`
372374

373375
// +optional
@@ -382,7 +384,10 @@ type FlowControlConfig struct {
382384
// +optional
383385
// DefaultNegativePriorityBand allows you to define a separate template for priority levels
384386
// strictly below zero. This enables designating negative-priority traffic as sheddable by
385-
// setting lower capacity limits (e.g., maxBytes: "0" to drop immediately).
387+
// setting lower capacity limits (e.g., a small maxRequests, so that under saturation the band
388+
// fills quickly and subsequent requests are rejected immediately rather than queued).
389+
// Note that a value of "0" is treated as unset and receives the system default, not zero
390+
// capacity.
386391
// If not specified, negative priorities fall back to DefaultPriorityBand.
387392
DefaultNegativePriorityBand *PriorityBandConfig `json:"defaultNegativePriorityBand,omitempty"`
388393

@@ -457,13 +462,15 @@ type PriorityBandConfig struct {
457462
// +optional
458463
// MaxBytes is the maximum number of bytes allowed for this priority band.
459464
// Accepts standard Kubernetes resource quantities (e.g., "1Gi", "500M").
460-
// If omitted, the system default is used.
465+
// If omitted or "0", the system default (1G) is used. Per-band limits are always bounded; to
466+
// effectively remove the bound, set an explicit large value.
461467
MaxBytes *resource.Quantity `json:"maxBytes,omitempty"`
462468

463469
// +optional
464470
// MaxRequests is the maximum number of concurrent requests allowed for this priority band.
465471
// Accepts standard Kubernetes resource quantities (e.g., "100", "1k").
466-
// If omitted, no request limit is enforced.
472+
// If omitted or "0", the system default (5000) is used. Per-band limits are always bounded; to
473+
// effectively remove the bound, set an explicit large value.
467474
MaxRequests *resource.Quantity `json:"maxRequests,omitempty"`
468475

469476
// +optional

pkg/epp/config/loader/flowcontrol_test.go

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,13 @@ func TestBuildRegistryConfig(t *testing.T) {
154154
apiConfig: nil,
155155
assertion: func(t *testing.T, cfg *registry.Config) {
156156
assert.Equal(t, uint64(0), cfg.MaxBytes, "Default global limit should be 0 (unlimited)")
157+
assert.Equal(t, uint64(0), cfg.MaxRequests, "Default global request limit should be 0 (unlimited)")
157158
require.NotNil(t, cfg.DefaultPriorityBand,
158159
"Default priority band template should be initialized automatically")
159160
assert.Equal(t, uint64(1_000_000_000) /* registry default: 1 GB */, cfg.DefaultPriorityBand.MaxBytes,
160161
"Default template should use system default capacity")
162+
assert.Equal(t, uint64(5000) /* registry default */, cfg.DefaultPriorityBand.MaxRequests,
163+
"Default template should use the system default request-count capacity")
161164
},
162165
},
163166

@@ -207,6 +210,49 @@ func TestBuildRegistryConfig(t *testing.T) {
207210
"Explicit 0 in DefaultPriorityBand template should be treated as 'Use Default'")
208211
},
209212
},
213+
{
214+
name: "ShouldApplyDefault_WhenBandMaxRequestsIsNilOrZero",
215+
apiConfig: &configapi.FlowControlConfig{
216+
PriorityBands: []configapi.PriorityBandConfig{
217+
{
218+
Priority: 1,
219+
// MaxRequests omitted
220+
},
221+
{
222+
Priority: 2,
223+
MaxRequests: ptr.To(resource.MustParse("0")), // Explicitly zero
224+
},
225+
{
226+
Priority: 3,
227+
MaxRequests: ptr.To(resource.MustParse("250")),
228+
},
229+
},
230+
},
231+
assertion: func(t *testing.T, cfg *registry.Config) {
232+
require.Contains(t, cfg.PriorityBands, 1)
233+
assert.Equal(t, uint64(5000) /* registry default */, cfg.PriorityBands[1].MaxRequests,
234+
"Omitted MaxRequests (nil) should result in the system default (5000)")
235+
require.Contains(t, cfg.PriorityBands, 2)
236+
assert.Equal(t, uint64(5000) /* registry default */, cfg.PriorityBands[2].MaxRequests,
237+
"Explicit MaxRequests (0) should be treated as 'Use Default' (5000)")
238+
require.Contains(t, cfg.PriorityBands, 3)
239+
assert.Equal(t, uint64(250), cfg.PriorityBands[3].MaxRequests,
240+
"Explicit MaxRequests should be preserved")
241+
},
242+
},
243+
{
244+
name: "ShouldApplyDefault_WhenNegativeBandTemplateMaxRequestsIsZero",
245+
apiConfig: &configapi.FlowControlConfig{
246+
DefaultNegativePriorityBand: &configapi.PriorityBandConfig{
247+
MaxRequests: ptr.To(resource.MustParse("0")), // Explicitly zero
248+
},
249+
},
250+
assertion: func(t *testing.T, cfg *registry.Config) {
251+
require.NotNil(t, cfg.DefaultNegativePriorityBand)
252+
assert.Equal(t, uint64(5000) /* registry default */, cfg.DefaultNegativePriorityBand.MaxRequests,
253+
"Explicit 0 in the negative band template should be treated as 'Use Default', not zero capacity")
254+
},
255+
},
210256

211257
// --- Validation Errors ---
212258
{
@@ -299,37 +345,8 @@ func TestBuildRegistryConfig(t *testing.T) {
299345
},
300346

301347
// --- MaxRequests: Defaulting Logic ---
302-
{
303-
name: "ShouldDefaultToZero_WhenMaxRequestsIsNil",
304-
apiConfig: &configapi.FlowControlConfig{
305-
PriorityBands: []configapi.PriorityBandConfig{
306-
{
307-
Priority: 1,
308-
},
309-
},
310-
},
311-
assertion: func(t *testing.T, cfg *registry.Config) {
312-
require.Contains(t, cfg.PriorityBands, 1)
313-
assert.Equal(t, uint64(0), cfg.PriorityBands[1].MaxRequests,
314-
"Omitted MaxRequests should default to 0 (no request limit)")
315-
},
316-
},
317-
{
318-
name: "ShouldDefaultToZero_WhenMaxRequestsIsZero",
319-
apiConfig: &configapi.FlowControlConfig{
320-
PriorityBands: []configapi.PriorityBandConfig{
321-
{
322-
Priority: 1,
323-
MaxRequests: ptr.To(resource.MustParse("0")),
324-
},
325-
},
326-
},
327-
assertion: func(t *testing.T, cfg *registry.Config) {
328-
require.Contains(t, cfg.PriorityBands, 1)
329-
assert.Equal(t, uint64(0), cfg.PriorityBands[1].MaxRequests,
330-
"Explicit MaxRequests=0 should remain 0 (no request limit)")
331-
},
332-
},
348+
// Nil and explicit-zero band MaxRequests defaulting is covered by
349+
// ShouldApplyDefault_WhenBandMaxRequestsIsNilOrZero above.
333350

334351
// --- MaxRequests: Validation Errors ---
335352
{

pkg/epp/flowcontrol/controller/config.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ import (
2424
)
2525

2626
const (
27+
// defaultRequestTTL is the default Time-To-Live applied to queued requests when the configuration
28+
// does not specify one. It is a queue-wait budget: a request still undispatched after this long is
29+
// shed with a retryable backpressure signal instead of being served with severely degraded
30+
// time-to-first-token. It is also the only bound on queue wait when neither the client nor the
31+
// gateway enforces a request deadline (the well-lit guides configure no gateway request timeout);
32+
// where such deadlines exist and fire sooner, context cancellation evicts the request first.
33+
defaultRequestTTL = 60 * time.Second
2734
// defaultExpiryCleanupInterval is the default frequency for scanning for expired items.
2835
defaultExpiryCleanupInterval = 1 * time.Second
2936
// defaultEnqueueChannelBufferSize is the default size of a worker's incoming request buffer.
@@ -32,9 +39,12 @@ const (
3239

3340
// Config holds the configuration for the `FlowController`.
3441
type Config struct {
35-
// DefaultRequestTTL is the default Time-To-Live applied to requests that do not
36-
// specify their own TTL hint.
37-
// Optional: If zero, no TTL is applied by default and we rely solely on request context cancellation.
42+
// DefaultRequestTTL is the default Time-To-Live applied to requests that do not specify their own
43+
// TTL hint. Because the admission adapter does not currently plumb a per-request hint, this value
44+
// governs every request entering flow control.
45+
// Optional: Defaults to `defaultRequestTTL` (60s). An explicit zero disables the TTL entirely, in
46+
// which case queued requests are bounded only by request context cancellation (client disconnect
47+
// or gateway timeout).
3848
DefaultRequestTTL time.Duration
3949

4050
// ExpiryCleanupInterval is the interval at which each processor scans its queues for expired items.
@@ -76,6 +86,7 @@ func NewConfigFromAPI(apiConfig *configapi.FlowControlConfig) (*Config, error) {
7686
// NewConfig creates a new Config with the given options, applying defaults and validation.
7787
func NewConfig(opts ...ConfigOption) (*Config, error) {
7888
c := &Config{
89+
DefaultRequestTTL: defaultRequestTTL,
7990
ExpiryCleanupInterval: defaultExpiryCleanupInterval,
8091
EnqueueChannelBufferSize: defaultEnqueueChannelBufferSize,
8192
}

pkg/epp/flowcontrol/controller/config_test.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,18 @@ func TestNewConfig(t *testing.T) {
4040
name: "Defaults_ShouldBeApplied_WhenNoOptionsProvided",
4141
opts: nil,
4242
expectErr: false,
43+
expectedCfg: Config{
44+
DefaultRequestTTL: defaultRequestTTL,
45+
ExpiryCleanupInterval: defaultExpiryCleanupInterval,
46+
EnqueueChannelBufferSize: defaultEnqueueChannelBufferSize,
47+
},
48+
},
49+
{
50+
name: "ZeroDefaultRequestTTL_ShouldDisableTTL",
51+
opts: []ConfigOption{
52+
WithDefaultRequestTTL(0),
53+
},
54+
expectErr: false,
4355
expectedCfg: Config{
4456
DefaultRequestTTL: 0,
4557
ExpiryCleanupInterval: defaultExpiryCleanupInterval,
@@ -136,7 +148,7 @@ func TestNewConfigFromAPI(t *testing.T) {
136148
"ExpiryCleanupInterval should be defaulted")
137149
assert.Equal(t, defaultEnqueueChannelBufferSize, cfg.EnqueueChannelBufferSize,
138150
"EnqueueChannelBufferSize should be defaulted")
139-
assert.Equal(t, time.Duration(0), cfg.DefaultRequestTTL, "DefaultRequestTTL should default to 0 (disabled)")
151+
assert.Equal(t, defaultRequestTTL, cfg.DefaultRequestTTL, "DefaultRequestTTL should be defaulted when unset")
140152
},
141153
},
142154
{

pkg/epp/flowcontrol/registry/config.go

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,14 @@ const (
3939
)
4040

4141
const (
42-
// defaultPriorityBandMaxBytes is the default global capacity for a priority band if not explicitly configured.
43-
// It is set to 1 GB.
42+
// defaultPriorityBandMaxBytes is the default byte-size capacity for a priority band if not explicitly
43+
// configured. It is set to 1 GB.
4444
defaultPriorityBandMaxBytes uint64 = 1_000_000_000
45+
// defaultPriorityBandMaxRequests is the default request-count capacity for a priority band if not
46+
// explicitly configured. Together with the default request TTL (60s), it only binds when a band's
47+
// arrival rate under a dispatch halt exceeds maxRequests/TTL (~83 req/s); below that, TTL expiry
48+
// bounds occupancy first.
49+
defaultPriorityBandMaxRequests uint64 = 5000
4550
// defaultFlowGCTimeout is the default duration of inactivity after which an idle flow is garbage collected.
4651
// This also serves as the interval for the periodic garbage collection scan.
4752
defaultFlowGCTimeout time.Duration = 5 * time.Minute
@@ -85,14 +90,15 @@ type Config struct {
8590
DefaultPriorityBand *PriorityBandConfig
8691

8792
// DefaultNegativePriorityBand is an optional template for dynamically provisioning priority bands when a request
88-
// arrives with a priority level strictly below zero. This allows setting lower capacity limits (or zero) for
89-
// negative-priority traffic to designate it as sheddable.
93+
// arrives with a priority level strictly below zero. This allows setting lower capacity limits for
94+
// negative-priority traffic to designate it as sheddable (a value of 0 is treated as unset and receives the
95+
// default).
9096
// If nil, negative priorities fall back to DefaultPriorityBand.
9197
DefaultNegativePriorityBand *PriorityBandConfig
9298

9399
// FlowGCTimeout defines the interval at which the registry scans for and garbage collects idle flows.
94100
// A flow is collected if it has been observed to be Idle for at least one full scan interval.
95-
// Optional: Defaults to `defaultFlowGCTimeout` (1 hour).
101+
// Optional: Defaults to `defaultFlowGCTimeout` (5 minutes).
96102
FlowGCTimeout time.Duration
97103

98104
// PriorityBandGCTimeout defines the duration of inactivity after which a dynamically provisioned priority band
@@ -136,12 +142,15 @@ type PriorityBandConfig struct {
136142
FairnessPolicy flowcontrol.FairnessPolicy
137143

138144
// MaxBytes defines the maximum total byte size for this priority band.
139-
// Optional: Defaults to defaultPriorityBandMaxBytes (1 GB).
145+
// Optional: Defaults to defaultPriorityBandMaxBytes (1 GB). A value of 0 is treated as unset and receives the
146+
// default; per-band limits are always bounded, unlike the optional global limits. To effectively remove the
147+
// bound, set an explicit large value.
140148
MaxBytes uint64
141149

142150
// MaxRequests defines the maximum total request count for this priority band.
143-
// A value of 0 signifies no request-count limit is enforced.
144-
// Optional: Defaults to defaultPriorityBandMaxRequests (5000).
151+
// Optional: Defaults to defaultPriorityBandMaxRequests (5000). A value of 0 is treated as unset and receives
152+
// the default; per-band limits are always bounded, unlike the optional global limits. To effectively remove
153+
// the bound, set an explicit large value.
145154
MaxRequests uint64
146155
}
147156

@@ -386,6 +395,9 @@ func (p *PriorityBandConfig) applyDefaults(defaults PriorityBandPolicyDefaults)
386395
if p.MaxBytes == 0 {
387396
p.MaxBytes = defaultPriorityBandMaxBytes
388397
}
398+
if p.MaxRequests == 0 {
399+
p.MaxRequests = defaultPriorityBandMaxRequests
400+
}
389401
if p.FairnessPolicy == nil {
390402
if defaults.FairnessPolicy == nil {
391403
return fmt.Errorf("no default fairness policy provided and none set on band %d", p.Priority)

pkg/epp/flowcontrol/registry/config_test.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ func TestNewConfig(t *testing.T) {
8888
require.NotNil(t, band.FairnessPolicy)
8989
assert.Equal(t, DefaultFairnessPolicyRef, band.FairnessPolicy.TypedName().Name)
9090
assert.Equal(t, defaultPriorityBandMaxBytes, band.MaxBytes)
91+
assert.Equal(t, defaultPriorityBandMaxRequests, band.MaxRequests)
9192
},
9293
},
9394
{
@@ -129,9 +130,17 @@ func TestNewConfig(t *testing.T) {
129130
assertion: func(t *testing.T, cfg *Config) {
130131
assert.Len(t, cfg.PriorityBands, 1, "PriorityBands should contain only the always-injected priority 0")
131132
assert.Contains(t, cfg.PriorityBands, 0, "PriorityBands should contain priority 0")
133+
assert.Equal(t, defaultPriorityBandMaxBytes, cfg.PriorityBands[0].MaxBytes,
134+
"Auto-provisioned priority 0 band should receive the byte-size default")
135+
assert.Equal(t, defaultPriorityBandMaxRequests, cfg.PriorityBands[0].MaxRequests,
136+
"Auto-provisioned priority 0 band should receive the request-count default")
132137
require.NotNil(t, cfg.DefaultPriorityBand, "DefaultPriorityBand template must be initialized")
133138
assert.NotNil(t, cfg.DefaultPriorityBand.FairnessPolicy)
134139
assert.Equal(t, DefaultFairnessPolicyRef, cfg.DefaultPriorityBand.FairnessPolicy.TypedName().Name)
140+
assert.Equal(t, defaultPriorityBandMaxBytes, cfg.DefaultPriorityBand.MaxBytes,
141+
"Dynamic provisioning template should receive the byte-size default")
142+
assert.Equal(t, defaultPriorityBandMaxRequests, cfg.DefaultPriorityBand.MaxRequests,
143+
"Dynamic provisioning template should receive the request-count default")
135144
},
136145
},
137146
{
@@ -372,15 +381,16 @@ func TestNewConfig_DefaultNegativePriorityBand(t *testing.T) {
372381
"Defaults should be applied to DefaultNegativePriorityBand")
373382
})
374383

375-
t.Run("ShouldAllowZeroMaxBytes_ForSheddableTraffic", func(t *testing.T) {
384+
t.Run("ShouldApplyCapacityDefaults_ToEmptyNegativeBandTemplate", func(t *testing.T) {
376385
t.Parallel()
377386
cfg, err := NewConfig(defaults,
378387
WithDefaultNegativePriorityBand(&PriorityBandConfig{}),
379388
)
380389
require.NoError(t, err)
381390
require.NotNil(t, cfg.DefaultNegativePriorityBand)
382-
// MaxBytes=0 gets defaulted to 1GB via applyDefaults
391+
// Zero capacity values are treated as unset: bands are always bounded, never zero-capacity.
383392
assert.Equal(t, defaultPriorityBandMaxBytes, cfg.DefaultNegativePriorityBand.MaxBytes)
393+
assert.Equal(t, defaultPriorityBandMaxRequests, cfg.DefaultNegativePriorityBand.MaxRequests)
384394
})
385395

386396
t.Run("ShouldCloneNegativeBandTemplate", func(t *testing.T) {

pkg/epp/requestcontrol/admission.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,13 @@ func (r *flowControlRequest) ID() string {
194194
}
195195
return r.inferenceRequest.RequestID
196196
}
197-
func (r *flowControlRequest) InitialEffectiveTTL() time.Duration { return 0 } // Use controller default.
197+
198+
// InitialEffectiveTTL returns 0 to defer to the controller-level default TTL, which is therefore the only TTL
199+
// source for every request today.
200+
// TODO(https://github.com/llm-d/llm-d-router/issues/1090): plumb more specific TTL scopes and resolve
201+
// most-specific-wins: per-request (clamped), then per-band (effectively priority band config, eventually
202+
// codifiable in the InferenceObjective CRD), then the controller default.
203+
func (r *flowControlRequest) InitialEffectiveTTL() time.Duration { return 0 }
198204
func (r *flowControlRequest) ByteSize() uint64 { return r.requestByteSize }
199205

200206
func (r *flowControlRequest) InferenceRequest() *scheduling.InferenceRequest {

0 commit comments

Comments
 (0)