feat(P1): MQ Worker 死信队列 — 重试上限3次后 Ack 移入 DLX
This commit is contained in:
53
backend/internal/middleware/rabbitmq/dlx.go
Normal file
53
backend/internal/middleware/rabbitmq/dlx.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user