package rabbitmq import ( "context" "time" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts" mq "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/library/rabbitmq" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model" ) // defaultBalanceRechargeMaxRetry 是充值超时消息的默认最大重试次数。 // 支付宝凭据未配置等永久性失败会让 CheckRechargeTimeout 一直返回错误, // 必须设置上限,否则消息会无限重投并刷屏日志。 const defaultBalanceRechargeMaxRetry = 100 // 包级变量便于测试注入发布行为,与 trafficUsagePublish 保持一致。 var balanceRechargePublish = mq.Publish var balanceRechargePublishDelayed = mq.PublishDelayed func (c *Consumers) initBalanceRechargeTimeout(ctx context.Context) error { if !mq.Enabled(ctx) { g.Log().Info(ctx, "balance recharge timeout consumer skipped, RabbitMQ disabled") return nil } if c.balance == nil { return gerror.New("balance recharge handler is not configured") } retryDelay, err := getBalanceRechargeRetryDelay(ctx) if err != nil { return err } maxRetry, err := getBalanceRechargeMaxRetry(ctx) if err != nil { return err } c.balanceRechargeRetryDelay = retryDelay c.balanceRechargeMaxRetry = maxRetry if err := mq.DeclareQueue(ctx, consts.QueueBalanceRechargeDeadLetter); err != nil { return gerror.Wrap(err, "declare balance recharge dead-letter queue failed") } if err := mq.ConsumeJSON[model.BalanceRechargeTimeoutMessage]( ctx, consts.QueueBalanceRechargeTimeout, "iotlcm-balance-recharge-timeout", c.handleBalanceRechargeTimeout, ); err != nil { return err } g.Log().Infof(ctx, "balance recharge timeout consumer started, queue:%s, retryDelay:%s, maxRetry:%d", consts.QueueBalanceRechargeTimeout, retryDelay, maxRetry) return nil } func (c *Consumers) handleBalanceRechargeTimeout(ctx context.Context, message model.BalanceRechargeTimeoutMessage) error { if message.OutTradeNo == "" { g.Log().Errorf(ctx, "balance recharge timeout message dropped, invalid message:%+v", message) return nil } if c.balance == nil { return gerror.New("balance recharge handler is not configured") } if err := c.balance.CheckRechargeTimeout(ctx, message.OutTradeNo); err != nil { message.RetryCount++ if shouldDeadLetterBalanceRecharge(message.RetryCount, c.balanceRechargeMaxRetry) { if publishErr := balanceRechargePublish(ctx, consts.QueueBalanceRechargeDeadLetter, message); publishErr != nil { return gerror.Wrap(publishErr, "publish balance recharge dead-letter message failed") } g.Log().Errorf(ctx, "balance recharge timeout retries exhausted and message sent to dead-letter queue, outTradeNo:%s, retryCount:%d, maxRetry:%d, err:%+v", message.OutTradeNo, message.RetryCount, c.balanceRechargeMaxRetry, err) return nil } if retryErr := balanceRechargePublishDelayed(ctx, consts.QueueBalanceRechargeDelay, consts.QueueBalanceRechargeTimeout, c.balanceRechargeRetryDelay, message); retryErr != nil { return gerror.Wrap(retryErr, "republish balance recharge timeout message failed") } g.Log().Warningf(ctx, "balance recharge timeout query failed and will retry, outTradeNo:%s, retryCount:%d, err:%+v", message.OutTradeNo, message.RetryCount, err) } return nil } // shouldDeadLetterBalanceRecharge 判断本次失败后是否放弃重试。 // retryCount 是自增后的重试次数,因此 maxRetry 表示最多重投多少次。 func shouldDeadLetterBalanceRecharge(retryCount, maxRetry int) bool { return retryCount > maxRetry } func getBalanceRechargeRetryDelay(ctx context.Context) (time.Duration, error) { value := g.Cfg().MustGet(ctx, "rabbitmq.balanceRecharge.retryDelay", "1m").String() delay, err := time.ParseDuration(value) if err != nil || delay <= 0 { return 0, gerror.Newf("balance recharge retry delay is invalid: %q", value) } return delay, nil } func getBalanceRechargeMaxRetry(ctx context.Context) (int, error) { maxRetry := g.Cfg().MustGet(ctx, "rabbitmq.balanceRecharge.maxRetry", defaultBalanceRechargeMaxRetry).Int() if maxRetry <= 0 { return 0, gerror.Newf("balance recharge max retry is invalid: %d", maxRetry) } return maxRetry, nil }