navidrome/navidrome · error
schedule ID %q already exists
Error message
schedule ID %q already exists
What it means
HostScheduler.ScheduleOneTime registers a one-shot timer under a caller-supplied scheduleID. Before registering it locks the map and rejects any ID that is already present in s.schedules, returning this error instead of overwriting. Duplicate IDs are never silently replaced — the caller must pick a unique ID.
Source
Thrown at plugins/host_scheduler.go:72
func newSchedulerService(pluginName string, manager *Manager, sched scheduler.Scheduler) *schedulerServiceImpl {
return &schedulerServiceImpl{
pluginName: pluginName,
manager: manager,
scheduler: sched,
schedules: make(map[string]*scheduleEntry),
}
}
func (s *schedulerServiceImpl) ScheduleOneTime(ctx context.Context, delaySeconds int32, payload string, scheduleID string) (string, error) {
if scheduleID == "" {
scheduleID = id.NewRandom()
}
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.schedules[scheduleID]; exists {
return "", fmt.Errorf("schedule ID %q already exists", scheduleID)
}
capturedID := scheduleID
timer := timeAfterFunc(time.Duration(delaySeconds)*time.Second, func() {
s.invokeCallback(context.Background(), capturedID)
// Clean up the entry after firing
s.mu.Lock()
delete(s.schedules, capturedID)
s.mu.Unlock()
})
s.schedules[scheduleID] = &scheduleEntry{
pluginName: s.pluginName,
payload: payload,
isRecurring: false,
timer: timer,
}
View on GitHub (pinned to 4ed7494a32)
Solutions
- Generate unique IDs (e.g. fmt.Sprintf("job-%d", time.Now().UnixNano()) or a UUID) instead of hardcoded names.
- Check-and-skip: treat the error as 'already scheduled' if re-registration is expected and harmless.
- Cancel the existing schedule (CancelSchedule with the old ID) before re-registering the same ID.
- Namespace IDs per component/plugin feature to avoid collisions ("report-daily" vs "cleanup-daily").
- On restart, rebuild schedule state or use CancelAll/Shutdown before re-registering.
Example fix
// before
scheduler.ScheduleOneTime(ctx, "report", payload, 60)
// after
id := fmt.Sprintf("report-%d", time.Now().UnixNano())
scheduler.ScheduleOneTime(ctx, id, payload, 60) Defensive patterns
Strategy: try-catch
Validate before calling
// best-effort: cancel any prior registration with the same ID first
_ = scheduler.CancelSchedule(ctx, scheduleID) // no-op if absent
if id, err := scheduler.ScheduleOneTime(ctx, scheduleID, payload, delay); err != nil {
return err
} else {
_ = id
} Try / catch
id, err := scheduler.ScheduleOneTime(ctx, scheduleID, payload, delaySeconds)
if err != nil {
if strings.Contains(err.Error(), "already exists") {
return nil // idempotent: already scheduled
}
return err
} Prevention
- Derive schedule IDs from UUIDs or timestamps, not hardcoded strings.
- Namespace IDs per component (feature + purpose).
- Cancel old schedules on shutdown/reload before re-registering.
- Treat 'already exists' as idempotent success where re-registration is expected.
When it happens
Trigger: Calling ScheduleOneTime(ctx, scheduleID, ...) with a scheduleID that was previously registered (by ScheduleOneTime or ScheduleRecurring) and has not yet fired or been cancelled.
Common situations: Re-registering the same static ID on plugin restart without clearing prior state; retry logic calling ScheduleOneTime with the same ID after a transient failure elsewhere; two components in one plugin hardcoding the same schedule name; a one-time schedule that has not fired yet being scheduled again.
Related errors
- failed to schedule task: %w
- package missing manifest.json
- package missing plugin.wasm
- queue %q already exists
- parsing plugin users: %w
AI-assisted analysis of navidrome/navidrome@4ed7494a32 (2026-09-01).
Data as JSON: /api/errors/7ceba8342d33a7ee.
Report an issue: GitHub.