From 4631d0508227ed1d583bc44732b6d020015917cf Mon Sep 17 00:00:00 2001 From: Deluan Date: Tue, 23 Dec 2025 23:48:23 -0500 Subject: [PATCH] feat(scheduler): add scheduler callback schema and implementation for plugins Signed-off-by: Deluan --- plugins/README.md | 100 +++++++++++++++++++++ plugins/schemas/README.md | 9 +- plugins/schemas/scheduler_callback.yaml | 49 ++++++++++ plugins/testdata/fake-scheduler/main.go | 59 ++++-------- plugins/testdata/fake-scheduler/pdk.gen.go | 57 ++++++++++++ 5 files changed, 227 insertions(+), 47 deletions(-) create mode 100644 plugins/schemas/scheduler_callback.yaml create mode 100644 plugins/testdata/fake-scheduler/pdk.gen.go diff --git a/plugins/README.md b/plugins/README.md index a209ef17e..18d6384f7 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -200,6 +200,106 @@ func main() {} Scrobbler plugins are automatically discovered and used by Navidrome's PlayTracker alongside built-in scrobblers (Last.fm, ListenBrainz). +### Scheduler + +Allows plugins to schedule one-time or recurring tasks. Plugins that use the scheduler host service must export a callback function to receive scheduled events. + +| Function | Input | Output | Description | +|-------------------------|----------------------------------------------|-------------------|------------------------------------|| +| `nd_scheduler_callback` | `{schedule_id, payload, is_recurring}` | `{error?}` | Called when a scheduled task fires | + +#### Scheduler Callback Input + +```json +{ + "schedule_id": "string", + "payload": "string", + "is_recurring": true +} +``` + +- `schedule_id`: The unique identifier for the scheduled task +- `payload`: Data passed when the task was scheduled +- `is_recurring`: `true` for recurring schedules, `false` for one-time + +#### Scheduler Callback Output + +The output is optional on success. On error, return: + +```json +{ + "error": "error message" +} +``` + +#### Using the Scheduler Host Service + +To schedule tasks, plugins call these host functions (provided by Navidrome): + +| Host Function | Parameters | Description | +|------------------------------|-----------------------------------------------|------------------------------------------| +| `scheduler_scheduleonetime` | `delay_seconds, payload, schedule_id` | Schedule a one-time callback | +| `scheduler_schedulerecurring`| `cron_expression, payload, schedule_id` | Schedule a recurring callback | +| `scheduler_cancelschedule` | `schedule_id` | Cancel a scheduled task | + +#### Manifest Permissions + +Plugins using the scheduler must declare the permission in their manifest: + +```json +{ + "permissions": { + "scheduler": { + "reason": "Schedule periodic metadata refresh" + } + } +} +``` + +#### Example Scheduler Plugin + +```go +package main + +import ( + "github.com/extism/go-pdk" +) + +type SchedulerCallbackInput struct { + ScheduleId string `json:"schedule_id"` + Payload string `json:"payload"` + IsRecurring bool `json:"is_recurring"` +} + +type SchedulerCallbackOutput struct { + Error *string `json:"error,omitempty"` +} + +//go:wasmexport nd_scheduler_callback +func ndSchedulerCallback() int32 { + var input SchedulerCallbackInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(err) + return 1 + } + + // Handle the scheduled task based on payload + pdk.Log(pdk.LogInfo, "Task fired: " + input.ScheduleId) + + // Return success (empty output) + output := SchedulerCallbackOutput{} + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return 1 + } + return 0 +} + +func main() {} +``` + +To schedule a task from your plugin, use the generated SDK functions (see `plugins/host/go/nd_host_scheduler.go`). + ## Developing Plugins Plugins can be written in any language that compiles to WebAssembly. We recommend using the [Extism PDK](https://extism.org/docs/category/write-a-plug-in) for your language. diff --git a/plugins/schemas/README.md b/plugins/schemas/README.md index 712491582..8fdde7d5e 100644 --- a/plugins/schemas/README.md +++ b/plugins/schemas/README.md @@ -6,10 +6,11 @@ new plugins using the `xtp` CLI tool. ## Available Schemas -| Schema | Description | -|--------------------------------------------|-----------------------------------------------------------------| -| [metadata_agent.yaml](metadata_agent.yaml) | Metadata agent for retrieving artist/album information | -| [scrobbler.yaml](scrobbler.yaml) | Scrobbler capability for sending play data to external services | +| Schema | Description | +|------------------------------------------------------|-----------------------------------------------------------------| +| [metadata_agent.yaml](metadata_agent.yaml) | Metadata agent for retrieving artist/album information | +| [scheduler_callback.yaml](scheduler_callback.yaml) | Scheduler callback for plugins using the scheduler host service | +| [scrobbler.yaml](scrobbler.yaml) | Scrobbler capability for sending play data to external services | ## Prerequisites diff --git a/plugins/schemas/scheduler_callback.yaml b/plugins/schemas/scheduler_callback.yaml new file mode 100644 index 000000000..f2903ac66 --- /dev/null +++ b/plugins/schemas/scheduler_callback.yaml @@ -0,0 +1,49 @@ +version: v1-draft + +exports: + nd_scheduler_callback: + description: | + Called when a scheduled task fires. Plugins that use the scheduler host service + must export this function to receive callbacks when their scheduled tasks execute. + input: + $ref: "#/components/schemas/SchedulerCallbackInput" + contentType: application/json + output: + $ref: "#/components/schemas/SchedulerCallbackOutput" + contentType: application/json + +components: + schemas: + SchedulerCallbackInput: + description: Input provided to the scheduler callback when a scheduled task fires + properties: + schedule_id: + type: string + description: | + The unique identifier for this scheduled task. This is either the ID + provided when scheduling, or an auto-generated UUID if none was specified. + payload: + type: string + description: | + The payload data that was provided when the task was scheduled. + Can be used to pass context or parameters to the callback handler. + is_recurring: + type: boolean + description: | + True if this is a recurring schedule (created via ScheduleRecurring), + false if it's a one-time schedule (created via ScheduleOneTime). + required: + - schedule_id + - payload + - is_recurring + + SchedulerCallbackOutput: + description: Output from the scheduler callback + properties: + error: + type: string + nullable: true + description: | + Error message if the callback failed to process the scheduled task. + Empty or null indicates success. The error is logged but does not + affect the scheduling system. diff --git a/plugins/testdata/fake-scheduler/main.go b/plugins/testdata/fake-scheduler/main.go index 3ed2a1c2f..ef9d41863 100644 --- a/plugins/testdata/fake-scheduler/main.go +++ b/plugins/testdata/fake-scheduler/main.go @@ -1,12 +1,15 @@ // Fake scheduler plugin for Navidrome plugin system integration tests. +// This plugin was created based on the scheduler_callback.yaml XTP schema. // Build with: tinygo build -o ../fake-scheduler.wasm -target wasip1 -buildmode=c-shared . +// +// Note: pdk.gen.go contains the domain types from the XTP schema where your plugin will run. package main import ( "encoding/json" "strconv" - "github.com/extism/go-pdk" + pdk "github.com/extism/go-pdk" ) // Manifest types @@ -26,19 +29,7 @@ type SchedulerPermission struct { Reason string `json:"reason,omitempty"` } -// Scheduler callback input -type SchedulerCallbackInput struct { - ScheduleID string `json:"schedule_id"` - Payload string `json:"payload"` - IsRecurring bool `json:"is_recurring"` -} - -// Scheduler callback output -type SchedulerCallbackOutput struct { - Error string `json:"error,omitempty"` -} - -// CallRecord stores information about a callback that was received +// CallRecord stores information about a callback that was received (for testing) type CallRecord struct { ScheduleID string `json:"schedule_id"` Payload string `json:"payload"` @@ -73,51 +64,33 @@ func ndManifest() int32 { return 0 } -//go:wasmexport nd_scheduler_callback -func ndSchedulerCallback() int32 { - var input SchedulerCallbackInput - if err := pdk.InputJSON(&input); err != nil { - pdk.SetError(err) - return 1 - } - - // Payload is now a plain string, no decoding needed - payload := input.Payload - +// NdSchedulerCallback implements the scheduler callback logic. +// Called when a scheduled task fires. +// This function is called by the generated wrapper in pdk.gen.go. +func NdSchedulerCallback(input SchedulerCallbackInput) (SchedulerCallbackOutput, error) { // Check for configured error response errCfg, hasErr := pdk.GetConfig("callback_error") if hasErr && errCfg != "" { - output := SchedulerCallbackOutput{Error: errCfg} - if err := pdk.OutputJSON(output); err != nil { - pdk.SetError(err) - return 1 - } - return 0 + return SchedulerCallbackOutput{Error: &errCfg}, nil } // Track the callback totalCallCount++ - if record, exists := callRecords[input.ScheduleID]; exists { + if record, exists := callRecords[input.ScheduleId]; exists { record.CallCount++ } else { - callRecords[input.ScheduleID] = &CallRecord{ - ScheduleID: input.ScheduleID, - Payload: payload, + callRecords[input.ScheduleId] = &CallRecord{ + ScheduleID: input.ScheduleId, + Payload: input.Payload, IsRecurring: input.IsRecurring, CallCount: 1, } } // Log the callback for debugging - pdk.Log(pdk.LogInfo, "Scheduler callback received: "+input.ScheduleID+" payload="+payload) + pdk.Log(pdk.LogInfo, "Scheduler callback received: "+input.ScheduleId+" payload="+input.Payload) - // Success - output := SchedulerCallbackOutput{} - if err := pdk.OutputJSON(output); err != nil { - pdk.SetError(err) - return 1 - } - return 0 + return SchedulerCallbackOutput{}, nil } // Helper function to get call records (for testing) diff --git a/plugins/testdata/fake-scheduler/pdk.gen.go b/plugins/testdata/fake-scheduler/pdk.gen.go new file mode 100644 index 000000000..2b12df1f5 --- /dev/null +++ b/plugins/testdata/fake-scheduler/pdk.gen.go @@ -0,0 +1,57 @@ +// THIS FILE WAS GENERATED BY `xtp-go-bindgen`. DO NOT EDIT. +package main + +import ( + pdk "github.com/extism/go-pdk" +) + +//go:wasmexport nd_scheduler_callback +func _NdSchedulerCallback() int32 { + var err error + _ = err + pdk.Log(pdk.LogDebug, "NdSchedulerCallback: getting JSON input") + var input SchedulerCallbackInput + err = pdk.InputJSON(&input) + if err != nil { + pdk.SetError(err) + return -1 + } + + pdk.Log(pdk.LogDebug, "NdSchedulerCallback: calling implementation function") + output, err := NdSchedulerCallback(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + pdk.Log(pdk.LogDebug, "NdSchedulerCallback: setting JSON output") + err = pdk.OutputJSON(output) + if err != nil { + pdk.SetError(err) + return -1 + } + + pdk.Log(pdk.LogDebug, "NdSchedulerCallback: returning") + return 0 +} + +// Input provided to the scheduler callback when a scheduled task fires +type SchedulerCallbackInput struct { + // True if this is a recurring schedule (created via ScheduleRecurring), + // false if it's a one-time schedule (created via ScheduleOneTime). + IsRecurring bool `json:"is_recurring"` + // The payload data that was provided when the task was scheduled. + // Can be used to pass context or parameters to the callback handler. + Payload string `json:"payload"` + // The unique identifier for this scheduled task. This is either the ID + // provided when scheduling, or an auto-generated UUID if none was specified. + ScheduleId string `json:"schedule_id"` +} + +// Output from the scheduler callback +type SchedulerCallbackOutput struct { + // Error message if the callback failed to process the scheduled task. + // Empty or null indicates success. The error is logged but does not + // affect the scheduling system. + Error *string `json:"error,omitempty"` +}