2
0
mirror of https://github.com/hibiken/asynq.git synced 2024-12-27 08:12:19 +08:00

Update rdb.ArchiveTask with more specific error types

This commit is contained in:
Ken Hibino 2021-05-01 16:13:40 -07:00
parent 76486b5cb4
commit 4d65024bd7
3 changed files with 76 additions and 31 deletions

View File

@ -533,19 +533,6 @@ func (r *RDB) removeAndRunAll(zset, qkey string) (int64, error) {
return n, nil return n, nil
} }
// ArchiveTask finds a task that matches the id from the given queue and archives it.
// If there's no match, it returns ErrTaskNotFound.
func (r *RDB) ArchiveTask(qname string, id uuid.UUID) error {
n, err := r.archiveTask(qname, id.String())
if err != nil {
return err
}
if n == 0 {
return ErrTaskNotFound
}
return nil
}
// ArchiveAllRetryTasks archives all retry tasks from the given queue and // ArchiveAllRetryTasks archives all retry tasks from the given queue and
// returns the number of tasks that were moved. // returns the number of tasks that were moved.
func (r *RDB) ArchiveAllRetryTasks(qname string) (int64, error) { func (r *RDB) ArchiveAllRetryTasks(qname string) (int64, error) {
@ -594,37 +581,49 @@ func (r *RDB) ArchiveAllPendingTasks(qname string) (int64, error) {
return n, nil return n, nil
} }
// archiveTaskCmd is a Lua script that arhives a task given a task id. // archiveTaskCmd is a Lua script that archives a task given a task id.
// //
// Input: // Input:
// KEYS[1] -> task key (asynq:{<qname>}:t:<task_id>) // KEYS[1] -> task key (asynq:{<qname>}:t:<task_id>)
// KEYS[2] -> archived key (asynq:{<qname>}:archived) // KEYS[2] -> archived key (asynq:{<qname>}:archived)
// KEYS[3] -> all queues key
// --
// ARGV[1] -> id of the task to archive // ARGV[1] -> id of the task to archive
// ARGV[2] -> current timestamp // ARGV[2] -> current timestamp
// ARGV[3] -> cutoff timestamp (e.g., 90 days ago) // ARGV[3] -> cutoff timestamp (e.g., 90 days ago)
// ARGV[4] -> max number of tasks in archived state (e.g., 100) // ARGV[4] -> max number of tasks in archived state (e.g., 100)
// ARGV[5] -> queue key prefix (asynq:{<qname>}:) // ARGV[5] -> queue key prefix (asynq:{<qname>}:)
// ARGV[6] -> queue name
// //
// Output: // Output:
// TODO: document return value of the script // Numeric code indicating the status:
// Returns 1 if task is successfully archived.
// Returns 0 if task is not found.
// Returns -1 if task is already archived.
// Returns -2 if task is in active state.
// Returns -3 if queue doesn't exist.
// Returns error reply if unexpected error occurs.
var archiveTaskCmd = redis.NewScript(` var archiveTaskCmd = redis.NewScript(`
if redis.call("SISMEMBER", KEYS[3], ARGV[6]) == 0 then
return -3
end
if redis.call("EXISTS", KEYS[1]) == 0 then if redis.call("EXISTS", KEYS[1]) == 0 then
return 0 return 0
end end
local state = redis.call("HGET", KEYS[1], "state") local state = redis.call("HGET", KEYS[1], "state")
if state == "active" then if state == "active" then
return redis.error_reply("Cannot archive active task. Use cancel instead.") return -2
end end
if state == "archived" then if state == "archived" then
return redis.error_reply("Task is already archived") return -1
end end
if state == "pending" then if state == "pending" then
if redis.call("LREM", ARGV[5] .. state, 1, ARGV[1]) == 0 then if redis.call("LREM", ARGV[5] .. state, 1, ARGV[1]) == 0 then
return redis.error_reply("internal error: task id not found in list " .. tostring(state)) return redis.error_reply("task id not found in list " .. tostring(state))
end end
else else
if redis.call("ZREM", ARGV[5] .. state, ARGV[1]) == 0 then if redis.call("ZREM", ARGV[5] .. state, ARGV[1]) == 0 then
return redis.error_reply("internal error: task id not found in zset " .. tostring(state)) return redis.error_reply("task id not found in zset " .. tostring(state))
end end
end end
redis.call("ZADD", KEYS[2], ARGV[2], ARGV[1]) redis.call("ZADD", KEYS[2], ARGV[2], ARGV[1])
@ -634,28 +633,50 @@ redis.call("ZREMRANGEBYRANK", KEYS[2], 0, -ARGV[4])
return 1 return 1
`) `)
func (r *RDB) archiveTask(qname, id string) (int64, error) { // ArchiveTask finds a task that matches the id from the given queue and archives it.
// It returns nil if it successfully archived the task.
//
// If a queue with the given name doesn't exist, it returns ErrQueueNotFound.
// If a task with the given id doesn't exist in the queue, it returns ErrTaskNotFound
// If a task is already archived, it returns ErrTaskAlreadyArchived.
// If a task is in active state it returns non-nil error.
func (r *RDB) ArchiveTask(qname string, id uuid.UUID) error {
keys := []string{ keys := []string{
base.TaskKey(qname, id), base.TaskKey(qname, id.String()),
base.ArchivedKey(qname), base.ArchivedKey(qname),
base.AllQueues,
} }
now := time.Now() now := time.Now()
argv := []interface{}{ argv := []interface{}{
id, id.String(),
now.Unix(), now.Unix(),
now.AddDate(0, 0, -archivedExpirationInDays).Unix(), now.AddDate(0, 0, -archivedExpirationInDays).Unix(),
maxArchiveSize, maxArchiveSize,
base.QueueKeyPrefix(qname), base.QueueKeyPrefix(qname),
qname,
} }
res, err := archiveTaskCmd.Run(r.client, keys, argv...).Result() res, err := archiveTaskCmd.Run(r.client, keys, argv...).Result()
if err != nil { if err != nil {
return 0, err return err
} }
n, ok := res.(int64) n, ok := res.(int64)
if !ok { if !ok {
return 0, fmt.Errorf("internal error: could not cast %v to int64", res) return fmt.Errorf("%w: could not cast %v to int64", base.ErrInternal, res)
}
switch n {
case 1:
return nil
case 0:
return ErrTaskNotFound
case -1:
return ErrTaskAlreadyArchived
case -2:
return fmt.Errorf("%w: cannot archive task in active state. use CancelTask instead.", base.ErrFailedPrecondition)
case -3:
return ErrQueueNotFound
default:
return fmt.Errorf("%w: unexpected return value from archiveTaskCmd script: %d", base.ErrInternal, n)
} }
return n, nil
} }
// KEYS[1] -> ZSET to move task from (e.g., asynq:{<qname>}:retry) // KEYS[1] -> ZSET to move task from (e.g., asynq:{<qname>}:retry)

View File

@ -5,6 +5,7 @@
package rdb package rdb
import ( import (
"errors"
"fmt" "fmt"
"testing" "testing"
"time" "time"
@ -1715,7 +1716,7 @@ func TestArchiveRetryTask(t *testing.T) {
h.SeedAllArchivedQueues(t, r.client, tc.archived) h.SeedAllArchivedQueues(t, r.client, tc.archived)
got := r.ArchiveTask(tc.qname, tc.id) got := r.ArchiveTask(tc.qname, tc.id)
if got != tc.want { if !errors.Is(got, tc.want) {
t.Errorf("(*RDB).ArchiveTask(%q, %v) = %v, want %v", t.Errorf("(*RDB).ArchiveTask(%q, %v) = %v, want %v",
tc.qname, tc.id, got, tc.want) tc.qname, tc.id, got, tc.want)
continue continue
@ -1780,6 +1781,23 @@ func TestArchiveScheduledTask(t *testing.T) {
"default": {{Message: m1, Score: time.Now().Unix()}}, "default": {{Message: m1, Score: time.Now().Unix()}},
}, },
}, },
{
scheduled: map[string][]base.Z{
"default": {{Message: m1, Score: t1.Unix()}},
},
archived: map[string][]base.Z{
"default": {{Message: m2, Score: t2.Unix()}},
},
qname: "nonexistent",
id: m2.ID,
want: ErrQueueNotFound,
wantScheduled: map[string][]base.Z{
"default": {{Message: m1, Score: t1.Unix()}},
},
wantArchived: map[string][]base.Z{
"default": {{Message: m2, Score: t2.Unix()}},
},
},
{ {
scheduled: map[string][]base.Z{ scheduled: map[string][]base.Z{
"default": {{Message: m1, Score: t1.Unix()}}, "default": {{Message: m1, Score: t1.Unix()}},
@ -1789,7 +1807,7 @@ func TestArchiveScheduledTask(t *testing.T) {
}, },
qname: "default", qname: "default",
id: m2.ID, id: m2.ID,
want: ErrTaskNotFound, want: ErrTaskAlreadyArchived,
wantScheduled: map[string][]base.Z{ wantScheduled: map[string][]base.Z{
"default": {{Message: m1, Score: t1.Unix()}}, "default": {{Message: m1, Score: t1.Unix()}},
}, },
@ -1837,7 +1855,7 @@ func TestArchiveScheduledTask(t *testing.T) {
h.SeedAllArchivedQueues(t, r.client, tc.archived) h.SeedAllArchivedQueues(t, r.client, tc.archived)
got := r.ArchiveTask(tc.qname, tc.id) got := r.ArchiveTask(tc.qname, tc.id)
if got != tc.want { if !errors.Is(got, tc.want) {
t.Errorf("(*RDB).ArchiveTask(%q, %v) = %v, want %v", t.Errorf("(*RDB).ArchiveTask(%q, %v) = %v, want %v",
tc.qname, tc.id, got, tc.want) tc.qname, tc.id, got, tc.want)
continue continue
@ -1905,7 +1923,7 @@ func TestArchivePendingTask(t *testing.T) {
"default": {{Message: m2, Score: oneHourAgo.Unix()}}, "default": {{Message: m2, Score: oneHourAgo.Unix()}},
}, },
qname: "default", qname: "default",
id: m2.ID, id: uuid.New(),
want: ErrTaskNotFound, want: ErrTaskNotFound,
wantPending: map[string][]*base.TaskMessage{ wantPending: map[string][]*base.TaskMessage{
"default": {m1}, "default": {m1},
@ -1943,7 +1961,7 @@ func TestArchivePendingTask(t *testing.T) {
h.SeedAllArchivedQueues(t, r.client, tc.archived) h.SeedAllArchivedQueues(t, r.client, tc.archived)
got := r.ArchiveTask(tc.qname, tc.id) got := r.ArchiveTask(tc.qname, tc.id)
if got != tc.want { if !errors.Is(got, tc.want) {
t.Errorf("(*RDB).ArchiveTask(%q, %v) = %v, want %v", t.Errorf("(*RDB).ArchiveTask(%q, %v) = %v, want %v",
tc.qname, tc.id, got, tc.want) tc.qname, tc.id, got, tc.want)
continue continue

View File

@ -20,10 +20,16 @@ var (
ErrNoProcessableTask = errors.New("no tasks are ready for processing") ErrNoProcessableTask = errors.New("no tasks are ready for processing")
// ErrTaskNotFound indicates that a task that matches the given identifier was not found. // ErrTaskNotFound indicates that a task that matches the given identifier was not found.
ErrTaskNotFound = errors.New("could not find a task") ErrTaskNotFound = fmt.Errorf("%w: could not find a task in the queue", base.ErrNotFound)
// ErrTaskAlreadyArchived indicates that the task in question is already in archive state.
ErrTaskAlreadyArchived = fmt.Errorf("%w: task is already archived", base.ErrFailedPrecondition)
// ErrDuplicateTask indicates that another task with the same unique key holds the uniqueness lock. // ErrDuplicateTask indicates that another task with the same unique key holds the uniqueness lock.
ErrDuplicateTask = errors.New("task already exists") ErrDuplicateTask = errors.New("task already exists")
// ErrQueueNotFound indicates that a queue with the given name does not exist.
ErrQueueNotFound = fmt.Errorf("%w: queue does not exist", base.ErrNotFound)
) )
const statsTTL = 90 * 24 * time.Hour // 90 days const statsTTL = 90 * 24 * time.Hour // 90 days