Files
dify-plugin-daemon/internal/core/plugin_manager/watcher.go
homejim 6ae762ba11 feat(plugin_manager): optimize local plugin startup with concurrency (#375)
* feat(plugin_manager): optimize local plugin startup with concurrent control

- Add semaphore-based concurrency control for plugin launches
- Implement parallel plugin startup using goroutines
- Optimize error handling to prevent goroutine blocking
- Add concurrency metrics logging

Note: handleNewLocalPlugins now accepts config parameter with default concurrency limit

* feat(plugin_manager): make local plugin launching concurrency configurable

* fix(plugin_manager): optimize comment and error handling

- Updated comments to clarify the concurrent plugin launching configuration.
- Added a nil check for the error channel during plugin startup to improve code robustness.

* refactor(plugin_manager): refactor plugin startup logic

- Remove the semaphore mechanism and switch to using routine.Submit for concurrency management

* fix(plugin_manager): Optimize plugin startup logs and concurrency control

- Added log output for maximum concurrency when starting local plugins
- Implemented a channel-based concurrency control mechanism to ensure limits are not exceeded
- Fixed closure variable capture issue to prevent incorrect plugin information
- Improved error handling to avoid deadlocks during startup

* fix(plugin_manager): simplify error channel handling and semaphore release logic

---------

Co-authored-by: jim02.he <jim02.he@vipshop.com>
2025-07-08 19:09:31 +08:00

155 lines
4.2 KiB
Go

package plugin_manager
import (
"sync"
"time"
"github.com/langgenius/dify-plugin-daemon/internal/core/plugin_manager/debugging_runtime"
"github.com/langgenius/dify-plugin-daemon/internal/core/plugin_manager/local_runtime"
"github.com/langgenius/dify-plugin-daemon/internal/types/app"
"github.com/langgenius/dify-plugin-daemon/internal/utils/log"
"github.com/langgenius/dify-plugin-daemon/internal/utils/routine"
"github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities"
)
func (p *PluginManager) startLocalWatcher(config *app.Config) {
go func() {
log.Info("start to handle new plugins in path: %s", p.config.PluginInstalledPath)
log.Info("Launching plugins with max concurrency: %d", p.config.PluginLocalLaunchingConcurrent)
p.handleNewLocalPlugins(config)
for range time.NewTicker(time.Second * 30).C {
p.handleNewLocalPlugins(config)
p.removeUninstalledLocalPlugins()
}
}()
}
func (p *PluginManager) initRemotePluginServer(config *app.Config) {
if p.remotePluginServer != nil {
return
}
p.remotePluginServer = debugging_runtime.NewRemotePluginServer(config, p.mediaBucket)
}
func (p *PluginManager) startRemoteWatcher(config *app.Config) {
// launch TCP debugging server if enabled
if config.PluginRemoteInstallingEnabled != nil && *config.PluginRemoteInstallingEnabled {
p.initRemotePluginServer(config)
go func() {
err := p.remotePluginServer.Launch()
if err != nil {
log.Error("start remote plugin server failed: %s", err.Error())
}
}()
go func() {
p.remotePluginServer.Wrap(func(rpr plugin_entities.PluginFullDuplexLifetime) {
identity, err := rpr.Identity()
if err != nil {
log.Error("get remote plugin identity failed: %s", err.Error())
return
}
p.m.Store(identity.String(), rpr)
routine.Submit(map[string]string{
"module": "plugin_manager",
"function": "startRemoteWatcher",
"plugin_id": identity.String(),
"type": "remote",
}, func() {
defer func() {
if err := recover(); err != nil {
log.Error("plugin runtime error: %v", err)
}
p.m.Delete(identity.String())
}()
p.fullDuplexLifecycle(rpr, nil, nil)
})
})
}()
}
}
func (p *PluginManager) handleNewLocalPlugins(config *app.Config) {
// walk through all plugins
plugins, err := p.installedBucket.List()
if err != nil {
log.Error("list installed plugins failed: %s", err.Error())
return
}
var wg sync.WaitGroup
maxConcurrency := config.PluginLocalLaunchingConcurrent
sem := make(chan struct{}, maxConcurrency)
for _, plugin := range plugins {
wg.Add(1)
// Fix closure issue: create local variable copy
currentPlugin := plugin
routine.Submit(map[string]string{
"module": "plugin_manager",
"function": "handleNewLocalPlugins",
}, func() {
// Acquire sem inside goroutine
sem <- struct{}{}
defer func() {
if err := recover(); err != nil {
log.Error("plugin launch runtime error: %v", err)
}
<-sem
wg.Done()
}()
_, launchedChan, errChan, err := p.launchLocal(currentPlugin)
if err != nil {
log.Error("launch local plugin failed: %s", err.Error())
return
}
// Handle error channel
if errChan != nil {
for err := range errChan {
log.Error("plugin launch error: %s", err.Error())
}
}
// Wait for plugin to complete startup
if launchedChan != nil {
<-launchedChan
}
})
}
// wait for all plugins to be launched
wg.Wait()
}
// an async function to remove uninstalled local plugins
func (p *PluginManager) removeUninstalledLocalPlugins() {
// read all local plugin runtimes
p.m.Range(func(key string, value plugin_entities.PluginLifetime) bool {
// try to convert to local runtime
runtime, ok := value.(*local_runtime.LocalPluginRuntime)
if !ok {
return true
}
pluginUniqueIdentifier, err := runtime.Identity()
if err != nil {
log.Error("get plugin identity failed: %s", err.Error())
return true
}
// check if plugin is deleted, stop it if so
exists, err := p.installedBucket.Exists(pluginUniqueIdentifier)
if err != nil {
log.Error("check if plugin is deleted failed: %s", err.Error())
return true
}
if !exists {
runtime.Stop()
}
return true
})
}