2
0
mirror of https://github.com/hibiken/asynq.git synced 2026-07-25 07:09:32 +08:00

Merge pull request #1094 from fanatics-live/batch-enqueue

Add BatchEnqueue for pipelined multi-task enqueue
This commit is contained in:
Mohamed Sohail
2026-05-14 15:34:54 +03:00
committed by GitHub
8 changed files with 658 additions and 0 deletions

View File

@@ -139,6 +139,118 @@ func (r *RDB) Enqueue(ctx context.Context, msg *base.TaskMessage) error {
return nil
}
// BatchEnqueue adds all given tasks to Redis using a single pipeline round-trip.
// Each item is either enqueued immediately (ProcessAt is zero) or added to the
// scheduled sorted set.
//
// WARNING: tasks whose IDs already exist in Redis are silently skipped.
//
// The pipeline executes independent Lua scripts per task — there is no
// MULTI/EXEC wrapping the batch, so individual tasks may succeed or fail
// independently. The returned int is the number of tasks actually written;
// skipped duplicates do not count. The returned error is non-nil only when the
// pipeline call itself fails (network error, context cancellation, etc.), in
// which case no individual result should be trusted.
//
// Message encoding errors cause an immediate return before any Redis I/O.
func (r *RDB) BatchEnqueue(ctx context.Context, items []base.BatchEnqueueItem) (int, error) {
var op errors.Op = "rdb.BatchEnqueue"
if len(items) == 0 {
return 0, nil
}
// Preload Lua scripts so that EVALSHA inside the pipeline does not fail with
// NOSCRIPT. Script.Run on a pipeline only sends EVALSHA (unlike non-pipeline
// Run which retries with EVAL on NOSCRIPT).
needsEnqueue, needsSchedule := false, false
for _, item := range items {
if item.ProcessAt.IsZero() {
needsEnqueue = true
} else {
needsSchedule = true
}
if needsEnqueue && needsSchedule {
break
}
}
if needsEnqueue {
if err := enqueueCmd.Load(ctx, r.client).Err(); err != nil {
return 0, errors.E(op, errors.Unknown, fmt.Sprintf("failed to load enqueue script: %v", err))
}
}
if needsSchedule {
if err := scheduleCmd.Load(ctx, r.client).Err(); err != nil {
return 0, errors.E(op, errors.Unknown, fmt.Sprintf("failed to load schedule script: %v", err))
}
}
pipe := r.client.Pipeline()
// Track which pipeline slot holds each item's script result.
scriptIdxs := make([]int, 0, len(items))
pipeLen := 0
// Track queues we add to AllQueues in this pipeline so we can roll back the
// in-memory cache on failure.
var newQueues []string
now := r.clock.Now().UnixNano()
for _, item := range items {
encoded, err := base.EncodeMessage(item.Msg)
if err != nil {
return 0, errors.E(op, errors.Unknown, fmt.Sprintf("cannot encode message: %v", err))
}
if _, found := r.queuesPublished.Load(item.Msg.Queue); !found {
pipe.SAdd(ctx, base.AllQueues, item.Msg.Queue)
r.queuesPublished.Store(item.Msg.Queue, true)
newQueues = append(newQueues, item.Msg.Queue)
pipeLen++
}
if item.ProcessAt.IsZero() {
keys := []string{
base.TaskKey(item.Msg.Queue, item.Msg.ID),
base.PendingKey(item.Msg.Queue),
}
argv := []interface{}{encoded, item.Msg.ID, now}
enqueueCmd.Run(ctx, pipe, keys, argv...)
} else {
keys := []string{
base.TaskKey(item.Msg.Queue, item.Msg.ID),
base.ScheduledKey(item.Msg.Queue),
}
argv := []interface{}{encoded, item.ProcessAt.Unix(), item.Msg.ID}
scheduleCmd.Run(ctx, pipe, keys, argv...)
}
scriptIdxs = append(scriptIdxs, pipeLen)
pipeLen++
}
cmds, err := pipe.Exec(ctx)
if err != nil && err != redis.Nil {
for _, q := range newQueues {
r.queuesPublished.Delete(q)
}
return 0, errors.E(op, errors.Unknown, fmt.Sprintf("redis pipeline error: %v", err))
}
enqueued := 0
for _, idx := range scriptIdxs {
if idx >= len(cmds) {
continue
}
res, err := cmds[idx].(*redis.Cmd).Result()
if err != nil {
continue
}
if n, ok := res.(int64); ok && n == 1 {
enqueued++
}
}
return enqueued, nil
}
// enqueueUniqueCmd enqueues the task message if the task is unique.
//
// KEYS[1] -> unique key