Files
seaweedfs/weed/admin/plugin/registry.go

334 lines
8.4 KiB
Go

package plugin
import (
"fmt"
"sync"
"time"
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
)
// Registry manages all connected plugins and their capabilities
type Registry struct {
plugins map[string]*ConnectedPlugin
// Index by job type and capability (detector/executor)
detectorsByJobType map[string][]*ConnectedPlugin
executorsByJobType map[string][]*ConnectedPlugin
mu sync.RWMutex
// Configuration
healthCheckInterval time.Duration
healthCheckTimeout time.Duration
healthCheckTicker *time.Ticker
stopChan chan struct{}
}
// NewRegistry creates a new plugin registry
func NewRegistry() *Registry {
return &Registry{
plugins: make(map[string]*ConnectedPlugin),
detectorsByJobType: make(map[string][]*ConnectedPlugin),
executorsByJobType: make(map[string][]*ConnectedPlugin),
healthCheckInterval: 30 * time.Second,
healthCheckTimeout: 90 * time.Second,
stopChan: make(chan struct{}),
}
}
// RegisterPlugin adds a new plugin to the registry
func (r *Registry) RegisterPlugin(pluginID string, register *plugin_pb.PluginRegister) (*ConnectedPlugin, error) {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.plugins[pluginID]; exists {
return nil, fmt.Errorf("plugin %s already registered", pluginID)
}
plugin := &ConnectedPlugin{
ID: pluginID,
Name: register.Name,
Version: register.Version,
ProtocolVersion: register.ProtocolVersion,
ConnectedAt: time.Now(),
LastHeartbeat: time.Now(),
State: PluginStateConnected,
Healthy: true,
Capabilities: make(map[string]*plugin_pb.JobTypeCapability),
StreamConnected: true,
}
// Index capabilities
for _, cap := range register.Capabilities {
plugin.Capabilities[cap.JobType] = cap
if cap.CanDetect {
r.detectorsByJobType[cap.JobType] = append(r.detectorsByJobType[cap.JobType], plugin)
}
if cap.CanExecute {
r.executorsByJobType[cap.JobType] = append(r.executorsByJobType[cap.JobType], plugin)
}
}
r.plugins[pluginID] = plugin
return plugin, nil
}
// UnregisterPlugin removes a plugin from the registry
func (r *Registry) UnregisterPlugin(pluginID string) error {
r.mu.Lock()
defer r.mu.Unlock()
plugin, exists := r.plugins[pluginID]
if !exists {
return fmt.Errorf("plugin %s not found", pluginID)
}
// Remove from capability indexes
for jobType := range plugin.Capabilities {
r.removePluginFromDetectors(jobType, plugin)
r.removePluginFromExecutors(jobType, plugin)
}
delete(r.plugins, pluginID)
plugin.SetState(PluginStateDisconnected)
return nil
}
// removePluginFromDetectors removes plugin from detector index for a job type
func (r *Registry) removePluginFromDetectors(jobType string, plugin *ConnectedPlugin) {
detectors := r.detectorsByJobType[jobType]
for i, p := range detectors {
if p.ID == plugin.ID {
r.detectorsByJobType[jobType] = append(detectors[:i], detectors[i+1:]...)
break
}
}
if len(r.detectorsByJobType[jobType]) == 0 {
delete(r.detectorsByJobType, jobType)
}
}
// removePluginFromExecutors removes plugin from executor index for a job type
func (r *Registry) removePluginFromExecutors(jobType string, plugin *ConnectedPlugin) {
executors := r.executorsByJobType[jobType]
for i, p := range executors {
if p.ID == plugin.ID {
r.executorsByJobType[jobType] = append(executors[:i], executors[i+1:]...)
break
}
}
if len(r.executorsByJobType[jobType]) == 0 {
delete(r.executorsByJobType, jobType)
}
}
// GetPlugin retrieves a plugin by ID
func (r *Registry) GetPlugin(pluginID string) (*ConnectedPlugin, error) {
r.mu.RLock()
defer r.mu.RUnlock()
plugin, exists := r.plugins[pluginID]
if !exists {
return nil, fmt.Errorf("plugin %s not found", pluginID)
}
return plugin, nil
}
// GetDetectorForJobType returns an available detector plugin for a job type
func (r *Registry) GetDetectorForJobType(jobType string) (*ConnectedPlugin, error) {
r.mu.RLock()
defer r.mu.RUnlock()
detectors, exists := r.detectorsByJobType[jobType]
if !exists || len(detectors) == 0 {
return nil, fmt.Errorf("no detector available for job type %s", jobType)
}
// Return the first healthy detector
for _, detector := range detectors {
if detector.IsHealthy() {
return detector, nil
}
}
return nil, fmt.Errorf("no healthy detector available for job type %s", jobType)
}
// GetExecutorForJobType returns an available executor plugin for a job type
func (r *Registry) GetExecutorForJobType(jobType string) (*ConnectedPlugin, error) {
r.mu.RLock()
defer r.mu.RUnlock()
executors, exists := r.executorsByJobType[jobType]
if !exists || len(executors) == 0 {
return nil, fmt.Errorf("no executor available for job type %s", jobType)
}
// Return the first healthy executor with lowest workload
var bestExecutor *ConnectedPlugin
var minLoad int32 = int32(^uint32(0) >> 1)
for _, executor := range executors {
if executor.IsHealthy() {
load := executor.PendingJobs + executor.RunningJobs
if load < minLoad {
minLoad = load
bestExecutor = executor
}
}
}
if bestExecutor == nil {
return nil, fmt.Errorf("no healthy executor available for job type %s", jobType)
}
return bestExecutor, nil
}
// ListPlugins returns all connected plugins
func (r *Registry) ListPlugins() []*ConnectedPlugin {
r.mu.RLock()
defer r.mu.RUnlock()
plugins := make([]*ConnectedPlugin, 0, len(r.plugins))
for _, plugin := range r.plugins {
plugins = append(plugins, plugin)
}
return plugins
}
// ListPluginsByCapability returns plugins that support a specific job type
func (r *Registry) ListPluginsByCapability(jobType string) []*ConnectedPlugin {
r.mu.RLock()
defer r.mu.RUnlock()
detectors := r.detectorsByJobType[jobType]
executors := r.executorsByJobType[jobType]
// Combine and deduplicate
pluginMap := make(map[string]*ConnectedPlugin)
for _, p := range detectors {
pluginMap[p.ID] = p
}
for _, p := range executors {
pluginMap[p.ID] = p
}
plugins := make([]*ConnectedPlugin, 0, len(pluginMap))
for _, p := range pluginMap {
plugins = append(plugins, p)
}
return plugins
}
// UpdateHeartbeat updates plugin heartbeat and health status
func (r *Registry) UpdateHeartbeat(pluginID string, heartbeat *plugin_pb.PluginHeartbeat) error {
r.mu.Lock()
plugin, exists := r.plugins[pluginID]
r.mu.Unlock()
if !exists {
return fmt.Errorf("plugin %s not found", pluginID)
}
plugin.UpdateHeartbeat(
heartbeat.CpuUsagePercent,
heartbeat.MemoryUsageMb,
heartbeat.PendingJobs,
int32(len(plugin.Capabilities)), // rough estimate
)
return nil
}
// Start starts the health check loop
func (r *Registry) Start() {
r.healthCheckTicker = time.NewTicker(r.healthCheckInterval)
go r.healthCheckLoop()
}
// Stop stops the health check loop
func (r *Registry) Stop() {
close(r.stopChan)
if r.healthCheckTicker != nil {
r.healthCheckTicker.Stop()
}
}
// healthCheckLoop periodically checks plugin health
func (r *Registry) healthCheckLoop() {
for {
select {
case <-r.healthCheckTicker.C:
r.checkHealth()
case <-r.stopChan:
return
}
}
}
// checkHealth checks the health of all plugins
func (r *Registry) checkHealth() {
r.mu.RLock()
plugins := make([]*ConnectedPlugin, 0, len(r.plugins))
for _, p := range r.plugins {
plugins = append(plugins, p)
}
r.mu.RUnlock()
now := time.Now()
for _, plugin := range plugins {
plugin.mu.RLock()
lastHeartbeat := plugin.LastHeartbeat
plugin.mu.RUnlock()
timeSinceHeartbeat := now.Sub(lastHeartbeat)
if timeSinceHeartbeat > r.healthCheckTimeout {
plugin.SetState(PluginStateUnhealthy)
plugin.mu.Lock()
plugin.Healthy = false
plugin.mu.Unlock()
} else if timeSinceHeartbeat > r.healthCheckTimeout/2 {
plugin.SetState(PluginStateUnhealthy)
} else {
plugin.SetState(PluginStateHealthy)
plugin.mu.Lock()
plugin.Healthy = true
plugin.mu.Unlock()
}
}
}
// GetStats returns registry statistics
func (r *Registry) GetStats() map[string]interface{} {
r.mu.RLock()
defer r.mu.RUnlock()
stats := map[string]interface{}{
"total_plugins": len(r.plugins),
"job_types": len(r.detectorsByJobType),
}
var healthyCount int
for _, p := range r.plugins {
if p.IsHealthy() {
healthyCount++
}
}
stats["healthy_plugins"] = healthyCount
jobTypeStats := make(map[string]map[string]interface{})
for jobType := range r.detectorsByJobType {
jobTypeStats[jobType] = map[string]interface{}{
"detectors": len(r.detectorsByJobType[jobType]),
"executors": len(r.executorsByJobType[jobType]),
}
}
stats["job_type_stats"] = jobTypeStats
return stats
}