mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
feat: add support for experimental WebAssembly threads
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
a2ace6e84e
commit
c6fe02a49c
@ -132,6 +132,27 @@ Every plugin must include a `manifest.json` file. Example:
|
|||||||
|
|
||||||
**Required fields:** `name`, `author`, `version`
|
**Required fields:** `name`, `author`, `version`
|
||||||
|
|
||||||
|
#### Experimental Features
|
||||||
|
|
||||||
|
Plugins can opt-in to experimental WebAssembly features that may change or be removed in future versions. Currently supported:
|
||||||
|
|
||||||
|
- **`threads`** – Enables WebAssembly threads support (for plugins compiled with multi-threading)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Threaded Plugin",
|
||||||
|
"author": "Author Name",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"experimental": {
|
||||||
|
"threads": {
|
||||||
|
"reason": "Required for concurrent audio processing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** Experimental features may have compatibility or performance implications. Use only when necessary.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Capabilities
|
## Capabilities
|
||||||
|
|||||||
@ -12,6 +12,8 @@ import (
|
|||||||
"github.com/navidrome/navidrome/plugins/host"
|
"github.com/navidrome/navidrome/plugins/host"
|
||||||
"github.com/navidrome/navidrome/scheduler"
|
"github.com/navidrome/navidrome/scheduler"
|
||||||
"github.com/tetratelabs/wazero"
|
"github.com/tetratelabs/wazero"
|
||||||
|
"github.com/tetratelabs/wazero/api"
|
||||||
|
"github.com/tetratelabs/wazero/experimental"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -268,11 +270,19 @@ func (m *Manager) loadPluginWithConfig(name, ndpPath, configJSON string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compile the plugin with all host functions
|
// Compile the plugin with all host functions
|
||||||
|
runtimeConfig := wazero.NewRuntimeConfig().
|
||||||
|
WithCompilationCache(m.cache).
|
||||||
|
WithCloseOnContextDone(true)
|
||||||
|
|
||||||
|
// Enable experimental threads if requested in manifest
|
||||||
|
if pkg.Manifest.HasExperimentalThreads() {
|
||||||
|
runtimeConfig = runtimeConfig.WithCoreFeatures(api.CoreFeaturesV2 | experimental.CoreFeaturesThreads)
|
||||||
|
log.Debug(m.ctx, "Enabling experimental threads support", "plugin", name)
|
||||||
|
}
|
||||||
|
|
||||||
extismConfig := extism.PluginConfig{
|
extismConfig := extism.PluginConfig{
|
||||||
EnableWasi: true,
|
EnableWasi: true,
|
||||||
RuntimeConfig: wazero.NewRuntimeConfig().
|
RuntimeConfig: runtimeConfig,
|
||||||
WithCompilationCache(m.cache).
|
|
||||||
WithCloseOnContextDone(true),
|
|
||||||
}
|
}
|
||||||
compiled, err := extism.NewCompiledPlugin(m.ctx, pluginManifest, extismConfig, hostFunctions)
|
compiled, err := extism.NewCompiledPlugin(m.ctx, pluginManifest, extismConfig, hostFunctions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -33,9 +33,33 @@
|
|||||||
},
|
},
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"$ref": "#/$defs/Permissions"
|
"$ref": "#/$defs/Permissions"
|
||||||
|
},
|
||||||
|
"experimental": {
|
||||||
|
"$ref": "#/$defs/Experimental"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"$defs": {
|
"$defs": {
|
||||||
|
"Experimental": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Experimental features that may change or be removed in future versions",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"threads": {
|
||||||
|
"$ref": "#/$defs/ThreadsFeature"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ThreadsFeature": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Enable experimental WebAssembly threads support",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"reason": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Explanation for why threads support is needed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Permissions": {
|
"Permissions": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Permissions required by the plugin",
|
"description": "Permissions required by the plugin",
|
||||||
|
|||||||
@ -11,5 +11,7 @@ func (m *Manifest) AllowedHosts() []string {
|
|||||||
return m.Permissions.Http.AllowedHosts
|
return m.Permissions.Http.AllowedHosts
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: ConfigPermission is defined in the schema but not currently enforced.
|
// HasExperimentalThreads returns true if the manifest requests experimental threads support.
|
||||||
// Plugins always receive their config section. Implement permission checking or remove from schema.
|
func (m *Manifest) HasExperimentalThreads() bool {
|
||||||
|
return m.Experimental != nil && m.Experimental.Threads != nil
|
||||||
|
}
|
||||||
|
|||||||
@ -23,6 +23,12 @@ type ConfigPermission struct {
|
|||||||
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
|
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Experimental features that may change or be removed in future versions
|
||||||
|
type Experimental struct {
|
||||||
|
// Threads corresponds to the JSON schema field "threads".
|
||||||
|
Threads *ThreadsFeature `json:"threads,omitempty" yaml:"threads,omitempty" mapstructure:"threads,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// HTTP access permissions for a plugin
|
// HTTP access permissions for a plugin
|
||||||
type HTTPPermission struct {
|
type HTTPPermission struct {
|
||||||
// List of allowed host patterns for HTTP requests (e.g., 'api.example.com',
|
// List of allowed host patterns for HTTP requests (e.g., 'api.example.com',
|
||||||
@ -78,6 +84,9 @@ type Manifest struct {
|
|||||||
// A brief description of what the plugin does
|
// A brief description of what the plugin does
|
||||||
Description *string `json:"description,omitempty" yaml:"description,omitempty" mapstructure:"description,omitempty"`
|
Description *string `json:"description,omitempty" yaml:"description,omitempty" mapstructure:"description,omitempty"`
|
||||||
|
|
||||||
|
// Experimental corresponds to the JSON schema field "experimental".
|
||||||
|
Experimental *Experimental `json:"experimental,omitempty" yaml:"experimental,omitempty" mapstructure:"experimental,omitempty"`
|
||||||
|
|
||||||
// The display name of the plugin
|
// The display name of the plugin
|
||||||
Name string `json:"name" yaml:"name" mapstructure:"name"`
|
Name string `json:"name" yaml:"name" mapstructure:"name"`
|
||||||
|
|
||||||
@ -187,6 +196,12 @@ func (j *SubsonicAPIPermission) UnmarshalJSON(value []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable experimental WebAssembly threads support
|
||||||
|
type ThreadsFeature struct {
|
||||||
|
// Explanation for why threads support is needed
|
||||||
|
Reason *string `json:"reason,omitempty" yaml:"reason,omitempty" mapstructure:"reason,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// WebSocket service permissions for establishing WebSocket connections
|
// WebSocket service permissions for establishing WebSocket connections
|
||||||
type WebSocketPermission struct {
|
type WebSocketPermission struct {
|
||||||
// List of allowed host patterns for WebSocket connections (e.g.,
|
// List of allowed host patterns for WebSocket connections (e.g.,
|
||||||
|
|||||||
@ -145,4 +145,75 @@ var _ = Describe("Manifest", func() {
|
|||||||
Expect(hosts).To(Equal([]string{"api.example.com", "*.spotify.com"}))
|
Expect(hosts).To(Equal([]string{"api.example.com", "*.spotify.com"}))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Describe("HasExperimentalThreads", func() {
|
||||||
|
It("returns false when no experimental section", func() {
|
||||||
|
m := &Manifest{}
|
||||||
|
Expect(m.HasExperimentalThreads()).To(BeFalse())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns false when experimental section has no threads", func() {
|
||||||
|
m := &Manifest{
|
||||||
|
Experimental: &Experimental{},
|
||||||
|
}
|
||||||
|
Expect(m.HasExperimentalThreads()).To(BeFalse())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns true when threads feature is present", func() {
|
||||||
|
m := &Manifest{
|
||||||
|
Experimental: &Experimental{
|
||||||
|
Threads: &ThreadsFeature{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Expect(m.HasExperimentalThreads()).To(BeTrue())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns true when threads feature has a reason", func() {
|
||||||
|
reason := "Required for concurrent processing"
|
||||||
|
m := &Manifest{
|
||||||
|
Experimental: &Experimental{
|
||||||
|
Threads: &ThreadsFeature{
|
||||||
|
Reason: &reason,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Expect(m.HasExperimentalThreads()).To(BeTrue())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("parses experimental.threads from JSON", func() {
|
||||||
|
data := []byte(`{
|
||||||
|
"name": "Threaded Plugin",
|
||||||
|
"author": "Test Author",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"experimental": {
|
||||||
|
"threads": {
|
||||||
|
"reason": "To use multi-threaded WASM module"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
var m Manifest
|
||||||
|
err := json.Unmarshal(data, &m)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(m.HasExperimentalThreads()).To(BeTrue())
|
||||||
|
Expect(m.Experimental.Threads.Reason).ToNot(BeNil())
|
||||||
|
Expect(*m.Experimental.Threads.Reason).To(Equal("To use multi-threaded WASM module"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("parses experimental.threads without reason from JSON", func() {
|
||||||
|
data := []byte(`{
|
||||||
|
"name": "Threaded Plugin",
|
||||||
|
"author": "Test Author",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"experimental": {
|
||||||
|
"threads": {}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
var m Manifest
|
||||||
|
err := json.Unmarshal(data, &m)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(m.HasExperimentalThreads()).To(BeTrue())
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user