Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 118 additions & 85 deletions cmd/epp/runner/runner.go

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions cmd/epp/runner/test_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,13 @@ func NewTestRunnerSetup(ctx context.Context, cfg *rest.Config, opts *runserver.O

if mockDataSource != nil {
mockType := mockDataSource.TypedName().Type
fwkplugin.Register(mockType, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
fwkplugin.Register(mockType, fwkplugin.StabilityStable, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
return mockDataSource, nil
})
defer delete(fwkplugin.Registry, mockType)
defer func() {
delete(fwkplugin.Registry, mockType)
delete(fwkplugin.RegistryMetadata, mockType)
}()
}

// Skip controller name validation in integration tests to avoid collisions
Expand Down
23 changes: 16 additions & 7 deletions pkg/epp/config/loader/configloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,16 @@ func InstantiateAndConfigure(
handle fwkplugin.Handle,
logger logr.Logger,
) (*config.Config, error) {
featureGates, err := loadFeatureConfig(rawConfig.FeatureGates)
if err != nil {
return nil, fmt.Errorf("failed to load feature gates: %w", err)
}

if err := validatePlugins(rawConfig.Plugins); err != nil {
return nil, fmt.Errorf("configuration validation failed: %w", err)
}

if err := instantiatePlugins(rawConfig.Plugins, handle); err != nil {
if err := instantiatePlugins(rawConfig.Plugins, handle, featureGates, logger); err != nil {
return nil, fmt.Errorf("plugin instantiation failed: %w", err)
}

Expand All @@ -172,11 +177,6 @@ func InstantiateAndConfigure(
return nil, fmt.Errorf("scheduler config build failed: %w", err)
}

featureGates, err := loadFeatureConfig(rawConfig.FeatureGates)
if err != nil {
return nil, fmt.Errorf("failed to load feature gates: %w", err)
}

dataConfig, err := buildDataLayerConfig(rawConfig.DataLayer, handle)
if err != nil {
return nil, fmt.Errorf("data layer config build failed: %w", err)
Expand Down Expand Up @@ -226,13 +226,22 @@ func decodeRawConfig(configBytes []byte) (*configapi.EndpointPickerConfig, error
return cfg, nil
}

func instantiatePlugins(configuredPlugins []configapi.PluginSpec, handle fwkplugin.Handle) error {
func instantiatePlugins(configuredPlugins []configapi.PluginSpec, handle fwkplugin.Handle, featureGates map[string]bool, logger logr.Logger) error {
orderedPlugins, err := buildPluginDAG(configuredPlugins, handle)
if err != nil {
return fmt.Errorf("failed to build plugin dependency graph: %w", err)
}

for _, spec := range orderedPlugins {
stability := fwkplugin.GetPluginStability(spec.Type)
if stability == fwkplugin.StabilityAlpha && !featureGates[fwkplugin.ExperimentalPluginsFeatureGate] {
return fmt.Errorf("plugin '%s' (type: '%s') has %s stability level, but feature gate '%s' is not enabled", spec.Name, spec.Type, stability, fwkplugin.ExperimentalPluginsFeatureGate)
}

if meta, ok := fwkplugin.RegistryMetadata[spec.Type]; ok && meta.Deprecated {
logger.Info("DEPRECATION warning: plugin is deprecated", "plugin", spec.Name, "type", spec.Type)
}

factory := fwkplugin.Registry[spec.Type]
plugin, err := factory(spec.Name, fwkplugin.StrictDecoder(spec.Parameters), handle)
if err != nil {
Expand Down
108 changes: 79 additions & 29 deletions pkg/epp/config/loader/configloader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ func TestLoadRawConfiguration(t *testing.T) {
// Register known feature gates for validation.
RegisterFeatureGate(testFeatureGate, true)
RegisterFeatureGate(flowcontrol.FeatureGate, false)
RegisterFeatureGate(fwkplugin.ExperimentalPluginsFeatureGate, false)

queueScorerWeight := 2.0
kvCacheUtilizationScorerWeight := 2.0
Expand Down Expand Up @@ -129,8 +130,9 @@ func TestLoadRawConfiguration(t *testing.T) {
},
},
wantFeatures: map[string]bool{
testFeatureGate: true,
flowcontrol.FeatureGate: true,
testFeatureGate: true,
flowcontrol.FeatureGate: true,
fwkplugin.ExperimentalPluginsFeatureGate: false,
},
wantErr: false,
deprecated: false,
Expand Down Expand Up @@ -188,8 +190,9 @@ func TestLoadRawConfiguration(t *testing.T) {
},
},
wantFeatures: map[string]bool{
testFeatureGate: false,
flowcontrol.FeatureGate: false,
testFeatureGate: false,
flowcontrol.FeatureGate: false,
fwkplugin.ExperimentalPluginsFeatureGate: false,
},
wantErr: false,
deprecated: false,
Expand Down Expand Up @@ -256,8 +259,9 @@ func TestLoadRawConfiguration(t *testing.T) {
},
},
wantFeatures: map[string]bool{
testFeatureGate: true,
flowcontrol.FeatureGate: false,
testFeatureGate: true,
flowcontrol.FeatureGate: false,
fwkplugin.ExperimentalPluginsFeatureGate: false,
},
wantErr: false,
deprecated: false,
Expand Down Expand Up @@ -429,7 +433,8 @@ func TestPluginsWithDependencies(t *testing.T) {

// 2. Instantiate
handle := testutils.NewTestHandle(context.Background())
err = instantiatePlugins(rawConfig.Plugins, handle)
featureGates, _ := loadFeatureConfig(rawConfig.FeatureGates)
err = instantiatePlugins(rawConfig.Plugins, handle, featureGates, logger)
if tc.wantErr {
require.Error(t, err, "Expected instantiatePlugins to fail")
return
Expand Down Expand Up @@ -1008,7 +1013,7 @@ func registerTestPlugins(t *testing.T) {

// Helper to generate simple factories.
register := func(name string, factory fwkplugin.FactoryFunc) {
fwkplugin.Register(name, factory)
fwkplugin.Register(name, fwkplugin.StabilityStable, factory)
}

mockFactory := func(tType string) fwkplugin.FactoryFunc {
Expand All @@ -1020,7 +1025,7 @@ func registerTestPlugins(t *testing.T) {
// Register standard test mocks.
register(testPluginType, mockFactory(testPluginType))

fwkplugin.Register(testScorerType, func(name string, params *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
fwkplugin.Register(testScorerType, fwkplugin.StabilityStable, func(name string, params *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
// Attempt to unmarshal to trigger errors for invalid JSON in tests.
if params != nil {
var p struct {
Expand All @@ -1033,19 +1038,19 @@ func registerTestPlugins(t *testing.T) {
return &mockScorer{mockPlugin{t: fwkplugin.TypedName{Name: name, Type: testScorerType}}}, nil
})

fwkplugin.Register("utilization-detector", func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
fwkplugin.Register("utilization-detector", fwkplugin.StabilityStable, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
return &mockSaturationDetector{mockPlugin{t: fwkplugin.TypedName{Name: name, Type: "utilization-detector"}}}, nil
})

fwkplugin.Register(testPickerType, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
fwkplugin.Register(testPickerType, fwkplugin.StabilityStable, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
return &mockPicker{mockPlugin{t: fwkplugin.TypedName{Name: name, Type: testPickerType}}}, nil
})

fwkplugin.Register(testProfileHandler, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
fwkplugin.Register(testProfileHandler, fwkplugin.StabilityStable, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
return &mockHandler{mockPlugin{t: fwkplugin.TypedName{Name: name, Type: testProfileHandler}}}, nil
})

fwkplugin.RegisterWithPluginDependencies(testWithDependencies,
fwkplugin.RegisterWithPluginDependencies(testWithDependencies, fwkplugin.StabilityStable,
func(name string, decoder *json.Decoder, handle fwkplugin.Handle) (fwkplugin.Plugin, error) {
rawCfg, err := mockWithDependenciesConfigParser(decoder, handle)
if err != nil {
Expand All @@ -1064,7 +1069,7 @@ func registerTestPlugins(t *testing.T) {
}, mockWithDependenciesConfigParser,
)

fwkplugin.RegisterWithPluginDependencies(testWithNestedDependencies,
fwkplugin.RegisterWithPluginDependencies(testWithNestedDependencies, fwkplugin.StabilityStable,
func(name string, decoder *json.Decoder, handle fwkplugin.Handle) (fwkplugin.Plugin, error) {
rawCfg, err := mockWithNestedDependenciesConfigParser(decoder, handle)
if err != nil {
Expand Down Expand Up @@ -1093,38 +1098,38 @@ func registerTestPlugins(t *testing.T) {
}, mockWithNestedDependenciesConfigParser,
)

fwkplugin.Register(testSourceType, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
fwkplugin.Register(testSourceType, fwkplugin.StabilityStable, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
return &mockSource{mockPlugin{t: fwkplugin.TypedName{Name: name, Type: testSourceType}}}, nil
})

fwkplugin.Register(testExtractorType, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
fwkplugin.Register(testExtractorType, fwkplugin.StabilityStable, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
return &mockExtractor{mockPlugin{t: fwkplugin.TypedName{Name: name, Type: testExtractorType}}}, nil
})

fwkplugin.Register(globalstrict.GlobalStrictFairnessPolicyType, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
fwkplugin.Register(globalstrict.GlobalStrictFairnessPolicyType, fwkplugin.StabilityStable, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
return &fwkfcmocks.MockFairnessPolicy{
TypedNameV: fwkplugin.TypedName{Name: name, Type: globalstrict.GlobalStrictFairnessPolicyType},
}, nil
})
fwkplugin.Register(fcfs.FCFSOrderingPolicyType, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
fwkplugin.Register(fcfs.FCFSOrderingPolicyType, fwkplugin.StabilityStable, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
return &fwkfcmocks.MockOrderingPolicy{
TypedNameV: fwkplugin.TypedName{Name: name, Type: fcfs.FCFSOrderingPolicyType},
}, nil
})

// Ensure system defaults are registered too.
fwkplugin.Register(maxscore.MaxScorePickerType, maxscore.MaxScorePickerFactory)
fwkplugin.Register(single.SingleProfileHandlerType, single.SingleProfileHandlerFactory)
fwkplugin.Register(openai.OpenAIParserType, openai.OpenAIParserPluginFactory)
fwkplugin.Register(vertexai.VertexAIParserType, vertexai.VertexAIParserPluginFactory)
fwkplugin.Register(anthropic.AnthropicParserType, anthropic.AnthropicParserPluginFactory)
fwkplugin.Register(vllmhttp.VllmHTTPParserType, vllmhttp.VllmHTTPParserPluginFactory)
fwkplugin.Register(usagelimits.StaticUsageLimitPolicyType, usagelimits.StaticPolicyFactory)
fwkplugin.Register(prefix.PrefixCacheScorerPluginType, prefix.PrefixCachePluginFactory)
fwkplugin.Register(reqdataprodprefix.ApproxPrefixCachePluginType, reqdataprodprefix.ApproxPrefixCacheFactory)
fwkplugin.Register(maxscore.MaxScorePickerType, fwkplugin.StabilityStable, maxscore.MaxScorePickerFactory)
fwkplugin.Register(single.SingleProfileHandlerType, fwkplugin.StabilityStable, single.SingleProfileHandlerFactory)
fwkplugin.Register(openai.OpenAIParserType, fwkplugin.StabilityStable, openai.OpenAIParserPluginFactory)
fwkplugin.Register(vertexai.VertexAIParserType, fwkplugin.StabilityStable, vertexai.VertexAIParserPluginFactory)
fwkplugin.Register(anthropic.AnthropicParserType, fwkplugin.StabilityStable, anthropic.AnthropicParserPluginFactory)
fwkplugin.Register(vllmhttp.VllmHTTPParserType, fwkplugin.StabilityStable, vllmhttp.VllmHTTPParserPluginFactory)
fwkplugin.Register(usagelimits.StaticUsageLimitPolicyType, fwkplugin.StabilityStable, usagelimits.StaticPolicyFactory)
fwkplugin.Register(prefix.PrefixCacheScorerPluginType, fwkplugin.StabilityStable, prefix.PrefixCachePluginFactory)
fwkplugin.Register(reqdataprodprefix.ApproxPrefixCachePluginType, fwkplugin.StabilityStable, reqdataprodprefix.ApproxPrefixCacheFactory)
// Datalayer plugins are now defaults; register their real factories.
fwkplugin.Register(sourcemetrics.MetricsDataSourceType, sourcemetrics.MetricsDataSourceFactory)
fwkplugin.Register(extractormetrics.MetricsExtractorType, extractormetrics.CoreMetricsExtractorFactory)
fwkplugin.Register(sourcemetrics.MetricsDataSourceType, fwkplugin.StabilityStable, sourcemetrics.MetricsDataSourceFactory)
fwkplugin.Register(extractormetrics.MetricsExtractorType, fwkplugin.StabilityStable, extractormetrics.CoreMetricsExtractorFactory)
}

func TestValidateSaturationDetector(t *testing.T) {
Expand Down Expand Up @@ -1266,3 +1271,48 @@ func TestFilterExecutionOrderFromYAML(t *testing.T) {
require.Equal(t, []string{"filter-A", "filter-B", "filter-C", "scorer-X", "scorer-Y", "maxScorePicker"}, pluginRefs,
"Plugins slice must preserve YAML declaration order")
}

func TestExperimentalPluginsFeatureGate(t *testing.T) {
RegisterFeatureGate(fwkplugin.ExperimentalPluginsFeatureGate, false)

const alphaPluginType = "test-alpha-plugin"
fwkplugin.Register(alphaPluginType, fwkplugin.StabilityAlpha, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
return &mockPlugin{t: fwkplugin.TypedName{Name: name, Type: alphaPluginType}}, nil
})
fwkplugin.Register(single.SingleProfileHandlerType, fwkplugin.StabilityStable, single.SingleProfileHandlerFactory)
fwkplugin.Register("utilization-detector", fwkplugin.StabilityStable, func(name string, _ *json.Decoder, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
return &mockSaturationDetector{mockPlugin{t: fwkplugin.TypedName{Name: name, Type: "utilization-detector"}}}, nil
})

handle := testutils.NewTestHandle(context.Background())
logger := logging.NewTestLogger()

// 1. Without feature gate enabled -> should fail
rawConfigNoGate := &configapi.EndpointPickerConfig{
Plugins: []configapi.PluginSpec{
{Name: "alpha-inst", Type: alphaPluginType},
},
}
_, err := InstantiateAndConfigure(rawConfigNoGate, handle, logger)
require.Error(t, err)
require.Contains(t, err.Error(), "has Alpha stability level, but feature gate 'experimentalPlugins' is not enabled")

// 2. With feature gate enabled -> should succeed (given required profile handler)
rawConfigWithGate := &configapi.EndpointPickerConfig{
FeatureGates: configapi.FeatureGates{fwkplugin.ExperimentalPluginsFeatureGate},
Plugins: []configapi.PluginSpec{
{Name: "alpha-inst", Type: alphaPluginType},
{Name: "ph", Type: single.SingleProfileHandlerType},
{Name: "sat", Type: "utilization-detector"},
},
SchedulingProfiles: []configapi.SchedulingProfile{
{Name: "default", Plugins: []configapi.SchedulingPlugin{{PluginRef: "ph"}}},
},
FlowControl: &configapi.FlowControlConfig{
SaturationDetector: &configapi.SaturationDetectorConfig{PluginRef: "sat"},
},
}

_, err = InstantiateAndConfigure(rawConfigWithGate, testutils.NewTestHandle(context.Background()), logger)
require.NoError(t, err)
}
61 changes: 49 additions & 12 deletions pkg/epp/framework/interface/plugin/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,30 +45,67 @@ func StrictDecoder(raw json.RawMessage) *json.Decoder {
return dec
}

// Register is a static function that can be called to register plugin factory functions.
func Register(pluginType string, factory FactoryFunc) {
// Register registers a factory function for the given plugin type along with its stability level.
func Register(pluginType string, stability StabilityLevel, factory FactoryFunc) {
Registry[pluginType] = factory
RegistryMetadata[pluginType] = PluginMetadata{
Type: pluginType,
Stability: stability,
}
}

// RegisterAsDefaultProducer registers a factory for the given plugin type and records it as the
// default producer for the given data key. Only one producer may be registered as default per key.
// Out-of-tree projects that extend the EPP can call this to make their producers eligible for
// auto-configuration alongside in-tree producers.
func RegisterAsDefaultProducer(pluginType string, factory FactoryFunc, key DataKey) {
Register(pluginType, factory)
// RegisterAsDefaultProducer registers a factory for the given plugin type with an explicit
// stability level and records it as the default producer for the given data key.
func RegisterAsDefaultProducer(pluginType string, stability StabilityLevel, factory FactoryFunc, key DataKey) {
Register(pluginType, stability, factory)
DefaultProducerRegistry[key.String()] = pluginType
}

// RegisterWithPluginDependencies registers a factory for the given plugin type and records it as dependent on
// other plugins referenced in the configuration struct returned by the plugin's configuration parser function.
func RegisterWithPluginDependencies(pluginType string, factory FactoryFunc, parser ConfigParserFunc) {
Register(pluginType, factory)
// RegisterWithPluginDependencies registers a factory for the given plugin type with an explicit
// stability level and records it as dependent on other plugins referenced in the configuration struct
// returned by the plugin's configuration parser function.
func RegisterWithPluginDependencies(pluginType string, stability StabilityLevel, factory FactoryFunc, parser ConfigParserFunc) {
Register(pluginType, stability, factory)
PluginsWithPluginDependencies[pluginType] = parser
}

// RegisterDeprecated registers a plugin factory function with stability and deprecation metadata.
func RegisterDeprecated(pluginType string, stability StabilityLevel, factory FactoryFunc, deprecatedIn, scheduledRemovalIn, replacementType string) {
Register(pluginType, stability, factory)
meta := RegistryMetadata[pluginType]
meta.Deprecated = true
meta.DeprecatedIn = deprecatedIn
meta.ScheduledRemovalIn = scheduledRemovalIn
meta.ReplacementType = replacementType
RegistryMetadata[pluginType] = meta
}

// RegisterDeprecatedWithPluginDependencies registers a plugin with dependencies and deprecation metadata.
func RegisterDeprecatedWithPluginDependencies(pluginType string, stability StabilityLevel, factory FactoryFunc, parser ConfigParserFunc, deprecatedIn, scheduledRemovalIn, replacementType string) {
RegisterWithPluginDependencies(pluginType, stability, factory, parser)
meta := RegistryMetadata[pluginType]
meta.Deprecated = true
meta.DeprecatedIn = deprecatedIn
meta.ScheduledRemovalIn = scheduledRemovalIn
meta.ReplacementType = replacementType
RegistryMetadata[pluginType] = meta
}

// GetPluginStability looks up the stability level of a registered plugin type.
// Returns StabilityStable by default for unregistered plugins.
func GetPluginStability(pluginType string) StabilityLevel {
if meta, ok := RegistryMetadata[pluginType]; ok {
return meta.Stability
}
return StabilityStable
}

// Registry is a mapping from plugin type to Factory function.
var Registry = map[string]FactoryFunc{}

// RegistryMetadata maps a plugin type to its PluginMetadata.
var RegistryMetadata = map[string]PluginMetadata{}

// DefaultProducerRegistry maps a data key to the default producer plugin name (same as type).
// Populated via RegisterAsDefaultProducer.
var DefaultProducerRegistry = map[string]string{}
Expand Down
Loading
Loading