mirror of
https://github.com/hibiken/asynq.git
synced 2025-10-23 10:16:12 +08:00
Refactor redis keys and store messages in protobuf
Changes: - Task messages are stored under "asynq:{<qname>}:t:<task_id>" key in redis, value is a HASH type and message are stored under "msg" key in the hash. The hash also stores "deadline", "timeout". - Redis LIST and ZSET stores task message IDs - Task messages are serialized using protocol buffer
This commit is contained in:
@@ -6,10 +6,8 @@
|
||||
package rdb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v7"
|
||||
@@ -50,7 +48,19 @@ func (r *RDB) Ping() error {
|
||||
return r.client.Ping().Err()
|
||||
}
|
||||
|
||||
// Enqueue inserts the given task to the tail of the queue.
|
||||
// KEYS[1] -> asynq:{<qname>}:t:<task_id>
|
||||
// KEYS[2] -> asynq:{<qname>}:pending
|
||||
// ARGV[1] -> task message data
|
||||
// ARGV[2] -> task ID
|
||||
// ARGV[3] -> task timeout in seconds (0 if not timeout)
|
||||
// ARGV[4] -> task deadline in unix time (0 if no deadline)
|
||||
var enqueueCmd = redis.NewScript(`
|
||||
redis.call("HSET", KEYS[1], "msg", ARGV[1], "timeout", ARGV[3], "deadline", ARGV[4])
|
||||
redis.call("LPUSH", KEYS[2], ARGV[2])
|
||||
return 1
|
||||
`)
|
||||
|
||||
// Enqueue adds the given task to the pending list of the queue.
|
||||
func (r *RDB) Enqueue(msg *base.TaskMessage) error {
|
||||
encoded, err := base.EncodeMessage(msg)
|
||||
if err != nil {
|
||||
@@ -59,21 +69,34 @@ func (r *RDB) Enqueue(msg *base.TaskMessage) error {
|
||||
if err := r.client.SAdd(base.AllQueues, msg.Queue).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
key := base.QueueKey(msg.Queue)
|
||||
return r.client.LPush(key, encoded).Err()
|
||||
keys := []string{
|
||||
base.TaskKey(msg.Queue, msg.ID.String()),
|
||||
base.PendingKey(msg.Queue),
|
||||
}
|
||||
argv := []interface{}{
|
||||
encoded,
|
||||
msg.ID.String(),
|
||||
msg.Timeout,
|
||||
msg.Deadline,
|
||||
}
|
||||
return enqueueCmd.Run(r.client, keys, argv...).Err()
|
||||
}
|
||||
|
||||
// KEYS[1] -> unique key
|
||||
// KEYS[2] -> asynq:{<qname>}
|
||||
// KEYS[2] -> asynq:{<qname>}:t:<taskid>
|
||||
// KEYS[3] -> asynq:{<qname>}:pending
|
||||
// ARGV[1] -> task ID
|
||||
// ARGV[2] -> uniqueness lock TTL
|
||||
// ARGV[3] -> task message data
|
||||
// ARGV[4] -> task timeout in seconds (0 if not timeout)
|
||||
// ARGV[5] -> task deadline in unix time (0 if no deadline)
|
||||
var enqueueUniqueCmd = redis.NewScript(`
|
||||
local ok = redis.call("SET", KEYS[1], ARGV[1], "NX", "EX", ARGV[2])
|
||||
if not ok then
|
||||
return 0
|
||||
end
|
||||
redis.call("LPUSH", KEYS[2], ARGV[3])
|
||||
redis.call("HSET", KEYS[2], "msg", ARGV[3], "timeout", ARGV[4], "deadline", ARGV[5])
|
||||
redis.call("LPUSH", KEYS[3], ARGV[1])
|
||||
return 1
|
||||
`)
|
||||
|
||||
@@ -87,9 +110,19 @@ func (r *RDB) EnqueueUnique(msg *base.TaskMessage, ttl time.Duration) error {
|
||||
if err := r.client.SAdd(base.AllQueues, msg.Queue).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := enqueueUniqueCmd.Run(r.client,
|
||||
[]string{msg.UniqueKey, base.QueueKey(msg.Queue)},
|
||||
msg.ID.String(), int(ttl.Seconds()), encoded).Result()
|
||||
keys := []string{
|
||||
msg.UniqueKey,
|
||||
base.TaskKey(msg.Queue, msg.ID.String()),
|
||||
base.PendingKey(msg.Queue),
|
||||
}
|
||||
argv := []interface{}{
|
||||
msg.ID.String(),
|
||||
int(ttl.Seconds()),
|
||||
encoded,
|
||||
msg.Timeout,
|
||||
msg.Deadline,
|
||||
}
|
||||
res, err := enqueueUniqueCmd.Run(r.client, keys, argv...).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -108,21 +141,22 @@ func (r *RDB) EnqueueUnique(msg *base.TaskMessage, ttl time.Duration) error {
|
||||
// Dequeue skips a queue if the queue is paused.
|
||||
// If all queues are empty, ErrNoProcessableTask error is returned.
|
||||
func (r *RDB) Dequeue(qnames ...string) (msg *base.TaskMessage, deadline time.Time, err error) {
|
||||
data, d, err := r.dequeue(qnames...)
|
||||
encoded, d, err := r.dequeue(qnames...)
|
||||
if err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
if msg, err = base.DecodeMessage(data); err != nil {
|
||||
if msg, err = base.DecodeMessage([]byte(encoded)); err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
return msg, time.Unix(d, 0), nil
|
||||
}
|
||||
|
||||
// KEYS[1] -> asynq:{<qname>}
|
||||
// KEYS[1] -> asynq:{<qname>}:pending
|
||||
// KEYS[2] -> asynq:{<qname>}:paused
|
||||
// KEYS[3] -> asynq:{<qname>}:active
|
||||
// KEYS[4] -> asynq:{<qname>}:deadlines
|
||||
// ARGV[1] -> current time in Unix time
|
||||
// ARGV[1] -> current time in Unix time
|
||||
// ARGV[2] -> task key prefix
|
||||
//
|
||||
// dequeueCmd checks whether a queue is paused first, before
|
||||
// calling RPOPLPUSH to pop a task from the queue.
|
||||
@@ -130,11 +164,13 @@ func (r *RDB) Dequeue(qnames ...string) (msg *base.TaskMessage, deadline time.Ti
|
||||
// and inserts the task with deadlines set.
|
||||
var dequeueCmd = redis.NewScript(`
|
||||
if redis.call("EXISTS", KEYS[2]) == 0 then
|
||||
local msg = redis.call("RPOPLPUSH", KEYS[1], KEYS[3])
|
||||
if msg then
|
||||
local decoded = cjson.decode(msg)
|
||||
local timeout = decoded["Timeout"]
|
||||
local deadline = decoded["Deadline"]
|
||||
local id = redis.call("RPOPLPUSH", KEYS[1], KEYS[3])
|
||||
if id then
|
||||
local key = ARGV[2] .. id
|
||||
local data = redis.call("HMGET", key, "msg", "timeout", "deadline")
|
||||
local msg = data[1]
|
||||
local timeout = tonumber(data[2])
|
||||
local deadline = tonumber(data[3])
|
||||
local score
|
||||
if timeout ~= 0 and deadline ~= 0 then
|
||||
score = math.min(ARGV[1]+timeout, deadline)
|
||||
@@ -145,21 +181,25 @@ if redis.call("EXISTS", KEYS[2]) == 0 then
|
||||
else
|
||||
return redis.error_reply("asynq internal error: both timeout and deadline are not set")
|
||||
end
|
||||
redis.call("ZADD", KEYS[4], score, msg)
|
||||
redis.call("ZADD", KEYS[4], score, id)
|
||||
return {msg, score}
|
||||
end
|
||||
end
|
||||
return nil`)
|
||||
|
||||
func (r *RDB) dequeue(qnames ...string) (msgjson string, deadline int64, err error) {
|
||||
func (r *RDB) dequeue(qnames ...string) (encoded string, deadline int64, err error) {
|
||||
for _, qname := range qnames {
|
||||
keys := []string{
|
||||
base.QueueKey(qname),
|
||||
base.PendingKey(qname),
|
||||
base.PausedKey(qname),
|
||||
base.ActiveKey(qname),
|
||||
base.DeadlinesKey(qname),
|
||||
}
|
||||
res, err := dequeueCmd.Run(r.client, keys, time.Now().Unix()).Result()
|
||||
argv := []interface{}{
|
||||
time.Now().Unix(),
|
||||
base.TaskKeyPrefix(qname),
|
||||
}
|
||||
res, err := dequeueCmd.Run(r.client, keys, argv...).Result()
|
||||
if err == redis.Nil {
|
||||
continue
|
||||
} else if err != nil {
|
||||
@@ -172,21 +212,22 @@ func (r *RDB) dequeue(qnames ...string) (msgjson string, deadline int64, err err
|
||||
if len(data) != 2 {
|
||||
return "", 0, fmt.Errorf("asynq: internal error: dequeue command returned %d values", len(data))
|
||||
}
|
||||
if msgjson, err = cast.ToStringE(data[0]); err != nil {
|
||||
if encoded, err = cast.ToStringE(data[0]); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
if deadline, err = cast.ToInt64E(data[1]); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
return msgjson, deadline, nil
|
||||
return encoded, deadline, nil
|
||||
}
|
||||
return "", 0, ErrNoProcessableTask
|
||||
}
|
||||
|
||||
// KEYS[1] -> asynq:{<qname>}:active
|
||||
// KEYS[2] -> asynq:{<qname>}:deadlines
|
||||
// KEYS[3] -> asynq:{<qname>}:processed:<yyyy-mm-dd>
|
||||
// ARGV[1] -> base.TaskMessage value
|
||||
// KEYS[3] -> asynq:{<qname>}:t:<task_id>
|
||||
// KEYS[4] -> asynq:{<qname>}:processed:<yyyy-mm-dd>
|
||||
// ARGV[1] -> task ID
|
||||
// ARGV[2] -> stats expiration timestamp
|
||||
var doneCmd = redis.NewScript(`
|
||||
if redis.call("LREM", KEYS[1], 0, ARGV[1]) == 0 then
|
||||
@@ -195,20 +236,23 @@ end
|
||||
if redis.call("ZREM", KEYS[2], ARGV[1]) == 0 then
|
||||
return redis.error_reply("NOT FOUND")
|
||||
end
|
||||
local n = redis.call("INCR", KEYS[3])
|
||||
if redis.call("DEL", KEYS[3]) == 0 then
|
||||
return redis.error_reply("NOT FOUND")
|
||||
end
|
||||
local n = redis.call("INCR", KEYS[4])
|
||||
if tonumber(n) == 1 then
|
||||
redis.call("EXPIREAT", KEYS[3], ARGV[2])
|
||||
redis.call("EXPIREAT", KEYS[4], ARGV[2])
|
||||
end
|
||||
return redis.status_reply("OK")
|
||||
`)
|
||||
|
||||
// KEYS[1] -> asynq:{<qname>}:active
|
||||
// KEYS[2] -> asynq:{<qname>}:deadlines
|
||||
// KEYS[3] -> asynq:{<qname>}:processed:<yyyy-mm-dd>
|
||||
// KEYS[4] -> unique key
|
||||
// ARGV[1] -> base.TaskMessage value
|
||||
// KEYS[3] -> asynq:{<qname>}:t:<task_id>
|
||||
// KEYS[4] -> asynq:{<qname>}:processed:<yyyy-mm-dd>
|
||||
// KEYS[5] -> unique key
|
||||
// ARGV[1] -> task ID
|
||||
// ARGV[2] -> stats expiration timestamp
|
||||
// ARGV[3] -> task ID
|
||||
var doneUniqueCmd = redis.NewScript(`
|
||||
if redis.call("LREM", KEYS[1], 0, ARGV[1]) == 0 then
|
||||
return redis.error_reply("NOT FOUND")
|
||||
@@ -216,12 +260,15 @@ end
|
||||
if redis.call("ZREM", KEYS[2], ARGV[1]) == 0 then
|
||||
return redis.error_reply("NOT FOUND")
|
||||
end
|
||||
local n = redis.call("INCR", KEYS[3])
|
||||
if tonumber(n) == 1 then
|
||||
redis.call("EXPIREAT", KEYS[3], ARGV[2])
|
||||
if redis.call("DEL", KEYS[3]) == 0 then
|
||||
return redis.error_reply("NOT FOUND")
|
||||
end
|
||||
if redis.call("GET", KEYS[4]) == ARGV[3] then
|
||||
redis.call("DEL", KEYS[4])
|
||||
local n = redis.call("INCR", KEYS[4])
|
||||
if tonumber(n) == 1 then
|
||||
redis.call("EXPIREAT", KEYS[4], ARGV[2])
|
||||
end
|
||||
if redis.call("GET", KEYS[5]) == ARGV[1] then
|
||||
redis.call("DEL", KEYS[5])
|
||||
end
|
||||
return redis.status_reply("OK")
|
||||
`)
|
||||
@@ -229,30 +276,29 @@ return redis.status_reply("OK")
|
||||
// Done removes the task from active queue to mark the task as done.
|
||||
// It removes a uniqueness lock acquired by the task, if any.
|
||||
func (r *RDB) Done(msg *base.TaskMessage) error {
|
||||
encoded, err := base.EncodeMessage(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
expireAt := now.Add(statsTTL)
|
||||
keys := []string{
|
||||
base.ActiveKey(msg.Queue),
|
||||
base.DeadlinesKey(msg.Queue),
|
||||
base.TaskKey(msg.Queue, msg.ID.String()),
|
||||
base.ProcessedKey(msg.Queue, now),
|
||||
}
|
||||
args := []interface{}{encoded, expireAt.Unix()}
|
||||
argv := []interface{}{
|
||||
msg.ID.String(),
|
||||
expireAt.Unix(),
|
||||
}
|
||||
if len(msg.UniqueKey) > 0 {
|
||||
keys = append(keys, msg.UniqueKey)
|
||||
args = append(args, msg.ID.String())
|
||||
return doneUniqueCmd.Run(r.client, keys, args...).Err()
|
||||
return doneUniqueCmd.Run(r.client, keys, argv...).Err()
|
||||
}
|
||||
return doneCmd.Run(r.client, keys, args...).Err()
|
||||
return doneCmd.Run(r.client, keys, argv...).Err()
|
||||
}
|
||||
|
||||
// KEYS[1] -> asynq:{<qname>}:active
|
||||
// KEYS[2] -> asynq:{<qname>}:deadlines
|
||||
// KEYS[3] -> asynq:{<qname>}
|
||||
// ARGV[1] -> base.TaskMessage value
|
||||
// KEYS[3] -> asynq:{<qname>}:pending
|
||||
// ARGV[1] -> task ID
|
||||
// Note: Use RPUSH to push to the head of the queue.
|
||||
var requeueCmd = redis.NewScript(`
|
||||
if redis.call("LREM", KEYS[1], 0, ARGV[1]) == 0 then
|
||||
@@ -266,16 +312,25 @@ return redis.status_reply("OK")`)
|
||||
|
||||
// Requeue moves the task from active queue to the specified queue.
|
||||
func (r *RDB) Requeue(msg *base.TaskMessage) error {
|
||||
encoded, err := base.EncodeMessage(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return requeueCmd.Run(r.client,
|
||||
[]string{base.ActiveKey(msg.Queue), base.DeadlinesKey(msg.Queue), base.QueueKey(msg.Queue)},
|
||||
encoded).Err()
|
||||
[]string{base.ActiveKey(msg.Queue), base.DeadlinesKey(msg.Queue), base.PendingKey(msg.Queue)},
|
||||
msg.ID.String()).Err()
|
||||
}
|
||||
|
||||
// Schedule adds the task to the backlog queue to be processed in the future.
|
||||
// KEYS[1] -> asynq:{<qname>}:t:<task_id>
|
||||
// KEYS[2] -> asynq:{<qname>}:scheduled
|
||||
// ARGV[1] -> task message data
|
||||
// ARGV[2] -> process_at time in Unix time
|
||||
// ARGV[3] -> task ID
|
||||
// ARGV[4] -> task timeout in seconds (0 if not timeout)
|
||||
// ARGV[5] -> task deadline in unix time (0 if no deadline)
|
||||
var scheduleCmd = redis.NewScript(`
|
||||
redis.call("HSET", KEYS[1], "msg", ARGV[1], "timeout", ARGV[4], "deadline", ARGV[5])
|
||||
redis.call("ZADD", KEYS[2], ARGV[2], ARGV[3])
|
||||
return 1
|
||||
`)
|
||||
|
||||
// Schedule adds the task to the scheduled set to be processed in the future.
|
||||
func (r *RDB) Schedule(msg *base.TaskMessage, processAt time.Time) error {
|
||||
encoded, err := base.EncodeMessage(msg)
|
||||
if err != nil {
|
||||
@@ -284,22 +339,36 @@ func (r *RDB) Schedule(msg *base.TaskMessage, processAt time.Time) error {
|
||||
if err := r.client.SAdd(base.AllQueues, msg.Queue).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
score := float64(processAt.Unix())
|
||||
return r.client.ZAdd(base.ScheduledKey(msg.Queue), &redis.Z{Score: score, Member: encoded}).Err()
|
||||
keys := []string{
|
||||
base.TaskKey(msg.Queue, msg.ID.String()),
|
||||
base.ScheduledKey(msg.Queue),
|
||||
}
|
||||
argv := []interface{}{
|
||||
encoded,
|
||||
processAt.Unix(),
|
||||
msg.ID.String(),
|
||||
msg.Timeout,
|
||||
msg.Deadline,
|
||||
}
|
||||
return scheduleCmd.Run(r.client, keys, argv...).Err()
|
||||
}
|
||||
|
||||
// KEYS[1] -> unique key
|
||||
// KEYS[2] -> asynq:{<qname>}:scheduled
|
||||
// KEYS[2] -> asynq:{<qname>}:t:<task_id>
|
||||
// KEYS[3] -> asynq:{<qname>}:scheduled
|
||||
// ARGV[1] -> task ID
|
||||
// ARGV[2] -> uniqueness lock TTL
|
||||
// ARGV[3] -> score (process_at timestamp)
|
||||
// ARGV[4] -> task message
|
||||
// ARGV[5] -> task timeout in seconds (0 if not timeout)
|
||||
// ARGV[6] -> task deadline in unix time (0 if no deadline)
|
||||
var scheduleUniqueCmd = redis.NewScript(`
|
||||
local ok = redis.call("SET", KEYS[1], ARGV[1], "NX", "EX", ARGV[2])
|
||||
if not ok then
|
||||
return 0
|
||||
end
|
||||
redis.call("ZADD", KEYS[2], ARGV[3], ARGV[4])
|
||||
redis.call("HSET", KEYS[2], "msg", ARGV[4], "timeout", ARGV[5], "deadline", ARGV[6])
|
||||
redis.call("ZADD", KEYS[3], ARGV[3], ARGV[1])
|
||||
return 1
|
||||
`)
|
||||
|
||||
@@ -313,10 +382,20 @@ func (r *RDB) ScheduleUnique(msg *base.TaskMessage, processAt time.Time, ttl tim
|
||||
if err := r.client.SAdd(base.AllQueues, msg.Queue).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
score := float64(processAt.Unix())
|
||||
res, err := scheduleUniqueCmd.Run(r.client,
|
||||
[]string{msg.UniqueKey, base.ScheduledKey(msg.Queue)},
|
||||
msg.ID.String(), int(ttl.Seconds()), score, encoded).Result()
|
||||
keys := []string{
|
||||
msg.UniqueKey,
|
||||
base.TaskKey(msg.Queue, msg.ID.String()),
|
||||
base.ScheduledKey(msg.Queue),
|
||||
}
|
||||
argv := []interface{}{
|
||||
msg.ID.String(),
|
||||
int(ttl.Seconds()),
|
||||
processAt.Unix(),
|
||||
encoded,
|
||||
msg.Timeout,
|
||||
msg.Deadline,
|
||||
}
|
||||
res, err := scheduleUniqueCmd.Run(r.client, keys, argv...).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -330,54 +409,62 @@ func (r *RDB) ScheduleUnique(msg *base.TaskMessage, processAt time.Time, ttl tim
|
||||
return nil
|
||||
}
|
||||
|
||||
// KEYS[1] -> asynq:{<qname>}:active
|
||||
// KEYS[2] -> asynq:{<qname>}:deadlines
|
||||
// KEYS[3] -> asynq:{<qname>}:retry
|
||||
// KEYS[4] -> asynq:{<qname>}:processed:<yyyy-mm-dd>
|
||||
// KEYS[5] -> asynq:{<qname>}:failed:<yyyy-mm-dd>
|
||||
// ARGV[1] -> base.TaskMessage value to remove from base.ActiveQueue queue
|
||||
// ARGV[2] -> base.TaskMessage value to add to Retry queue
|
||||
// KEYS[1] -> asynq:{<qname>}:t:<task_id>
|
||||
// KEYS[2] -> asynq:{<qname>}:active
|
||||
// KEYS[3] -> asynq:{<qname>}:deadlines
|
||||
// KEYS[4] -> asynq:{<qname>}:retry
|
||||
// KEYS[5] -> asynq:{<qname>}:processed:<yyyy-mm-dd>
|
||||
// KEYS[6] -> asynq:{<qname>}:failed:<yyyy-mm-dd>
|
||||
// ARGV[1] -> task ID
|
||||
// ARGV[2] -> updated base.TaskMessage value
|
||||
// ARGV[3] -> retry_at UNIX timestamp
|
||||
// ARGV[4] -> stats expiration timestamp
|
||||
var retryCmd = redis.NewScript(`
|
||||
if redis.call("LREM", KEYS[1], 0, ARGV[1]) == 0 then
|
||||
if redis.call("LREM", KEYS[2], 0, ARGV[1]) == 0 then
|
||||
return redis.error_reply("NOT FOUND")
|
||||
end
|
||||
if redis.call("ZREM", KEYS[2], ARGV[1]) == 0 then
|
||||
if redis.call("ZREM", KEYS[3], ARGV[1]) == 0 then
|
||||
return redis.error_reply("NOT FOUND")
|
||||
end
|
||||
redis.call("ZADD", KEYS[3], ARGV[3], ARGV[2])
|
||||
local n = redis.call("INCR", KEYS[4])
|
||||
redis.call("ZADD", KEYS[4], ARGV[3], ARGV[1])
|
||||
redis.call("HSET", KEYS[1], "msg", ARGV[2])
|
||||
local n = redis.call("INCR", KEYS[5])
|
||||
if tonumber(n) == 1 then
|
||||
redis.call("EXPIREAT", KEYS[4], ARGV[4])
|
||||
end
|
||||
local m = redis.call("INCR", KEYS[5])
|
||||
if tonumber(m) == 1 then
|
||||
redis.call("EXPIREAT", KEYS[5], ARGV[4])
|
||||
end
|
||||
local m = redis.call("INCR", KEYS[6])
|
||||
if tonumber(m) == 1 then
|
||||
redis.call("EXPIREAT", KEYS[6], ARGV[4])
|
||||
end
|
||||
return redis.status_reply("OK")`)
|
||||
|
||||
// Retry moves the task from active to retry queue, incrementing retry count
|
||||
// and assigning error message to the task message.
|
||||
func (r *RDB) Retry(msg *base.TaskMessage, processAt time.Time, errMsg string) error {
|
||||
msgToRemove, err := base.EncodeMessage(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
modified := *msg
|
||||
modified.Retried++
|
||||
modified.ErrorMsg = errMsg
|
||||
msgToAdd, err := base.EncodeMessage(&modified)
|
||||
encoded, err := base.EncodeMessage(&modified)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
processedKey := base.ProcessedKey(msg.Queue, now)
|
||||
failedKey := base.FailedKey(msg.Queue, now)
|
||||
expireAt := now.Add(statsTTL)
|
||||
return retryCmd.Run(r.client,
|
||||
[]string{base.ActiveKey(msg.Queue), base.DeadlinesKey(msg.Queue), base.RetryKey(msg.Queue), processedKey, failedKey},
|
||||
msgToRemove, msgToAdd, processAt.Unix(), expireAt.Unix()).Err()
|
||||
keys := []string{
|
||||
base.TaskKey(msg.Queue, msg.ID.String()),
|
||||
base.ActiveKey(msg.Queue),
|
||||
base.DeadlinesKey(msg.Queue),
|
||||
base.RetryKey(msg.Queue),
|
||||
base.ProcessedKey(msg.Queue, now),
|
||||
base.FailedKey(msg.Queue, now),
|
||||
}
|
||||
argv := []interface{}{
|
||||
msg.ID.String(),
|
||||
encoded,
|
||||
processAt.Unix(),
|
||||
expireAt.Unix(),
|
||||
}
|
||||
return retryCmd.Run(r.client, keys, argv...).Err()
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -385,68 +472,78 @@ const (
|
||||
archivedExpirationInDays = 90 // number of days before an archived task gets deleted permanently
|
||||
)
|
||||
|
||||
// KEYS[1] -> asynq:{<qname>}:active
|
||||
// KEYS[2] -> asynq:{<qname>}:deadlines
|
||||
// KEYS[3] -> asynq:{<qname>}:archived
|
||||
// KEYS[4] -> asynq:{<qname>}:processed:<yyyy-mm-dd>
|
||||
// KEYS[5] -> asynq:{<qname>}:failed:<yyyy-mm-dd>
|
||||
// ARGV[1] -> base.TaskMessage value to remove
|
||||
// ARGV[2] -> base.TaskMessage value to add
|
||||
// KEYS[1] -> asynq:{<qname>}:t:<task_id>
|
||||
// KEYS[2] -> asynq:{<qname>}:active
|
||||
// KEYS[3] -> asynq:{<qname>}:deadlines
|
||||
// KEYS[4] -> asynq:{<qname>}:archived
|
||||
// KEYS[5] -> asynq:{<qname>}:processed:<yyyy-mm-dd>
|
||||
// KEYS[6] -> asynq:{<qname>}:failed:<yyyy-mm-dd>
|
||||
// ARGV[1] -> task ID
|
||||
// ARGV[2] -> updated base.TaskMessage value
|
||||
// ARGV[3] -> died_at UNIX timestamp
|
||||
// ARGV[4] -> cutoff timestamp (e.g., 90 days ago)
|
||||
// ARGV[5] -> max number of tasks in archive (e.g., 100)
|
||||
// ARGV[6] -> stats expiration timestamp
|
||||
var archiveCmd = redis.NewScript(`
|
||||
if redis.call("LREM", KEYS[1], 0, ARGV[1]) == 0 then
|
||||
if redis.call("LREM", KEYS[2], 0, ARGV[1]) == 0 then
|
||||
return redis.error_reply("NOT FOUND")
|
||||
end
|
||||
if redis.call("ZREM", KEYS[2], ARGV[1]) == 0 then
|
||||
if redis.call("ZREM", KEYS[3], ARGV[1]) == 0 then
|
||||
return redis.error_reply("NOT FOUND")
|
||||
end
|
||||
redis.call("ZADD", KEYS[3], ARGV[3], ARGV[2])
|
||||
redis.call("ZREMRANGEBYSCORE", KEYS[3], "-inf", ARGV[4])
|
||||
redis.call("ZREMRANGEBYRANK", KEYS[3], 0, -ARGV[5])
|
||||
local n = redis.call("INCR", KEYS[4])
|
||||
redis.call("ZADD", KEYS[4], ARGV[3], ARGV[1])
|
||||
redis.call("ZREMRANGEBYSCORE", KEYS[4], "-inf", ARGV[4])
|
||||
redis.call("ZREMRANGEBYRANK", KEYS[4], 0, -ARGV[5])
|
||||
redis.call("HSET", KEYS[1], "msg", ARGV[2])
|
||||
local n = redis.call("INCR", KEYS[5])
|
||||
if tonumber(n) == 1 then
|
||||
redis.call("EXPIREAT", KEYS[4], ARGV[6])
|
||||
end
|
||||
local m = redis.call("INCR", KEYS[5])
|
||||
if tonumber(m) == 1 then
|
||||
redis.call("EXPIREAT", KEYS[5], ARGV[6])
|
||||
end
|
||||
local m = redis.call("INCR", KEYS[6])
|
||||
if tonumber(m) == 1 then
|
||||
redis.call("EXPIREAT", KEYS[6], ARGV[6])
|
||||
end
|
||||
return redis.status_reply("OK")`)
|
||||
|
||||
// Archive sends the given task to archive, attaching the error message to the task.
|
||||
// It also trims the archive by timestamp and set size.
|
||||
func (r *RDB) Archive(msg *base.TaskMessage, errMsg string) error {
|
||||
msgToRemove, err := base.EncodeMessage(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
modified := *msg
|
||||
modified.ErrorMsg = errMsg
|
||||
msgToAdd, err := base.EncodeMessage(&modified)
|
||||
encoded, err := base.EncodeMessage(&modified)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
limit := now.AddDate(0, 0, -archivedExpirationInDays).Unix() // 90 days ago
|
||||
processedKey := base.ProcessedKey(msg.Queue, now)
|
||||
failedKey := base.FailedKey(msg.Queue, now)
|
||||
cutoff := now.AddDate(0, 0, -archivedExpirationInDays)
|
||||
expireAt := now.Add(statsTTL)
|
||||
return archiveCmd.Run(r.client,
|
||||
[]string{base.ActiveKey(msg.Queue), base.DeadlinesKey(msg.Queue), base.ArchivedKey(msg.Queue), processedKey, failedKey},
|
||||
msgToRemove, msgToAdd, now.Unix(), limit, maxArchiveSize, expireAt.Unix()).Err()
|
||||
keys := []string{
|
||||
base.TaskKey(msg.Queue, msg.ID.String()),
|
||||
base.ActiveKey(msg.Queue),
|
||||
base.DeadlinesKey(msg.Queue),
|
||||
base.ArchivedKey(msg.Queue),
|
||||
base.ProcessedKey(msg.Queue, now),
|
||||
base.FailedKey(msg.Queue, now),
|
||||
}
|
||||
argv := []interface{}{
|
||||
msg.ID.String(),
|
||||
encoded,
|
||||
now.Unix(),
|
||||
cutoff.Unix(),
|
||||
maxArchiveSize,
|
||||
expireAt.Unix(),
|
||||
}
|
||||
return archiveCmd.Run(r.client, keys, argv...).Err()
|
||||
}
|
||||
|
||||
// CheckAndEnqueue checks for scheduled/retry tasks for the given queues
|
||||
//and enqueues any tasks that are ready to be processed.
|
||||
func (r *RDB) CheckAndEnqueue(qnames ...string) error {
|
||||
// ForwardIfReady checks scheduled and retry sets of the given queues
|
||||
// and move any tasks that are ready to be processed to the pending set.
|
||||
func (r *RDB) ForwardIfReady(qnames ...string) error {
|
||||
for _, qname := range qnames {
|
||||
if err := r.forwardAll(base.ScheduledKey(qname), base.QueueKey(qname)); err != nil {
|
||||
if err := r.forwardAll(base.ScheduledKey(qname), base.PendingKey(qname)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.forwardAll(base.RetryKey(qname), base.QueueKey(qname)); err != nil {
|
||||
if err := r.forwardAll(base.RetryKey(qname), base.PendingKey(qname)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -458,12 +555,12 @@ func (r *RDB) CheckAndEnqueue(qnames ...string) error {
|
||||
// ARGV[1] -> current unix time
|
||||
// Note: Script moves tasks up to 100 at a time to keep the runtime of script short.
|
||||
var forwardCmd = redis.NewScript(`
|
||||
local msgs = redis.call("ZRANGEBYSCORE", KEYS[1], "-inf", ARGV[1], "LIMIT", 0, 100)
|
||||
for _, msg in ipairs(msgs) do
|
||||
redis.call("LPUSH", KEYS[2], msg)
|
||||
redis.call("ZREM", KEYS[1], msg)
|
||||
local ids = redis.call("ZRANGEBYSCORE", KEYS[1], "-inf", ARGV[1], "LIMIT", 0, 100)
|
||||
for _, id in ipairs(ids) do
|
||||
redis.call("LPUSH", KEYS[2], id)
|
||||
redis.call("ZREM", KEYS[1], id)
|
||||
end
|
||||
return table.getn(msgs)`)
|
||||
return table.getn(ids)`)
|
||||
|
||||
// forward moves tasks with a score less than the current unix time
|
||||
// from the src zset to the dst list. It returns the number of tasks moved.
|
||||
@@ -489,20 +586,35 @@ func (r *RDB) forwardAll(src, dst string) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// KEYS[1] -> asynq:{<qname>}:deadlines
|
||||
// ARGV[1] -> deadline in unix time
|
||||
// ARGV[2] -> task key prefix
|
||||
var listDeadlineExceededCmd = redis.NewScript(`
|
||||
local res = {}
|
||||
local ids = redis.call("ZRANGEBYSCORE", KEYS[1], "-inf", ARGV[1])
|
||||
for _, id in ipairs(ids) do
|
||||
local key = ARGV[2] .. id
|
||||
table.insert(res, redis.call("HGET", key, "msg"))
|
||||
end
|
||||
return res
|
||||
`)
|
||||
|
||||
// ListDeadlineExceeded returns a list of task messages that have exceeded the deadline from the given queues.
|
||||
func (r *RDB) ListDeadlineExceeded(deadline time.Time, qnames ...string) ([]*base.TaskMessage, error) {
|
||||
var msgs []*base.TaskMessage
|
||||
opt := &redis.ZRangeBy{
|
||||
Min: "-inf",
|
||||
Max: strconv.FormatInt(deadline.Unix(), 10),
|
||||
}
|
||||
for _, qname := range qnames {
|
||||
res, err := r.client.ZRangeByScore(base.DeadlinesKey(qname), opt).Result()
|
||||
res, err := listDeadlineExceededCmd.Run(r.client,
|
||||
[]string{base.DeadlinesKey(qname)},
|
||||
deadline.Unix(), base.TaskKeyPrefix(qname)).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, s := range res {
|
||||
msg, err := base.DecodeMessage(s)
|
||||
data, err := cast.ToStringSliceE(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, s := range data {
|
||||
msg, err := base.DecodeMessage([]byte(s))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -530,14 +642,14 @@ return redis.status_reply("OK")`)
|
||||
|
||||
// WriteServerState writes server state data to redis with expiration set to the value ttl.
|
||||
func (r *RDB) WriteServerState(info *base.ServerInfo, workers []*base.WorkerInfo, ttl time.Duration) error {
|
||||
bytes, err := json.Marshal(info)
|
||||
bytes, err := base.EncodeServerInfo(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exp := time.Now().Add(ttl).UTC()
|
||||
args := []interface{}{ttl.Seconds(), bytes} // args to the lua script
|
||||
for _, w := range workers {
|
||||
bytes, err := json.Marshal(w)
|
||||
bytes, err := base.EncodeWorkerInfo(w)
|
||||
if err != nil {
|
||||
continue // skip bad data
|
||||
}
|
||||
@@ -589,7 +701,7 @@ return redis.status_reply("OK")`)
|
||||
func (r *RDB) WriteSchedulerEntries(schedulerID string, entries []*base.SchedulerEntry, ttl time.Duration) error {
|
||||
args := []interface{}{ttl.Seconds()}
|
||||
for _, e := range entries {
|
||||
bytes, err := json.Marshal(e)
|
||||
bytes, err := base.EncodeSchedulerEntry(e)
|
||||
if err != nil {
|
||||
continue // skip bad data
|
||||
}
|
||||
@@ -644,7 +756,7 @@ const maxEvents = 1000
|
||||
// RecordSchedulerEnqueueEvent records the time when the given task was enqueued.
|
||||
func (r *RDB) RecordSchedulerEnqueueEvent(entryID string, event *base.SchedulerEnqueueEvent) error {
|
||||
key := base.SchedulerHistoryKey(entryID)
|
||||
data, err := json.Marshal(event)
|
||||
data, err := base.EncodeSchedulerEnqueueEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
Reference in New Issue
Block a user