feat(runner): implement cluster mode with Redis-based task dispatching and registry
All checks were successful
Build Backend / build (push) Successful in 2m22s
Build Backend / build-docker (push) Successful in 1m20s

Introduce a distributed task execution system for gRPC runners. This includes:
- A `TaskBus` for cluster-mode task dispatching via Redis Pub/Sub.
- A `RunnerRegistry` to track active runner instances in Redis.
- Support for both standalone and cluster modes via configuration.
- Refactored `RunnerHub` and `TaskManager` to use a `TaskDispatcher` interface, decoupling task submission from local execution.
- Added distributed locking to `HotRankWorker` to ensure only one instance performs periodic hot rank recalculations in a multi-instance deployment.
- Updated dependency injection (Wire) to support the new runner components and registry.
This commit is contained in:
2026-05-07 01:08:39 +08:00
parent dc27eb5cde
commit 8aa85ca4b2
15 changed files with 1125 additions and 86 deletions

View File

@@ -26,22 +26,20 @@ type RunnerConnection struct {
type RunnerHub struct {
runner.UnimplementedRunnerHubServer // 嵌入以实现 gRPC 接口
mu sync.RWMutex
runners map[string]*RunnerConnection
taskManager *TaskManager
registerChan chan *RunnerConnection
unregisterChan chan string
logger *zap.Logger
mu sync.RWMutex
runners map[string]*RunnerConnection
taskManager *TaskManager
bus *TaskBus
registry *RunnerRegistry
logger *zap.Logger
}
// NewRunnerHub 创建新的 RunnerHub
func NewRunnerHub(taskManager *TaskManager, logger *zap.Logger) *RunnerHub {
return &RunnerHub{
runners: make(map[string]*RunnerConnection),
taskManager: taskManager,
registerChan: make(chan *RunnerConnection, 100),
unregisterChan: make(chan string, 100),
logger: logger,
runners: make(map[string]*RunnerConnection),
taskManager: taskManager,
logger: logger,
}
}
@@ -137,6 +135,15 @@ func (h *RunnerHub) handleRegister(req *runner.RegisterRequest, stream runner.Ru
h.runners[req.RunnerId] = conn
h.mu.Unlock()
// 同步到 Redis 注册中心
if h.registry != nil {
if err := h.registry.Register(req.RunnerId, req.Version, req.Capabilities); err != nil {
h.logger.Error("Failed to register runner in registry",
zap.String("runner_id", req.RunnerId),
zap.Error(err))
}
}
// 发送注册响应
if err := conn.send(&runner.StreamMessage{
Message: &runner.StreamMessage_RegisterResponse{
@@ -161,6 +168,15 @@ func (h *RunnerHub) handleHeartbeat(conn *RunnerConnection, hb *runner.Heartbeat
}
conn.LastSeen = time.Now()
// 刷新 Redis 注册中心的 TTL
if h.registry != nil {
if err := h.registry.RefreshHeartbeat(conn.ID); err != nil {
h.logger.Debug("Failed to refresh runner heartbeat in registry",
zap.String("runner_id", conn.ID),
zap.Error(err))
}
}
if err := conn.send(&runner.StreamMessage{
Message: &runner.StreamMessage_HeartbeatAck{
HeartbeatAck: &runner.HeartbeatAck{
@@ -179,6 +195,12 @@ func (h *RunnerHub) handleTaskResult(result *runner.TaskResult) {
zap.String("task_id", result.TaskId),
zap.String("status", result.Status.String()))
// 如果有 Bus委托给 Bus 处理(可能需要转发到远程实例)
if h.bus != nil {
h.bus.HandleResult(result)
return
}
if h.taskManager != nil {
h.taskManager.HandleResult(result)
}
@@ -187,12 +209,19 @@ func (h *RunnerHub) handleTaskResult(result *runner.TaskResult) {
// unregisterRunner 注销 Runner
func (h *RunnerHub) unregisterRunner(runnerID string) {
h.mu.Lock()
var capabilities map[string]string
if conn, ok := h.runners[runnerID]; ok {
capabilities = conn.Capabilities
close(conn.sendChan)
delete(h.runners, runnerID)
}
h.mu.Unlock()
// 从 Redis 注册中心移除
if h.registry != nil && capabilities != nil {
h.registry.Unregister(runnerID, capabilities)
}
h.logger.Info("Runner unregistered", zap.String("runner_id", runnerID))
}
@@ -257,6 +286,37 @@ func (h *RunnerHub) SetTaskManager(tm *TaskManager) {
h.taskManager = tm
}
// SetBus 设置 TaskBus集群模式
func (h *RunnerHub) SetBus(bus *TaskBus) {
h.bus = bus
}
// SetRegistry 设置 RunnerRegistry集群模式
func (h *RunnerHub) SetRegistry(registry *RunnerRegistry) {
h.registry = registry
}
// DispatchTask 实现 TaskDispatcher 接口standalone 模式)
func (h *RunnerHub) DispatchTask(task *runner.Task) error {
conn, err := h.GetAvailableRunner(task.Type)
if err != nil {
return err
}
return h.SendTask(conn.ID, task)
}
// GetRunnerByID 根据 ID 获取 Runner 连接
func (h *RunnerHub) GetRunnerByID(runnerID string) (*RunnerConnection, error) {
h.mu.RLock()
defer h.mu.RUnlock()
conn, ok := h.runners[runnerID]
if !ok {
return nil, fmt.Errorf("runner not found: %s", runnerID)
}
return conn, nil
}
// send 向 RunnerConnection 发送消息
func (c *RunnerConnection) send(msg *runner.StreamMessage) error {
select {
@@ -270,8 +330,11 @@ func (c *RunnerConnection) send(msg *runner.StreamMessage) error {
// CheckStaleRunners 检查并清理超时的 Runner 连接
func (h *RunnerHub) CheckStaleRunners(timeout time.Duration) {
h.mu.Lock()
defer h.mu.Unlock()
var toRemove []struct {
id string
capabilities map[string]string
}
now := time.Now()
for id, conn := range h.runners {
if now.Sub(conn.LastSeen) > timeout {
@@ -280,6 +343,18 @@ func (h *RunnerHub) CheckStaleRunners(timeout time.Duration) {
zap.Time("last_seen", conn.LastSeen))
close(conn.sendChan)
delete(h.runners, id)
toRemove = append(toRemove, struct {
id string
capabilities map[string]string
}{id: id, capabilities: conn.Capabilities})
}
}
h.mu.Unlock()
// 从 Redis 注册中心批量移除
if h.registry != nil {
for _, r := range toRemove {
h.registry.Unregister(r.id, r.capabilities)
}
}
}