fix(plugins): clear plugin errors on startup to allow retrying

Plugins that entered an error state (e.g., incompatible with the
Navidrome version) would remain in that state across restarts, blocking
the user from retrying. This adds a ClearErrors method to
PluginRepository that resets the last_error field on all plugins, and
calls it during plugin manager startup before syncing and loading.

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan
2026-03-02 08:56:56 -05:00
parent d004f99f8f
commit 27a83547f7
5 changed files with 53 additions and 0 deletions
+1
View File
@@ -23,6 +23,7 @@ type Plugins []Plugin
type PluginRepository interface {
ResourceRepository
ClearErrors() error
CountAll(options ...QueryOptions) (int64, error)
Delete(id string) error
Get(id string) (*Plugin, error)
+8
View File
@@ -31,6 +31,14 @@ func (r *pluginRepository) isPermitted() bool {
return user.IsAdmin
}
func (r *pluginRepository) ClearErrors() error {
if !r.isPermitted() {
return rest.ErrPermissionDenied
}
_, err := r.db.NewQuery("UPDATE plugin SET last_error = '' WHERE last_error != ''").Execute()
return err
}
func (r *pluginRepository) CountAll(options ...model.QueryOptions) (int64, error) {
if !r.isPermitted() {
return 0, rest.ErrPermissionDenied
+24
View File
@@ -175,6 +175,30 @@ var _ = Describe("PluginRepository", func() {
Expect(err.Error()).To(ContainSubstring("ID cannot be empty"))
})
})
Describe("ClearErrors", func() {
It("clears last_error on all plugins with errors", func() {
_ = repo.Put(&model.Plugin{ID: "ok-plugin", Path: "/plugins/ok.wasm", Manifest: "{}", SHA256: "h1"})
_ = repo.Put(&model.Plugin{ID: "err-plugin-1", Path: "/plugins/e1.wasm", Manifest: "{}", SHA256: "h2", LastError: "incompatible version"})
_ = repo.Put(&model.Plugin{ID: "err-plugin-2", Path: "/plugins/e2.wasm", Manifest: "{}", SHA256: "h3", LastError: "missing export"})
err := repo.ClearErrors()
Expect(err).To(BeNil())
all, err := repo.GetAll()
Expect(err).To(BeNil())
for _, p := range all {
Expect(p.LastError).To(BeEmpty(), "plugin %s should have no error", p.ID)
}
})
It("succeeds when no plugins have errors", func() {
_ = repo.Put(&model.Plugin{ID: "clean-plugin", Path: "/plugins/c.wasm", Manifest: "{}", SHA256: "h1"})
err := repo.ClearErrors()
Expect(err).To(BeNil())
})
})
})
Describe("Regular User", func() {
+6
View File
@@ -146,6 +146,12 @@ func (m *Manager) Start(ctx context.Context) error {
log.Info(ctx, "Starting plugin manager", "folder", folder)
// Clear previous error states so plugins can be retried on restart
adminCtx := adminContext(ctx)
if err := m.ds.Plugin(adminCtx).ClearErrors(); err != nil {
log.Error(ctx, "Error clearing plugin errors", err)
}
// Sync plugins folder with DB
if err := m.syncPlugins(ctx, folder); err != nil {
log.Error(ctx, "Error syncing plugins with DB", err)
+14
View File
@@ -29,6 +29,20 @@ func (m *MockPluginRepo) SetError(err bool) {
m.Err = err
}
func (m *MockPluginRepo) ClearErrors() error {
if m.Err {
return errors.New("unexpected error")
}
for i := range m.All {
m.All[i].LastError = ""
}
for k, p := range m.Data {
p.LastError = ""
m.Data[k] = p
}
return nil
}
func (m *MockPluginRepo) SetData(plugins model.Plugins) {
m.Data = make(map[string]*model.Plugin, len(plugins))
m.All = plugins