feat(P1): MQ Worker 死信队列 — 重试上限3次后 Ack 移入 DLX

This commit is contained in:
Sisyphus
2026-04-25 15:54:55 +08:00
parent d68a4f3f65
commit 9d903ad8e1
6 changed files with 637 additions and 554 deletions

View File

@@ -0,0 +1,53 @@
package rabbitmq
import (
"log"
amqp "github.com/rabbitmq/amqp091-go"
)
const (
DLXExchange = "dlx.events"
MaxRetryCount = 3
)
// DeclareDLX 声明死信交换机和对应的死信队列
func DeclareDLX(ch *amqp.Channel, queueName string) error {
if ch == nil {
return nil
}
if err := ch.ExchangeDeclare(
DLXExchange, "topic", true, false, false, false, nil,
); err != nil {
return err
}
dlxQueue := queueName + ".dlx"
_, err := ch.QueueDeclare(
dlxQueue, true, false, false, false, nil,
)
if err != nil {
return err
}
if err := ch.QueueBind(dlxQueue, "#", DLXExchange, false, nil); err != nil {
return err
}
log.Printf("DLX ready: exchange=%s queue=%s", DLXExchange, dlxQueue)
return nil
}
// GetRetryCount 从 AMQP x-death header 中提取当前消息已被重试的次数
func GetRetryCount(d amqp.Delivery) int {
deaths, ok := d.Headers["x-death"].([]interface{})
if !ok || len(deaths) == 0 {
return 0
}
death, ok := deaths[0].(amqp.Table)
if !ok {
return 0
}
count, ok := death["count"].(int64)
if !ok {
return 0
}
return int(count)
}

View File

@@ -1,116 +1,123 @@
package rabbitmq package rabbitmq
import ( import (
"context" "context"
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"feedsystem_video_go/internal/config" "feedsystem_video_go/internal/config"
"strconv" "log"
"time" "strconv"
"time"
amqp "github.com/rabbitmq/amqp091-go"
) amqp "github.com/rabbitmq/amqp091-go"
)
type RabbitMQ struct {
Conn *amqp.Connection type RabbitMQ struct {
Ch *amqp.Channel Conn *amqp.Connection
} Ch *amqp.Channel
}
func NewRabbitMQ(cfg *config.RabbitMQConfig) (*RabbitMQ, error) {
if cfg == nil { func NewRabbitMQ(cfg *config.RabbitMQConfig) (*RabbitMQ, error) {
return nil, errors.New("rabbitmq config is nil") if cfg == nil {
} return nil, errors.New("rabbitmq config is nil")
url := "amqp://" + cfg.Username + ":" + cfg.Password + "@" + cfg.Host + ":" + strconv.Itoa(cfg.Port) + "/" }
conn, err := amqp.Dial(url) url := "amqp://" + cfg.Username + ":" + cfg.Password + "@" + cfg.Host + ":" + strconv.Itoa(cfg.Port) + "/"
if err != nil { conn, err := amqp.Dial(url)
return nil, err if err != nil {
} return nil, err
ch, err := conn.Channel() }
if err != nil { ch, err := conn.Channel()
return nil, err if err != nil {
} return nil, err
return &RabbitMQ{Conn: conn, Ch: ch}, nil }
} return &RabbitMQ{Conn: conn, Ch: ch}, nil
}
func (r *RabbitMQ) Close() error {
if r == nil || r.Ch == nil || r.Conn == nil { func (r *RabbitMQ) Close() error {
return nil if r == nil || r.Ch == nil || r.Conn == nil {
} return nil
if err := r.Ch.Close(); err != nil { }
return err if err := r.Ch.Close(); err != nil {
} return err
if err := r.Conn.Close(); err != nil { }
return err if err := r.Conn.Close(); err != nil {
} return err
return nil }
} return nil
}
func (r *RabbitMQ) DeclareTopic(exchange string, queue string, bindingKey string) error {
if r == nil || r.Ch == nil { func (r *RabbitMQ) DeclareTopic(exchange string, queue string, bindingKey string) error {
return errors.New("rabbitmq is not initialized") if r == nil || r.Ch == nil {
} return errors.New("rabbitmq is not initialized")
if exchange == "" || queue == "" || bindingKey == "" { }
return errors.New("exchange/queue/bindingKey is required") if exchange == "" || queue == "" || bindingKey == "" {
} return errors.New("exchange/queue/bindingKey is required")
}
if err := r.Ch.ExchangeDeclare(
exchange, if err := r.Ch.ExchangeDeclare(
"topic", exchange,
true, "topic",
false, true,
false, false,
false, false,
nil, false,
); err != nil { nil,
return err ); err != nil {
} return err
}
q, err := r.Ch.QueueDeclare(
queue, q, err := r.Ch.QueueDeclare(
true, queue,
false, true,
false, false,
false, false,
nil, false,
) amqp.Table{"x-dead-letter-exchange": DLXExchange},
if err != nil { )
return err if err != nil {
} return err
}
return r.Ch.QueueBind(
q.Name, if err := r.Ch.QueueBind(
bindingKey, q.Name,
exchange, bindingKey,
false, exchange,
nil, false,
) nil,
} ); err != nil {
return err
func (r *RabbitMQ) PublishJSON(ctx context.Context, exchange string, routingKey string, payload any) error { }
if r == nil || r.Ch == nil { if err := DeclareDLX(r.Ch, queue); err != nil {
return errors.New("rabbitmq is not initialized") log.Printf("DLX declare failed for %s: %v", queue, err)
} }
if exchange == "" || routingKey == "" { return nil
return errors.New("exchange and routingKey are required") }
}
b, err := json.Marshal(payload) func (r *RabbitMQ) PublishJSON(ctx context.Context, exchange string, routingKey string, payload any) error {
if err != nil { if r == nil || r.Ch == nil {
return err return errors.New("rabbitmq is not initialized")
} }
return r.Ch.PublishWithContext(ctx, exchange, routingKey, false, false, amqp.Publishing{ if exchange == "" || routingKey == "" {
ContentType: "application/json", return errors.New("exchange and routingKey are required")
DeliveryMode: amqp.Persistent, }
Timestamp: time.Now(), b, err := json.Marshal(payload)
Body: b, if err != nil {
}) return err
} }
return r.Ch.PublishWithContext(ctx, exchange, routingKey, false, false, amqp.Publishing{
func newEventID(n int) (string, error) { ContentType: "application/json",
b := make([]byte, n) DeliveryMode: amqp.Persistent,
if _, err := rand.Read(b); err != nil { Timestamp: time.Now(),
return "", err Body: b,
} })
return hex.EncodeToString(b), nil }
}
func newEventID(n int) (string, error) {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}

View File

@@ -1,122 +1,128 @@
package worker package worker
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"feedsystem_video_go/internal/middleware/rabbitmq" "feedsystem_video_go/internal/middleware/rabbitmq"
"feedsystem_video_go/internal/video" "feedsystem_video_go/internal/video"
"log" "log"
"strings" "strings"
amqp "github.com/rabbitmq/amqp091-go" amqp "github.com/rabbitmq/amqp091-go"
) )
type CommentWorker struct { type CommentWorker struct {
ch *amqp.Channel ch *amqp.Channel
comments *video.CommentRepository comments *video.CommentRepository
videos *video.VideoRepository videos *video.VideoRepository
queue string queue string
} }
func NewCommentWorker(ch *amqp.Channel, comments *video.CommentRepository, videos *video.VideoRepository, queue string) *CommentWorker { func NewCommentWorker(ch *amqp.Channel, comments *video.CommentRepository, videos *video.VideoRepository, queue string) *CommentWorker {
return &CommentWorker{ch: ch, comments: comments, videos: videos, queue: queue} return &CommentWorker{ch: ch, comments: comments, videos: videos, queue: queue}
} }
func (w *CommentWorker) Run(ctx context.Context) error { func (w *CommentWorker) Run(ctx context.Context) error {
if w == nil || w.ch == nil || w.comments == nil || w.videos == nil { if w == nil || w.ch == nil || w.comments == nil || w.videos == nil {
return errors.New("comment worker is not initialized") return errors.New("comment worker is not initialized")
} }
if w.queue == "" { if w.queue == "" {
return errors.New("queue is required") return errors.New("queue is required")
} }
deliveries, err := w.ch.Consume( deliveries, err := w.ch.Consume(
w.queue, w.queue,
"", "",
false, false,
false, false,
false, false,
false, false,
nil, nil,
) )
if err != nil { if err != nil {
return err return err
} }
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
case d, ok := <-deliveries: case d, ok := <-deliveries:
if !ok { if !ok {
return errors.New("deliveries channel closed") return errors.New("deliveries channel closed")
} }
w.handleDelivery(ctx, d) w.handleDelivery(ctx, d)
} }
} }
} }
func (w *CommentWorker) handleDelivery(ctx context.Context, d amqp.Delivery) { func (w *CommentWorker) handleDelivery(ctx context.Context, d amqp.Delivery) {
if err := w.process(ctx, d.Body); err != nil { if err := w.process(ctx, d.Body); err != nil {
log.Printf("comment worker: failed to process message: %v", err) retryCount := rabbitmq.GetRetryCount(d)
_ = d.Nack(false, true) if retryCount >= rabbitmq.MaxRetryCount {
return log.Printf("comment worker: max retries exceeded (%d), moving to DLX: %v", retryCount, err)
} _ = d.Ack(false)
_ = d.Ack(false) return
} }
log.Printf("comment worker: failed (retry %d/%d): %v", retryCount+1, rabbitmq.MaxRetryCount, err)
func (w *CommentWorker) process(ctx context.Context, body []byte) error { _ = d.Nack(false, true)
var evt rabbitmq.CommentEvent return
if err := json.Unmarshal(body, &evt); err != nil { }
return nil _ = d.Ack(false)
} }
switch evt.Action {
case "publish": func (w *CommentWorker) process(ctx context.Context, body []byte) error {
return w.applyPublish(ctx, &evt) var evt rabbitmq.CommentEvent
case "delete": if err := json.Unmarshal(body, &evt); err != nil {
return w.applyDelete(ctx, &evt) return nil
default: }
return nil switch evt.Action {
} case "publish":
} return w.applyPublish(ctx, &evt)
case "delete":
func (w *CommentWorker) applyPublish(ctx context.Context, evt *rabbitmq.CommentEvent) error { return w.applyDelete(ctx, &evt)
if evt == nil || evt.VideoID == 0 || evt.AuthorID == 0 || strings.TrimSpace(evt.Content) == "" { default:
return nil return nil
} }
}
ok, err := w.videos.IsExist(ctx, evt.VideoID)
if err != nil { func (w *CommentWorker) applyPublish(ctx context.Context, evt *rabbitmq.CommentEvent) error {
return err if evt == nil || evt.VideoID == 0 || evt.AuthorID == 0 || strings.TrimSpace(evt.Content) == "" {
} return nil
if !ok { }
return nil
} ok, err := w.videos.IsExist(ctx, evt.VideoID)
if err != nil {
c := &video.Comment{ return err
Username: strings.TrimSpace(evt.Username), }
VideoID: evt.VideoID, if !ok {
AuthorID: evt.AuthorID, return nil
Content: strings.TrimSpace(evt.Content), }
}
if err := w.comments.CreateComment(ctx, c); err != nil { c := &video.Comment{
return err Username: strings.TrimSpace(evt.Username),
} VideoID: evt.VideoID,
return w.videos.ChangePopularity(ctx, evt.VideoID, 1) AuthorID: evt.AuthorID,
} Content: strings.TrimSpace(evt.Content),
}
func (w *CommentWorker) applyDelete(ctx context.Context, evt *rabbitmq.CommentEvent) error { if err := w.comments.CreateComment(ctx, c); err != nil {
if evt == nil || evt.CommentID == 0 { return err
return nil }
} return w.videos.ChangePopularity(ctx, evt.VideoID, 1)
c, err := w.comments.GetByID(ctx, evt.CommentID) }
if err != nil {
return err func (w *CommentWorker) applyDelete(ctx context.Context, evt *rabbitmq.CommentEvent) error {
} if evt == nil || evt.CommentID == 0 {
if c == nil { return nil
return nil }
} c, err := w.comments.GetByID(ctx, evt.CommentID)
return w.comments.DeleteComment(ctx, c) if err != nil {
} return err
}
if c == nil {
return nil
}
return w.comments.DeleteComment(ctx, c)
}

View File

@@ -1,136 +1,142 @@
package worker package worker
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"feedsystem_video_go/internal/middleware/rabbitmq" "feedsystem_video_go/internal/middleware/rabbitmq"
"feedsystem_video_go/internal/video" "feedsystem_video_go/internal/video"
"log" "log"
amqp "github.com/rabbitmq/amqp091-go" amqp "github.com/rabbitmq/amqp091-go"
"time" "time"
) )
type LikeWorker struct { type LikeWorker struct {
ch *amqp.Channel ch *amqp.Channel
likes *video.LikeRepository likes *video.LikeRepository
videos *video.VideoRepository videos *video.VideoRepository
queue string queue string
} }
func NewLikeWorker(ch *amqp.Channel, likes *video.LikeRepository, videos *video.VideoRepository, queue string) *LikeWorker { func NewLikeWorker(ch *amqp.Channel, likes *video.LikeRepository, videos *video.VideoRepository, queue string) *LikeWorker {
return &LikeWorker{ch: ch, likes: likes, videos: videos, queue: queue} return &LikeWorker{ch: ch, likes: likes, videos: videos, queue: queue}
} }
func (w *LikeWorker) Run(ctx context.Context) error { func (w *LikeWorker) Run(ctx context.Context) error {
if w == nil || w.ch == nil || w.likes == nil || w.videos == nil { if w == nil || w.ch == nil || w.likes == nil || w.videos == nil {
return errors.New("like worker is not initialized") return errors.New("like worker is not initialized")
} }
if w.queue == "" { if w.queue == "" {
return errors.New("queue is required") return errors.New("queue is required")
} }
deliveries, err := w.ch.Consume( deliveries, err := w.ch.Consume(
w.queue, w.queue,
"", "",
false, false,
false, false,
false, false,
false, false,
nil, nil,
) )
if err != nil { if err != nil {
return err return err
} }
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
case d, ok := <-deliveries: case d, ok := <-deliveries:
if !ok { if !ok {
return errors.New("deliveries channel closed") return errors.New("deliveries channel closed")
} }
w.handleDelivery(ctx, d) w.handleDelivery(ctx, d)
} }
} }
} }
func (w *LikeWorker) handleDelivery(ctx context.Context, d amqp.Delivery) { func (w *LikeWorker) handleDelivery(ctx context.Context, d amqp.Delivery) {
if err := w.process(ctx, d.Body); err != nil { if err := w.process(ctx, d.Body); err != nil {
log.Printf("like worker: failed to process message: %v", err) retryCount := rabbitmq.GetRetryCount(d)
_ = d.Nack(false, true) if retryCount >= rabbitmq.MaxRetryCount {
return log.Printf("like worker: max retries exceeded (%d), moving to DLX: %v", retryCount, err)
} _ = d.Ack(false)
_ = d.Ack(false) return
} }
log.Printf("like worker: failed (retry %d/%d): %v", retryCount+1, rabbitmq.MaxRetryCount, err)
func (w *LikeWorker) process(ctx context.Context, body []byte) error { _ = d.Nack(false, true)
var evt rabbitmq.LikeEvent return
if err := json.Unmarshal(body, &evt); err != nil { }
// 解析事件失败,直接丢弃 _ = d.Ack(false)
return nil }
}
if evt.UserID == 0 || evt.VideoID == 0 { func (w *LikeWorker) process(ctx context.Context, body []byte) error {
return nil var evt rabbitmq.LikeEvent
} if err := json.Unmarshal(body, &evt); err != nil {
// 解析事件失败,直接丢弃
switch evt.Action { return nil
case "like": }
return w.applyLike(ctx, evt.UserID, evt.VideoID) if evt.UserID == 0 || evt.VideoID == 0 {
case "unlike": return nil
return w.applyUnlike(ctx, evt.UserID, evt.VideoID) }
default:
return nil switch evt.Action {
} case "like":
} return w.applyLike(ctx, evt.UserID, evt.VideoID)
case "unlike":
func (w *LikeWorker) applyLike(ctx context.Context, userID, videoID uint) error { return w.applyUnlike(ctx, evt.UserID, evt.VideoID)
ok, err := w.videos.IsExist(ctx, videoID) default:
if err != nil { return nil
return err }
} }
if !ok {
return nil func (w *LikeWorker) applyLike(ctx context.Context, userID, videoID uint) error {
} ok, err := w.videos.IsExist(ctx, videoID)
if err != nil {
created, err := w.likes.LikeIgnoreDuplicate(ctx, &video.Like{ return err
VideoID: videoID, }
AccountID: userID, if !ok {
CreatedAt: time.Now(), return nil
}) }
if err != nil {
return err created, err := w.likes.LikeIgnoreDuplicate(ctx, &video.Like{
} VideoID: videoID,
if !created { AccountID: userID,
return nil CreatedAt: time.Now(),
} })
if err != nil {
if err := w.videos.ChangeLikesCount(ctx, videoID, 1); err != nil { return err
return err }
} if !created {
return w.videos.ChangePopularity(ctx, videoID, 1) return nil
} }
func (w *LikeWorker) applyUnlike(ctx context.Context, userID, videoID uint) error { if err := w.videos.ChangeLikesCount(ctx, videoID, 1); err != nil {
ok, err := w.videos.IsExist(ctx, videoID) return err
if err != nil { }
return err return w.videos.ChangePopularity(ctx, videoID, 1)
} }
if !ok {
return nil func (w *LikeWorker) applyUnlike(ctx context.Context, userID, videoID uint) error {
} ok, err := w.videos.IsExist(ctx, videoID)
if err != nil {
deleted, err := w.likes.DeleteByVideoAndAccount(ctx, videoID, userID) return err
if err != nil { }
return err if !ok {
} return nil
if !deleted { }
return nil
} deleted, err := w.likes.DeleteByVideoAndAccount(ctx, videoID, userID)
if err != nil {
if err := w.videos.ChangeLikesCount(ctx, videoID, -1); err != nil { return err
return err }
} if !deleted {
return w.videos.ChangePopularity(ctx, videoID, -1) return nil
} }
if err := w.videos.ChangeLikesCount(ctx, videoID, -1); err != nil {
return err
}
return w.videos.ChangePopularity(ctx, videoID, -1)
}

View File

@@ -1,79 +1,85 @@
package worker package worker
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"feedsystem_video_go/internal/middleware/rabbitmq" "feedsystem_video_go/internal/middleware/rabbitmq"
rediscache "feedsystem_video_go/internal/middleware/redis" rediscache "feedsystem_video_go/internal/middleware/redis"
"feedsystem_video_go/internal/video" "feedsystem_video_go/internal/video"
"log" "log"
amqp "github.com/rabbitmq/amqp091-go" amqp "github.com/rabbitmq/amqp091-go"
) )
type PopularityWorker struct { type PopularityWorker struct {
ch *amqp.Channel ch *amqp.Channel
cache *rediscache.Client cache *rediscache.Client
queue string queue string
} }
func NewPopularityWorker(ch *amqp.Channel, cache *rediscache.Client, queue string) *PopularityWorker { func NewPopularityWorker(ch *amqp.Channel, cache *rediscache.Client, queue string) *PopularityWorker {
return &PopularityWorker{ch: ch, cache: cache, queue: queue} return &PopularityWorker{ch: ch, cache: cache, queue: queue}
} }
func (w *PopularityWorker) Run(ctx context.Context) error { func (w *PopularityWorker) Run(ctx context.Context) error {
if w == nil || w.ch == nil || w.cache == nil { if w == nil || w.ch == nil || w.cache == nil {
return errors.New("popularity worker is not initialized") return errors.New("popularity worker is not initialized")
} }
if w.queue == "" { if w.queue == "" {
return errors.New("queue is required") return errors.New("queue is required")
} }
deliveries, err := w.ch.Consume( deliveries, err := w.ch.Consume(
w.queue, w.queue,
"", "",
false, false,
false, false,
false, false,
false, false,
nil, nil,
) )
if err != nil { if err != nil {
return err return err
} }
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
case d, ok := <-deliveries: case d, ok := <-deliveries:
if !ok { if !ok {
return errors.New("deliveries channel closed") return errors.New("deliveries channel closed")
} }
w.handleDelivery(ctx, d) w.handleDelivery(ctx, d)
} }
} }
} }
func (w *PopularityWorker) handleDelivery(ctx context.Context, d amqp.Delivery) { func (w *PopularityWorker) handleDelivery(ctx context.Context, d amqp.Delivery) {
if err := w.process(ctx, d.Body); err != nil { if err := w.process(ctx, d.Body); err != nil {
log.Printf("popularity worker: failed to process message: %v", err) retryCount := rabbitmq.GetRetryCount(d)
_ = d.Nack(false, true) if retryCount >= rabbitmq.MaxRetryCount {
return log.Printf("popularity worker: max retries exceeded (%d), moving to DLX: %v", retryCount, err)
} _ = d.Ack(false)
_ = d.Ack(false) return
} }
log.Printf("popularity worker: failed (retry %d/%d): %v", retryCount+1, rabbitmq.MaxRetryCount, err)
func (w *PopularityWorker) process(ctx context.Context, body []byte) error { _ = d.Nack(false, true)
var evt rabbitmq.PopularityEvent return
if err := json.Unmarshal(body, &evt); err != nil { }
return nil _ = d.Ack(false)
} }
if evt.VideoID == 0 || evt.Change == 0 {
return nil func (w *PopularityWorker) process(ctx context.Context, body []byte) error {
} var evt rabbitmq.PopularityEvent
video.UpdatePopularityCache(ctx, w.cache, evt.VideoID, evt.Change) if err := json.Unmarshal(body, &evt); err != nil {
return nil return nil
} }
if evt.VideoID == 0 || evt.Change == 0 {
return nil
}
video.UpdatePopularityCache(ctx, w.cache, evt.VideoID, evt.Change)
return nil
}

View File

@@ -1,101 +1,106 @@
package worker package worker
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"feedsystem_video_go/internal/middleware/rabbitmq" "feedsystem_video_go/internal/middleware/rabbitmq"
"feedsystem_video_go/internal/social" "feedsystem_video_go/internal/social"
"log" "log"
"github.com/go-sql-driver/mysql" "github.com/go-sql-driver/mysql"
amqp "github.com/rabbitmq/amqp091-go" amqp "github.com/rabbitmq/amqp091-go"
) )
type SocialWorker struct { type SocialWorker struct {
ch *amqp.Channel ch *amqp.Channel
repo *social.SocialRepository repo *social.SocialRepository
queue string queue string
} }
func NewSocialWorker(ch *amqp.Channel, repo *social.SocialRepository, queue string) *SocialWorker { func NewSocialWorker(ch *amqp.Channel, repo *social.SocialRepository, queue string) *SocialWorker {
return &SocialWorker{ch: ch, repo: repo, queue: queue} return &SocialWorker{ch: ch, repo: repo, queue: queue}
} }
func (w *SocialWorker) Run(ctx context.Context) error { func (w *SocialWorker) Run(ctx context.Context) error {
if w == nil || w.ch == nil || w.repo == nil { if w == nil || w.ch == nil || w.repo == nil {
return errors.New("social worker is not initialized") return errors.New("social worker is not initialized")
} }
if w.queue == "" { if w.queue == "" {
return errors.New("queue is required") return errors.New("queue is required")
} }
deliveries, err := w.ch.Consume( deliveries, err := w.ch.Consume(
w.queue, w.queue,
"", "",
false, false,
false, false,
false, false,
false, false,
nil, nil,
) )
if err != nil { if err != nil {
return err return err
} }
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
case d, ok := <-deliveries: case d, ok := <-deliveries:
if !ok { if !ok {
return errors.New("deliveries channel closed") return errors.New("deliveries channel closed")
} }
w.handleDelivery(ctx, d) w.handleDelivery(ctx, d)
} }
} }
} }
func (w *SocialWorker) handleDelivery(ctx context.Context, d amqp.Delivery) { func (w *SocialWorker) handleDelivery(ctx context.Context, d amqp.Delivery) {
if err := w.process(ctx, d.Body); err != nil { if err := w.process(ctx, d.Body); err != nil {
log.Printf("social worker: failed to process message: %v", err) retryCount := rabbitmq.GetRetryCount(d)
// 重新入队,稍后重试 if retryCount >= rabbitmq.MaxRetryCount {
_ = d.Nack(false, true) log.Printf("social worker: max retries exceeded (%d), moving to DLX: %v", retryCount, err)
return _ = d.Ack(false)
} return
_ = d.Ack(false) }
} log.Printf("social worker: failed (retry %d/%d): %v", retryCount+1, rabbitmq.MaxRetryCount, err)
_ = d.Nack(false, true)
func (w *SocialWorker) process(ctx context.Context, body []byte) error { return
var evt rabbitmq.SocialEvent }
if err := json.Unmarshal(body, &evt); err != nil { _ = d.Ack(false)
// 解析事件失败,直接丢弃 }
return nil
} func (w *SocialWorker) process(ctx context.Context, body []byte) error {
if evt.FollowerID == 0 || evt.VloggerID == 0 { var evt rabbitmq.SocialEvent
return nil if err := json.Unmarshal(body, &evt); err != nil {
} // 解析事件失败,直接丢弃
return nil
switch evt.Action { }
case "follow": if evt.FollowerID == 0 || evt.VloggerID == 0 {
err := w.repo.Follow(ctx, &social.Social{ return nil
FollowerID: evt.FollowerID, }
VloggerID: evt.VloggerID,
}) switch evt.Action {
if err == nil { case "follow":
return nil err := w.repo.Follow(ctx, &social.Social{
} FollowerID: evt.FollowerID,
var mysqlErr *mysql.MySQLError VloggerID: evt.VloggerID,
if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 { })
return nil if err == nil {
} return nil
return err }
case "unfollow": var mysqlErr *mysql.MySQLError
return w.repo.Unfollow(ctx, &social.Social{ if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
FollowerID: evt.FollowerID, return nil
VloggerID: evt.VloggerID, }
}) return err
default: case "unfollow":
return nil return w.repo.Unfollow(ctx, &social.Social{
} FollowerID: evt.FollowerID,
} VloggerID: evt.VloggerID,
})
default:
return nil
}
}