e75c8b37c8
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
157 lines
4.5 KiB
Go
157 lines
4.5 KiB
Go
// Package lifecycle provides agent runtime abstractions.
|
|
package lifecycle
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/kandev/kandev/internal/agent/executor"
|
|
"github.com/kandev/kandev/internal/common/logger"
|
|
"github.com/kandev/kandev/internal/task/models"
|
|
)
|
|
|
|
// ErrExecutorNotFound is returned when a runtime doesn't exist in the registry.
|
|
var ErrExecutorNotFound = fmt.Errorf("runtime not found")
|
|
|
|
// ExecutorRegistry manages multiple Runtime implementations and provides
|
|
// thread-safe access to them. It supports registering runtimes by name,
|
|
// health checking, and instance recovery across all registered runtimes.
|
|
type ExecutorRegistry struct {
|
|
backends map[executor.Name]ExecutorBackend
|
|
mu sync.RWMutex
|
|
logger *logger.Logger
|
|
}
|
|
|
|
// NewExecutorRegistry creates a new ExecutorRegistry with the given logger.
|
|
func NewExecutorRegistry(log *logger.Logger) *ExecutorRegistry {
|
|
return &ExecutorRegistry{
|
|
backends: make(map[executor.Name]ExecutorBackend),
|
|
logger: log,
|
|
}
|
|
}
|
|
|
|
// Register adds a runtime to the registry using its Name() as the key.
|
|
// If a runtime with the same name already exists, it will be replaced.
|
|
func (r *ExecutorRegistry) Register(rt ExecutorBackend) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
name := rt.Name()
|
|
r.backends[name] = rt
|
|
r.logger.Info("registered runtime", zap.String("name", string(name)))
|
|
}
|
|
|
|
// GetRuntime returns a runtime by its name.
|
|
// Returns ErrExecutorNotFound if the runtime doesn't exist.
|
|
func (r *ExecutorRegistry) GetBackend(name executor.Name) (ExecutorBackend, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
|
|
rt, exists := r.backends[name]
|
|
if !exists {
|
|
return nil, fmt.Errorf("%w: %s", ErrExecutorNotFound, name)
|
|
}
|
|
return rt, nil
|
|
}
|
|
|
|
// List returns the names of all registered runtimes.
|
|
func (r *ExecutorRegistry) List() []executor.Name {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
|
|
names := make([]executor.Name, 0, len(r.backends))
|
|
for name := range r.backends {
|
|
names = append(names, name)
|
|
}
|
|
return names
|
|
}
|
|
|
|
// HealthCheckAll performs health checks on all registered runtimes.
|
|
// Returns a map of runtime names to errors. A nil error indicates the runtime is healthy.
|
|
func (r *ExecutorRegistry) HealthCheckAll(ctx context.Context) map[executor.Name]error {
|
|
r.mu.RLock()
|
|
backends := make(map[executor.Name]ExecutorBackend, len(r.backends))
|
|
for name, rt := range r.backends {
|
|
backends[name] = rt
|
|
}
|
|
r.mu.RUnlock()
|
|
|
|
results := make(map[executor.Name]error, len(backends))
|
|
for name, rt := range backends {
|
|
err := rt.HealthCheck(ctx)
|
|
results[name] = err
|
|
if err != nil {
|
|
r.logger.Warn("runtime health check failed",
|
|
zap.String("runtime", string(name)),
|
|
zap.Error(err))
|
|
} else {
|
|
r.logger.Debug("runtime health check passed",
|
|
zap.String("runtime", string(name)))
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
// Closeable is an optional interface for executors that hold resources
|
|
// requiring cleanup on shutdown (e.g., Docker SDK client connections).
|
|
type Closeable interface {
|
|
Close() error
|
|
}
|
|
|
|
// CloseAll closes all registered backends that implement Closeable.
|
|
func (r *ExecutorRegistry) CloseAll() {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
|
|
for name, rt := range r.backends {
|
|
if closer, ok := rt.(Closeable); ok {
|
|
if err := closer.Close(); err != nil {
|
|
r.logger.Warn("failed to close runtime",
|
|
zap.String("runtime", string(name)),
|
|
zap.Error(err))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// RecoverAll recovers instances from all registered runtimes. records is the
|
|
// live standalone recovery-inventory read at startup step 3
|
|
// (AC-EXECUTORS-SURVIVAL-002.8); it is passed to every registered runtime
|
|
// unchanged, and only the standalone runtime acts on it today.
|
|
// Returns all recovered instances and any error encountered.
|
|
// If multiple runtimes fail, only the last error is returned.
|
|
func (r *ExecutorRegistry) RecoverAll(ctx context.Context, records []*models.ExecutorRunning) ([]*ExecutorInstance, error) {
|
|
r.mu.RLock()
|
|
backends := make(map[executor.Name]ExecutorBackend, len(r.backends))
|
|
for name, rt := range r.backends {
|
|
backends[name] = rt
|
|
}
|
|
r.mu.RUnlock()
|
|
|
|
var allInstances []*ExecutorInstance
|
|
var lastErr error
|
|
|
|
for name, rt := range backends {
|
|
instances, err := rt.RecoverInstances(ctx, records)
|
|
if err != nil {
|
|
r.logger.Error("failed to recover instances from runtime",
|
|
zap.String("runtime", string(name)),
|
|
zap.Error(err))
|
|
lastErr = err
|
|
continue
|
|
}
|
|
|
|
if len(instances) > 0 {
|
|
r.logger.Info("recovered instances from runtime",
|
|
zap.String("runtime", string(name)),
|
|
zap.Int("count", len(instances)))
|
|
allInstances = append(allInstances, instances...)
|
|
}
|
|
}
|
|
|
|
return allInstances, lastErr
|
|
}
|