From afaa8f79eba8e49a17fd67f73c8ba9f3a5d917f5 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 29 Aug 2026 09:53:10 +0800 Subject: [PATCH] feat(core): add skipped fiber state for configuration gates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fiber 新增 SKIPPED 态与 Skip/Skipped 方法:门禁为假的插件在 Apply 之前 即被排除并释放其作用域 Context,为互斥实现(cache 与 cache_memory 等) 同时挂载由内核择一激活铺路。 --- backend/core/fiber.go | 24 +++++++++++++++++ backend/core/fiber_test.go | 54 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/backend/core/fiber.go b/backend/core/fiber.go index 635dcaae..899711b7 100644 --- a/backend/core/fiber.go +++ b/backend/core/fiber.go @@ -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() diff --git a/backend/core/fiber_test.go b/backend/core/fiber_test.go index b37e6ab7..273b56bf 100644 --- a/backend/core/fiber_test.go +++ b/backend/core/fiber_test.go @@ -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()) +}