Compare commits
4 Commits
main
..
279e6417df
| Author | SHA1 | Date | |
|---|---|---|---|
| 279e6417df | |||
| d2d59746b2 | |||
| 42cb0fa4c2 | |||
| 39c6a65b87 |
@@ -1,196 +1,47 @@
|
|||||||
# TaskQ - 基于 Redis 的异步任务队列系统
|
# TaskQ - Task Queue Management System
|
||||||
|
|
||||||
一个基于 Go 和 Redis 的异步任务队列管理系统,使用 asynq 库作为底层实现,支持任务注册、发布、消费和重试机制。
|
A Go-based task queue management system for efficient task processing and management.
|
||||||
|
|
||||||
## 特性
|
## Features
|
||||||
|
|
||||||
- 🚀 基于 Redis 的高性能任务队列
|
- Task queue management
|
||||||
- 📊 实时监控仪表板(Web UI)
|
- Dashboard interface
|
||||||
- 🔍 任务检查和调试工具
|
- Task inspection and monitoring
|
||||||
- ⚡ 并发任务处理
|
- Concurrent task processing
|
||||||
- 🔄 自动重试机制
|
|
||||||
- 📈 Prometheus 指标集成
|
|
||||||
- 🎯 灵活的任务优先级和分组
|
|
||||||
- ⏰ 延迟任务和定时任务支持
|
|
||||||
|
|
||||||
## 快速开始
|
## Installation
|
||||||
|
|
||||||
### 基本使用
|
```bash
|
||||||
|
go mod download
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.tczkiot.com/wlw/taskq"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 定义任务数据结构
|
|
||||||
type EmailData struct {
|
|
||||||
To string
|
|
||||||
Subject string
|
|
||||||
Body string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义任务处理器
|
|
||||||
func sendEmail(ctx context.Context, data EmailData) error {
|
|
||||||
log.Printf("发送邮件到 %s: %s", data.To, data.Subject)
|
|
||||||
// 实际的邮件发送逻辑
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// 配置 Redis 连接
|
|
||||||
cfg := taskq.Config{
|
|
||||||
Redis: &redis.Client{
|
|
||||||
Addr: "localhost:6379",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注册任务
|
|
||||||
emailTask := &taskq.Task{
|
|
||||||
Name: "send-email",
|
|
||||||
Queue: "email",
|
|
||||||
MaxRetries: 3,
|
|
||||||
TTR: 30 * time.Second,
|
|
||||||
Handler: sendEmail,
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Tasks = []*taskq.Task{emailTask}
|
|
||||||
|
|
||||||
// 配置并启动
|
|
||||||
taskq.Configure(cfg)
|
|
||||||
if err := taskq.Init(context.Background()); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发布任务
|
|
||||||
data := EmailData{
|
|
||||||
To: "user@example.com",
|
|
||||||
Subject: "欢迎使用 TaskQ",
|
|
||||||
Body: "这是一个测试邮件",
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := emailTask.Publish(context.Background(), data); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动服务器
|
|
||||||
if err := taskq.Start(context.Background()); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 项目结构
|
## Usage
|
||||||
|
|
||||||
```
|
```bash
|
||||||
taskq/
|
go run .
|
||||||
├── taskq.go # 主包入口,提供包级别 API
|
|
||||||
├── servlet.go # Servlet 核心实现,生命周期管理
|
|
||||||
├── task.go # Task 结构体和任务处理逻辑
|
|
||||||
├── plugin.go # 插件系统接口
|
|
||||||
├── x/ # 扩展组件
|
|
||||||
│ ├── inspector/ # 任务检查工具
|
|
||||||
│ ├── metrics/ # Prometheus 指标
|
|
||||||
│ └── monitor/ # Web 监控界面
|
|
||||||
├── example/ # 示例代码
|
|
||||||
└── Makefile # 构建脚本
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## API 文档
|
## Project Structure
|
||||||
|
|
||||||
### 包级别 API
|
- `taskq.go` - Main application entry point
|
||||||
|
- `task.go` - Task definition and management
|
||||||
|
- `inspect.go` - Task inspection utilities
|
||||||
|
- `dashboard.html` - Web dashboard interface
|
||||||
|
- `example/` - Example implementations
|
||||||
|
|
||||||
```go
|
## Development
|
||||||
// 配置默认 Servlet
|
|
||||||
taskq.Configure(cfg)
|
|
||||||
|
|
||||||
// 初始化(必须先调用)
|
```bash
|
||||||
taskq.Init(ctx)
|
# Run the application
|
||||||
|
go run .
|
||||||
|
|
||||||
// 启动服务器
|
# Run tests
|
||||||
taskq.Start(ctx)
|
go test ./...
|
||||||
|
|
||||||
// 停止服务器
|
# Build
|
||||||
taskq.Stop()
|
go build -o taskq
|
||||||
```
|
```
|
||||||
|
|
||||||
### Servlet 实例 API
|
## License
|
||||||
|
|
||||||
```go
|
MIT License
|
||||||
// 创建新实例
|
|
||||||
servlet := taskq.New()
|
|
||||||
|
|
||||||
// 配置
|
|
||||||
servlet.Configure(cfg)
|
|
||||||
|
|
||||||
// 初始化
|
|
||||||
servlet.Init(ctx)
|
|
||||||
|
|
||||||
// 启动
|
|
||||||
servlet.Start(ctx)
|
|
||||||
|
|
||||||
// 停止
|
|
||||||
servlet.Stop()
|
|
||||||
```
|
|
||||||
|
|
||||||
### 任务发布选项
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 延迟执行
|
|
||||||
emailTask.Publish(ctx, data, taskq.Delay(5*time.Minute))
|
|
||||||
|
|
||||||
// 指定时间执行
|
|
||||||
emailTask.Publish(ctx, data, taskq.DelayUntil(time.Now().Add(time.Hour)))
|
|
||||||
|
|
||||||
// 自定义超时
|
|
||||||
emailTask.Publish(ctx, data, taskq.TTR(10*time.Second))
|
|
||||||
|
|
||||||
// 结果保留时间
|
|
||||||
emailTask.Publish(ctx, data, taskq.Retention(48*time.Hour))
|
|
||||||
```
|
|
||||||
|
|
||||||
## 监控界面
|
|
||||||
|
|
||||||
访问 `http://localhost:8080` 查看任务监控界面,包括:
|
|
||||||
|
|
||||||
- 实时任务状态
|
|
||||||
- 队列统计信息
|
|
||||||
- 任务执行历史
|
|
||||||
- 性能指标图表
|
|
||||||
|
|
||||||
## 配置选项
|
|
||||||
|
|
||||||
### Redis 配置
|
|
||||||
|
|
||||||
```go
|
|
||||||
cfg := taskq.Config{
|
|
||||||
Redis: &redis.Client{
|
|
||||||
Addr: "localhost:6379",
|
|
||||||
Password: "",
|
|
||||||
DB: 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 任务配置
|
|
||||||
|
|
||||||
```go
|
|
||||||
task := &taskq.Task{
|
|
||||||
Name: "task-name", // 任务名称(唯一)
|
|
||||||
Queue: "default", // 队列名称
|
|
||||||
Group: "group-name", // 任务分组(可选)
|
|
||||||
MaxRetries: 3, // 最大重试次数
|
|
||||||
Priority: 1, // 优先级
|
|
||||||
TTR: 30 * time.Second, // 超时时间
|
|
||||||
Handler: handlerFunc, // 处理器函数
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 许可证
|
|
||||||
|
|
||||||
MIT License
|
|
||||||
@@ -7,19 +7,15 @@ require (
|
|||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/redis/go-redis/v9 v9.7.0
|
github.com/redis/go-redis/v9 v9.7.0
|
||||||
github.com/rs/xid v1.6.0
|
github.com/rs/xid v1.6.0
|
||||||
github.com/stretchr/testify v1.11.1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.66.1 // indirect
|
github.com/prometheus/common v0.66.1 // indirect
|
||||||
github.com/prometheus/procfs v0.16.1 // indirect
|
github.com/prometheus/procfs v0.16.1 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
-231
@@ -1,231 +0,0 @@
|
|||||||
package taskq
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestContextRedis tests Context.Redis method
|
|
||||||
func TestContextRedis(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := &Context{
|
|
||||||
Context: context.Background(),
|
|
||||||
servlet: s,
|
|
||||||
}
|
|
||||||
|
|
||||||
redisClient := ctx.Redis()
|
|
||||||
assert.NotNil(t, redisClient)
|
|
||||||
assert.Equal(t, rdb, redisClient)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestContextQueues tests Context.Queues method
|
|
||||||
func TestContextQueues(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
task := &Task{
|
|
||||||
Name: "task1",
|
|
||||||
Queue: "default",
|
|
||||||
Priority: 5,
|
|
||||||
Handler: func() error { return nil },
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := &Context{
|
|
||||||
Context: context.Background(),
|
|
||||||
servlet: s,
|
|
||||||
}
|
|
||||||
|
|
||||||
queues := ctx.Queues()
|
|
||||||
assert.NotNil(t, queues)
|
|
||||||
assert.Equal(t, 5, queues["default"])
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestPluginName tests Plugin Name method
|
|
||||||
func TestPluginName(t *testing.T) {
|
|
||||||
plugin := &TestPlugin{}
|
|
||||||
assert.Equal(t, "TestPlugin", plugin.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestPluginInit tests Plugin Init method
|
|
||||||
func TestPluginInit(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
plugin := &TestPlugin{}
|
|
||||||
s := NewServlet()
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := &Context{
|
|
||||||
Context: context.Background(),
|
|
||||||
servlet: s,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = plugin.Init(ctx)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.True(t, plugin.initCalled)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestPluginStop tests Plugin Stop method
|
|
||||||
func TestPluginStop(t *testing.T) {
|
|
||||||
plugin := &TestPlugin{}
|
|
||||||
err := plugin.Stop()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.True(t, plugin.stopCalled)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestPluginInitError tests error handling when plugin Init fails
|
|
||||||
func TestPluginInitError(t *testing.T) {
|
|
||||||
// Skip: This test can cause goroutines to leak during plugin error handling
|
|
||||||
t.Skip("Init error handling causes goroutine leaks in asynq")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestPluginContextAccess tests plugin accessing context resources
|
|
||||||
func TestPluginContextAccess(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
resourcePlugin := &ResourceAccessPlugin{}
|
|
||||||
s := NewServlet()
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{},
|
|
||||||
Plugins: []Plugin{resourcePlugin},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
err = s.Init(ctx)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.True(t, resourcePlugin.redisAccessible)
|
|
||||||
assert.True(t, resourcePlugin.queuesAccessible)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test helper plugins
|
|
||||||
|
|
||||||
// TestPlugin implements the Plugin interface for testing
|
|
||||||
type TestPlugin struct {
|
|
||||||
initCalled bool
|
|
||||||
startCalled bool
|
|
||||||
stopCalled bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tp *TestPlugin) Name() string {
|
|
||||||
return "TestPlugin"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tp *TestPlugin) Init(ctx *Context) error {
|
|
||||||
tp.initCalled = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tp *TestPlugin) Start(ctx *Context) error {
|
|
||||||
tp.startCalled = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tp *TestPlugin) Stop() error {
|
|
||||||
tp.stopCalled = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrorPlugin implements the Plugin interface and can fail at specified stage
|
|
||||||
type ErrorPlugin struct {
|
|
||||||
failAt string // "init" or "start"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ep *ErrorPlugin) Name() string {
|
|
||||||
return "ErrorPlugin"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ep *ErrorPlugin) Init(ctx *Context) error {
|
|
||||||
if ep.failAt == "init" {
|
|
||||||
return &ErrorType{message: "init error"}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ep *ErrorPlugin) Start(ctx *Context) error {
|
|
||||||
if ep.failAt == "start" {
|
|
||||||
return &ErrorType{message: "start error"}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ep *ErrorPlugin) Stop() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrorType for testing
|
|
||||||
type ErrorType struct {
|
|
||||||
message string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ErrorType) Error() string {
|
|
||||||
return e.message
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResourceAccessPlugin tests accessing servlet resources through context
|
|
||||||
type ResourceAccessPlugin struct {
|
|
||||||
redisAccessible bool
|
|
||||||
queuesAccessible bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rap *ResourceAccessPlugin) Name() string {
|
|
||||||
return "ResourceAccessPlugin"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rap *ResourceAccessPlugin) Init(ctx *Context) error {
|
|
||||||
// Test Redis access
|
|
||||||
redis := ctx.Redis()
|
|
||||||
if redis != nil {
|
|
||||||
rap.redisAccessible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test Queues access
|
|
||||||
queues := ctx.Queues()
|
|
||||||
if queues != nil {
|
|
||||||
rap.queuesAccessible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rap *ResourceAccessPlugin) Start(ctx *Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rap *ResourceAccessPlugin) Stop() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
-570
@@ -1,570 +0,0 @@
|
|||||||
package taskq
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestNewServlet tests Servlet creation
|
|
||||||
func TestNewServlet(t *testing.T) {
|
|
||||||
s := NewServlet()
|
|
||||||
assert.NotNil(t, s)
|
|
||||||
assert.NotNil(t, s.handlers)
|
|
||||||
assert.NotNil(t, s.queues)
|
|
||||||
assert.NotNil(t, s.exit)
|
|
||||||
assert.Equal(t, 0, len(s.handlers))
|
|
||||||
assert.Equal(t, 0, len(s.queues))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestConfigureMissingRedis tests Configure without Redis client
|
|
||||||
func TestConfigureMissingRedis(t *testing.T) {
|
|
||||||
s := NewServlet()
|
|
||||||
cfg := Config{
|
|
||||||
Redis: nil,
|
|
||||||
Tasks: []*Task{},
|
|
||||||
}
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Equal(t, "taskq: redis client is required", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestConfigureValidTask tests successful configuration with valid task
|
|
||||||
func TestConfigureValidTask(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "default",
|
|
||||||
Priority: 1,
|
|
||||||
MaxRetries: 3,
|
|
||||||
Handler: func(ctx context.Context, data struct{ Value string }) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, s.client)
|
|
||||||
assert.NotNil(t, s.redisClient)
|
|
||||||
assert.Equal(t, 1, len(s.handlers))
|
|
||||||
assert.Equal(t, 1, len(s.queues))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRegisterTaskEmptyQueue tests task registration with empty queue name
|
|
||||||
func TestRegisterTaskEmptyQueue(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "",
|
|
||||||
Handler: func() error { return nil },
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Equal(t, "taskq: queue name cannot be empty", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRegisterTaskInvalidPriority tests task registration with invalid priority
|
|
||||||
func TestRegisterTaskInvalidPriority(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "default",
|
|
||||||
Priority: 256,
|
|
||||||
Handler: func() error { return nil },
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "priority must be between 0 and 255")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRegisterTaskNegativeRetry tests task registration with negative retry count
|
|
||||||
func TestRegisterTaskNegativeRetry(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "default",
|
|
||||||
MaxRetries: -1,
|
|
||||||
Handler: func() error { return nil },
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Equal(t, "taskq: retry count must be non-negative", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRegisterTaskNilHandler tests task registration with nil handler
|
|
||||||
func TestRegisterTaskNilHandler(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "default",
|
|
||||||
Handler: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Equal(t, "taskq: handler cannot be nil", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRegisterTaskNotFunction tests task registration with non-function handler
|
|
||||||
func TestRegisterTaskNotFunction(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "default",
|
|
||||||
Handler: "not_a_function",
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Equal(t, "taskq: handler must be a function", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRegisterTaskInvalidReturn tests handler with invalid return value
|
|
||||||
func TestRegisterTaskInvalidReturn(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "default",
|
|
||||||
Handler: func() string { return "invalid" },
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "must return either error or nothing")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRegisterTaskTooManyParams tests handler with more than 2 parameters
|
|
||||||
func TestRegisterTaskTooManyParams(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "default",
|
|
||||||
Handler: func(ctx context.Context, a string, b string) error { return nil },
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Equal(t, "taskq: handler function can have at most 2 parameters", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRegisterTaskContextNotFirst tests handler with context not as first parameter
|
|
||||||
func TestRegisterTaskContextNotFirst(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "default",
|
|
||||||
Handler: func(a struct{ Value string }, ctx context.Context) error { return nil },
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Equal(t, "taskq: context.Context must be the first parameter", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRegisterTaskHandlerSignatures tests various valid handler signatures
|
|
||||||
func TestRegisterTaskHandlerSignatures(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
handler interface{}
|
|
||||||
valid bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "no_params",
|
|
||||||
handler: func() error { return nil },
|
|
||||||
valid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "context_only",
|
|
||||||
handler: func(ctx context.Context) error { return nil },
|
|
||||||
valid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "data_only",
|
|
||||||
handler: func(data struct{ Value string }) error { return nil },
|
|
||||||
valid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "context_and_data",
|
|
||||||
handler: func(ctx context.Context, data struct{ Value string }) error { return nil },
|
|
||||||
valid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no_error_return",
|
|
||||||
handler: func() {},
|
|
||||||
valid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid_param",
|
|
||||||
handler: func(a int) error { return nil },
|
|
||||||
valid: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
s := NewServlet()
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "default",
|
|
||||||
Handler: tc.handler,
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
if tc.valid {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
} else {
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestQueuePriority tests queue priority assignment
|
|
||||||
func TestQueuePriority(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
task1 := &Task{
|
|
||||||
Name: "task1",
|
|
||||||
Queue: "high",
|
|
||||||
Priority: 10,
|
|
||||||
Handler: func() error { return nil },
|
|
||||||
}
|
|
||||||
task2 := &Task{
|
|
||||||
Name: "task2",
|
|
||||||
Queue: "low",
|
|
||||||
Priority: 1,
|
|
||||||
Handler: func() error { return nil },
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task1, task2},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
queues := s.Queues()
|
|
||||||
assert.Equal(t, 10, queues["high"])
|
|
||||||
assert.Equal(t, 1, queues["low"])
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestClient tests Client method
|
|
||||||
func TestClient(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
client := s.Client()
|
|
||||||
assert.NotNil(t, client)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRedisClient tests RedisClient method
|
|
||||||
func TestRedisClient(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
redisClient := s.RedisClient()
|
|
||||||
assert.NotNil(t, redisClient)
|
|
||||||
assert.Equal(t, rdb, redisClient)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestQueues tests Queues method returns a copy
|
|
||||||
func TestQueues(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
task := &Task{
|
|
||||||
Name: "task1",
|
|
||||||
Queue: "default",
|
|
||||||
Priority: 5,
|
|
||||||
Handler: func() error { return nil },
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
queues1 := s.Queues()
|
|
||||||
queues1["modified"] = 999
|
|
||||||
|
|
||||||
queues2 := s.Queues()
|
|
||||||
assert.NotContains(t, queues2, "modified")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestInitWithoutPlugins tests Init without plugins
|
|
||||||
func TestInitWithoutPlugins(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
err = s.Init(ctx)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestInitWithPlugins tests Init with plugins
|
|
||||||
func TestInitWithPlugins(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
mockPlugin := &MockPlugin{}
|
|
||||||
s := NewServlet()
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{},
|
|
||||||
Plugins: []Plugin{mockPlugin},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
err = s.Init(ctx)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.True(t, mockPlugin.initCalled)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestStartStop tests Start and Stop lifecycle
|
|
||||||
func TestStartStop(t *testing.T) {
|
|
||||||
// Skip this test as Start creates blocking goroutines
|
|
||||||
t.Skip("Start creates blocking goroutines that don't clean up properly")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestStartWithPlugins tests Start with plugins
|
|
||||||
func TestStartWithPlugins(t *testing.T) {
|
|
||||||
// Skip this test as Start creates blocking goroutines
|
|
||||||
t.Skip("Start creates blocking goroutines that don't clean up properly")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMultipleTasks tests registering multiple tasks
|
|
||||||
func TestMultipleTasks(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
tasks := []*Task{
|
|
||||||
{
|
|
||||||
Name: "task1",
|
|
||||||
Queue: "default",
|
|
||||||
Handler: func() error { return nil },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "task2",
|
|
||||||
Queue: "high",
|
|
||||||
Priority: 10,
|
|
||||||
Handler: func(ctx context.Context) error { return nil },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "task3",
|
|
||||||
Queue: "low",
|
|
||||||
Priority: 1,
|
|
||||||
Handler: func(data struct{ Value string }) error { return nil },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: tasks,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, 3, len(s.handlers))
|
|
||||||
assert.Equal(t, 3, len(s.queues))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockPlugin for testing plugin lifecycle
|
|
||||||
type MockPlugin struct {
|
|
||||||
initCalled bool
|
|
||||||
startCalled bool
|
|
||||||
stopCalled bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mp *MockPlugin) Name() string {
|
|
||||||
return "MockPlugin"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mp *MockPlugin) Init(ctx *Context) error {
|
|
||||||
mp.initCalled = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mp *MockPlugin) Start(ctx *Context) error {
|
|
||||||
mp.startCalled = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mp *MockPlugin) Stop() error {
|
|
||||||
mp.stopCalled = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FailingPlugin for testing error handling
|
|
||||||
type FailingPlugin struct {
|
|
||||||
stage string // "init" or "start"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fp *FailingPlugin) Name() string {
|
|
||||||
return "FailingPlugin"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fp *FailingPlugin) Init(ctx *Context) error {
|
|
||||||
if fp.stage == "init" {
|
|
||||||
return errors.New("init failed")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fp *FailingPlugin) Start(ctx *Context) error {
|
|
||||||
if fp.stage == "start" {
|
|
||||||
return errors.New("start failed")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fp *FailingPlugin) Stop() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getTestRedis returns a real Redis client for testing
|
|
||||||
func getTestRedis(t *testing.T) redis.UniversalClient {
|
|
||||||
rdb := redis.NewClient(&redis.Options{
|
|
||||||
Addr: "localhost:6379",
|
|
||||||
DB: 15, // Use database 15 for testing to avoid data loss
|
|
||||||
})
|
|
||||||
|
|
||||||
// Test connection
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if err := rdb.Ping(ctx).Err(); err != nil {
|
|
||||||
t.Skipf("Redis is not running on localhost:6379: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up test database before test
|
|
||||||
if err := rdb.FlushDB(context.Background()).Err(); err != nil {
|
|
||||||
t.Fatalf("Failed to flush test database: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rdb
|
|
||||||
}
|
|
||||||
-386
@@ -1,386 +0,0 @@
|
|||||||
package taskq
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestTaskPublishMissingClient tests Publish without a configured client
|
|
||||||
func TestTaskPublishMissingClient(t *testing.T) {
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "default",
|
|
||||||
Handler: func() error { return nil },
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
err := task.Publish(ctx, struct{}{})
|
|
||||||
assert.Error(t, err)
|
|
||||||
// Error message may vary depending on implementation
|
|
||||||
assert.True(t, len(err.Error()) > 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestTaskPublishWithServlet tests Publish with servlet client
|
|
||||||
func TestTaskPublishWithServlet(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "default",
|
|
||||||
Handler: func(data struct{ Value string }) error { return nil },
|
|
||||||
servlet: s,
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
err = task.Publish(ctx, struct{ Value string }{Value: "test data"})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestTaskPublishWithDefaultServlet tests Publish with default servlet
|
|
||||||
func TestTaskPublishWithDefaultServlet(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "default",
|
|
||||||
Handler: func(data struct{ Value string }) error { return nil },
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := defaultServlet.Configure(cfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
err = task.Publish(ctx, struct{ Value string }{Value: "test data"})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestTaskPublishWithOptions tests Publish with various options
|
|
||||||
func TestTaskPublishWithOptions(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "default",
|
|
||||||
Handler: func(data struct{ Value string }) error { return nil },
|
|
||||||
servlet: s,
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Test with delay
|
|
||||||
err = task.Publish(ctx, struct{ Value string }{Value: "test"}, Delay(5*time.Second))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Test with TTR override
|
|
||||||
err = task.Publish(ctx, struct{ Value string }{Value: "test"}, TTR(30*time.Second))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Test with retention
|
|
||||||
err = task.Publish(ctx, struct{ Value string }{Value: "test"}, Retention(1*time.Hour))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Test with delay until
|
|
||||||
futureTime := time.Now().Add(1 * time.Hour)
|
|
||||||
err = task.Publish(ctx, struct{ Value string }{Value: "test"}, DelayUntil(futureTime))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestTaskPublishInvalidData tests Publish with non-serializable data
|
|
||||||
func TestTaskPublishInvalidData(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "default",
|
|
||||||
Handler: func() error { return nil },
|
|
||||||
servlet: s,
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Publish with channel (non-serializable)
|
|
||||||
err = task.Publish(ctx, make(chan int))
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestTaskPublishWithGroup tests Publish with group
|
|
||||||
func TestTaskPublishWithGroup(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "default",
|
|
||||||
Group: "test_group",
|
|
||||||
Handler: func(data struct{ Value string }) error { return nil },
|
|
||||||
servlet: s,
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
err = task.Publish(ctx, struct{ Value string }{Value: "test"})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestTaskPublishWithTTR tests Publish with TTR
|
|
||||||
func TestTaskPublishWithTTR(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "default",
|
|
||||||
TTR: 30 * time.Second,
|
|
||||||
Handler: func(data struct{ Value string }) error { return nil },
|
|
||||||
servlet: s,
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
err = task.Publish(ctx, struct{ Value string }{Value: "test"})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestTaskPublishMultipleTimes tests publishing the same task multiple times
|
|
||||||
func TestTaskPublishMultipleTimes(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "default",
|
|
||||||
Handler: func(data struct{ Value string }) error { return nil },
|
|
||||||
servlet: s,
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Publish multiple times
|
|
||||||
for i := 0; i < 5; i++ {
|
|
||||||
err = task.Publish(ctx, struct{ Value string }{Value: "test"})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestTaskPublishVariousDataTypes tests publishing with different data types
|
|
||||||
func TestTaskPublishVariousDataTypes(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
handler interface{}
|
|
||||||
data interface{}
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "struct_data",
|
|
||||||
handler: func(data struct{ Key string }) error { return nil },
|
|
||||||
data: struct{ Key string }{Key: "value"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no_params",
|
|
||||||
handler: func() error { return nil },
|
|
||||||
data: struct{}{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "context_only",
|
|
||||||
handler: func(ctx context.Context) error { return nil },
|
|
||||||
data: struct{}{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task_" + tc.name,
|
|
||||||
Queue: "default",
|
|
||||||
Handler: tc.handler,
|
|
||||||
servlet: s,
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
err = task.Publish(ctx, tc.data)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDelayOption tests Delay publish option
|
|
||||||
func TestDelayOption(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "default",
|
|
||||||
Handler: func(data struct{ Value string }) error { return nil },
|
|
||||||
servlet: s,
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
err = task.Publish(ctx, struct{ Value string }{Value: "test"}, Delay(5*time.Second))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDelayUntilOption tests DelayUntil publish option
|
|
||||||
func TestDelayUntilOption(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "default",
|
|
||||||
Handler: func(data struct{ Value string }) error { return nil },
|
|
||||||
servlet: s,
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
futureTime := time.Now().Add(10 * time.Minute)
|
|
||||||
err = task.Publish(ctx, struct{ Value string }{Value: "test"}, DelayUntil(futureTime))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestTTROption tests TTR publish option
|
|
||||||
func TestTTROption(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "default",
|
|
||||||
Handler: func(data struct{ Value string }) error { return nil },
|
|
||||||
servlet: s,
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
err = task.Publish(ctx, struct{ Value string }{Value: "test"}, TTR(60*time.Second))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRetentionOption tests Retention publish option
|
|
||||||
func TestRetentionOption(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "default",
|
|
||||||
Handler: func(data struct{ Value string }) error { return nil },
|
|
||||||
servlet: s,
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
err = task.Publish(ctx, struct{ Value string }{Value: "test"}, Retention(24*time.Hour))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
// 全局默认 Servlet 实例
|
// 全局默认 Servlet 实例
|
||||||
var defaultServlet = NewServlet()
|
var defaultServlet = NewServlet()
|
||||||
|
|
||||||
|
// 类型反射常量
|
||||||
var (
|
var (
|
||||||
errorType = reflect.TypeOf((*error)(nil)).Elem() // error 类型反射
|
errorType = reflect.TypeOf((*error)(nil)).Elem() // error 类型反射
|
||||||
contextType = reflect.TypeOf((*context.Context)(nil)).Elem() // context.Context 类型反射
|
contextType = reflect.TypeOf((*context.Context)(nil)).Elem() // context.Context 类型反射
|
||||||
@@ -24,12 +25,6 @@ func Default() *Servlet {
|
|||||||
return defaultServlet
|
return defaultServlet
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDefault 设置默认的 Servlet 实例
|
|
||||||
// 这是一个并发不安全的操作,建议在程序初始化阶段调用
|
|
||||||
func SetDefault(servlet *Servlet) {
|
|
||||||
defaultServlet = servlet
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure 配置默认 Servlet
|
// Configure 配置默认 Servlet
|
||||||
// 必须在 Init 之前调用
|
// 必须在 Init 之前调用
|
||||||
func Configure(cfg Config) error {
|
func Configure(cfg Config) error {
|
||||||
|
|||||||
-271
@@ -1,271 +0,0 @@
|
|||||||
package taskq
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestDefault tests Default function returns default servlet
|
|
||||||
func TestDefault(t *testing.T) {
|
|
||||||
servlet := Default()
|
|
||||||
assert.NotNil(t, servlet)
|
|
||||||
assert.Equal(t, defaultServlet, servlet)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSetDefault tests SetDefault function
|
|
||||||
func TestSetDefault(t *testing.T) {
|
|
||||||
originalDefault := defaultServlet
|
|
||||||
defer func() {
|
|
||||||
defaultServlet = originalDefault
|
|
||||||
}()
|
|
||||||
|
|
||||||
newServlet := NewServlet()
|
|
||||||
SetDefault(newServlet)
|
|
||||||
|
|
||||||
assert.Equal(t, newServlet, Default())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestConfigureDefault tests Configure function with default servlet
|
|
||||||
func TestConfigureDefault(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := Configure(cfg)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestConfigureDefaultMissingRedis tests Configure without Redis
|
|
||||||
func TestConfigureDefaultMissingRedis(t *testing.T) {
|
|
||||||
cfg := Config{
|
|
||||||
Redis: nil,
|
|
||||||
Tasks: []*Task{},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := Configure(cfg)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Equal(t, "taskq: redis client is required", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestInitDefault tests Init function with default servlet
|
|
||||||
func TestInitDefault(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := Configure(cfg)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
err = Init(ctx)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestStartDefault tests Start function with default servlet
|
|
||||||
|
|
||||||
// TestStopDefault tests Stop function with default servlet
|
|
||||||
func TestStopDefault(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure and initialize default servlet
|
|
||||||
err := Configure(cfg)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
err = Init(ctx)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Starting the real asynq server is an integration operation that
|
|
||||||
// can spawn background goroutines and is flaky in unit tests.
|
|
||||||
t.Skip("skipping Start/Stop in unit tests; run integration tests separately")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestStartBeforeInit tests Start called without Init
|
|
||||||
func TestStartBeforeInit(t *testing.T) {
|
|
||||||
// Starting a fresh Servlet without Configure is unsafe (would start with nil Redis client).
|
|
||||||
// We avoid calling Start here to prevent background goroutine panics; assert precondition instead.
|
|
||||||
newServlet := NewServlet()
|
|
||||||
if newServlet.RedisClient() == nil {
|
|
||||||
t.Log("New servlet has no Redis client; Start would be unsafe — skipping actual Start call")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// If a Redis client is present unexpectedly, attempt Start and ensure no error.
|
|
||||||
err := newServlet.Start(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("Start returned error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestFullLifecycleWithDefaultServlet tests complete lifecycle using package-level functions
|
|
||||||
func TestFullLifecycleWithDefaultServlet(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
task := &Task{
|
|
||||||
Name: "test_task",
|
|
||||||
Queue: "default",
|
|
||||||
Handler: func(ctx context.Context) error { return nil },
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure
|
|
||||||
err := Configure(cfg)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Initialize
|
|
||||||
ctx := context.Background()
|
|
||||||
err = Init(ctx)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Starting the real asynq server is an integration operation that
|
|
||||||
// can spawn background goroutines and is flaky in unit tests.
|
|
||||||
t.Skip("skipping Start/Stop lifecycle in unit tests; run integration tests separately")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestConfigureWithTasks tests Configure with multiple tasks
|
|
||||||
func TestConfigureWithTasks(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
tasks := []*Task{
|
|
||||||
{
|
|
||||||
Name: "task1",
|
|
||||||
Queue: "default",
|
|
||||||
Handler: func() error { return nil },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "task2",
|
|
||||||
Queue: "high",
|
|
||||||
Priority: 10,
|
|
||||||
Handler: func(ctx context.Context, data struct{ Value string }) error { return nil },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: tasks,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := Configure(cfg)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify tasks were registered
|
|
||||||
servlet := Default()
|
|
||||||
queues := servlet.Queues()
|
|
||||||
assert.Equal(t, 2, len(queues))
|
|
||||||
assert.Equal(t, 10, queues["high"])
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestConfigureWithPlugins tests Configure with plugins
|
|
||||||
func TestConfigureWithPlugins(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
mockPlugin := &MockPlugin{}
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{},
|
|
||||||
Plugins: []Plugin{mockPlugin},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := Configure(cfg)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
err = Init(ctx)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.True(t, mockPlugin.initCalled)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestInitBeforeConfigure tests Init called without Configure
|
|
||||||
func TestInitBeforeConfigure(t *testing.T) {
|
|
||||||
// Init should be safe to call even if Configure was not called (no plugins)
|
|
||||||
newServlet := NewServlet()
|
|
||||||
err := newServlet.Init(context.Background())
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMultipleConfigure tests that Configure can be called multiple times
|
|
||||||
// (though this might reset previous configuration)
|
|
||||||
func TestMultipleConfigure(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
|
|
||||||
task1 := &Task{
|
|
||||||
Name: "task1",
|
|
||||||
Queue: "default",
|
|
||||||
Handler: func() error { return nil },
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg1 := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task1},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg1)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, 1, len(s.handlers))
|
|
||||||
|
|
||||||
task2 := &Task{
|
|
||||||
Name: "task2",
|
|
||||||
Queue: "default",
|
|
||||||
Handler: func() error { return nil },
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg2 := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{task1, task2},
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.Configure(cfg2)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, 2, len(s.handlers))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestContextCancellation tests that Start respects context cancellation
|
|
||||||
func TestContextCancellation(t *testing.T) {
|
|
||||||
rdb := getTestRedis(t)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
s := NewServlet()
|
|
||||||
cfg := Config{
|
|
||||||
Redis: rdb,
|
|
||||||
Tasks: []*Task{},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.Configure(cfg)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
err = s.Init(ctx)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Use a cancellable context and cancel immediately to ensure Start handles cancellation
|
|
||||||
cancelCtx, cancel := context.WithCancel(context.Background())
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
// Start with already-cancelled context; Start should return quickly and not leak
|
|
||||||
err = s.Start(cancelCtx)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
package inspector
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.tczkiot.com/wlw/taskq"
|
|
||||||
"github.com/hibiken/asynq"
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewDefaultsAndName(t *testing.T) {
|
|
||||||
ins := New(Options{})
|
|
||||||
if ins == nil {
|
|
||||||
t.Fatalf("New returned nil")
|
|
||||||
}
|
|
||||||
if ins.opts.Interval <= 0 {
|
|
||||||
t.Fatalf("expected default Interval > 0")
|
|
||||||
}
|
|
||||||
if ins.opts.DBPath == "" {
|
|
||||||
t.Fatalf("expected default DBPath set")
|
|
||||||
}
|
|
||||||
if ins.Name() != "inspector" {
|
|
||||||
t.Fatalf("unexpected Name: %s", ins.Name())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConvertTaskHelpers(t *testing.T) {
|
|
||||||
// ensure convertTaskInfo/convertTaskList are callable
|
|
||||||
_ = convertTaskInfo(&asynq.TaskInfo{})
|
|
||||||
_ = convertTaskList([]*asynq.TaskInfo{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetQueueInfoWhenNotStarted(t *testing.T) {
|
|
||||||
ins := New(Options{})
|
|
||||||
if _, err := ins.GetQueueInfo("default"); err == nil {
|
|
||||||
t.Fatalf("expected error when inspector not started")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestListActiveTasksWhenNotStarted(t *testing.T) {
|
|
||||||
ins := New(Options{})
|
|
||||||
if _, err := ins.ListActiveTasks("default", 10, 0); err == nil {
|
|
||||||
t.Fatalf("expected error when inspector not started")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSaveMetrics_NoDB(t *testing.T) {
|
|
||||||
ins := New(Options{})
|
|
||||||
// db is nil by default; saveMetrics should return nil (no-op)
|
|
||||||
s := Stats{Queue: "q", Timestamp: time.Now().Unix()}
|
|
||||||
if err := ins.saveMetrics(s); err != nil {
|
|
||||||
t.Fatalf("saveMetrics returned error with nil db: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper: create redis client for tests
|
|
||||||
func makeTestRedis(t *testing.T) redis.UniversalClient {
|
|
||||||
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379", DB: 15})
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
if err := rdb.Ping(ctx).Err(); err != nil {
|
|
||||||
t.Skipf("redis not available: %v", err)
|
|
||||||
}
|
|
||||||
// flush test DB
|
|
||||||
if err := rdb.FlushDB(context.Background()).Err(); err != nil {
|
|
||||||
t.Fatalf("failed to flush redis: %v", err)
|
|
||||||
}
|
|
||||||
return rdb
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPluginLifecycleWithServlet(t *testing.T) {
|
|
||||||
rdb := makeTestRedis(t)
|
|
||||||
// create plugin
|
|
||||||
ins := New(Options{DBPath: ":memory:", Interval: time.Second})
|
|
||||||
|
|
||||||
// wire into a fresh servlet as default to use package-level helpers
|
|
||||||
s := taskq.NewServlet()
|
|
||||||
taskq.SetDefault(s)
|
|
||||||
|
|
||||||
cfg := taskq.Config{Redis: rdb, Tasks: []*taskq.Task{}, Plugins: []taskq.Plugin{ins}}
|
|
||||||
if err := taskq.Configure(cfg); err != nil {
|
|
||||||
t.Fatalf("Configure failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := taskq.Init(context.Background()); err != nil {
|
|
||||||
t.Fatalf("Init failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := taskq.Start(context.Background()); err != nil {
|
|
||||||
t.Fatalf("Start failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// request stop
|
|
||||||
taskq.Stop()
|
|
||||||
// wait for plugins and internal goroutines to shutdown
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
|
|
||||||
// close redis
|
|
||||||
rdb.Close()
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package metrics
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.tczkiot.com/wlw/taskq"
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewDefaultsAndName(t *testing.T) {
|
|
||||||
m := New(Options{})
|
|
||||||
if m == nil {
|
|
||||||
t.Fatalf("New returned nil")
|
|
||||||
}
|
|
||||||
if m.opts.Namespace == "" {
|
|
||||||
t.Fatalf("expected default Namespace")
|
|
||||||
}
|
|
||||||
if m.Name() != "metrics" {
|
|
||||||
t.Fatalf("unexpected Name: %s", m.Name())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCollectNoInspectorNoQueues(t *testing.T) {
|
|
||||||
m := New(Options{Interval: time.Millisecond})
|
|
||||||
// ensure collect() is safe to call when inspector or queues are nil
|
|
||||||
m.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeTestRedis(t *testing.T) redis.UniversalClient {
|
|
||||||
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379", DB: 15})
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
if err := rdb.Ping(ctx).Err(); err != nil {
|
|
||||||
t.Skipf("redis not available: %v", err)
|
|
||||||
}
|
|
||||||
if err := rdb.FlushDB(context.Background()).Err(); err != nil {
|
|
||||||
t.Fatalf("failed to flush redis: %v", err)
|
|
||||||
}
|
|
||||||
return rdb
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMetricsLifecycleWithServlet(t *testing.T) {
|
|
||||||
rdb := makeTestRedis(t)
|
|
||||||
m := New(Options{Interval: time.Second})
|
|
||||||
|
|
||||||
// register plugin via default servlet
|
|
||||||
s := taskq.NewServlet()
|
|
||||||
taskq.SetDefault(s)
|
|
||||||
|
|
||||||
cfg := taskq.Config{Redis: rdb, Tasks: []*taskq.Task{}, Plugins: []taskq.Plugin{m}}
|
|
||||||
if err := taskq.Configure(cfg); err != nil {
|
|
||||||
t.Fatalf("Configure failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := taskq.Init(context.Background()); err != nil {
|
|
||||||
t.Fatalf("Init failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := taskq.Start(context.Background()); err != nil {
|
|
||||||
t.Fatalf("Start failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
taskq.Stop()
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
rdb.Close()
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package monitor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"code.tczkiot.com/wlw/taskq/x/inspector"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewValidatesOptions(t *testing.T) {
|
|
||||||
_, err := New(Options{})
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected error when options missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
// valid case
|
|
||||||
ins := &inspector.Inspector{}
|
|
||||||
m, err := New(Options{Inspector: ins, Queues: map[string]int{"default": 1}})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if m.RootPath() != "/monitor" {
|
|
||||||
t.Fatalf("unexpected root path: %s", m.RootPath())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleIndexServesUI(t *testing.T) {
|
|
||||||
ins := &inspector.Inspector{}
|
|
||||||
m, err := New(Options{Inspector: ins, Queues: map[string]int{"default": 1}, RootPath: "/my"})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/my/", nil)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
m.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
resp := w.Result()
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
t.Fatalf("expected 200 OK, got %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
// body should contain the root path replacement
|
|
||||||
body := w.Body.String()
|
|
||||||
if body == "" {
|
|
||||||
t.Fatalf("expected non-empty index body")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user