chore: 初始开源发布
iotlcm-worker-svr:异步执行中枢(RabbitMQ 消费者 + 全量同步编排)。 - Go module:iotlcm-worker-svr → git.5iot.com/iotlcm-public/iotlcm-worker-svr(214 文件) - 凭据与内部标识脱敏:MySQL/Redis 凭据、私网与生产 IP、内网域名、 库名 xiujie_admin → iotlcm、哨兵名占位 - 159 处真实手机号 → 合法 11 位编造号(保留号段与后 4 位,维持互异, 不破坏「校验 MSISDN 格式」等测试断言) - 移除内部资产:CI 触发器、Jenkins 触发测试文档、预演门禁与镜像 digest 记录 - Dockerfile 改用公共基础镜像(golang:1.25-alpine → alpine:3.23), 移除写死的 git 凭据 - README 重写(架构/全量同步模型/配置/已知失败),补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes 已知:3 个测试包在源仓同一提交上同样失败(client/upstream、 service/allocate、service/inbound),属既有缺陷,本仓未引入亦未修复。
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
package rabbitmq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/batch"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
|
||||
mq "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/library/rabbitmq"
|
||||
)
|
||||
|
||||
const batchRetryCountHeader = "x-iotlcm-retry-count"
|
||||
|
||||
func (c *Consumers) initBatchExecute(ctx context.Context) error {
|
||||
if !mq.Enabled(ctx) {
|
||||
g.Log().Info(ctx, "batch execute consumer skipped, RabbitMQ disabled")
|
||||
return nil
|
||||
}
|
||||
if c.batch == nil {
|
||||
return gerror.New("batch execute handler is not configured")
|
||||
}
|
||||
retryDelay, maxRetries, err := getBatchExecuteRetryConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.batchExecuteRetryDelay = retryDelay
|
||||
c.batchExecuteMaxRetries = maxRetries
|
||||
if err = mq.DeclareQueue(ctx, consts.QueueBatchExecuteDeadLetter); err != nil {
|
||||
return gerror.Wrap(err, "declare batch execute dead-letter queue failed")
|
||||
}
|
||||
if err = mq.ConsumeJSON[batch.Message](
|
||||
ctx,
|
||||
consts.QueueBatchExecute,
|
||||
"iotlcm-batch-execute",
|
||||
c.handleBatchExecute,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
g.Log().Infof(ctx, "batch execute consumer started, queue:%s, retryDelay:%s, maxRetries:%d", consts.QueueBatchExecute, retryDelay, maxRetries)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Consumers) handleBatchExecute(ctx context.Context, message batch.Message) error {
|
||||
if c.batch == nil {
|
||||
return gerror.New("batch execute handler is not configured")
|
||||
}
|
||||
g.Log().Infof(ctx, "batch execute message received, batchNo:%s, batchType:%d", message.BatchNo, message.BatchType)
|
||||
if err := c.batch.Handle(ctx, message); err != nil {
|
||||
if _, delivered := mq.DeliveryMetadataFromContext(ctx); !delivered {
|
||||
return err
|
||||
}
|
||||
if batch.IsRetryable(err) {
|
||||
message.Retryable = true
|
||||
}
|
||||
var failureMarkErr *batch.FailureMarkError
|
||||
if errors.As(err, &failureMarkErr) {
|
||||
message.FailureReason = failureMarkErr.Reason
|
||||
}
|
||||
return c.retryOrDeadLetterBatchMessage(ctx, message, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Consumers) retryOrDeadLetterBatchMessage(ctx context.Context, message batch.Message, handlerErr error) error {
|
||||
metadata, _ := mq.DeliveryMetadataFromContext(ctx)
|
||||
retryCount := batchRetryCount(metadata.Headers)
|
||||
headers := amqp.Table{batchRetryCountHeader: retryCount + 1}
|
||||
if shouldDeadLetterBatch(retryCount, c.batchExecuteMaxRetries) {
|
||||
if message.Retryable {
|
||||
finalizationMessage := message
|
||||
finalizationMessage.Retryable = false
|
||||
finalizationMessage.FailureReason = handlerErr.Error()
|
||||
if err := c.batch.Handle(ctx, finalizationMessage); err != nil {
|
||||
return gerror.Wrap(err, "finalize retryable batch failure before dead-lettering")
|
||||
}
|
||||
message.FailureReason = handlerErr.Error()
|
||||
}
|
||||
deadLetterBody, marshalErr := json.Marshal(batchDeadLetterMessage{
|
||||
Message: message,
|
||||
RetryCount: retryCount,
|
||||
Error: handlerErr.Error(),
|
||||
FailedAt: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
if marshalErr != nil {
|
||||
return gerror.Wrap(marshalErr, "marshal batch execute dead-letter message failed")
|
||||
}
|
||||
if err := mq.PublishBytesWithHeaders(ctx, consts.QueueBatchExecuteDeadLetter, deadLetterBody, nil); err != nil {
|
||||
return gerror.Wrap(err, "publish batch execute dead-letter message failed")
|
||||
}
|
||||
g.Log().Errorf(ctx, "batch execute message sent to dead-letter queue, batchNo:%s, batchType:%d, retryCount:%d, err:%+v", message.BatchNo, message.BatchType, retryCount, handlerErr)
|
||||
return nil
|
||||
}
|
||||
body, marshalErr := json.Marshal(message)
|
||||
if marshalErr != nil {
|
||||
return gerror.Wrap(marshalErr, "marshal batch execute retry message failed")
|
||||
}
|
||||
if err := mq.PublishDelayedBytesWithHeaders(
|
||||
ctx,
|
||||
consts.QueueBatchExecuteRetry,
|
||||
consts.QueueBatchExecute,
|
||||
c.batchExecuteRetryDelay,
|
||||
body,
|
||||
headers,
|
||||
); err != nil {
|
||||
return gerror.Wrap(err, "republish batch execute retry message failed")
|
||||
}
|
||||
g.Log().Warningf(ctx, "batch execute message scheduled for retry, batchNo:%s, batchType:%d, retryCount:%d, delay:%s, err:%+v", message.BatchNo, message.BatchType, retryCount+1, c.batchExecuteRetryDelay, handlerErr)
|
||||
return nil
|
||||
}
|
||||
|
||||
type batchDeadLetterMessage struct {
|
||||
Message batch.Message `json:"message"`
|
||||
RetryCount int `json:"retryCount"`
|
||||
Error string `json:"error"`
|
||||
FailedAt string `json:"failedAt"`
|
||||
}
|
||||
|
||||
func shouldDeadLetterBatch(retryCount, maxRetries int) bool {
|
||||
return retryCount >= maxRetries
|
||||
}
|
||||
|
||||
func batchRetryCount(headers map[string]any) int {
|
||||
if headers == nil {
|
||||
return 0
|
||||
}
|
||||
value, ok := headers[batchRetryCountHeader]
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
switch count := value.(type) {
|
||||
case int:
|
||||
return max(count, 0)
|
||||
case int8:
|
||||
return max(int(count), 0)
|
||||
case int16:
|
||||
return max(int(count), 0)
|
||||
case int32:
|
||||
return max(int(count), 0)
|
||||
case int64:
|
||||
return max(int(count), 0)
|
||||
case uint8:
|
||||
return int(count)
|
||||
case uint16:
|
||||
return int(count)
|
||||
case uint32:
|
||||
return int(count)
|
||||
case uint64:
|
||||
if count > uint64(^uint(0)>>1) {
|
||||
return int(^uint(0) >> 1)
|
||||
}
|
||||
return int(count)
|
||||
case string:
|
||||
parsed, err := strconv.Atoi(count)
|
||||
if err == nil {
|
||||
return max(parsed, 0)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func getBatchExecuteRetryConfig(ctx context.Context) (time.Duration, int, error) {
|
||||
delay := g.Cfg().MustGet(ctx, "rabbitmq.batchExecute.retryDelay", "30s").Duration()
|
||||
if delay <= 0 {
|
||||
return 0, 0, gerror.New("batch execute retry delay must be positive")
|
||||
}
|
||||
maxRetries := g.Cfg().MustGet(ctx, "rabbitmq.batchExecute.maxRetries", 3).Int()
|
||||
if maxRetries < 0 {
|
||||
return 0, 0, gerror.New("batch execute max retries cannot be negative")
|
||||
}
|
||||
return delay, maxRetries, nil
|
||||
}
|
||||
Reference in New Issue
Block a user