feat(plugins): implement asynchronous config reload after plugin deletion

This commit is contained in:
LTbinglingfeng
2026-06-15 15:35:55 +08:00
parent bbef8da454
commit 3b0cc913ec
3 changed files with 50 additions and 9 deletions
@@ -20,6 +20,7 @@ import (
"github.com/router-for-me/CLIProxyAPI/v7/internal/pluginstore"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/bcrypt"
)
@@ -168,6 +169,24 @@ func (h *Handler) reloadConfigAfterManagementSave(ctx context.Context, cfg *conf
}
}
func (h *Handler) reloadConfigAfterManagementSaveAsync(ctx context.Context, cfg *config.Config) {
if h == nil || cfg == nil {
return
}
reloadCtx := context.Background()
if ctx != nil {
reloadCtx = context.WithoutCancel(ctx)
}
go func() {
defer func() {
if recovered := recover(); recovered != nil {
log.WithField("panic", recovered).Error("management: async config reload panicked")
}
}()
h.reloadConfigAfterManagementSave(reloadCtx, cfg)
}()
}
// SetLocalPassword configures the runtime-local password accepted for localhost requests.
func (h *Handler) SetLocalPassword(password string) { h.localPassword = password }
+1 -1
View File
@@ -369,7 +369,7 @@ func (h *Handler) DeletePlugin(c *gin.Context) {
reloadCfg := h.cfg
h.mu.Unlock()
h.reloadConfigAfterManagementSave(c.Request.Context(), reloadCfg)
h.reloadConfigAfterManagementSaveAsync(c.Request.Context(), reloadCfg)
c.JSON(http.StatusOK, gin.H{
"status": "deleted",
"id": htmlsanitize.String(id),
@@ -12,6 +12,7 @@ import (
"runtime"
"strings"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
@@ -20,6 +21,17 @@ import (
"gopkg.in/yaml.v3"
)
func waitForAsyncReload(t *testing.T, reloads <-chan *config.Config) *config.Config {
t.Helper()
select {
case cfg := <-reloads:
return cfg
case <-time.After(time.Second):
t.Fatal("timed out waiting for async config reload")
return nil
}
}
func TestListPluginsIncludesScannedAndConfiguredPlugins(t *testing.T) {
t.Parallel()
gin.SetMode(gin.TestMode)
@@ -342,12 +354,12 @@ func TestDeletePluginRemovesDiscoveredFileAndConfig(t *testing.T) {
},
configFilePath: writeTestConfigFile(t),
}
reloads := 0
reloads := make(chan *config.Config, 1)
releaseReload := make(chan struct{})
defer close(releaseReload)
h.SetConfigReloadHook(func(_ context.Context, cfg *config.Config) {
reloads++
if cfg != h.cfg {
t.Fatalf("reload config = %p, want handler config %p", cfg, h.cfg)
}
reloads <- cfg
<-releaseReload
})
path, errPath := pluginFilePath(pluginsDir, "sample")
@@ -363,7 +375,17 @@ func TestDeletePluginRemovesDiscoveredFileAndConfig(t *testing.T) {
c.Params = gin.Params{{Key: "id", Value: "sample"}}
c.Request = httptest.NewRequest(http.MethodDelete, "/v0/management/plugins/sample", nil)
h.DeletePlugin(c)
done := make(chan struct{})
go func() {
h.DeletePlugin(c)
close(done)
}()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("DeletePlugin blocked waiting for config reload")
}
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
@@ -374,8 +396,8 @@ func TestDeletePluginRemovesDiscoveredFileAndConfig(t *testing.T) {
if _, errStat := os.Stat(path); !os.IsNotExist(errStat) {
t.Fatalf("plugin file stat error = %v, want not exist", errStat)
}
if reloads != 1 {
t.Fatalf("reloads = %d, want 1", reloads)
if cfg := waitForAsyncReload(t, reloads); cfg != h.cfg {
t.Fatalf("reload config = %p, want handler config %p", cfg, h.cfg)
}
}