Files
wangxiaolei 4314b8f1ee fix: synchronize remote debugging plugins across cluster nodes (#589)
Remote debugging plugins were not being synchronized across cluster nodes,
causing "no plugin available nodes found" errors when trying to invoke
plugins from different nodes.

1. **Remote debugging plugins not registered to cluster** - The
   `ClusterTunnel` notifier was not being added to ControlPanel
2. **Plugin ID inconsistency** - Remote plugins used different plugin_id
   formats during installation vs. querying
3. **Non-idempotent registration** - `RegisterPlugin` failed on reconnection
   with "plugin has been registered" error

- **internal/types/models/curd/atomic.go**:
  - Unify plugin_id calculation for remote plugins (author/name without version)
  - Remove plugin_id from plugin query conditions
  - Clear old cache when plugin_id is updated

- **internal/cluster/plugin.go**:
  - Make `RegisterPlugin` idempotent by updating existing plugin instead
    of returning error

- **internal/core/control_panel/daemon.go**:
  - Add cluster field to ControlPanel
  - Add SetCluster() method for lazy cluster initialization

- **internal/core/control_panel/server_debugger.go**:
  - Register remote debugging plugins to cluster on connection
  - Unregister from cluster on disconnection

- **internal/core/plugin_manager/manager.go**:
  - Add SetCluster() method to set cluster after initialization

- **internal/server/server.go**:
  - Call SetCluster() instead of AddClusterTunnel()

Only remote debugging plugins are synchronized across cluster nodes.
Local plugins run only on the node where they are installed and are
not registered to the cluster.

- Error handling improvements using `errors.Is()` instead of `==`
- Handle 404 for missing plugin assets gracefully
- Handle already-installed debugging plugins gracefully

- Remote debugging plugin can be invoked from any node in the cluster
- Plugin reconnection works without errors
- Cache invalidation works correctly when plugin_id changes
2026-03-05 14:46:13 +08:00

112 lines
3.2 KiB
Go

package controlpanel
import (
"sync"
"time"
"github.com/langgenius/dify-plugin-daemon/internal/cluster"
"github.com/langgenius/dify-plugin-daemon/internal/core/debugging_runtime"
"github.com/langgenius/dify-plugin-daemon/internal/core/local_runtime"
"github.com/langgenius/dify-plugin-daemon/internal/core/plugin_manager/media_transport"
"github.com/langgenius/dify-plugin-daemon/internal/types/app"
"github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities"
"github.com/langgenius/dify-plugin-daemon/pkg/utils/lock"
"github.com/langgenius/dify-plugin-daemon/pkg/utils/mapping"
)
type ControlPanel struct {
// app config
config *app.Config
// cluster for remote debugging plugins
cluster *cluster.Cluster
// debugging server
debuggingServer *debugging_runtime.RemotePluginServer
// media bucket
mediaBucket *media_transport.MediaBucket
// installed bucket
installedBucket *media_transport.InstalledBucket
// package bucket
packageBucket *media_transport.PackageBucket
// notifiers
controlPanelNotifiers []ControlPanelNotifier
controlPanelNotifierLock *sync.RWMutex
// local plugin runtimes map
// plugin unique identifier -> local plugin runtime
localPluginRuntimes mapping.Map[
plugin_entities.PluginUniqueIdentifier,
*local_runtime.LocalPluginRuntime,
]
// local plugin launching semaphore
// we allow multiple plugins to be installed concurrently
// to control the concurrency, this semaphore is introduced
localPluginLaunchingSemaphore chan bool
// how many times a local plugin failed to launch
// controls retries and waiting time after failures
localPluginFailsRecord mapping.Map[
plugin_entities.PluginUniqueIdentifier,
LocalPluginFailsRecord,
]
// this map marks plugins which should be ignored by `WatchDog`
// once a plugin is added, the launch process will be prevented
localPluginWatchIgnoreList mapping.Map[
plugin_entities.PluginUniqueIdentifier,
bool,
]
// local plugin installation lock
// locks when a plugin is on its installation process, avoid the same plugin
// to be processed concurrently
localPluginInstallationLock *lock.GranularityLock
// debugging plugin runtime
debuggingPluginRuntime mapping.Map[
plugin_entities.PluginUniqueIdentifier,
*debugging_runtime.RemotePluginRuntime,
]
}
type LocalPluginFailsRecord struct {
RetryCount int32
LastTriedAt time.Time
}
// create a new control panel as the engine of the local plugin daemon
func NewControlPanel(
config *app.Config,
mediaBucket *media_transport.MediaBucket,
packageBucket *media_transport.PackageBucket,
installedBucket *media_transport.InstalledBucket,
cluster *cluster.Cluster,
) *ControlPanel {
return &ControlPanel{
config: config,
mediaBucket: mediaBucket,
packageBucket: packageBucket,
installedBucket: installedBucket,
cluster: cluster,
localPluginLaunchingSemaphore: make(chan bool, config.PluginLocalLaunchingConcurrent),
// notifiers initialization
controlPanelNotifiers: []ControlPanelNotifier{},
controlPanelNotifierLock: &sync.RWMutex{},
// local plugin installation lock
localPluginInstallationLock: lock.NewGranularityLock(),
}
}
func (c *ControlPanel) SetCluster(cluster *cluster.Cluster) {
c.cluster = cluster
}