All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m50s
198 lines
6.2 KiB
Go
198 lines
6.2 KiB
Go
package task
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/hibiken/asynq"
|
|
"github.com/redis/go-redis/v9"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/break/junhong_cmp_fiber/internal/exporter"
|
|
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
|
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
|
"github.com/break/junhong_cmp_fiber/pkg/storage"
|
|
)
|
|
|
|
// ExportShardHandler 导出 shard 处理器。
|
|
type ExportShardHandler struct {
|
|
redis *redis.Client
|
|
taskStore *postgres.ExportTaskStore
|
|
shardStore *postgres.ExportShardTaskStore
|
|
asynqClient *asynq.Client
|
|
storageSvc *storage.Service
|
|
sceneRegistry *exporter.Registry
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewExportShardHandler 创建导出 shard 处理器。
|
|
func NewExportShardHandler(
|
|
redis *redis.Client,
|
|
taskStore *postgres.ExportTaskStore,
|
|
shardStore *postgres.ExportShardTaskStore,
|
|
asynqClient *asynq.Client,
|
|
storageSvc *storage.Service,
|
|
sceneRegistry *exporter.Registry,
|
|
logger *zap.Logger,
|
|
) *ExportShardHandler {
|
|
return &ExportShardHandler{
|
|
redis: redis,
|
|
taskStore: taskStore,
|
|
shardStore: shardStore,
|
|
asynqClient: asynqClient,
|
|
storageSvc: storageSvc,
|
|
sceneRegistry: sceneRegistry,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// HandleExportShard 处理导出 shard 任务。
|
|
func (h *ExportShardHandler) HandleExportShard(ctx context.Context, task *asynq.Task) error {
|
|
var payload ExportShardPayload
|
|
if err := parseExportPayload(task.Payload(), &payload); err != nil {
|
|
h.logger.Error("解析导出 shard 任务载荷失败", zap.Error(err))
|
|
return asynq.SkipRetry
|
|
}
|
|
|
|
lockKey := constants.RedisExportShardLockKey(payload.TaskID, payload.ShardID)
|
|
locked, err := tryAcquireLock(ctx, h.redis, lockKey, exportShardLockTTL)
|
|
if err != nil {
|
|
h.logger.Error("获取导出 shard 锁失败", zap.Uint("task_id", payload.TaskID), zap.Uint("shard_id", payload.ShardID), zap.Error(err))
|
|
return err
|
|
}
|
|
if !locked {
|
|
return nil
|
|
}
|
|
defer releaseLock(ctx, h.redis, lockKey)
|
|
|
|
exportTask, err := h.taskStore.GetByIDForWorker(ctx, payload.TaskID)
|
|
if err != nil {
|
|
h.logger.Error("查询导出任务失败", zap.Uint("task_id", payload.TaskID), zap.Error(err))
|
|
return asynq.SkipRetry
|
|
}
|
|
|
|
shardTask, err := h.shardStore.GetByIDForWorker(ctx, payload.ShardID)
|
|
if err != nil {
|
|
h.logger.Error("查询导出分片失败", zap.Uint("task_id", payload.TaskID), zap.Uint("shard_id", payload.ShardID), zap.Error(err))
|
|
return asynq.SkipRetry
|
|
}
|
|
|
|
if shardTask.Status == constants.ExportShardStatusSuccess || shardTask.Status == constants.ExportShardStatusFailed || shardTask.Status == constants.ExportShardStatusCancelled {
|
|
return nil
|
|
}
|
|
|
|
updater := dispatchUpdater(exportTask)
|
|
if exportTask.CancelRequested {
|
|
_, _ = h.shardStore.MarkCancelled(ctx, shardTask.ID, updater, "任务已取消")
|
|
_ = h.enqueueFinalize(ctx, exportTask.ID)
|
|
return nil
|
|
}
|
|
|
|
if shardTask.Status == constants.ExportShardStatusPending {
|
|
started, err := h.shardStore.TryMarkProcessingIfPending(ctx, shardTask.ID, updater)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !started {
|
|
latest, err := h.shardStore.GetByIDForWorker(ctx, shardTask.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if latest.Status != constants.ExportShardStatusProcessing {
|
|
return nil
|
|
}
|
|
shardTask = latest
|
|
}
|
|
}
|
|
|
|
source, ok := h.sceneRegistry.Get(exportTask.Scene)
|
|
if !ok {
|
|
_ = h.markShardFinalFailed(ctx, exportTask.ID, shardTask.ID, updater, "导出场景未注册")
|
|
return asynq.SkipRetry
|
|
}
|
|
|
|
headers := exporter.ResolvedHeadersFromTask(exportTask)
|
|
if len(headers) == 0 {
|
|
return h.handleShardError(ctx, exportTask.ID, shardTask.ID, updater, "读取导出表头失败", fmt.Errorf("resolved_headers 为空"))
|
|
}
|
|
|
|
params := exporter.ParseExportParams(exportTask)
|
|
rows, err := source.Fetch(ctx, params, shardTask.ShardOffset, shardTask.ShardLimit)
|
|
if err != nil {
|
|
return h.handleShardError(ctx, exportTask.ID, shardTask.ID, updater, "查询分片数据失败", err)
|
|
}
|
|
if hasMisalignedRows(rows, headers) {
|
|
h.logger.Warn("导出分片数据列数与表头不一致,已按固定表头对齐",
|
|
zap.Uint("task_id", exportTask.ID),
|
|
zap.Uint("shard_id", shardTask.ID),
|
|
zap.Int("header_count", len(headers)),
|
|
)
|
|
}
|
|
rows = alignRowsToHeaders(rows, headers)
|
|
|
|
localPath, fileSize, err := writeExportFile(constants.ExportTaskFormatCSV, headers, rows, true)
|
|
if err != nil {
|
|
return h.handleShardError(ctx, exportTask.ID, shardTask.ID, updater, "生成分片文件失败", err)
|
|
}
|
|
defer func() { _ = os.Remove(localPath) }()
|
|
|
|
fileName := fmt.Sprintf("%s-shard-%04d.csv.shard", exportTask.TaskNo, shardTask.ShardNo)
|
|
fileKey, err := uploadExportFile(ctx, h.storageSvc, localPath, fileName)
|
|
if err != nil {
|
|
return h.handleShardError(ctx, exportTask.ID, shardTask.ID, updater, "上传分片文件失败", err)
|
|
}
|
|
|
|
updated, err := h.shardStore.MarkSuccess(ctx, shardTask.ID, updater, len(rows), fileKey, fileSize)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if updated {
|
|
if err := h.taskStore.ApplyShardSuccess(ctx, exportTask.ID, updater, len(rows)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := h.enqueueFinalize(ctx, exportTask.ID); err != nil {
|
|
h.logger.Warn("触发 finalize 入队失败", zap.Uint("task_id", exportTask.ID), zap.Error(err))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (h *ExportShardHandler) handleShardError(ctx context.Context, taskID, shardID uint, updater uint, prefix string, err error) error {
|
|
if isFinalRetry(ctx) {
|
|
_ = h.markShardFinalFailed(ctx, taskID, shardID, updater, prefix+": "+err.Error())
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (h *ExportShardHandler) markShardFinalFailed(ctx context.Context, taskID, shardID uint, updater uint, message string) error {
|
|
updated, err := h.shardStore.MarkFailed(ctx, shardID, updater, message)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if updated {
|
|
if err := h.taskStore.ApplyShardFailed(ctx, taskID, updater, message); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_ = h.enqueueFinalize(ctx, taskID)
|
|
return nil
|
|
}
|
|
|
|
func (h *ExportShardHandler) enqueueFinalize(ctx context.Context, taskID uint) error {
|
|
return enqueueTask(
|
|
ctx,
|
|
h.asynqClient,
|
|
constants.TaskTypeExportFinalize,
|
|
ExportFinalizePayload{TaskID: taskID},
|
|
asynq.MaxRetry(constants.ExportFinalizeRetryMax),
|
|
asynq.Timeout(constants.ExportFinalizeTaskTimeout),
|
|
asynq.ProcessIn(2*time.Second),
|
|
asynq.Queue(constants.QueueForTaskType(constants.TaskTypeExportFinalize)),
|
|
)
|
|
}
|