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:
@@ -23,6 +23,7 @@ type Plugins []Plugin
|
|||||||
|
|
||||||
type PluginRepository interface {
|
type PluginRepository interface {
|
||||||
ResourceRepository
|
ResourceRepository
|
||||||
|
ClearErrors() error
|
||||||
CountAll(options ...QueryOptions) (int64, error)
|
CountAll(options ...QueryOptions) (int64, error)
|
||||||
Delete(id string) error
|
Delete(id string) error
|
||||||
Get(id string) (*Plugin, error)
|
Get(id string) (*Plugin, error)
|
||||||
|
|||||||
@@ -31,6 +31,14 @@ func (r *pluginRepository) isPermitted() bool {
|
|||||||
return user.IsAdmin
|
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) {
|
func (r *pluginRepository) CountAll(options ...model.QueryOptions) (int64, error) {
|
||||||
if !r.isPermitted() {
|
if !r.isPermitted() {
|
||||||
return 0, rest.ErrPermissionDenied
|
return 0, rest.ErrPermissionDenied
|
||||||
|
|||||||
@@ -175,6 +175,30 @@ var _ = Describe("PluginRepository", func() {
|
|||||||
Expect(err.Error()).To(ContainSubstring("ID cannot be empty"))
|
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() {
|
Describe("Regular User", func() {
|
||||||
|
|||||||
@@ -146,6 +146,12 @@ func (m *Manager) Start(ctx context.Context) error {
|
|||||||
|
|
||||||
log.Info(ctx, "Starting plugin manager", "folder", folder)
|
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
|
// Sync plugins folder with DB
|
||||||
if err := m.syncPlugins(ctx, folder); err != nil {
|
if err := m.syncPlugins(ctx, folder); err != nil {
|
||||||
log.Error(ctx, "Error syncing plugins with DB", err)
|
log.Error(ctx, "Error syncing plugins with DB", err)
|
||||||
|
|||||||
@@ -29,6 +29,20 @@ func (m *MockPluginRepo) SetError(err bool) {
|
|||||||
m.Err = err
|
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) {
|
func (m *MockPluginRepo) SetData(plugins model.Plugins) {
|
||||||
m.Data = make(map[string]*model.Plugin, len(plugins))
|
m.Data = make(map[string]*model.Plugin, len(plugins))
|
||||||
m.All = plugins
|
m.All = plugins
|
||||||
|
|||||||
Reference in New Issue
Block a user