diff --git a/plugins/manager.go b/plugins/manager.go index 3b98fdc65..075e10a88 100644 --- a/plugins/manager.go +++ b/plugins/manager.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strings" "sync" + "sync/atomic" "time" extism "github.com/extism/go-sdk" @@ -38,6 +39,8 @@ type Manager struct { ctx context.Context cancel context.CancelFunc cache wazero.CompilationCache + stopped atomic.Bool // Set to true when Stop() is called + loadWg sync.WaitGroup // Tracks in-flight plugin load operations // File watcher fields (used when AutoReload is enabled) watcherEvents chan notify.EventInfo @@ -122,13 +125,21 @@ func (m *Manager) Start(ctx context.Context) error { // Stop shuts down the plugin manager and releases all resources. func (m *Manager) Stop() error { - // Stop file watcher first - m.stopWatcher() + // Mark as stopped first to prevent new operations + m.stopped.Store(true) + // Cancel context to signal all goroutines to stop if m.cancel != nil { m.cancel() } + // Stop file watcher + m.stopWatcher() + + // Wait for all in-flight plugin load operations to complete + // This is critical to avoid races with cache.Close() + m.loadWg.Wait() + m.mu.Lock() defer m.mu.Unlock() @@ -147,6 +158,7 @@ func (m *Manager) Stop() error { if err := m.cache.Close(context.Background()); err != nil { log.Error("Error closing wazero cache", err) } + m.cache = nil } return nil @@ -247,6 +259,20 @@ func (m *Manager) discoverPlugins(folder string) error { // loadPlugin loads a single plugin from a wasm file func (m *Manager) loadPlugin(name, wasmPath string) error { + // Check if manager is stopped (cache may be closed) + if m.stopped.Load() { + return fmt.Errorf("manager is stopped") + } + + // Track this operation so Stop() can wait for it to complete + m.loadWg.Add(1) + defer m.loadWg.Done() + + // Double-check after adding to WaitGroup (Stop may have been called between check and Add) + if m.stopped.Load() { + return fmt.Errorf("manager is stopped") + } + // Read wasm file wasmBytes, err := os.ReadFile(wasmPath) if err != nil { diff --git a/plugins/watcher.go b/plugins/watcher.go index 8ce1268c3..eef3b6901 100644 --- a/plugins/watcher.go +++ b/plugins/watcher.go @@ -108,6 +108,11 @@ func (m *Manager) handleWatcherEvent(event notify.EventInfo) { // processPluginEvent handles the actual plugin load/unload/reload after debouncing func (m *Manager) processPluginEvent(pluginName string, eventType notify.Event) { + // Don't process if manager is stopping/stopped (atomic check to avoid race with Stop()) + if m.stopped.Load() { + return + } + // Clean up debounce timer entry m.debounceMu.Lock() delete(m.debounceTimers, pluginName)