GoWind CMS integrates an Asynq-based distributed task queue in the Core Service to handle asynchronous operations such as index synchronization, notification push (SSE), and data cleanup. This tutorial explains the configuration, definition, and management of task scheduling in the CMS.
| Comparison | Admin | CMS |
|---|
| Deployment location | Inside the Admin Service | Inside the Core Service |
| Task types | System-level (email/cleanup) | Content-level (index/sync/push) |
| Task sources | Backend operations | Backend + frontend user operations |
server:
asynq:
uri: "redis://:*Abcd123456@redis:6379/1"
enable_gracefully_shutdown: true
shutdown_timeout: 3s
codec: "json"
concurrency: 10
queues:
critical: 10
default: 5
low: 1
func NewAsynqServer(ctx *bootstrap.Context, logger log.Logger) *asynq.Server {
cfg := ctx.GetConfig().GetServer().GetAsynq()
srv := asynq.NewServer(
asynq.RedisClientOpt{Addr: cfg.Uri},
asynq.Config{
Concurrency: int(cfg.Concurrency),
Queues: parseQueues(cfg.Queues),
},
)
mux := asynq.NewServeMux()
mux.HandleFunc(task.TypeIndexPost, HandleIndexPost)
mux.HandleFunc(task.TypeSendNotification, HandleSendNotification)
mux.HandleFunc(task.TypeCleanupExpiredData, HandleCleanupExpiredData)
mux.HandleFunc(task.TypeGenerateReport, HandleGenerateReport)
mux.HandleFunc(task.TypeSyncTranslations, HandleSyncTranslations)
return srv
}
const (
TypeIndexPost = "post:index"
TypeIndexPage = "page:index"
TypeRebuildIndex = "search:rebuild_index"
TypeSendNotification = "notification:send"
TypeSendEmail = "email:send"
TypeCleanupExpiredData = "cleanup:expired_data"
TypeCleanupTempFiles = "cleanup:temp_files"
TypeSyncTranslations = "translation:sync"
TypeGenerateReport = "report:generate"
TypeUpdateViewCount = "stats:update_view_count"
)
type IndexPostPayload struct {
PostId uint32 `json:"postId"`
Action string `json:"action"`
}
type SendNotificationPayload struct {
UserId uint32 `json:"userId"`
Title string `json:"title"`
Content string `json:"content"`
Type string `json:"type"`
}
type CleanupExpiredDataPayload struct {
EntityType string `json:"entityType"`
OlderThan int64 `json:"olderThan"`
}
type UpdateViewCountPayload struct {
PostId uint32 `json:"postId"`
Increment uint32 `json:"increment"`
}
func HandleIndexPost(ctx context.Context, t *asynq.Task) error {
var payload task.IndexPostPayload
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
return fmt.Errorf("解析载荷失败: %w", err)
}
switch payload.Action {
case "create", "update":
post, err := postRepo.Get(ctx, &contentV1.GetPostRequest{
QueryBy: &contentV1.GetPostRequest_Id{Id: payload.PostId},
})
if err != nil {
return err
}
return searchClient.Index(ctx, "cms_posts", post)
case "delete":
return searchClient.Delete(ctx, "cms_posts", fmt.Sprint(payload.PostId))
}
return nil
}
func HandleSendNotification(ctx context.Context, t *asynq.Task) error {
var payload task.SendNotificationPayload
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
return err
}
_, err := internalMessageRepo.Create(ctx, &internalMessageV1.CreateInternalMessageRequest{
Data: &internalMessageV1.InternalMessage{
UserId: payload.UserId,
Title: payload.Title,
Content: payload.Content,
Type: payload.Type,
},
})
if err != nil {
return err
}
sseServer.Broadcast(ctx, payload.UserId, map[string]any{
"event": "notification",
"title": payload.Title,
"content": payload.Content,
})
return nil
}
func HandleCleanupExpiredData(ctx context.Context, t *asynq.Task) error {
var payload task.CleanupExpiredDataPayload
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
return err
}
switch payload.EntityType {
case "posts":
_, err := postRepo.PermanentlyDelete(ctx, payload.OlderThan)
return err
case "comments":
_, err := commentRepo.CleanupRejected(ctx, payload.OlderThan)
return err
case "files":
return cleanupTempFiles(payload.OlderThan)
}
return nil
}
func HandleUpdateViewCount(ctx context.Context, t *asynq.Task) error {
var payload task.UpdateViewCountPayload
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
return err
}
cacheKey := fmt.Sprintf("post:view:%d", payload.PostId)
count, err := redisClient.IncrBy(ctx, cacheKey, int64(payload.Increment)).Result()
if err != nil {
return err
}
if count%100 == 0 {
return postRepo.UpdateViewCount(ctx, payload.PostId, count)
}
return nil
}
func (s *PostService) Create(ctx context.Context, req *contentV1.CreatePostRequest) (*contentV1.Post, error) {
post, err := s.postRepo.Create(ctx, req)
if err != nil {
return nil, err
}
payload, _ := json.Marshal(task.IndexPostPayload{
PostId: post.Id,
Action: "create",
})
s.asynqClient.Enqueue(
asynq.NewTask(task.TypeIndexPost, payload),
asynq.Queue("default"),
)
return post, nil
}
func (s *CommentService) Create(ctx context.Context, req *commentV1.CreateCommentRequest) (*commentV1.Comment, error) {
comment, err := s.commentRepo.Create(ctx, req)
if err != nil {
return nil, err
}
payload, _ := json.Marshal(task.SendNotificationPayload{
UserId: post.AuthorId,
Title: "新评论通知",
Content: fmt.Sprintf("您的文章收到了新评论"),
Type: "comment",
})
s.asynqClient.Enqueue(
asynq.NewTask(task.TypeSendNotification, payload),
asynn.Queue("critical"),
)
return comment, nil
}
func RegisterScheduledTasks(scheduler *asynq.Scheduler) {
scheduler.Register("0 3 * * *", asynq.NewTask(
task.TypeCleanupExpiredData,
jsonMustMarshal(task.CleanupExpiredDataPayload{
EntityType: "posts",
OlderThan: time.Now().AddDate(0, 0, -30).Unix(),
}),
))
scheduler.Register("0 * * * *", asynq.NewTask(
task.TypeSyncTranslations,
nil,
))
scheduler.Register("0 9 * * 1", asynq.NewTask(
task.TypeGenerateReport,
jsonMustMarshal(task.GenerateReportPayload{
Type: "weekly",
}),
))
}
The CMS provides a visual task management interface:
# 查看任务列表
GET /admin/v1/tasks?page=1&pageSize=20
# 立即执行任务
POST /admin/v1/tasks/1/run
# 暂停定时任务
PUT /admin/v1/tasks/1/pause
# 查看任务执行日志
GET /admin/v1/tasks/1/logs?page=1
Asynq provides a Web UI for monitoring task status:
asynqmon --port=8080 --redis-addr=localhost:6379
| Monitoring Item | Description |
|---|
| Active Tasks | Tasks currently executing |
| Pending Tasks | Tasks waiting to execute |
| Retry Tasks | Tasks being retried |
| Dead Tasks | Failed tasks |
| Scheduled Tasks | Scheduled tasks |
| Processed/Failed | Statistics |
| Principle | Description |
|---|
| Idempotency | Repeated task execution should not produce side effects |
| Timeout control | Set reasonable timeout values |
| Retry strategy | Exponential backoff retries |
| Priority | Critical tasks get high priority |
s.asynqClient.Enqueue(
asynq.NewTask(task.TypeIndexPost, payload),
asynq.Queue("default"),
asynq.MaxRetry(5),
asynq.RetryDelay(30*time.Second),
asynq.Timeout(2*time.Minute),
)
| Check Item | Description |
|---|
| Asynq configuration | Redis connection + queue priorities |
| Task handler registration | All task types registered in the ServeMux |
| Task enqueueing | Tasks correctly enqueued in business logic |
| Scheduled tasks | cron expression configuration |
| Task monitoring | Asynqmon Web UI |
| Retry strategy | Exponential backoff + max retries |
| Idempotency | Tasks can be safely retried |