feat(core): add skipped fiber state for configuration gates

Fiber 新增 SKIPPED 态与 Skip/Skipped 方法:门禁为假的插件在 Apply 之前
即被排除并释放其作用域 Context,为互斥实现(cache 与 cache_memory 等)
同时挂载由内核择一激活铺路。
This commit is contained in:
ryan
2026-08-29 09:53:10 +08:00
parent 39a81f7723
commit afaa8f79eb
2 changed files with 78 additions and 0 deletions
+24
View File
@@ -27,6 +27,10 @@ const (
// FiberDisposed indicates the plugin has been completely unmounted and its context disposed.
FiberDisposed FiberState = "DISPOSED"
// FiberSkipped indicates the plugin never activated because its configuration gate
// evaluated to false, so an alternative provider took over.
FiberSkipped FiberState = "SKIPPED"
)
// Fiber wraps a Plugin instance with a dedicated scoped Context and manages its
@@ -134,6 +138,26 @@ func (f *Fiber) Load() error {
return nil
}
// Skip transitions a pending plugin to FiberSkipped and releases its scoped Context.
// Active plugins are left untouched, which makes the call safe to replay on every
// reconciliation pass, including for plugins mounted after the first gate evaluation.
func (f *Fiber) Skip() error {
f.mu.Lock()
if f.state != FiberPending {
f.mu.Unlock()
return nil
}
f.state = FiberSkipped
f.mu.Unlock()
return f.ctx.Dispose()
}
// Skipped reports whether the plugin was excluded by its configuration gate.
func (f *Fiber) Skipped() bool {
return f.State() == FiberSkipped
}
// Unload tears down the plugin: ACTIVE -> UNLOADING -> DISPOSED.
func (f *Fiber) Unload() error {
f.mu.Lock()
+54
View File
@@ -5,6 +5,7 @@ package core_test
import (
"Wavelet/core"
"Wavelet/core/extpoints"
"context"
"reflect"
"testing"
@@ -108,3 +109,56 @@ func TestFiber_UnsatisfiedDependencyReturnsError(t *testing.T) {
assert.Error(t, err)
assert.Contains(t, err.Error(), "unsatisfied dependencies")
}
// gateConfig is the configuration section consumed by gatedPlugin below.
type gateConfig struct {
Enabled bool `config:"enabled" env:"GATE_ENABLED"`
}
// gatedPlugin implements core.ConfigGatedPlugin: two instances with opposite
// expectations model the mutually exclusive cache / cache_memory driver pair.
type gatedPlugin struct {
name string
enabled bool
applied bool
}
func (g *gatedPlugin) Name() string { return g.name }
func (g *gatedPlugin) Apply(_ *core.Context) error {
g.applied = true
return nil
}
func (g *gatedPlugin) DeclareConfig() []extpoints.ConfigBinding {
return []extpoints.ConfigBinding{{Prefix: "gate", Target: &gateConfig{}}}
}
func (g *gatedPlugin) ConfigEnabled(view extpoints.ConfigView) bool {
return view.Bool("gate.enabled", false) == g.enabled
}
func TestFiberSkipMovesToSkippedStateAndDisposesScope(t *testing.T) {
root := core.NewContext(nil)
plugin := &gatedPlugin{name: "cache", enabled: true}
f := core.NewFiber(root, plugin)
require.Equal(t, core.FiberPending, f.State())
require.NoError(t, f.Skip())
assert.Equal(t, core.FiberSkipped, f.State())
assert.True(t, f.Skipped())
assert.False(t, plugin.applied, "a skipped plugin must never reach Apply")
assert.NoError(t, f.Unload(), "unloading a skipped fiber is a no-op")
}
func TestFiberSkipIsInertForActiveFibers(t *testing.T) {
root := core.NewContext(nil)
f := core.NewFiber(root, &gatedPlugin{name: "cache", enabled: true})
require.NoError(t, f.Load())
require.NoError(t, f.Skip())
assert.Equal(t, core.FiberActive, f.State(), "Skip only applies to pending fibers")
assert.False(t, f.Skipped())
}