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

@@ -1,25 +1,108 @@
package wire
import (
"time"
"with_you/internal/config"
"with_you/internal/grpc/runner"
"with_you/internal/pkg/redis"
"github.com/google/wire"
"github.com/google/uuid"
"go.uber.org/zap"
)
// GRPCSet gRPC 相关的 Provider Set
var GRPCSet = wire.NewSet(
ProvideGRPCServer,
ProvideRunnerHub,
ProvideTaskManager,
ProvideRunnerRegistry,
ProvideTaskDispatcher,
)
// ProvideGRPCServer 提供 gRPC 服务器
func ProvideGRPCServer(cfg *config.Config, logger *zap.Logger) *runner.Server {
return runner.NewServer(&cfg.GRPC, logger)
func ProvideGRPCServer(cfg *config.Config, logger *zap.Logger, hub *runner.RunnerHub) *runner.Server {
return runner.NewServerWithDeps(&cfg.GRPC, logger, hub)
}
// ProvideRunnerHub 创建 RunnerHub
func ProvideRunnerHub(logger *zap.Logger) *runner.RunnerHub {
return runner.NewRunnerHub(nil, logger)
}
// ProvideTaskManager 提供 TaskManager
func ProvideTaskManager(server *runner.Server) *runner.TaskManager {
return server.GetTaskManager()
func ProvideTaskManager(hub *runner.RunnerHub, logger *zap.Logger) *runner.TaskManager {
tm := runner.NewTaskManager(nil, 60*time.Second, logger)
tm.SetDispatcher(hub)
return tm
}
// ProvideRunnerRegistry 提供 Runner 注册中心
// standalone 模式或 Redis 不可用时返回 nil
func ProvideRunnerRegistry(cfg *config.Config, redisClient *redis.Client) *runner.RunnerRegistry {
if cfg.Runner.Mode != "cluster" || redisClient == nil {
return nil
}
instanceID := cfg.Runner.Cluster.InstanceID
if instanceID == "" {
instanceID = uuid.New().String()
}
rdb := redisClient.GetClient()
return runner.NewRunnerRegistry(
rdb,
instanceID,
cfg.Runner.Cluster.RegistryPrefix,
cfg.Runner.Cluster.CapPrefix,
time.Duration(cfg.Runner.Cluster.RegistryTTL)*time.Second,
time.Duration(cfg.Runner.Cluster.HeartbeatInterval)*time.Second,
zap.L(),
)
}
// ProvideTaskDispatcher 提供任务分发器
// standalone 模式返回 *RunnerHubcluster 模式返回 *TaskBus
func ProvideTaskDispatcher(
hub *runner.RunnerHub,
registry *runner.RunnerRegistry,
cfg *config.Config,
redisClient *redis.Client,
logger *zap.Logger,
) runner.TaskDispatcher {
if cfg.Runner.Mode != "cluster" || redisClient == nil || registry == nil {
logger.Info("Runner running in standalone mode")
return hub
}
instanceID := cfg.Runner.Cluster.InstanceID
if instanceID == "" {
instanceID = uuid.New().String()
}
rdb := redisClient.GetClient()
bus := runner.NewTaskBus(
hub,
rdb,
registry,
instanceID,
cfg.Runner.Cluster.TaskChannel,
cfg.Runner.Cluster.ResultChannel,
logger,
)
if err := bus.Start(); err != nil {
logger.Error("Failed to start Runner Bus, falling back to standalone", zap.Error(err))
return hub
}
// 注入 Bus 和 Registry 到 Hub
hub.SetBus(bus)
hub.SetRegistry(registry)
registry.StartHeartbeat()
logger.Info("Runner running in cluster mode", zap.String("instance_id", bus.InstanceID()))
return bus
}

View File

@@ -18,6 +18,7 @@ import (
"with_you/internal/service"
"github.com/google/wire"
redislib "github.com/redis/go-redis/v9"
"go.uber.org/zap"
"gorm.io/gorm"
)
@@ -376,8 +377,12 @@ func ProvideLogService(
}
// ProvideQRCodeLoginService 提供二维码登录服务
func ProvideHotRankWorker(cfg *config.Config, postRepo repository.PostRepository, channelRepo repository.ChannelRepository, cacheBackend cache.Cache) *service.HotRankWorker {
return service.NewHotRankWorker(cfg, postRepo, channelRepo, cacheBackend)
func ProvideHotRankWorker(cfg *config.Config, postRepo repository.PostRepository, channelRepo repository.ChannelRepository, cacheBackend cache.Cache, redisClient *redis.Client) *service.HotRankWorker {
var rdb *redislib.Client
if redisClient != nil {
rdb = redisClient.GetClient()
}
return service.NewHotRankWorker(cfg, postRepo, channelRepo, cacheBackend, rdb)
}
func ProvideQRCodeLoginService(