feat: 添加监控仪表盘

- 新增 Lit.js 组件化 UI (ui/ 目录)
  - tasks-chart: 带十字准星和拖拽选择的图表
  - queue-table: 队列列表,支持暂停/恢复
  - queue-modal: 队列详情弹窗,支持任务重试
  - time-range-picker: Prometheus 风格时间选择器
  - help-tooltip: 可复用的提示组件

- HTTPHandler 功能
  - SSE 实时推送 (stats + queues)
  - 队列暂停/恢复 API
  - 任务重试 API
  - 时间范围查询 API

- Inspector 改进
  - Prometheus 风格单表存储
  - 集成到 Start/Stop 生命周期
  - 新增 PauseQueue/UnpauseQueue/RunTask 方法

- 代码重构
  - Start 函数拆分为小函数
  - 优雅关闭流程优化

- 其他
  - 忽略 SQLite 数据库文件
  - example 添加延迟/定点任务示例
This commit is contained in:
2025-12-09 19:58:18 +08:00
parent c88bde7b11
commit 1f9f1cab53
17 changed files with 3665 additions and 787 deletions

195
taskq.go
View File

@@ -19,10 +19,12 @@ import (
var (
started atomic.Bool // 服务器启动状态
exit chan chan struct{} // 优雅退出信号通道
done chan struct{} // 关闭完成信号通道
handlers map[string]asynq.Handler // 任务处理器映射表
queues map[string]int // 队列优先级配置
client atomic.Pointer[asynq.Client] // asynq 客户端实例
redisClient redis.UniversalClient // Redis 客户端实例
inspector *Inspector // 统计采集器实例
errorType = reflect.TypeOf((*error)(nil)).Elem() // error 类型反射
contextType = reflect.TypeOf((*context.Context)(nil)).Elem() // context.Context 类型反射
)
@@ -31,6 +33,7 @@ var (
// 创建必要的全局变量和映射表,必须在调用其他函数之前调用
func Init() {
exit = make(chan chan struct{}) // 创建优雅退出通道
done = make(chan struct{}) // 创建关闭完成通道
handlers = make(map[string]asynq.Handler) // 创建任务处理器映射
queues = make(map[string]int) // 创建队列优先级映射
}
@@ -39,6 +42,19 @@ func Init() {
// 使用泛型确保类型安全,通过反射验证处理器函数签名
// 处理器函数签名必须是func(context.Context, T) error 或 func(context.Context) 或 func(T) error 或 func()
func Register[T any](t *Task[T]) error {
if t.Queue == "" {
return errors.New("taskq: queue name cannot be empty")
}
if t.Priority < 0 || t.Priority > 255 {
return errors.New("taskq: priority must be between 0 and 255")
}
if t.MaxRetries < 0 {
return errors.New("taskq: retry count must be non-negative")
}
if t.Handler == nil {
return errors.New("taskq: handler cannot be nil")
}
rv := reflect.ValueOf(t.Handler)
if rv.Kind() != reflect.Func {
return errors.New("taskq: handler must be a function")
@@ -56,28 +72,36 @@ func Register[T any](t *Task[T]) error {
}
}
// 验证参数:最多2个参数第一个必须是 context.Context第二个必须是结构体
// 验证参数:支持以下签名
// - func(context.Context, T) error
// - func(context.Context) error
// - func(T) error
// - func()
var inContext bool
var inData bool
var dataType reflect.Type
for i := range rt.NumIn() {
if i == 0 {
fi := rt.In(i)
if !fi.Implements(contextType) {
return errors.New("taskq: handler function first parameter must be context.Context")
numIn := rt.NumIn()
if numIn > 2 {
return errors.New("taskq: handler function can have at most 2 parameters")
}
for i := range numIn {
fi := rt.In(i)
if fi.Implements(contextType) {
if i != 0 {
return errors.New("taskq: context.Context must be the first parameter")
}
inContext = true
continue
} else if fi.Kind() == reflect.Struct {
if inData {
return errors.New("taskq: handler function can only have one data parameter")
}
inData = true
dataType = fi
} else {
return errors.New("taskq: handler parameter must be context.Context or a struct")
}
if i != 1 {
return errors.New("taskq: handler function can have at most 2 parameters")
}
fi := rt.In(i)
if fi.Kind() != reflect.Struct {
return errors.New("taskq: handler function second parameter must be a struct")
}
inData = true
dataType = fi
}
// 检查服务器是否已启动
@@ -112,73 +136,130 @@ func SetRedis(rdb redis.UniversalClient) error {
return nil
}
// StartOptions 启动选项
type StartOptions struct {
// StatsInterval 统计采集间隔,默认 2 秒
StatsInterval time.Duration
// StatsDBPath SQLite 数据库文件路径,默认 "./taskq_stats.db"
StatsDBPath string
}
// Start 启动 taskq 服务器
// 开始监听任务队列并处理任务,包含健康检查和优雅退出机制
func Start(ctx context.Context) error {
// 原子操作确保只启动一次
func Start(ctx context.Context, opts ...StartOptions) error {
if !started.CompareAndSwap(false, true) {
return errors.New("taskq: server is already running")
}
// 检查 Redis 客户端是否已初始化
if redisClient == nil {
started.Store(false)
return errors.New("taskq: redis client not initialized, call SetRedis() first")
}
// 创建任务路由器
var opt StartOptions
if len(opts) > 0 {
opt = opts[0]
}
if err := startInspector(opt); err != nil {
started.Store(false)
return err
}
srv := createServer(ctx)
go runServer(srv)
go runMonitor(ctx, srv)
return nil
}
// startInspector 启动统计采集器
func startInspector(opt StartOptions) error {
ins, err := NewInspector(InspectorOptions{
Interval: opt.StatsInterval,
DBPath: opt.StatsDBPath,
})
if err != nil {
return err
}
inspector = ins
SetStatsDB(ins.GetStatsDB())
return nil
}
// createServer 创建 asynq 服务器
func createServer(ctx context.Context) *asynq.Server {
return asynq.NewServerFromRedisClient(redisClient, asynq.Config{
Concurrency: 30,
Queues: maps.Clone(queues),
BaseContext: func() context.Context { return ctx },
LogLevel: asynq.WarnLevel,
})
}
// runServer 运行任务处理服务器
func runServer(srv *asynq.Server) {
mux := asynq.NewServeMux()
for name, handler := range handlers {
mux.Handle(name, handler)
}
if err := srv.Run(mux); err != nil {
log.Fatal(err)
}
}
// 创建 asynq 服务器
srv := asynq.NewServerFromRedisClient(redisClient, asynq.Config{
Concurrency: 30, // 并发处理数
Queues: maps.Clone(queues), // 队列配置
BaseContext: func() context.Context { return ctx }, // 基础上下文
LogLevel: asynq.DebugLevel, // 日志级别
})
// runMonitor 运行监控协程,处理优雅退出和健康检查
func runMonitor(ctx context.Context, srv *asynq.Server) {
defer close(done)
defer started.Store(false)
defer closeInspector()
defer srv.Shutdown()
// 启动监控协程:处理优雅退出和健康检查
ctx, cancel := context.WithCancel(ctx)
go func() {
defer cancel()
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
ticker := time.NewTicker(time.Minute) // 每分钟健康检查
defer ticker.Stop()
for {
for {
select {
case quit := <-exit:
quit <- struct{}{}
return
case <-ctx.Done():
// ctx 取消时,排空 exit 通道中可能的信号
select {
case <-ctx.Done():
case quit := <-exit:
quit <- struct{}{}
default:
}
return
case <-ticker.C:
if err := srv.Ping(); err != nil {
log.Println(err)
return
case exit := <-exit: // 收到退出信号
srv.Stop()
exit <- struct{}{}
return
case <-ticker.C: // 定期健康检查
err := srv.Ping()
if err != nil {
log.Println(err)
Stop()
}
}
}
}()
}
}
// 启动任务处理服务
go func() {
if err := srv.Run(mux); err != nil {
log.Fatal(err)
}
}()
return nil
// closeInspector 关闭统计采集
func closeInspector() {
if inspector != nil {
inspector.Close()
inspector = nil
}
}
// Stop 优雅停止 taskq 服务器
// 发送停止信号并等待服务器完全关闭
func Stop() {
if !started.Load() {
return
}
quit := make(chan struct{})
exit <- quit // 发送退出信号
<-quit // 等待确认退出
select {
case exit <- quit:
<-quit // 等待确认收到退出信号
default:
// monitor 已经退出
}
<-done // 等待 runMonitor 完全结束
}