Skip to content

Commit 02adac9

Browse files
authored
Merge branch 'main' into fix/flowcontrol-finalize-requests-2099
2 parents 69b927d + 34478d1 commit 02adac9

33 files changed

Lines changed: 521 additions & 156 deletions

File tree

.github/workflows/pr-kind-label.yaml

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Apply kind labels from PR body
1+
name: Apply kind/area labels and milestone from PR body
22

33
on:
44
pull_request_target:
@@ -23,14 +23,53 @@ jobs:
2323
gh pr view "$PR_NUMBER" --repo "$REPO" --json labels \
2424
-q '.labels[].name | select(startswith("kind/"))' | \
2525
while IFS= read -r label; do
26-
gh pr edit "$PR_NUMBER" --repo "$REPO" --remove-label "$label"
26+
gh pr edit "$PR_NUMBER" --repo "$REPO" --remove-label "$label" || echo "::warning::Failed to remove label '$label'"
2727
done
2828
2929
# Extract /kind values from PR body and apply labels
30-
printf '%s' "$PR_BODY" | tr -d '\r' | grep '^/kind ' | \
31-
sed 's|^/kind ||' | sort -u | \
30+
printf '%s' "$PR_BODY" | tr -d '\r' | sed -n 's|^/kind[[:space:]][[:space:]]*||p' | sed 's|[[:space:]]*$||' | sort -u | \
3231
while IFS= read -r kind; do
3332
[ -z "$kind" ] && continue
3433
gh pr edit "$PR_NUMBER" --repo "$REPO" --add-label "kind/$kind" || \
3534
echo "::warning::Label 'kind/$kind' not found in repository"
3635
done
36+
37+
- name: Set area labels
38+
env:
39+
GH_TOKEN: ${{ github.token }}
40+
PR_BODY: ${{ github.event.pull_request.body }}
41+
PR_NUMBER: ${{ github.event.pull_request.number }}
42+
REPO: ${{ github.repository }}
43+
run: |
44+
# Absence of a directive is a no-op. Area labels are also set by the
45+
# /area comment command and clearing here would wipe those.
46+
areas=$(printf '%s' "$PR_BODY" | tr -d '\r' | sed -n 's|^/area[[:space:]][[:space:]]*||p' | \
47+
sed 's|[[:space:]]*$||' | sed '/^$/d' | sort -u)
48+
[ -z "$areas" ] && exit 0
49+
50+
# Remove existing area/* labels
51+
gh pr view "$PR_NUMBER" --repo "$REPO" --json labels \
52+
-q '.labels[].name | select(startswith("area/"))' | \
53+
while IFS= read -r label; do
54+
gh pr edit "$PR_NUMBER" --repo "$REPO" --remove-label "$label" || echo "::warning::Failed to remove label '$label'"
55+
done
56+
57+
printf '%s\n' "$areas" | \
58+
while IFS= read -r area; do
59+
gh pr edit "$PR_NUMBER" --repo "$REPO" --add-label "area/$area" || \
60+
echo "::warning::Label 'area/$area' not found in repository"
61+
done
62+
63+
- name: Set milestone
64+
env:
65+
GH_TOKEN: ${{ github.token }}
66+
PR_BODY: ${{ github.event.pull_request.body }}
67+
PR_NUMBER: ${{ github.event.pull_request.number }}
68+
REPO: ${{ github.repository }}
69+
run: |
70+
# Use the last /milestone directive in the PR body
71+
milestone=$(printf '%s' "$PR_BODY" | tr -d '\r' | sed -n 's|^/milestone[[:space:]][[:space:]]*||p' | \
72+
sed 's|[[:space:]]*$||' | sed '/^$/d' | tail -n 1)
73+
[ -z "$milestone" ] && exit 0
74+
gh pr edit "$PR_NUMBER" --repo "$REPO" --milestone "$milestone" || \
75+
echo "::warning::Milestone '$milestone' not found in repository"

CODEOWNERS

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
* @llm-d/router-maintainers
1515

1616
# Flow control framework + plugins
17-
/pkg/epp/flowcontrol/ @LukeAVanDrie @shmuelk @llm-d/router-maintainers
18-
/pkg/epp/framework/interface/flowcontrol/ @LukeAVanDrie @shmuelk @llm-d/router-maintainers
19-
/pkg/epp/framework/plugins/flowcontrol/ @LukeAVanDrie @shmuelk @llm-d/router-maintainers
17+
/pkg/epp/flowcontrol/ @LukeAVanDrie @RishabhSaini @shmuelk @llm-d/router-maintainers
18+
/pkg/epp/framework/interface/flowcontrol/ @LukeAVanDrie @RishabhSaini @shmuelk @llm-d/router-maintainers
19+
/pkg/epp/framework/plugins/flowcontrol/ @LukeAVanDrie @RishabhSaini @shmuelk @llm-d/router-maintainers
2020

2121
# Envoy ext_proc message layer
2222
/pkg/common/envoy/ @shmuelk @llm-d/router-maintainers

Makefile.coord.mk

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ endif
116116
# Env vars forwarded into the e2e test container.
117117
E2E_ENV_VARS = COORDINATOR_IMAGE VLLM_IMAGE EPP_IMAGE VLLM_RENDER_IMAGE VLLM_RENDER_PORT \
118118
E2E_GATEWAY_PORT E2E_KEEP_CLUSTER_ON_FAILURE \
119-
E2E_PRINT_COORDINATOR_LOGS K8S_CONTEXT READY_TIMEOUT MODEL_NAME
119+
E2E_PRINT_LOGS K8S_CONTEXT READY_TIMEOUT MODEL_NAME
120120
BUILDER_E2E_ENV_FLAGS = $(foreach v,$(E2E_ENV_VARS),$(if $($(v)),-e '$(v)=$($(v))'))
121121
ifneq ($(filter command line environment,$(origin NAMESPACE)),)
122122
BUILDER_E2E_ENV_FLAGS += -e NAMESPACE=$(NAMESPACE)

README.coord.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ kubectl --context kind-e2e-coordinator-tests get pods
341341
|---|---|---|
342342
| `E2E_KEEP_CLUSTER_ON_FAILURE` | `false` | Preserve the Kind cluster when the suite fails |
343343
| `E2E_GATEWAY_PORT` | `30080` | Host port mapped to the gateway NodePort |
344-
| `E2E_PRINT_COORDINATOR_LOGS` | `false` | Print coordinator pod logs during the run |
344+
| `E2E_PRINT_LOGS` | `false` | Print all pod logs (coordinator, EPPs, Envoy, workers) for every spec, not just on failure |
345345
| `CONTAINER_RUNTIME` | `docker` | Container runtime used to load images into Kind (`docker` or `podman`) |
346346
| `EPP_IMAGE` | `ghcr.io/llm-d/llm-d-router-endpoint-picker:dev` | EPP image loaded into the Kind cluster |
347347
| `VLLM_IMAGE` | `ghcr.io/llm-d/llm-d-inference-sim:v0.10.2` | vLLM image loaded into the Kind cluster |

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/README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,14 @@ their justifications, please refer to the detailed documentation within the rele
9999
processing loop. It manages a worker that distributes incoming requests, apply policies, and dispatch
100100
requests to the backends. Its design focuses on high throughput and backpressure.
101101

102-
2. **Pluggable `Policy` Framework (`./framework`)**: This defines the core interfaces for all pluggable logic. It
103-
features a two-tier policy system for `Fairness` (decisions *between* different flows) and `Ordering`
104-
(decisions *within* a single flow) logic, covering both request dispatch and displacement.
105-
106-
3. **Extensible `SafeQueue` System (`./framework`)**: This defines the `framework.SafeQueue` interface for
107-
concurrent-safe request storage. It uses a `QueueCapability` system that allows for diverse and extensible queue
108-
implementations (e.g., FIFO, Priority Heap) while maintaining a stable interface.
102+
2. **Pluggable `Policy` Contracts (`/pkg/epp/framework/interface/flowcontrol`)**: The plugin SDK defines the core
103+
interfaces for all pluggable logic. It features a two-tier policy system for `Fairness` (decisions *between*
104+
different flows) and `Ordering` (decisions *within* a single flow) logic, covering both request dispatch and
105+
displacement.
106+
107+
3. **The `SafeQueue` (`./contracts`, `./queue`)**: The `contracts.SafeQueue` interface defines concurrent-safe
108+
request storage, and `./queue` provides the priority-queue implementation whose dispatch order is determined by
109+
the flow's configured `OrderingPolicy`.
109110

110111
4. **The `FlowRegistry` (`./registry`, `./contracts`)**: This is the stateful control plane of the system. It manages
111112
the configuration and lifecycle of all flows, policies, and queues. It presents a view of its state to the

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
{
File renamed without changes.

0 commit comments

Comments
 (0)