Skip to content

Commit d0177ff

Browse files
committed
Add MayConsumePlugin interface for optional data key consumption
Some plugins optionally consume data keys (e.g. prefix cache scorer may use tokenized input but falls back to text). Previously there was no way to declare this intent - plugins would silently read undeclared keys or require a mandatory producer even when the key was optional. This change adds a MayConsumePlugin interface. At init time, if a plugin implements MayConsumePlugin and no producer exists for a declared key, the framework logs a warning instead of returning an error. The plugin is responsible for handling the absent case. Closes llm-d#1224 Signed-off-by: Alok Behera <alokbeherak061@gmail.com>
1 parent d25f9fe commit d0177ff

3 files changed

Lines changed: 131 additions & 0 deletions

File tree

pkg/epp/datalayer/data_graph.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,18 @@ func CreateMissingDataProducers(ctx context.Context, defaultProducerRegistry map
8181
}
8282
}
8383

84+
// Warn about optional keys (MayConsume) with no producer — no error, just a warning.
85+
for _, p := range handle.GetAllPlugins() {
86+
if mayConsumer, ok := p.(plugin.MayConsumePlugin); ok {
87+
for key := range mayConsumer.MayConsume() {
88+
if !producedKeys[key.String()] {
89+
logger.Info("Warning: optional data key has no producer, plugin will use fallback",
90+
"plugin", p.TypedName().Name, "dataKey", key.String())
91+
}
92+
}
93+
}
94+
}
95+
8496
// Build the set of keys that are consumed but not yet produced.
8597
missingKeys := make(map[string]string)
8698
for _, p := range handle.GetAllPlugins() {

pkg/epp/datalayer/data_graph_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,115 @@ func TestCreateMissingDataProducers(t *testing.T) {
429429
}
430430
}
431431

432+
// mockMayConsumerPlugin is a plugin that optionally consumes certain data keys.
433+
type mockMayConsumerPlugin struct {
434+
name string
435+
mayConsume map[fwkplugin.DataKey]any
436+
}
437+
438+
func (m *mockMayConsumerPlugin) TypedName() fwkplugin.TypedName {
439+
return fwkplugin.TypedName{Name: m.name, Type: "mock"}
440+
}
441+
442+
func (m *mockMayConsumerPlugin) MayConsume() map[fwkplugin.DataKey]any {
443+
return m.mayConsume
444+
}
445+
446+
// mockMixedConsumerPlugin is a plugin that has both required Consumes and optional MayConsume.
447+
// This models a real plugin like prefix cache scorer — requires prefix-match data,
448+
// but optionally uses tokenized input and falls back to raw text if unavailable.
449+
type mockMixedConsumerPlugin struct {
450+
name string
451+
consumes map[fwkplugin.DataKey]any
452+
mayConsume map[fwkplugin.DataKey]any
453+
}
454+
455+
func (m *mockMixedConsumerPlugin) TypedName() fwkplugin.TypedName {
456+
return fwkplugin.TypedName{Name: m.name, Type: "mock"}
457+
}
458+
459+
func (m *mockMixedConsumerPlugin) Consumes() map[fwkplugin.DataKey]any {
460+
return m.consumes
461+
}
462+
463+
func (m *mockMixedConsumerPlugin) MayConsume() map[fwkplugin.DataKey]any {
464+
return m.mayConsume
465+
}
466+
467+
func TestCreateMissingDataProducers_MayConsume(t *testing.T) {
468+
producerTypeA := "producer-a"
469+
keyA := fwkplugin.NewDataKey("keyA", producerTypeA)
470+
471+
producerAFactory := fwkplugin.FactoryFunc(func(name string, _ json.RawMessage, handle fwkplugin.Handle) (fwkplugin.Plugin, error) {
472+
return &mockDataProducerP{name: name, produces: map[fwkplugin.DataKey]any{keyA: nil}}, nil
473+
})
474+
475+
testCases := []struct {
476+
name string
477+
existingPlugins []fwkplugin.Plugin
478+
factoryRegistry map[string]fwkplugin.FactoryFunc
479+
wantErr bool
480+
}{
481+
{
482+
name: "MayConsume key with no producer — warning only, no error",
483+
existingPlugins: []fwkplugin.Plugin{
484+
&mockMayConsumerPlugin{
485+
name: "optional-consumer",
486+
mayConsume: map[fwkplugin.DataKey]any{keyA: nil},
487+
},
488+
},
489+
factoryRegistry: map[string]fwkplugin.FactoryFunc{},
490+
wantErr: false, // must NOT error
491+
},
492+
{
493+
name: "MayConsume key with a producer present — no warning, no error",
494+
existingPlugins: []fwkplugin.Plugin{
495+
&mockDataProducerP{name: "producer", produces: map[fwkplugin.DataKey]any{keyA: nil}},
496+
&mockMayConsumerPlugin{
497+
name: "optional-consumer",
498+
mayConsume: map[fwkplugin.DataKey]any{keyA: nil},
499+
},
500+
},
501+
factoryRegistry: map[string]fwkplugin.FactoryFunc{producerTypeA: producerAFactory},
502+
wantErr: false,
503+
},
504+
{
505+
// Models the real prefix cache scorer — it requires prefix-match data (Consumes)
506+
// but optionally uses tokenized input (MayConsume), falling back to raw text.
507+
// The required key has a producer. The optional key does not.
508+
// Result: no error. Warning logged for the missing optional key.
509+
name: "plugin with both Consumes and MayConsume — required key has producer, optional does not",
510+
existingPlugins: []fwkplugin.Plugin{
511+
&mockDataProducerP{name: "required-producer", produces: map[fwkplugin.DataKey]any{keyA: nil}},
512+
&mockMixedConsumerPlugin{
513+
name: "prefix-cache-scorer",
514+
consumes: map[fwkplugin.DataKey]any{keyA: nil},
515+
mayConsume: map[fwkplugin.DataKey]any{fwkplugin.NewDataKey("tokenized-input", "tokenizer"): nil},
516+
},
517+
},
518+
factoryRegistry: map[string]fwkplugin.FactoryFunc{},
519+
wantErr: false,
520+
},
521+
}
522+
523+
for _, tc := range testCases {
524+
t.Run(tc.name, func(t *testing.T) {
525+
handle := fwkplugin.NewEppHandle(context.Background(), func() []k8stypes.NamespacedName { return nil })
526+
for _, p := range tc.existingPlugins {
527+
handle.AddPlugin(p.TypedName().Name, p)
528+
}
529+
530+
err := CreateMissingDataProducers(context.Background(), map[string]string{}, tc.factoryRegistry, handle)
531+
532+
if tc.wantErr {
533+
assert.Error(t, err)
534+
return
535+
}
536+
assert.NoError(t, err)
537+
})
538+
}
539+
}
540+
432541
func assertTopologicalOrder(t *testing.T, dag map[string][]string, ordered []string) {
433542
t.Helper()
434543
positions := make(map[string]int)

pkg/epp/framework/interface/plugin/plugins.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ type ConsumerPlugin interface {
3232
Consumes() map[DataKey]any
3333
}
3434

35+
// MayConsumePlugin defines the interface for a plugin that optionally consumes data.
36+
// Unlike ConsumerPlugin, the framework will NOT error if no producer exists for these keys.
37+
// Instead it logs a warning at init time. The plugin must handle the case where this data is absent.
38+
type MayConsumePlugin interface {
39+
Plugin
40+
// MayConsume returns data keys the plugin can optionally consume.
41+
// The plugin must handle the case where this data is absent.
42+
MayConsume() map[DataKey]any
43+
}
44+
3545
// ProducerPlugin defines the interface for a producer.
3646
type ProducerPlugin interface {
3747
Plugin

0 commit comments

Comments
 (0)