Skip to content

Commit 862f97b

Browse files
committed
Implement dynamic attributes for EPP AttributeMap
Introduce dynamic attributes (getter providers) to the AttributeMap implementation. This allows retrieving live in-flight load data directly from InFlightLoadProducer's trackers, resolving the issue where concurrency-detector always saw 0 load during admission control. Signed-off-by: Luke Van Drie <lukevandrie@google.com>
1 parent bbb20ce commit 862f97b

7 files changed

Lines changed: 361 additions & 3 deletions

File tree

cmd/epp/runner/runner.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,24 @@ func (r *Runner) parseConfigurationPhaseTwo(ctx context.Context, rawConfig *conf
664664
r.parsers = handlers.NewParsers(cfg.ParserConfig)
665665
logger.Info("loaded configuration from file/text successfully")
666666

667+
var loadProducer *inflightload.InFlightLoadProducer
668+
for _, p := range handle.GetAllPlugins() {
669+
if prod, ok := p.(*inflightload.InFlightLoadProducer); ok {
670+
loadProducer = prod
671+
break
672+
}
673+
}
674+
675+
if loadProducer != nil {
676+
key := attrconcurrency.InFlightLoadDataKey.WithNonEmptyProducerName(loadProducer.TypedName().Name)
677+
r.dlRuntime.RegisterAttributeProvider(key.String(), func(eid string) fwkdl.Cloneable {
678+
return &attrconcurrency.InFlightLoad{
679+
Tokens: loadProducer.GetTokens(eid),
680+
Requests: loadProducer.GetRequests(eid),
681+
}
682+
})
683+
}
684+
667685
return cfg, nil
668686
}
669687

pkg/epp/datalayer/runtime.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ type Runtime struct {
5959

6060
collectors *collectorManager // per-endpoint poller, keyed by namespaced name
6161
logger logr.Logger // Set in Configure; used where no context is available (e.g. ReleaseEndpoint).
62+
63+
provMu sync.RWMutex
64+
providers map[string]func(endpointID string) fwkdl.Cloneable
6265
}
6366

6467
const (
@@ -80,6 +83,7 @@ func NewRuntime(pollingInterval time.Duration) *Runtime {
8083
extractors: newExtractorMap(),
8184
collectors: newCollectorManager(),
8285
logger: logr.Discard(),
86+
providers: make(map[string]func(endpointID string) fwkdl.Cloneable),
8387
}
8488
}
8589

@@ -222,6 +226,16 @@ func (r *Runtime) Register(reg fwkdl.PendingRegistration) error {
222226
return nil
223227
}
224228

229+
// RegisterAttributeProvider registers a provider function for a dynamic attribute.
230+
func (r *Runtime) RegisterAttributeProvider(key string, provider func(endpointID string) fwkdl.Cloneable) {
231+
r.provMu.Lock()
232+
defer r.provMu.Unlock()
233+
if r.providers == nil {
234+
r.providers = make(map[string]func(endpointID string) fwkdl.Cloneable)
235+
}
236+
r.providers[key] = provider
237+
}
238+
225239
// registerSource dispatches src to the matching variant manager. g enforces
226240
// per-Configure-call GVK uniqueness for NotificationSources. src may be a
227241
// PollingDispatcher (not a DataSource), so the parameter is plugin.Plugin.
@@ -371,18 +385,29 @@ func (r *Runtime) NewEndpoint(ctx context.Context, endpointMetadata *fwkdl.Endpo
371385
logger, _ := logr.FromContext(ctx)
372386
logger = logger.WithValues("endpoint", endpointMetadata.GetNamespacedName())
373387

388+
endpoint := fwkdl.NewEndpoint(endpointMetadata, nil)
389+
eid := endpointMetadata.GetNamespacedName().String()
390+
391+
r.provMu.RLock()
392+
for key, provider := range r.providers {
393+
endpoint.GetAttributes().Put(key, &fwkdl.DynamicAttribute{
394+
Get: func() fwkdl.Cloneable {
395+
return provider(eid)
396+
},
397+
})
398+
}
399+
r.provMu.RUnlock()
400+
374401
dispatchers := make([]fwkdl.PollingDispatcher, 0, r.dispatchers.Count())
375402
for _, d := range r.dispatchers.Dispatchers() {
376403
dispatchers = append(dispatchers, d)
377404
}
378405
if len(dispatchers) == 0 {
379406
logger.Info("No polling sources configured, creating endpoint without collector")
380-
endpoint := fwkdl.NewEndpoint(endpointMetadata, nil)
381407
r.dispatchEndpointEvent(ctx, logger, fwkdl.EndpointEvent{Type: fwkdl.EventAddOrUpdate, Endpoint: endpoint})
382408
return endpoint
383409
}
384410

385-
endpoint := fwkdl.NewEndpoint(endpointMetadata, nil)
386411
collector := NewCollector()
387412

388413
key := endpointMetadata.GetNamespacedName()

pkg/epp/datalayer/runtime_endpoint_dispatch_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ import (
3030
"github.com/llm-d/llm-d-router/pkg/epp/framework/plugins/datalayer/source/notifications"
3131
)
3232

33+
type dummy struct {
34+
Text string
35+
}
36+
37+
func (d *dummy) Clone() fwkdl.Cloneable {
38+
if d == nil {
39+
return nil
40+
}
41+
return &dummy{Text: d.Text}
42+
}
43+
3344
// TestNewEndpointDispatchesEventWithNoPollers verifies that endpoint lifecycle
3445
// events are dispatched to EndpointSource even when no PollingDataSource is configured.
3546
// Regression test for: endpoint-notification-source silently drops events when
@@ -101,3 +112,26 @@ func TestUpdateEndpointDispatchesEvent(t *testing.T) {
101112
assert.Equal(t, fwkdl.EventAddOrUpdate, events[0].Type)
102113
assert.Equal(t, "5.6.7.8", events[0].Endpoint.GetMetadata().Address)
103114
}
115+
116+
func TestRuntimeRegisterAttributeProvider(t *testing.T) {
117+
r := NewRuntime(1)
118+
119+
r.RegisterAttributeProvider("test-key", func(endpointID string) fwkdl.Cloneable {
120+
return &dummy{Text: endpointID + "-value"}
121+
})
122+
123+
pod := &fwkdl.EndpointMetadata{
124+
NamespacedName: types.NamespacedName{Name: "pod1", Namespace: "default"},
125+
Address: "1.2.3.4",
126+
}
127+
128+
endpoint := r.NewEndpoint(context.Background(), pod)
129+
assert.NotNil(t, endpoint)
130+
131+
val, ok := endpoint.GetAttributes().Get("test-key")
132+
assert.True(t, ok)
133+
134+
dVal, ok := val.(*dummy)
135+
assert.True(t, ok)
136+
assert.Equal(t, "default/pod1-value", dVal.Text)
137+
}

pkg/epp/framework/interface/datalayer/attributemap.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@ type Cloneable interface {
2525
Clone() Cloneable
2626
}
2727

28+
// DynamicAttribute wraps a getter function to allow on-demand resolution of attributes.
29+
type DynamicAttribute struct {
30+
Get func() Cloneable
31+
}
32+
33+
// Clone implements Cloneable. It copies the function pointer.
34+
func (d *DynamicAttribute) Clone() Cloneable {
35+
if d == nil {
36+
return nil
37+
}
38+
return &DynamicAttribute{Get: d.Get}
39+
}
40+
2841
// AttributeMap is used to store flexible metadata or traits
2942
// across different aspects of an inference server.
3043
// Stored values must be Cloneable.
@@ -52,12 +65,21 @@ func (a *Attributes) Put(key string, value Cloneable) {
5265
}
5366
}
5467

55-
// Get retrieves an attribute by key, returning a cloned copy.
68+
// Get retrieves an attribute by key, returning a cloned copy (or resolving it dynamically).
5669
func (a *Attributes) Get(key string) (Cloneable, bool) {
5770
val, ok := a.data.Load(key)
5871
if !ok {
5972
return nil, false
6073
}
74+
75+
if dynamic, ok := val.(*DynamicAttribute); ok {
76+
realVal := dynamic.Get()
77+
if realVal == nil {
78+
return nil, false
79+
}
80+
return realVal.Clone(), true
81+
}
82+
6183
if cloneable, ok := val.(Cloneable); ok {
6284
return cloneable.Clone(), true
6385
}

pkg/epp/framework/interface/datalayer/attributemap_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,29 @@ func TestReadAttribute(t *testing.T) {
102102
assert.Nil(t, other) // zero value of pointer is nil
103103

104104
}
105+
106+
func TestDynamicAttribute(t *testing.T) {
107+
attrs := NewAttributes()
108+
109+
val := &dummy{"live"}
110+
dynamic := &DynamicAttribute{
111+
Get: func() Cloneable {
112+
return val
113+
},
114+
}
115+
116+
attrs.Put("dynamic_key", dynamic)
117+
118+
// First read
119+
got, ok := attrs.Get("dynamic_key")
120+
assert.True(t, ok)
121+
assert.Equal(t, "live", got.(*dummy).Text)
122+
123+
// Mutate the source value
124+
val.Text = "changed"
125+
126+
// Second read should reflect change
127+
got2, ok := attrs.Get("dynamic_key")
128+
assert.True(t, ok)
129+
assert.Equal(t, "changed", got2.(*dummy).Text)
130+
}

pkg/epp/framework/plugins/requestcontrol/dataproducer/inflightload/producer.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,14 @@ func (p *InFlightLoadProducer) DeleteEndpoint(endpointID string) {
454454
p.tokenTracker.delete(endpointID)
455455
}
456456

457+
func (p *InFlightLoadProducer) GetTokens(eid string) int64 {
458+
return p.tokenTracker.get(eid)
459+
}
460+
461+
func (p *InFlightLoadProducer) GetRequests(eid string) int64 {
462+
return p.requestTracker.get(eid)
463+
}
464+
457465
// concurrencyTracker manages thread-safe counters for inflight requests.
458466
type concurrencyTracker struct {
459467
mu sync.RWMutex

0 commit comments

Comments
 (0)