mirror of
https://github.com/navidrome/navidrome.git
synced 2025-07-16 16:41:16 +03:00
* fix: eliminate race condition in plugin system Added compilation waiting mechanism to prevent WASM plugins from being instantiated before their background compilation completes. This fixes the intermittent error 'source module must be compiled before instantiation' that occurred when tests or plugin usage happened before asynchronous compilation finished. Changes include: - Added manager reference to wasmBasePlugin for compilation synchronization - Modified all plugin adapter constructors to accept manager parameter - Updated getInstance() to wait for compilation before loading instances - Fixed runtime test to handle manually created plugins appropriately The race condition was caused by plugins trying to compile WASM modules synchronously during Load() calls while background compilation was still in progress. This change ensures proper coordination between the compilation and instantiation phases. * fix: add plugin-clean target to Makefile for easier plugin cleanup Signed-off-by: Deluan <deluan@navidrome.org> * refactor: reorder plugin constructor parameters and add nil safety Moved manager parameter to third position in pluginConstructor signature for\nbetter parameter ordering consistency.\n\nAlso added nil check for adapter creation to prevent registration of failed\nplugin adapters, which could lead to nil-pointer dereferences. Plugin\ncreation failures are now logged with context and gracefully skipped.\n\nChanges:\n- Reordered pluginConstructor parameters: manager moved before runtime\n- Updated all 4 adapter constructor signatures to match new order\n- Added nil safety check in registerPlugin to skip failed adapters\n- Updated runtime test to use new parameter order\n\nThis improves both code consistency and runtime safety by preventing\nnil adapters from being registered in the plugin manager. * fix: prevent concurrent WASM compilation race condition * refactor: remove unnecessary manager parameter from plugin constructors * fix: update parameter name in newWasmSchedulerCallback for consistency Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
83 lines
2.4 KiB
Go
83 lines
2.4 KiB
Go
package plugins
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model/id"
|
|
)
|
|
|
|
// LoaderFunc is a generic function type that loads a plugin instance.
|
|
type loaderFunc[S any, P any] func(ctx context.Context, loader P, path string) (S, error)
|
|
|
|
// wasmBasePlugin is a generic base implementation for WASM plugins.
|
|
// S is the service interface type and P is the plugin loader type.
|
|
type wasmBasePlugin[S any, P any] struct {
|
|
wasmPath string
|
|
id string
|
|
capability string
|
|
loader P
|
|
loadFunc loaderFunc[S, P]
|
|
}
|
|
|
|
func (w *wasmBasePlugin[S, P]) PluginID() string {
|
|
return w.id
|
|
}
|
|
|
|
func (w *wasmBasePlugin[S, P]) Instantiate(ctx context.Context) (any, func(), error) {
|
|
return w.getInstance(ctx, "<none>")
|
|
}
|
|
|
|
func (w *wasmBasePlugin[S, P]) serviceName() string {
|
|
return w.id + "_" + w.capability
|
|
}
|
|
|
|
// getInstance loads a new plugin instance and returns a cleanup function.
|
|
func (w *wasmBasePlugin[S, P]) getInstance(ctx context.Context, methodName string) (S, func(), error) {
|
|
start := time.Now()
|
|
// Add context metadata for tracing
|
|
ctx = log.NewContext(ctx, "capability", w.serviceName(), "method", methodName)
|
|
|
|
inst, err := w.loadFunc(ctx, w.loader, w.wasmPath)
|
|
if err != nil {
|
|
var zero S
|
|
return zero, func() {}, fmt.Errorf("wasmBasePlugin: failed to load instance for %s: %w", w.serviceName(), err)
|
|
}
|
|
// Add context metadata for tracing
|
|
ctx = log.NewContext(ctx, "instanceID", getInstanceID(inst))
|
|
log.Trace(ctx, "wasmBasePlugin: loaded instance", "elapsed", time.Since(start))
|
|
return inst, func() {
|
|
log.Trace(ctx, "wasmBasePlugin: finished using instance", "elapsed", time.Since(start))
|
|
if closer, ok := any(inst).(interface{ Close(context.Context) error }); ok {
|
|
_ = closer.Close(ctx)
|
|
}
|
|
}, nil
|
|
}
|
|
|
|
type wasmPlugin[S any] interface {
|
|
getInstance(ctx context.Context, methodName string) (S, func(), error)
|
|
}
|
|
|
|
type errorMapper interface {
|
|
mapError(err error) error
|
|
}
|
|
|
|
func callMethod[S any, R any](ctx context.Context, w wasmPlugin[S], methodName string, fn func(inst S) (R, error)) (R, error) {
|
|
// Add a unique call ID to the context for tracing
|
|
ctx = log.NewContext(ctx, "callID", id.NewRandom())
|
|
|
|
inst, done, err := w.getInstance(ctx, methodName)
|
|
var r R
|
|
if err != nil {
|
|
return r, err
|
|
}
|
|
defer done()
|
|
r, err = fn(inst)
|
|
if em, ok := any(w).(errorMapper); ok {
|
|
return r, em.mapError(err)
|
|
}
|
|
return r, err
|
|
}
|