Skip to content

Commit 09596d2

Browse files
Treat loading plugins as busy
1 parent a3c87ce commit 09596d2

4 files changed

Lines changed: 136 additions & 9 deletions

File tree

internal/api/handlers/management/plugin_store.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -198,15 +198,15 @@ func (h *Handler) installPluginFromStore(c *gin.Context, goos, goarch string) {
198198
return
199199
}
200200

201-
pluginIsLoaded := func() bool { return pluginLoaded(host, id) }
201+
pluginIsBusy := func() bool { return pluginBusy(host, id) }
202202
unloadedBeforeWrite := false
203203
result, errInstall := client.Install(installCtx, plugin, pluginstore.InstallOptions{
204204
PluginsDir: pluginsDir,
205205
GOOS: goos,
206206
GOARCH: goarch,
207-
PluginLoaded: pluginIsLoaded,
207+
PluginLoaded: pluginIsBusy,
208208
BeforeWrite: func() error {
209-
if !pluginIsLoaded() {
209+
if !pluginIsBusy() {
210210
return nil
211211
}
212212
if host == nil {
@@ -215,8 +215,8 @@ func (h *Handler) installPluginFromStore(c *gin.Context, goos, goarch string) {
215215
log.WithFields(log.Fields{
216216
"plugin_id": id,
217217
"version": plugin.Version,
218-
}).Info("pluginstore: unloading loaded plugin before install")
219-
if !host.UnloadPlugin(id) && pluginIsLoaded() {
218+
}).Info("pluginstore: unloading busy plugin before install")
219+
if !host.UnloadPlugin(id) && pluginIsBusy() {
220220
return pluginstore.ErrLoadedPluginLocked
221221
}
222222
unloadedBeforeWrite = true
@@ -560,9 +560,9 @@ func pluginLocalStatuses(pluginsEnabled bool, pluginsDir string, configs map[str
560560
return statuses, nil
561561
}
562562

563-
func pluginLoaded(host *pluginhost.Host, id string) bool {
563+
func pluginBusy(host *pluginhost.Host, id string) bool {
564564
if host == nil {
565565
return false
566566
}
567-
return host.PluginLoaded(id)
567+
return host.PluginBusy(id)
568568
}

internal/api/handlers/management/plugins.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ func (h *Handler) DeletePlugin(c *gin.Context) {
338338
return
339339
}
340340

341-
if pluginLoaded(host, id) && (host == nil || !host.UnloadPlugin(id)) && pluginLoaded(host, id) {
341+
if pluginBusy(host, id) && (host == nil || !host.UnloadPlugin(id)) && pluginBusy(host, id) {
342342
c.JSON(http.StatusConflict, gin.H{
343343
"error": "plugin_delete_requires_restart",
344344
"message": "loaded plugin cannot be deleted while the server is running",

internal/pluginhost/host.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type Host struct {
3939
mu sync.Mutex
4040
loader pluginLoader
4141
loaded map[string]*loadedPlugin
42+
loading map[string]struct{}
4243
fused map[string]string
4344
runtimeConfig *config.Config
4445
authManager *coreauth.Manager
@@ -65,6 +66,7 @@ func New() *Host {
6566
h := &Host{
6667
loader: defaultPluginLoader(),
6768
loaded: make(map[string]*loadedPlugin),
69+
loading: make(map[string]struct{}),
6870
fused: make(map[string]string),
6971
modelClientIDs: make(map[string]struct{}),
7072
executorModelClientIDs: make(map[string]struct{}),
@@ -137,6 +139,24 @@ func (h *Host) PluginLoaded(id string) bool {
137139
return ok
138140
}
139141

142+
// PluginBusy reports whether a plugin dynamic library is loaded or being loaded.
143+
func (h *Host) PluginBusy(id string) bool {
144+
if h == nil {
145+
return false
146+
}
147+
id = strings.TrimSpace(id)
148+
if id == "" {
149+
return false
150+
}
151+
h.mu.Lock()
152+
defer h.mu.Unlock()
153+
if _, ok := h.loaded[id]; ok {
154+
return true
155+
}
156+
_, ok := h.loading[id]
157+
return ok
158+
}
159+
140160
func (h *Host) ApplyConfig(ctx context.Context, cfg *config.Config) {
141161
if h == nil {
142162
return
@@ -189,12 +209,18 @@ func (h *Host) ApplyConfig(ctx context.Context, cfg *config.Config) {
189209
}
190210

191211
if lp == nil {
212+
h.mu.Lock()
213+
h.loading[file.ID] = struct{}{}
214+
h.mu.Unlock()
215+
192216
loaded, errLoad := h.load(file)
217+
h.mu.Lock()
218+
delete(h.loading, file.ID)
193219
if errLoad != nil {
220+
h.mu.Unlock()
194221
log.Warnf("pluginhost: failed to load plugin %s from %s: %v", file.ID, file.Path, errLoad)
195222
continue
196223
}
197-
h.mu.Lock()
198224
// ApplyConfig, UnloadPlugin, and ShutdownAll are serialized by applyMu,
199225
// so a nil read cannot race into a duplicate load.
200226
lp = loaded
@@ -301,6 +327,7 @@ func (h *Host) ShutdownAll() {
301327
})
302328
}
303329
h.loaded = make(map[string]*loadedPlugin)
330+
h.loading = make(map[string]struct{})
304331
h.modelClientIDs = make(map[string]struct{})
305332
h.executorModelClientIDs = make(map[string]struct{})
306333
h.modelProviders = make(map[string]string)

internal/pluginhost/host_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,63 @@ func TestHostApplyConfigSerializesLifecycleCalls(t *testing.T) {
707707
}
708708
}
709709

710+
func TestHostPluginBusyReportsLoadingPlugin(t *testing.T) {
711+
h, cfg, openStarted, releaseOpen := newBlockingOpenHost(t)
712+
t.Cleanup(h.ShutdownAll)
713+
714+
applyDone := make(chan struct{})
715+
go func() {
716+
h.ApplyConfig(context.Background(), cfg)
717+
close(applyDone)
718+
}()
719+
720+
waitForHostTestSignal(t, openStarted, "plugin open start")
721+
if h.PluginLoaded("alpha") {
722+
t.Fatal("PluginLoaded(alpha) = true, want false while plugin is still loading")
723+
}
724+
if !h.PluginBusy("alpha") {
725+
t.Fatal("PluginBusy(alpha) = false, want true while plugin is loading")
726+
}
727+
728+
releaseOpen()
729+
waitForHostTestSignal(t, applyDone, "ApplyConfig completion")
730+
if !h.PluginLoaded("alpha") {
731+
t.Fatal("PluginLoaded(alpha) = false, want true after load")
732+
}
733+
if !h.PluginBusy("alpha") {
734+
t.Fatal("PluginBusy(alpha) = false, want true after load")
735+
}
736+
}
737+
738+
func TestHostUnloadWaitsForBlockingLoad(t *testing.T) {
739+
h, cfg, openStarted, releaseOpen := newBlockingOpenHost(t)
740+
applyDone := make(chan struct{})
741+
go func() {
742+
h.ApplyConfig(context.Background(), cfg)
743+
close(applyDone)
744+
}()
745+
waitForHostTestSignal(t, openStarted, "plugin open start")
746+
747+
unloadDone := make(chan bool)
748+
go func() {
749+
unloadDone <- h.UnloadPlugin("alpha")
750+
}()
751+
select {
752+
case <-unloadDone:
753+
t.Fatal("UnloadPlugin completed while ApplyConfig was still loading")
754+
case <-time.After(200 * time.Millisecond):
755+
}
756+
757+
releaseOpen()
758+
waitForHostTestSignal(t, applyDone, "ApplyConfig completion")
759+
if ok := waitForHostTestBool(t, unloadDone, "UnloadPlugin completion"); !ok {
760+
t.Fatal("UnloadPlugin returned false, want true after loading completes")
761+
}
762+
if h.PluginBusy("alpha") {
763+
t.Fatal("PluginBusy(alpha) = true, want false after unload")
764+
}
765+
}
766+
710767
func TestHostUnloadAndShutdownWaitForBlockingRegister(t *testing.T) {
711768
tests := []struct {
712769
name string
@@ -801,6 +858,49 @@ func (c *capturePluginClient) Call(ctx context.Context, method string, request [
801858

802859
func (c *capturePluginClient) Shutdown() {}
803860

861+
type blockingOpenLoader struct {
862+
inner *testSymbolLoader
863+
started chan struct{}
864+
release <-chan struct{}
865+
startOnce sync.Once
866+
}
867+
868+
func (l *blockingOpenLoader) Open(file pluginFile, host *Host) (pluginClient, error) {
869+
l.startOnce.Do(func() { close(l.started) })
870+
<-l.release
871+
return l.inner.Open(file, host)
872+
}
873+
874+
func newBlockingOpenHost(t *testing.T) (*Host, *config.Config, <-chan struct{}, func()) {
875+
t.Helper()
876+
877+
inner := newTestSymbolLoader()
878+
plugin := &testPlugin{
879+
registerResult: validTestPlugin("alpha"),
880+
reconfigureResult: validTestPlugin("alpha"),
881+
}
882+
inner.lookups["alpha"] = newTestSymbolLookup(plugin)
883+
884+
openStarted := make(chan struct{})
885+
release := make(chan struct{})
886+
var releaseOnce sync.Once
887+
releaseOpen := func() { releaseOnce.Do(func() { close(release) }) }
888+
t.Cleanup(releaseOpen)
889+
890+
h := NewForTest(&blockingOpenLoader{
891+
inner: inner,
892+
started: openStarted,
893+
release: release,
894+
})
895+
cfg := &config.Config{
896+
Plugins: config.PluginsConfig{
897+
Enabled: true,
898+
Dir: makePluginDir(t, "alpha"),
899+
},
900+
}
901+
return h, cfg, openStarted, releaseOpen
902+
}
903+
804904
func newBlockingRegisterHost(t *testing.T) (*Host, *config.Config, <-chan struct{}, func()) {
805905
t.Helper()
806906

0 commit comments

Comments
 (0)