github.com/wfusion/gofusion@v1.1.14/common/infra/asynq/pkg/rdb/rdb.go (about)

     1  // Copyright 2020 Kentaro Hibino. All rights reserved.
     2  // Use of this source code is governed by a MIT license
     3  // that can be found in the LICENSE file.
     4  
     5  // Package rdb encapsulates the interactions with redis.
     6  package rdb
     7  
     8  import (
     9  	"context"
    10  	"fmt"
    11  	"math"
    12  	"time"
    13  
    14  	"github.com/google/uuid"
    15  	"github.com/redis/go-redis/v9"
    16  	"github.com/spf13/cast"
    17  
    18  	"github.com/wfusion/gofusion/common/infra/asynq/pkg/base"
    19  	"github.com/wfusion/gofusion/common/infra/asynq/pkg/errors"
    20  	"github.com/wfusion/gofusion/common/infra/asynq/pkg/timeutil"
    21  )
    22  
    23  const statsTTL = 90 * 24 * time.Hour // 90 days
    24  
    25  // LeaseDuration is the duration used to initially create a lease and to extend it thereafter.
    26  const LeaseDuration = 30 * time.Second
    27  
    28  // RDB is a client interface to query and mutate task queues.
    29  type RDB struct {
    30  	client redis.UniversalClient
    31  	clock  timeutil.Clock
    32  }
    33  
    34  // NewRDB returns a new instance of RDB.
    35  func NewRDB(client redis.UniversalClient) *RDB {
    36  	return &RDB{
    37  		client: client,
    38  		clock:  timeutil.NewRealClock(),
    39  	}
    40  }
    41  
    42  // Close closes the connection with redis server.
    43  func (r *RDB) Close() error {
    44  	return r.client.Close()
    45  }
    46  
    47  // Client returns the reference to underlying redis client.
    48  func (r *RDB) Client() redis.UniversalClient {
    49  	return r.client
    50  }
    51  
    52  // SetClock sets the clock used by RDB to the given clock.
    53  //
    54  // Use this function to set the clock to SimulatedClock in tests.
    55  func (r *RDB) SetClock(c timeutil.Clock) {
    56  	r.clock = c
    57  }
    58  
    59  // Ping checks the connection with redis server.
    60  func (r *RDB) Ping() error {
    61  	return r.client.Ping(context.Background()).Err()
    62  }
    63  
    64  func (r *RDB) runScript(ctx context.Context, op errors.Op, script *redis.Script,
    65  	keys []string, args ...any) error {
    66  	if err := script.Run(ctx, r.client, keys, args...).Err(); err != nil {
    67  		return errors.E(op, errors.Internal, fmt.Sprintf("redis eval error: %v", err))
    68  	}
    69  	return nil
    70  }
    71  
    72  // Runs the given script with keys and args and returns the script's return value as int64.
    73  func (r *RDB) runScriptWithErrorCode(ctx context.Context, op errors.Op, script *redis.Script,
    74  	keys []string, args ...any) (int64, error) {
    75  	res, err := script.Run(ctx, r.client, keys, args...).Result()
    76  	if err != nil {
    77  		return 0, errors.E(op, errors.Unknown, fmt.Sprintf("redis eval error: %v", err))
    78  	}
    79  	n, ok := res.(int64)
    80  	if !ok {
    81  		return 0, errors.E(op, errors.Internal, fmt.Sprintf("unexpected return value from Lua script: %v", res))
    82  	}
    83  	return n, nil
    84  }
    85  
    86  // enqueueCmd enqueues a given task message.
    87  //
    88  // Input:
    89  // KEYS[1] -> asynq:{<qname>}:t:<task_id>
    90  // KEYS[2] -> asynq:{<qname>}:pending
    91  // --
    92  // ARGV[1] -> task message data
    93  // ARGV[2] -> task ID
    94  // ARGV[3] -> current unix time in nsec
    95  //
    96  // Output:
    97  // Returns 1 if successfully enqueued
    98  // Returns 0 if task ID already exists
    99  var enqueueCmd = redis.NewScript(`
   100  if redis.call("EXISTS", KEYS[1]) == 1 then
   101  	return 0
   102  end
   103  redis.call("HSET", KEYS[1],
   104             "msg", ARGV[1],
   105             "state", "pending",
   106             "pending_since", ARGV[3])
   107  redis.call("LPUSH", KEYS[2], ARGV[2])
   108  return 1
   109  `)
   110  
   111  // Enqueue adds the given task to the pending list of the queue.
   112  func (r *RDB) Enqueue(ctx context.Context, msg *base.TaskMessage) error {
   113  	var op errors.Op = "rdb.Enqueue"
   114  	encoded, err := base.EncodeMessage(msg)
   115  	if err != nil {
   116  		return errors.E(op, errors.Unknown, fmt.Sprintf("cannot encode message: %v", err))
   117  	}
   118  	if err := r.client.SAdd(ctx, base.AllQueues, msg.Queue).Err(); err != nil {
   119  		return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "sadd", Err: err})
   120  	}
   121  	keys := []string{
   122  		base.TaskKey(msg.Queue, msg.ID),
   123  		base.PendingKey(msg.Queue),
   124  	}
   125  	argv := []any{
   126  		encoded,
   127  		msg.ID,
   128  		r.clock.Now().UnixNano(),
   129  	}
   130  	n, err := r.runScriptWithErrorCode(ctx, op, enqueueCmd, keys, argv...)
   131  	if err != nil {
   132  		return err
   133  	}
   134  	if n == 0 {
   135  		return errors.E(op, errors.AlreadyExists, errors.ErrTaskIdConflict)
   136  	}
   137  	return nil
   138  }
   139  
   140  // enqueueUniqueCmd enqueues the task message if the task is unique.
   141  //
   142  // KEYS[1] -> unique key
   143  // KEYS[2] -> asynq:{<qname>}:t:<taskid>
   144  // KEYS[3] -> asynq:{<qname>}:pending
   145  // --
   146  // ARGV[1] -> task ID
   147  // ARGV[2] -> uniqueness lock TTL
   148  // ARGV[3] -> task message data
   149  // ARGV[4] -> current unix time in nsec
   150  //
   151  // Output:
   152  // Returns 1 if successfully enqueued
   153  // Returns 0 if task ID conflicts with another task
   154  // Returns -1 if task unique key already exists
   155  var enqueueUniqueCmd = redis.NewScript(`
   156  local ok = redis.call("SET", KEYS[1], ARGV[1], "NX", "EX", ARGV[2])
   157  if not ok then
   158    return -1
   159  end
   160  if redis.call("EXISTS", KEYS[2]) == 1 then
   161    return 0
   162  end
   163  redis.call("HSET", KEYS[2],
   164             "msg", ARGV[3],
   165             "state", "pending",
   166             "pending_since", ARGV[4],
   167             "unique_key", KEYS[1])
   168  redis.call("LPUSH", KEYS[3], ARGV[1])
   169  return 1
   170  `)
   171  
   172  // EnqueueUnique inserts the given task if the task's uniqueness lock can be acquired.
   173  // It returns ErrDuplicateTask if the lock cannot be acquired.
   174  func (r *RDB) EnqueueUnique(ctx context.Context, msg *base.TaskMessage, ttl time.Duration) error {
   175  	var op errors.Op = "rdb.EnqueueUnique"
   176  	encoded, err := base.EncodeMessage(msg)
   177  	if err != nil {
   178  		return errors.E(op, errors.Internal, "cannot encode task message: %v", err)
   179  	}
   180  	if err := r.client.SAdd(ctx, base.AllQueues, msg.Queue).Err(); err != nil {
   181  		return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "sadd", Err: err})
   182  	}
   183  	keys := []string{
   184  		msg.UniqueKey,
   185  		base.TaskKey(msg.Queue, msg.ID),
   186  		base.PendingKey(msg.Queue),
   187  	}
   188  	argv := []any{
   189  		msg.ID,
   190  		int(ttl.Seconds()),
   191  		encoded,
   192  		r.clock.Now().UnixNano(),
   193  	}
   194  	n, err := r.runScriptWithErrorCode(ctx, op, enqueueUniqueCmd, keys, argv...)
   195  	if err != nil {
   196  		return err
   197  	}
   198  	if n == -1 {
   199  		return errors.E(op, errors.AlreadyExists, errors.ErrDuplicateTask)
   200  	}
   201  	if n == 0 {
   202  		return errors.E(op, errors.AlreadyExists, errors.ErrTaskIdConflict)
   203  	}
   204  	return nil
   205  }
   206  
   207  // Input:
   208  // KEYS[1] -> asynq:{<qname>}:pending
   209  // KEYS[2] -> asynq:{<qname>}:paused
   210  // KEYS[3] -> asynq:{<qname>}:active
   211  // KEYS[4] -> asynq:{<qname>}:lease
   212  // --
   213  // ARGV[1] -> initial lease expiration Unix time
   214  // ARGV[2] -> task key prefix
   215  //
   216  // Output:
   217  // Returns nil if no processable task is found in the given queue.
   218  // Returns an encoded TaskMessage.
   219  //
   220  // Note: dequeueCmd checks whether a queue is paused first, before
   221  // calling RPOPLPUSH to pop a task from the queue.
   222  var dequeueCmd = redis.NewScript(`
   223  if redis.call("EXISTS", KEYS[2]) == 0 then
   224  	local id = redis.call("RPOPLPUSH", KEYS[1], KEYS[3])
   225  	if id then
   226  		local key = ARGV[2] .. id
   227  		redis.call("HSET", key, "state", "active")
   228  		redis.call("HDEL", key, "pending_since")
   229  		redis.call("ZADD", KEYS[4], ARGV[1], id)
   230  		return redis.call("HGET", key, "msg")
   231  	end
   232  end
   233  return nil`)
   234  
   235  // Dequeue queries given queues in order and pops a task message
   236  // off a queue if one exists and returns the message and its lease expiration time.
   237  // Dequeue skips a queue if the queue is paused.
   238  // If all queues are empty, ErrNoProcessableTask error is returned.
   239  func (r *RDB) Dequeue(qnames ...string) (msg *base.TaskMessage, leaseExpirationTime time.Time, err error) {
   240  	var op errors.Op = "rdb.Dequeue"
   241  	for _, qname := range qnames {
   242  		keys := []string{
   243  			base.PendingKey(qname),
   244  			base.PausedKey(qname),
   245  			base.ActiveKey(qname),
   246  			base.LeaseKey(qname),
   247  		}
   248  		leaseExpirationTime = r.clock.Now().Add(LeaseDuration)
   249  		argv := []any{
   250  			leaseExpirationTime.Unix(),
   251  			base.TaskKeyPrefix(qname),
   252  		}
   253  		res, err := dequeueCmd.Run(context.Background(), r.client, keys, argv...).Result()
   254  		if err == redis.Nil {
   255  			continue
   256  		} else if err != nil {
   257  			return nil, time.Time{}, errors.E(op, errors.Unknown, fmt.Sprintf("redis eval error: %v", err))
   258  		}
   259  		encoded, err := cast.ToStringE(res)
   260  		if err != nil {
   261  			return nil, time.Time{}, errors.E(op, errors.Internal, fmt.Sprintf("cast error: unexpected return value from Lua script: %v", res))
   262  		}
   263  		if msg, err = base.DecodeMessage([]byte(encoded)); err != nil {
   264  			return nil, time.Time{}, errors.E(op, errors.Internal, fmt.Sprintf("cannot decode message: %v", err))
   265  		}
   266  		return msg, leaseExpirationTime, nil
   267  	}
   268  	return nil, time.Time{}, errors.E(op, errors.NotFound, errors.ErrNoProcessableTask)
   269  }
   270  
   271  // KEYS[1] -> asynq:{<qname>}:active
   272  // KEYS[2] -> asynq:{<qname>}:lease
   273  // KEYS[3] -> asynq:{<qname>}:t:<task_id>
   274  // KEYS[4] -> asynq:{<qname>}:processed:<yyyy-mm-dd>
   275  // KEYS[5] -> asynq:{<qname>}:processed
   276  // -------
   277  // ARGV[1] -> task ID
   278  // ARGV[2] -> stats expiration timestamp
   279  // ARGV[3] -> max int64 value
   280  var doneCmd = redis.NewScript(`
   281  if redis.call("LREM", KEYS[1], 0, ARGV[1]) == 0 then
   282    return redis.error_reply("NOT FOUND")
   283  end
   284  if redis.call("ZREM", KEYS[2], ARGV[1]) == 0 then
   285    return redis.error_reply("NOT FOUND")
   286  end
   287  if redis.call("DEL", KEYS[3]) == 0 then
   288    return redis.error_reply("NOT FOUND")
   289  end
   290  local n = redis.call("INCR", KEYS[4])
   291  if tonumber(n) == 1 then
   292  	redis.call("EXPIREAT", KEYS[4], ARGV[2])
   293  end
   294  local total = redis.call("GET", KEYS[5])
   295  if tonumber(total) == tonumber(ARGV[3]) then
   296  	redis.call("SET", KEYS[5], 1)
   297  else
   298  	redis.call("INCR", KEYS[5])
   299  end
   300  return redis.status_reply("OK")
   301  `)
   302  
   303  // KEYS[1] -> asynq:{<qname>}:active
   304  // KEYS[2] -> asynq:{<qname>}:lease
   305  // KEYS[3] -> asynq:{<qname>}:t:<task_id>
   306  // KEYS[4] -> asynq:{<qname>}:processed:<yyyy-mm-dd>
   307  // KEYS[5] -> asynq:{<qname>}:processed
   308  // KEYS[6] -> unique key
   309  // -------
   310  // ARGV[1] -> task ID
   311  // ARGV[2] -> stats expiration timestamp
   312  // ARGV[3] -> max int64 value
   313  var doneUniqueCmd = redis.NewScript(`
   314  if redis.call("LREM", KEYS[1], 0, ARGV[1]) == 0 then
   315    return redis.error_reply("NOT FOUND")
   316  end
   317  if redis.call("ZREM", KEYS[2], ARGV[1]) == 0 then
   318    return redis.error_reply("NOT FOUND")
   319  end
   320  if redis.call("DEL", KEYS[3]) == 0 then
   321    return redis.error_reply("NOT FOUND")
   322  end
   323  local n = redis.call("INCR", KEYS[4])
   324  if tonumber(n) == 1 then
   325  	redis.call("EXPIREAT", KEYS[4], ARGV[2])
   326  end
   327  local total = redis.call("GET", KEYS[5])
   328  if tonumber(total) == tonumber(ARGV[3]) then
   329  	redis.call("SET", KEYS[5], 1)
   330  else
   331  	redis.call("INCR", KEYS[5])
   332  end
   333  if redis.call("GET", KEYS[6]) == ARGV[1] then
   334    redis.call("DEL", KEYS[6])
   335  end
   336  return redis.status_reply("OK")
   337  `)
   338  
   339  // Done removes the task from active queue and deletes the task.
   340  // It removes a uniqueness lock acquired by the task, if any.
   341  func (r *RDB) Done(ctx context.Context, msg *base.TaskMessage) error {
   342  	var op errors.Op = "rdb.Done"
   343  	now := r.clock.Now()
   344  	expireAt := now.Add(statsTTL)
   345  	keys := []string{
   346  		base.ActiveKey(msg.Queue),
   347  		base.LeaseKey(msg.Queue),
   348  		base.TaskKey(msg.Queue, msg.ID),
   349  		base.ProcessedKey(msg.Queue, now),
   350  		base.ProcessedTotalKey(msg.Queue),
   351  	}
   352  	argv := []any{
   353  		msg.ID,
   354  		expireAt.Unix(),
   355  		int64(math.MaxInt64),
   356  	}
   357  	// Note: We cannot pass empty unique key when running this script in redis-cluster.
   358  	if len(msg.UniqueKey) > 0 {
   359  		keys = append(keys, msg.UniqueKey)
   360  		return r.runScript(ctx, op, doneUniqueCmd, keys, argv...)
   361  	}
   362  	return r.runScript(ctx, op, doneCmd, keys, argv...)
   363  }
   364  
   365  // KEYS[1] -> asynq:{<qname>}:active
   366  // KEYS[2] -> asynq:{<qname>}:lease
   367  // KEYS[3] -> asynq:{<qname>}:completed
   368  // KEYS[4] -> asynq:{<qname>}:t:<task_id>
   369  // KEYS[5] -> asynq:{<qname>}:processed:<yyyy-mm-dd>
   370  // KEYS[6] -> asynq:{<qname>}:processed
   371  //
   372  // ARGV[1] -> task ID
   373  // ARGV[2] -> stats expiration timestamp
   374  // ARGV[3] -> task expiration time in unix time
   375  // ARGV[4] -> task message data
   376  // ARGV[5] -> max int64 value
   377  var markAsCompleteCmd = redis.NewScript(`
   378  if redis.call("LREM", KEYS[1], 0, ARGV[1]) == 0 then
   379    return redis.error_reply("NOT FOUND")
   380  end
   381  if redis.call("ZREM", KEYS[2], ARGV[1]) == 0 then
   382    return redis.error_reply("NOT FOUND")
   383  end
   384  if redis.call("ZADD", KEYS[3], ARGV[3], ARGV[1]) ~= 1 then
   385    return redis.error_reply("INTERNAL")
   386  end
   387  redis.call("HSET", KEYS[4], "msg", ARGV[4], "state", "completed")
   388  local n = redis.call("INCR", KEYS[5])
   389  if tonumber(n) == 1 then
   390  	redis.call("EXPIREAT", KEYS[5], ARGV[2])
   391  end
   392  local total = redis.call("GET", KEYS[6])
   393  if tonumber(total) == tonumber(ARGV[5]) then
   394  	redis.call("SET", KEYS[6], 1)
   395  else
   396  	redis.call("INCR", KEYS[6])
   397  end
   398  return redis.status_reply("OK")
   399  `)
   400  
   401  // KEYS[1] -> asynq:{<qname>}:active
   402  // KEYS[2] -> asynq:{<qname>}:lease
   403  // KEYS[3] -> asynq:{<qname>}:completed
   404  // KEYS[4] -> asynq:{<qname>}:t:<task_id>
   405  // KEYS[5] -> asynq:{<qname>}:processed:<yyyy-mm-dd>
   406  // KEYS[6] -> asynq:{<qname>}:processed
   407  // KEYS[7] -> asynq:{<qname>}:unique:{<checksum>}
   408  //
   409  // ARGV[1] -> task ID
   410  // ARGV[2] -> stats expiration timestamp
   411  // ARGV[3] -> task expiration time in unix time
   412  // ARGV[4] -> task message data
   413  // ARGV[5] -> max int64 value
   414  var markAsCompleteUniqueCmd = redis.NewScript(`
   415  if redis.call("LREM", KEYS[1], 0, ARGV[1]) == 0 then
   416    return redis.error_reply("NOT FOUND")
   417  end
   418  if redis.call("ZREM", KEYS[2], ARGV[1]) == 0 then
   419    return redis.error_reply("NOT FOUND")
   420  end
   421  if redis.call("ZADD", KEYS[3], ARGV[3], ARGV[1]) ~= 1 then
   422    return redis.error_reply("INTERNAL")
   423  end
   424  redis.call("HSET", KEYS[4], "msg", ARGV[4], "state", "completed")
   425  local n = redis.call("INCR", KEYS[5])
   426  if tonumber(n) == 1 then
   427  	redis.call("EXPIREAT", KEYS[5], ARGV[2])
   428  end
   429  local total = redis.call("GET", KEYS[6])
   430  if tonumber(total) == tonumber(ARGV[5]) then
   431  	redis.call("SET", KEYS[6], 1)
   432  else
   433  	redis.call("INCR", KEYS[6])
   434  end
   435  if redis.call("GET", KEYS[7]) == ARGV[1] then
   436    redis.call("DEL", KEYS[7])
   437  end
   438  return redis.status_reply("OK")
   439  `)
   440  
   441  // MarkAsComplete removes the task from active queue to mark the task as completed.
   442  // It removes a uniqueness lock acquired by the task, if any.
   443  func (r *RDB) MarkAsComplete(ctx context.Context, msg *base.TaskMessage) error {
   444  	var op errors.Op = "rdb.MarkAsComplete"
   445  	now := r.clock.Now()
   446  	statsExpireAt := now.Add(statsTTL)
   447  	msg.CompletedAt = now.Unix()
   448  	encoded, err := base.EncodeMessage(msg)
   449  	if err != nil {
   450  		return errors.E(op, errors.Unknown, fmt.Sprintf("cannot encode message: %v", err))
   451  	}
   452  	keys := []string{
   453  		base.ActiveKey(msg.Queue),
   454  		base.LeaseKey(msg.Queue),
   455  		base.CompletedKey(msg.Queue),
   456  		base.TaskKey(msg.Queue, msg.ID),
   457  		base.ProcessedKey(msg.Queue, now),
   458  		base.ProcessedTotalKey(msg.Queue),
   459  	}
   460  	argv := []any{
   461  		msg.ID,
   462  		statsExpireAt.Unix(),
   463  		now.Unix() + msg.Retention,
   464  		encoded,
   465  		int64(math.MaxInt64),
   466  	}
   467  	// Note: We cannot pass empty unique key when running this script in redis-cluster.
   468  	if len(msg.UniqueKey) > 0 {
   469  		keys = append(keys, msg.UniqueKey)
   470  		return r.runScript(ctx, op, markAsCompleteUniqueCmd, keys, argv...)
   471  	}
   472  	return r.runScript(ctx, op, markAsCompleteCmd, keys, argv...)
   473  }
   474  
   475  // KEYS[1] -> asynq:{<qname>}:active
   476  // KEYS[2] -> asynq:{<qname>}:lease
   477  // KEYS[3] -> asynq:{<qname>}:pending
   478  // KEYS[4] -> asynq:{<qname>}:t:<task_id>
   479  // ARGV[1] -> task ID
   480  // Note: Use RPUSH to push to the head of the queue.
   481  var requeueCmd = redis.NewScript(`
   482  if redis.call("LREM", KEYS[1], 0, ARGV[1]) == 0 then
   483    return redis.error_reply("NOT FOUND")
   484  end
   485  if redis.call("ZREM", KEYS[2], ARGV[1]) == 0 then
   486    return redis.error_reply("NOT FOUND")
   487  end
   488  redis.call("RPUSH", KEYS[3], ARGV[1])
   489  redis.call("HSET", KEYS[4], "state", "pending")
   490  return redis.status_reply("OK")`)
   491  
   492  // Requeue moves the task from active queue to the specified queue.
   493  func (r *RDB) Requeue(ctx context.Context, msg *base.TaskMessage) error {
   494  	var op errors.Op = "rdb.Requeue"
   495  	keys := []string{
   496  		base.ActiveKey(msg.Queue),
   497  		base.LeaseKey(msg.Queue),
   498  		base.PendingKey(msg.Queue),
   499  		base.TaskKey(msg.Queue, msg.ID),
   500  	}
   501  	return r.runScript(ctx, op, requeueCmd, keys, msg.ID)
   502  }
   503  
   504  // KEYS[1] -> asynq:{<qname>}:t:<task_id>
   505  // KEYS[2] -> asynq:{<qname>}:g:<group_key>
   506  // KEYS[3] -> asynq:{<qname>}:groups
   507  // -------
   508  // ARGV[1] -> task message data
   509  // ARGV[2] -> task ID
   510  // ARGV[3] -> current time in Unix time
   511  // ARGV[4] -> group key
   512  //
   513  // Output:
   514  // Returns 1 if successfully added
   515  // Returns 0 if task ID already exists
   516  var addToGroupCmd = redis.NewScript(`
   517  if redis.call("EXISTS", KEYS[1]) == 1 then
   518  	return 0
   519  end
   520  redis.call("HSET", KEYS[1],
   521             "msg", ARGV[1],
   522             "state", "aggregating",
   523  	       "group", ARGV[4])
   524  redis.call("ZADD", KEYS[2], ARGV[3], ARGV[2])
   525  redis.call("SADD", KEYS[3], ARGV[4])
   526  return 1
   527  `)
   528  
   529  func (r *RDB) AddToGroup(ctx context.Context, msg *base.TaskMessage, groupKey string) error {
   530  	var op errors.Op = "rdb.AddToGroup"
   531  	encoded, err := base.EncodeMessage(msg)
   532  	if err != nil {
   533  		return errors.E(op, errors.Unknown, fmt.Sprintf("cannot encode message: %v", err))
   534  	}
   535  	if err := r.client.SAdd(ctx, base.AllQueues, msg.Queue).Err(); err != nil {
   536  		return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "sadd", Err: err})
   537  	}
   538  	keys := []string{
   539  		base.TaskKey(msg.Queue, msg.ID),
   540  		base.GroupKey(msg.Queue, groupKey),
   541  		base.AllGroups(msg.Queue),
   542  	}
   543  	argv := []any{
   544  		encoded,
   545  		msg.ID,
   546  		r.clock.Now().Unix(),
   547  		groupKey,
   548  	}
   549  	n, err := r.runScriptWithErrorCode(ctx, op, addToGroupCmd, keys, argv...)
   550  	if err != nil {
   551  		return err
   552  	}
   553  	if n == 0 {
   554  		return errors.E(op, errors.AlreadyExists, errors.ErrTaskIdConflict)
   555  	}
   556  	return nil
   557  }
   558  
   559  // KEYS[1] -> asynq:{<qname>}:t:<task_id>
   560  // KEYS[2] -> asynq:{<qname>}:g:<group_key>
   561  // KEYS[3] -> asynq:{<qname>}:groups
   562  // KEYS[4] -> unique key
   563  // -------
   564  // ARGV[1] -> task message data
   565  // ARGV[2] -> task ID
   566  // ARGV[3] -> current time in Unix time
   567  // ARGV[4] -> group key
   568  // ARGV[5] -> uniqueness lock TTL
   569  //
   570  // Output:
   571  // Returns 1 if successfully added
   572  // Returns 0 if task ID already exists
   573  // Returns -1 if task unique key already exists
   574  var addToGroupUniqueCmd = redis.NewScript(`
   575  local ok = redis.call("SET", KEYS[4], ARGV[2], "NX", "EX", ARGV[5])
   576  if not ok then
   577    return -1
   578  end
   579  if redis.call("EXISTS", KEYS[1]) == 1 then
   580  	return 0
   581  end
   582  redis.call("HSET", KEYS[1],
   583             "msg", ARGV[1],
   584             "state", "aggregating",
   585  	       "group", ARGV[4])
   586  redis.call("ZADD", KEYS[2], ARGV[3], ARGV[2])
   587  redis.call("SADD", KEYS[3], ARGV[4])
   588  return 1
   589  `)
   590  
   591  func (r *RDB) AddToGroupUnique(ctx context.Context, msg *base.TaskMessage,
   592  	uniqueKey, groupKey string, ttl time.Duration) error {
   593  	var op errors.Op = "rdb.AddToGroupUnique"
   594  	encoded, err := base.EncodeMessage(msg)
   595  	if err != nil {
   596  		return errors.E(op, errors.Unknown, fmt.Sprintf("cannot encode message: %v", err))
   597  	}
   598  	if err := r.client.SAdd(ctx, base.AllQueues, msg.Queue).Err(); err != nil {
   599  		return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "sadd", Err: err})
   600  	}
   601  	if uniqueKey == "" {
   602  		uniqueKey = base.UniqueKey(msg.Queue, msg.Type, msg.Payload)
   603  	}
   604  	keys := []string{
   605  		base.TaskKey(msg.Queue, msg.ID),
   606  		base.GroupKey(msg.Queue, groupKey),
   607  		base.AllGroups(msg.Queue),
   608  		uniqueKey,
   609  	}
   610  	argv := []any{
   611  		encoded,
   612  		msg.ID,
   613  		r.clock.Now().Unix(),
   614  		groupKey,
   615  		int(ttl.Seconds()),
   616  	}
   617  	n, err := r.runScriptWithErrorCode(ctx, op, addToGroupUniqueCmd, keys, argv...)
   618  	if err != nil {
   619  		return err
   620  	}
   621  	if n == -1 {
   622  		return errors.E(op, errors.AlreadyExists, errors.ErrDuplicateTask)
   623  	}
   624  	if n == 0 {
   625  		return errors.E(op, errors.AlreadyExists, errors.ErrTaskIdConflict)
   626  	}
   627  	return nil
   628  }
   629  
   630  // KEYS[1] -> asynq:{<qname>}:t:<task_id>
   631  // KEYS[2] -> asynq:{<qname>}:scheduled
   632  // -------
   633  // ARGV[1] -> task message data
   634  // ARGV[2] -> process_at time in Unix time
   635  // ARGV[3] -> task ID
   636  //
   637  // Output:
   638  // Returns 1 if successfully enqueued
   639  // Returns 0 if task ID already exists
   640  var scheduleCmd = redis.NewScript(`
   641  if redis.call("EXISTS", KEYS[1]) == 1 then
   642  	return 0
   643  end
   644  redis.call("HSET", KEYS[1],
   645             "msg", ARGV[1],
   646             "state", "scheduled")
   647  redis.call("ZADD", KEYS[2], ARGV[2], ARGV[3])
   648  return 1
   649  `)
   650  
   651  // Schedule adds the task to the scheduled set to be processed in the future.
   652  func (r *RDB) Schedule(ctx context.Context, msg *base.TaskMessage, processAt time.Time) error {
   653  	var op errors.Op = "rdb.Schedule"
   654  	encoded, err := base.EncodeMessage(msg)
   655  	if err != nil {
   656  		return errors.E(op, errors.Unknown, fmt.Sprintf("cannot encode message: %v", err))
   657  	}
   658  	if err := r.client.SAdd(ctx, base.AllQueues, msg.Queue).Err(); err != nil {
   659  		return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "sadd", Err: err})
   660  	}
   661  	keys := []string{
   662  		base.TaskKey(msg.Queue, msg.ID),
   663  		base.ScheduledKey(msg.Queue),
   664  	}
   665  	argv := []any{
   666  		encoded,
   667  		processAt.Unix(),
   668  		msg.ID,
   669  	}
   670  	n, err := r.runScriptWithErrorCode(ctx, op, scheduleCmd, keys, argv...)
   671  	if err != nil {
   672  		return err
   673  	}
   674  	if n == 0 {
   675  		return errors.E(op, errors.AlreadyExists, errors.ErrTaskIdConflict)
   676  	}
   677  	return nil
   678  }
   679  
   680  // KEYS[1] -> unique key
   681  // KEYS[2] -> asynq:{<qname>}:t:<task_id>
   682  // KEYS[3] -> asynq:{<qname>}:scheduled
   683  // -------
   684  // ARGV[1] -> task ID
   685  // ARGV[2] -> uniqueness lock TTL
   686  // ARGV[3] -> score (process_at timestamp)
   687  // ARGV[4] -> task message
   688  //
   689  // Output:
   690  // Returns 1 if successfully scheduled
   691  // Returns 0 if task ID already exists
   692  // Returns -1 if task unique key already exists
   693  var scheduleUniqueCmd = redis.NewScript(`
   694  local ok = redis.call("SET", KEYS[1], ARGV[1], "NX", "EX", ARGV[2])
   695  if not ok then
   696    return -1
   697  end
   698  if redis.call("EXISTS", KEYS[2]) == 1 then
   699    return 0
   700  end
   701  redis.call("HSET", KEYS[2],
   702             "msg", ARGV[4],
   703             "state", "scheduled",
   704             "unique_key", KEYS[1])
   705  redis.call("ZADD", KEYS[3], ARGV[3], ARGV[1])
   706  return 1
   707  `)
   708  
   709  // ScheduleUnique adds the task to the backlog queue to be processed in the future if the uniqueness lock can be acquired.
   710  // It returns ErrDuplicateTask if the lock cannot be acquired.
   711  func (r *RDB) ScheduleUnique(ctx context.Context, msg *base.TaskMessage, processAt time.Time, ttl time.Duration) error {
   712  	var op errors.Op = "rdb.ScheduleUnique"
   713  	encoded, err := base.EncodeMessage(msg)
   714  	if err != nil {
   715  		return errors.E(op, errors.Internal, fmt.Sprintf("cannot encode task message: %v", err))
   716  	}
   717  	if err := r.client.SAdd(ctx, base.AllQueues, msg.Queue).Err(); err != nil {
   718  		return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "sadd", Err: err})
   719  	}
   720  	keys := []string{
   721  		msg.UniqueKey,
   722  		base.TaskKey(msg.Queue, msg.ID),
   723  		base.ScheduledKey(msg.Queue),
   724  	}
   725  	argv := []any{
   726  		msg.ID,
   727  		int(ttl.Seconds()),
   728  		processAt.Unix(),
   729  		encoded,
   730  	}
   731  	n, err := r.runScriptWithErrorCode(ctx, op, scheduleUniqueCmd, keys, argv...)
   732  	if err != nil {
   733  		return err
   734  	}
   735  	if n == -1 {
   736  		return errors.E(op, errors.AlreadyExists, errors.ErrDuplicateTask)
   737  	}
   738  	if n == 0 {
   739  		return errors.E(op, errors.AlreadyExists, errors.ErrTaskIdConflict)
   740  	}
   741  	return nil
   742  }
   743  
   744  // KEYS[1] -> asynq:{<qname>}:t:<task_id>
   745  // KEYS[2] -> asynq:{<qname>}:active
   746  // KEYS[3] -> asynq:{<qname>}:lease
   747  // KEYS[4] -> asynq:{<qname>}:retry
   748  // KEYS[5] -> asynq:{<qname>}:processed:<yyyy-mm-dd>
   749  // KEYS[6] -> asynq:{<qname>}:failed:<yyyy-mm-dd>
   750  // KEYS[7] -> asynq:{<qname>}:processed
   751  // KEYS[8] -> asynq:{<qname>}:failed
   752  // -------
   753  // ARGV[1] -> task ID
   754  // ARGV[2] -> updated base.TaskMessage value
   755  // ARGV[3] -> retry_at UNIX timestamp
   756  // ARGV[4] -> stats expiration timestamp
   757  // ARGV[5] -> is_failure (bool)
   758  // ARGV[6] -> max int64 value
   759  var retryCmd = redis.NewScript(`
   760  if redis.call("LREM", KEYS[2], 0, ARGV[1]) == 0 then
   761    return redis.error_reply("NOT FOUND")
   762  end
   763  if redis.call("ZREM", KEYS[3], ARGV[1]) == 0 then
   764    return redis.error_reply("NOT FOUND")
   765  end
   766  redis.call("ZADD", KEYS[4], ARGV[3], ARGV[1])
   767  redis.call("HSET", KEYS[1], "msg", ARGV[2], "state", "retry")
   768  if tonumber(ARGV[5]) == 1 then
   769  	local n = redis.call("INCR", KEYS[5])
   770  	if tonumber(n) == 1 then
   771  		redis.call("EXPIREAT", KEYS[5], ARGV[4])
   772  	end
   773  	local m = redis.call("INCR", KEYS[6])
   774  	if tonumber(m) == 1 then
   775  		redis.call("EXPIREAT", KEYS[6], ARGV[4])
   776  	end
   777      local total = redis.call("GET", KEYS[7])
   778      if tonumber(total) == tonumber(ARGV[6]) then
   779      	redis.call("SET", KEYS[7], 1)
   780      	redis.call("SET", KEYS[8], 1)
   781      else
   782      	redis.call("INCR", KEYS[7])
   783      	redis.call("INCR", KEYS[8])
   784      end
   785  end
   786  return redis.status_reply("OK")`)
   787  
   788  // Retry moves the task from active to retry queue.
   789  // It also annotates the message with the given error message and
   790  // if isFailure is true increments the retried counter.
   791  func (r *RDB) Retry(ctx context.Context, msg *base.TaskMessage,
   792  	processAt time.Time, errMsg string, isFailure bool) error {
   793  	var op errors.Op = "rdb.Retry"
   794  	now := r.clock.Now()
   795  	modified := *msg
   796  	if isFailure {
   797  		modified.Retried++
   798  	}
   799  	modified.ErrorMsg = errMsg
   800  	modified.LastFailedAt = now.Unix()
   801  	encoded, err := base.EncodeMessage(&modified)
   802  	if err != nil {
   803  		return errors.E(op, errors.Internal, fmt.Sprintf("cannot encode message: %v", err))
   804  	}
   805  	expireAt := now.Add(statsTTL)
   806  	keys := []string{
   807  		base.TaskKey(msg.Queue, msg.ID),
   808  		base.ActiveKey(msg.Queue),
   809  		base.LeaseKey(msg.Queue),
   810  		base.RetryKey(msg.Queue),
   811  		base.ProcessedKey(msg.Queue, now),
   812  		base.FailedKey(msg.Queue, now),
   813  		base.ProcessedTotalKey(msg.Queue),
   814  		base.FailedTotalKey(msg.Queue),
   815  	}
   816  	argv := []any{
   817  		msg.ID,
   818  		encoded,
   819  		processAt.Unix(),
   820  		expireAt.Unix(),
   821  		isFailure,
   822  		int64(math.MaxInt64),
   823  	}
   824  	return r.runScript(ctx, op, retryCmd, keys, argv...)
   825  }
   826  
   827  const (
   828  	maxArchiveSize           = 10000 // maximum number of tasks in archive
   829  	archivedExpirationInDays = 90    // number of days before an archived task gets deleted permanently
   830  )
   831  
   832  // KEYS[1] -> asynq:{<qname>}:t:<task_id>
   833  // KEYS[2] -> asynq:{<qname>}:active
   834  // KEYS[3] -> asynq:{<qname>}:lease
   835  // KEYS[4] -> asynq:{<qname>}:archived
   836  // KEYS[5] -> asynq:{<qname>}:processed:<yyyy-mm-dd>
   837  // KEYS[6] -> asynq:{<qname>}:failed:<yyyy-mm-dd>
   838  // KEYS[7] -> asynq:{<qname>}:processed
   839  // KEYS[8] -> asynq:{<qname>}:failed
   840  // -------
   841  // ARGV[1] -> task ID
   842  // ARGV[2] -> updated base.TaskMessage value
   843  // ARGV[3] -> died_at UNIX timestamp
   844  // ARGV[4] -> cutoff timestamp (e.g., 90 days ago)
   845  // ARGV[5] -> max number of tasks in archive (e.g., 100)
   846  // ARGV[6] -> stats expiration timestamp
   847  // ARGV[7] -> max int64 value
   848  var archiveCmd = redis.NewScript(`
   849  if redis.call("LREM", KEYS[2], 0, ARGV[1]) == 0 then
   850    return redis.error_reply("NOT FOUND")
   851  end
   852  if redis.call("ZREM", KEYS[3], ARGV[1]) == 0 then
   853    return redis.error_reply("NOT FOUND")
   854  end
   855  redis.call("ZADD", KEYS[4], ARGV[3], ARGV[1])
   856  redis.call("ZREMRANGEBYSCORE", KEYS[4], "-inf", ARGV[4])
   857  redis.call("ZREMRANGEBYRANK", KEYS[4], 0, -ARGV[5])
   858  redis.call("HSET", KEYS[1], "msg", ARGV[2], "state", "archived")
   859  local n = redis.call("INCR", KEYS[5])
   860  if tonumber(n) == 1 then
   861  	redis.call("EXPIREAT", KEYS[5], ARGV[6])
   862  end
   863  local m = redis.call("INCR", KEYS[6])
   864  if tonumber(m) == 1 then
   865  	redis.call("EXPIREAT", KEYS[6], ARGV[6])
   866  end
   867  local total = redis.call("GET", KEYS[7])
   868  if tonumber(total) == tonumber(ARGV[7]) then
   869     	redis.call("SET", KEYS[7], 1)
   870     	redis.call("SET", KEYS[8], 1)
   871  else
   872    	redis.call("INCR", KEYS[7])
   873     	redis.call("INCR", KEYS[8])
   874  end
   875  return redis.status_reply("OK")`)
   876  
   877  // Archive sends the given task to archive, attaching the error message to the task.
   878  // It also trims the archive by timestamp and set size.
   879  func (r *RDB) Archive(ctx context.Context, msg *base.TaskMessage, errMsg string) error {
   880  	var op errors.Op = "rdb.Archive"
   881  	now := r.clock.Now()
   882  	modified := *msg
   883  	modified.ErrorMsg = errMsg
   884  	modified.LastFailedAt = now.Unix()
   885  	encoded, err := base.EncodeMessage(&modified)
   886  	if err != nil {
   887  		return errors.E(op, errors.Internal, fmt.Sprintf("cannot encode message: %v", err))
   888  	}
   889  	cutoff := now.AddDate(0, 0, -archivedExpirationInDays)
   890  	expireAt := now.Add(statsTTL)
   891  	keys := []string{
   892  		base.TaskKey(msg.Queue, msg.ID),
   893  		base.ActiveKey(msg.Queue),
   894  		base.LeaseKey(msg.Queue),
   895  		base.ArchivedKey(msg.Queue),
   896  		base.ProcessedKey(msg.Queue, now),
   897  		base.FailedKey(msg.Queue, now),
   898  		base.ProcessedTotalKey(msg.Queue),
   899  		base.FailedTotalKey(msg.Queue),
   900  	}
   901  	argv := []any{
   902  		msg.ID,
   903  		encoded,
   904  		now.Unix(),
   905  		cutoff.Unix(),
   906  		maxArchiveSize,
   907  		expireAt.Unix(),
   908  		int64(math.MaxInt64),
   909  	}
   910  	return r.runScript(ctx, op, archiveCmd, keys, argv...)
   911  }
   912  
   913  // ForwardIfReady checks scheduled and retry sets of the given queues
   914  // and move any tasks that are ready to be processed to the pending set.
   915  func (r *RDB) ForwardIfReady(qnames ...string) error {
   916  	var op errors.Op = "rdb.ForwardIfReady"
   917  	for _, qname := range qnames {
   918  		if err := r.forwardAll(qname); err != nil {
   919  			return errors.E(op, errors.CanonicalCode(err), err)
   920  		}
   921  	}
   922  	return nil
   923  }
   924  
   925  // KEYS[1] -> source queue (e.g. asynq:{<qname>:scheduled or asynq:{<qname>}:retry})
   926  // KEYS[2] -> asynq:{<qname>}:pending
   927  // ARGV[1] -> current unix time in seconds
   928  // ARGV[2] -> task key prefix
   929  // ARGV[3] -> current unix time in nsec
   930  // ARGV[4] -> group key prefix
   931  // Note: Script moves tasks up to 100 at a time to keep the runtime of script short.
   932  var forwardCmd = redis.NewScript(`
   933  local ids = redis.call("ZRANGEBYSCORE", KEYS[1], "-inf", ARGV[1], "LIMIT", 0, 100)
   934  for _, id in ipairs(ids) do
   935  	local taskKey = ARGV[2] .. id
   936  	local group = redis.call("HGET", taskKey, "group")
   937  	if group and group ~= '' then
   938  	    redis.call("ZADD", ARGV[4] .. group, ARGV[1], id)
   939  		redis.call("ZREM", KEYS[1], id)
   940  		redis.call("HSET", taskKey,
   941  				   "state", "aggregating")
   942  	else
   943  		redis.call("LPUSH", KEYS[2], id)
   944  		redis.call("ZREM", KEYS[1], id)
   945  		redis.call("HSET", taskKey,
   946  				   "state", "pending",
   947  				   "pending_since", ARGV[3])
   948  	end
   949  end
   950  return table.getn(ids)`)
   951  
   952  // forward moves tasks with a score less than the current unix time from the delayed (i.e. scheduled | retry) zset
   953  // to the pending list or group set.
   954  // It returns the number of tasks moved.
   955  func (r *RDB) forward(delayedKey, pendingKey, taskKeyPrefix, groupKeyPrefix string) (int, error) {
   956  	now := r.clock.Now()
   957  	keys := []string{delayedKey, pendingKey}
   958  	argv := []any{
   959  		now.Unix(),
   960  		taskKeyPrefix,
   961  		now.UnixNano(),
   962  		groupKeyPrefix,
   963  	}
   964  	res, err := forwardCmd.Run(context.Background(), r.client, keys, argv...).Result()
   965  	if err != nil {
   966  		return 0, errors.E(errors.Internal, fmt.Sprintf("redis eval error: %v", err))
   967  	}
   968  	n, err := cast.ToIntE(res)
   969  	if err != nil {
   970  		return 0, errors.E(errors.Internal, fmt.Sprintf("cast error: Lua script returned unexpected value: %v", res))
   971  	}
   972  	return n, nil
   973  }
   974  
   975  // forwardAll checks for tasks in scheduled/retry state that are ready to be run, and updates
   976  // their state to "pending" or "aggregating".
   977  func (r *RDB) forwardAll(qname string) (err error) {
   978  	delayedKeys := []string{base.ScheduledKey(qname), base.RetryKey(qname)}
   979  	pendingKey := base.PendingKey(qname)
   980  	taskKeyPrefix := base.TaskKeyPrefix(qname)
   981  	groupKeyPrefix := base.GroupKeyPrefix(qname)
   982  	for _, delayedKey := range delayedKeys {
   983  		n := 1
   984  		for n != 0 {
   985  			n, err = r.forward(delayedKey, pendingKey, taskKeyPrefix, groupKeyPrefix)
   986  			if err != nil {
   987  				return err
   988  			}
   989  		}
   990  	}
   991  	return nil
   992  }
   993  
   994  // ListGroups returns a list of all known groups in the given queue.
   995  func (r *RDB) ListGroups(qname string) ([]string, error) {
   996  	var op errors.Op = "RDB.ListGroups"
   997  	groups, err := r.client.SMembers(context.Background(), base.AllGroups(qname)).Result()
   998  	if err != nil {
   999  		return nil, errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "smembers", Err: err})
  1000  	}
  1001  	return groups, nil
  1002  }
  1003  
  1004  // aggregationCheckCmd checks the given group for whether to create an aggregation set.
  1005  // An aggregation set is created if one of the aggregation criteria is met:
  1006  // 1) group has reached or exceeded its max size
  1007  // 2) group's oldest task has reached or exceeded its max delay
  1008  // 3) group's latest task has reached or exceeded its grace period
  1009  // if aggreation criteria is met, the command moves those tasks from the group
  1010  // and put them in an aggregation set. Additionally, if the creation of aggregation set
  1011  // empties the group, it will clear the group name from the all groups set.
  1012  //
  1013  // KEYS[1] -> asynq:{<qname>}:g:<gname>
  1014  // KEYS[2] -> asynq:{<qname>}:g:<gname>:<aggregation_set_id>
  1015  // KEYS[3] -> asynq:{<qname>}:aggregation_sets
  1016  // KEYS[4] -> asynq:{<qname>}:groups
  1017  // -------
  1018  // ARGV[1] -> max group size
  1019  // ARGV[2] -> max group delay in unix time
  1020  // ARGV[3] -> start time of the grace period
  1021  // ARGV[4] -> aggregation set expire time
  1022  // ARGV[5] -> current time in unix time
  1023  // ARGV[6] -> group name
  1024  //
  1025  // Output:
  1026  // Returns 0 if no aggregation set was created
  1027  // Returns 1 if an aggregation set was created
  1028  //
  1029  // Time Complexity:
  1030  // O(log(N) + M) with N being the number tasks in the group zset
  1031  // and M being the max size.
  1032  var aggregationCheckCmd = redis.NewScript(`
  1033  local size = redis.call("ZCARD", KEYS[1])
  1034  if size == 0 then
  1035  	return 0
  1036  end
  1037  local maxSize = tonumber(ARGV[1])
  1038  if maxSize ~= 0 and size >= maxSize then
  1039  	local res = redis.call("ZRANGE", KEYS[1], 0, maxSize-1, "WITHSCORES")
  1040  	for i=1, table.getn(res)-1, 2 do
  1041  		redis.call("ZADD", KEYS[2], tonumber(res[i+1]), res[i])
  1042  	end
  1043  	redis.call("ZREMRANGEBYRANK", KEYS[1], 0, maxSize-1)
  1044  	redis.call("ZADD", KEYS[3], ARGV[4], KEYS[2])
  1045  	if size == maxSize then
  1046  		redis.call("SREM", KEYS[4], ARGV[6])
  1047  	end
  1048  	return 1
  1049  end
  1050  local maxDelay = tonumber(ARGV[2])
  1051  local currentTime = tonumber(ARGV[5])
  1052  if maxDelay ~= 0 then
  1053  	local oldestEntry = redis.call("ZRANGE", KEYS[1], 0, 0, "WITHSCORES")
  1054  	local oldestEntryScore = tonumber(oldestEntry[2])
  1055  	local maxDelayTime = currentTime - maxDelay
  1056  	if oldestEntryScore <= maxDelayTime then
  1057  		local res = redis.call("ZRANGE", KEYS[1], 0, maxSize-1, "WITHSCORES")
  1058  		for i=1, table.getn(res)-1, 2 do
  1059  			redis.call("ZADD", KEYS[2], tonumber(res[i+1]), res[i])
  1060  		end
  1061  		redis.call("ZREMRANGEBYRANK", KEYS[1], 0, maxSize-1)
  1062  		redis.call("ZADD", KEYS[3], ARGV[4], KEYS[2])
  1063  		if size <= maxSize or maxSize == 0 then
  1064  			redis.call("SREM", KEYS[4], ARGV[6])
  1065  		end
  1066  		return 1
  1067  	end
  1068  end
  1069  local latestEntry = redis.call("ZREVRANGE", KEYS[1], 0, 0, "WITHSCORES")
  1070  local latestEntryScore = tonumber(latestEntry[2])
  1071  local gracePeriodStartTime = currentTime - tonumber(ARGV[3])
  1072  if latestEntryScore <= gracePeriodStartTime then
  1073  	local res = redis.call("ZRANGE", KEYS[1], 0, maxSize-1, "WITHSCORES")
  1074  	for i=1, table.getn(res)-1, 2 do
  1075  		redis.call("ZADD", KEYS[2], tonumber(res[i+1]), res[i])
  1076  	end
  1077  	redis.call("ZREMRANGEBYRANK", KEYS[1], 0, maxSize-1)
  1078  	redis.call("ZADD", KEYS[3], ARGV[4], KEYS[2])
  1079  	if size <= maxSize or maxSize == 0 then
  1080  		redis.call("SREM", KEYS[4], ARGV[6])
  1081  	end
  1082  	return 1
  1083  end
  1084  return 0
  1085  `)
  1086  
  1087  // Task aggregation should finish within this timeout.
  1088  // Otherwise an aggregation set should be reclaimed by the recoverer.
  1089  const aggregationTimeout = 2 * time.Minute
  1090  
  1091  // AggregationCheck checks the group identified by the given queue and group name to see if the tasks in the
  1092  // group are ready to be aggregated. If so, it moves the tasks to be aggregated to a aggregation set and returns
  1093  // the set ID. If not, it returns an empty string for the set ID.
  1094  // The time for gracePeriod and maxDelay is computed relative to the time t.
  1095  //
  1096  // Note: It assumes that this function is called at frequency less than or equal to the gracePeriod. In other words,
  1097  // the function only checks the most recently added task against the given gracePeriod.
  1098  func (r *RDB) AggregationCheck(qname, gname string, t time.Time,
  1099  	gracePeriod, maxDelay time.Duration, maxSize int) (string, error) {
  1100  	var op errors.Op = "RDB.AggregationCheck"
  1101  	aggregationSetID := uuid.NewString()
  1102  	expireTime := r.clock.Now().Add(aggregationTimeout)
  1103  	keys := []string{
  1104  		base.GroupKey(qname, gname),
  1105  		base.AggregationSetKey(qname, gname, aggregationSetID),
  1106  		base.AllAggregationSets(qname),
  1107  		base.AllGroups(qname),
  1108  	}
  1109  	argv := []any{
  1110  		maxSize,
  1111  		int64(maxDelay.Seconds()),
  1112  		int64(gracePeriod.Seconds()),
  1113  		expireTime.Unix(),
  1114  		t.Unix(),
  1115  		gname,
  1116  	}
  1117  	n, err := r.runScriptWithErrorCode(context.Background(), op, aggregationCheckCmd, keys, argv...)
  1118  	if err != nil {
  1119  		return "", err
  1120  	}
  1121  	switch n {
  1122  	case 0:
  1123  		return "", nil
  1124  	case 1:
  1125  		return aggregationSetID, nil
  1126  	default:
  1127  		return "", errors.E(op, errors.Internal, fmt.Sprintf("unexpected return value from lua script: %d", n))
  1128  	}
  1129  }
  1130  
  1131  // KEYS[1] -> asynq:{<qname>}:g:<gname>:<aggregation_set_id>
  1132  // ------
  1133  // ARGV[1] -> task key prefix
  1134  //
  1135  // Output:
  1136  // Array of encoded task messages
  1137  //
  1138  // Time Complexity:
  1139  // O(N) with N being the number of tasks in the aggregation set.
  1140  var readAggregationSetCmd = redis.NewScript(`
  1141  local msgs = {}
  1142  local ids = redis.call("ZRANGE", KEYS[1], 0, -1)
  1143  for _, id in ipairs(ids) do
  1144  	local key = ARGV[1] .. id
  1145  	table.insert(msgs, redis.call("HGET", key, "msg"))
  1146  end
  1147  return msgs
  1148  `)
  1149  
  1150  // ReadAggregationSet retrieves members of an aggregation set and returns a list of tasks in the set and
  1151  // the deadline for aggregating those tasks.
  1152  func (r *RDB) ReadAggregationSet(qname, gname, setID string) ([]*base.TaskMessage, time.Time, error) {
  1153  	var op errors.Op = "RDB.ReadAggregationSet"
  1154  	ctx := context.Background()
  1155  	aggSetKey := base.AggregationSetKey(qname, gname, setID)
  1156  	res, err := readAggregationSetCmd.Run(ctx, r.client,
  1157  		[]string{aggSetKey}, base.TaskKeyPrefix(qname)).Result()
  1158  	if err != nil {
  1159  		return nil, time.Time{}, errors.E(op, errors.Unknown, fmt.Sprintf("redis eval error: %v", err))
  1160  	}
  1161  	data, err := cast.ToStringSliceE(res)
  1162  	if err != nil {
  1163  		return nil, time.Time{}, errors.E(op, errors.Internal, fmt.Sprintf("cast error: Lua script returned unexpected value: %v", res))
  1164  	}
  1165  	var msgs []*base.TaskMessage
  1166  	for _, s := range data {
  1167  		msg, err := base.DecodeMessage([]byte(s))
  1168  		if err != nil {
  1169  			return nil, time.Time{}, errors.E(op, errors.Internal, fmt.Sprintf("cannot decode message: %v", err))
  1170  		}
  1171  		msgs = append(msgs, msg)
  1172  	}
  1173  	deadlineUnix, err := r.client.ZScore(ctx, base.AllAggregationSets(qname), aggSetKey).Result()
  1174  	if err != nil {
  1175  		return nil, time.Time{}, errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "zscore", Err: err})
  1176  	}
  1177  	return msgs, time.Unix(int64(deadlineUnix), 0), nil
  1178  }
  1179  
  1180  // KEYS[1] -> asynq:{<qname>}:g:<gname>:<aggregation_set_id>
  1181  // KEYS[2] -> asynq:{<qname>}:aggregation_sets
  1182  // -------
  1183  // ARGV[1] -> task key prefix
  1184  //
  1185  // Output:
  1186  // Redis status reply
  1187  //
  1188  // Time Complexity:
  1189  // max(O(N), O(log(M))) with N being the number of tasks in the aggregation set
  1190  // and M being the number of elements in the all-aggregation-sets list.
  1191  var deleteAggregationSetCmd = redis.NewScript(`
  1192  local ids = redis.call("ZRANGE", KEYS[1], 0, -1)
  1193  for _, id in ipairs(ids)  do
  1194  	redis.call("DEL", ARGV[1] .. id)
  1195  end
  1196  redis.call("DEL", KEYS[1])
  1197  redis.call("ZREM", KEYS[2], KEYS[1])
  1198  return redis.status_reply("OK")
  1199  `)
  1200  
  1201  // DeleteAggregationSet deletes the aggregation set and its members identified by the parameters.
  1202  func (r *RDB) DeleteAggregationSet(ctx context.Context, qname, gname, setID string) error {
  1203  	var op errors.Op = "RDB.DeleteAggregationSet"
  1204  	keys := []string{
  1205  		base.AggregationSetKey(qname, gname, setID),
  1206  		base.AllAggregationSets(qname),
  1207  	}
  1208  	return r.runScript(ctx, op, deleteAggregationSetCmd, keys, base.TaskKeyPrefix(qname))
  1209  }
  1210  
  1211  // KEYS[1] -> asynq:{<qname>}:aggregation_sets
  1212  // -------
  1213  // ARGV[1] -> current time in unix time
  1214  var reclaimStateAggregationSetsCmd = redis.NewScript(`
  1215  local staleSetKeys = redis.call("ZRANGEBYSCORE", KEYS[1], "-inf", ARGV[1])
  1216  for _, key in ipairs(staleSetKeys) do
  1217  	local idx = string.find(key, ":[^:]*$")
  1218  	local groupKey = string.sub(key, 1, idx-1)
  1219  	local res = redis.call("ZRANGE", key, 0, -1, "WITHSCORES")
  1220  	for i=1, table.getn(res)-1, 2 do
  1221  		redis.call("ZADD", groupKey, tonumber(res[i+1]), res[i])
  1222  	end
  1223  	redis.call("DEL", key)
  1224  end
  1225  redis.call("ZREMRANGEBYSCORE", KEYS[1], "-inf", ARGV[1])
  1226  return redis.status_reply("OK")
  1227  `)
  1228  
  1229  // ReclaimStateAggregationSets checks for any stale aggregation sets in the given queue, and
  1230  // reclaim tasks in the stale aggregation set by putting them back in the group.
  1231  func (r *RDB) ReclaimStaleAggregationSets(qname string) error {
  1232  	var op errors.Op = "RDB.ReclaimStaleAggregationSets"
  1233  	return r.runScript(context.Background(), op, reclaimStateAggregationSetsCmd,
  1234  		[]string{base.AllAggregationSets(qname)}, r.clock.Now().Unix())
  1235  }
  1236  
  1237  // KEYS[1] -> asynq:{<qname>}:completed
  1238  // ARGV[1] -> current time in unix time
  1239  // ARGV[2] -> task key prefix
  1240  // ARGV[3] -> batch size (i.e. maximum number of tasks to delete)
  1241  //
  1242  // Returns the number of tasks deleted.
  1243  var deleteExpiredCompletedTasksCmd = redis.NewScript(`
  1244  local ids = redis.call("ZRANGEBYSCORE", KEYS[1], "-inf", ARGV[1], "LIMIT", 0, tonumber(ARGV[3]))
  1245  for _, id in ipairs(ids) do
  1246  	redis.call("DEL", ARGV[2] .. id)
  1247  	redis.call("ZREM", KEYS[1], id)
  1248  end
  1249  return table.getn(ids)`)
  1250  
  1251  // DeleteExpiredCompletedTasks checks for any expired tasks in the given queue's completed set,
  1252  // and delete all expired tasks.
  1253  func (r *RDB) DeleteExpiredCompletedTasks(qname string) error {
  1254  	// Note: Do this operation in fix batches to prevent long running script.
  1255  	const batchSize = 100
  1256  	for {
  1257  		n, err := r.deleteExpiredCompletedTasks(qname, batchSize)
  1258  		if err != nil {
  1259  			return err
  1260  		}
  1261  		if n == 0 {
  1262  			return nil
  1263  		}
  1264  	}
  1265  }
  1266  
  1267  // deleteExpiredCompletedTasks runs the lua script to delete expired deleted task with the specified
  1268  // batch size. It reports the number of tasks deleted.
  1269  func (r *RDB) deleteExpiredCompletedTasks(qname string, batchSize int) (int64, error) {
  1270  	var op errors.Op = "rdb.DeleteExpiredCompletedTasks"
  1271  	keys := []string{base.CompletedKey(qname)}
  1272  	argv := []any{
  1273  		r.clock.Now().Unix(),
  1274  		base.TaskKeyPrefix(qname),
  1275  		batchSize,
  1276  	}
  1277  	res, err := deleteExpiredCompletedTasksCmd.Run(context.Background(), r.client, keys, argv...).Result()
  1278  	if err != nil {
  1279  		return 0, errors.E(op, errors.Internal, fmt.Sprintf("redis eval error: %v", err))
  1280  	}
  1281  	n, ok := res.(int64)
  1282  	if !ok {
  1283  		return 0, errors.E(op, errors.Internal, fmt.Sprintf("unexpected return value from Lua script: %v", res))
  1284  	}
  1285  	return n, nil
  1286  }
  1287  
  1288  // KEYS[1] -> asynq:{<qname>}:lease
  1289  // ARGV[1] -> cutoff in unix time
  1290  // ARGV[2] -> task key prefix
  1291  var listLeaseExpiredCmd = redis.NewScript(`
  1292  local res = {}
  1293  local ids = redis.call("ZRANGEBYSCORE", KEYS[1], "-inf", ARGV[1])
  1294  for _, id in ipairs(ids) do
  1295  	local key = ARGV[2] .. id
  1296  	table.insert(res, redis.call("HGET", key, "msg"))
  1297  end
  1298  return res
  1299  `)
  1300  
  1301  // ListLeaseExpired returns a list of task messages with an expired lease from the given queues.
  1302  func (r *RDB) ListLeaseExpired(cutoff time.Time, qnames ...string) ([]*base.TaskMessage, error) {
  1303  	var op errors.Op = "rdb.ListLeaseExpired"
  1304  	var msgs []*base.TaskMessage
  1305  	for _, qname := range qnames {
  1306  		res, err := listLeaseExpiredCmd.Run(context.Background(), r.client,
  1307  			[]string{base.LeaseKey(qname)},
  1308  			cutoff.Unix(), base.TaskKeyPrefix(qname)).Result()
  1309  		if err != nil {
  1310  			return nil, errors.E(op, errors.Internal, fmt.Sprintf("redis eval error: %v", err))
  1311  		}
  1312  		data, err := cast.ToStringSliceE(res)
  1313  		if err != nil {
  1314  			return nil, errors.E(op, errors.Internal, fmt.Sprintf("cast error: Lua script returned unexpected value: %v", res))
  1315  		}
  1316  		for _, s := range data {
  1317  			msg, err := base.DecodeMessage([]byte(s))
  1318  			if err != nil {
  1319  				return nil, errors.E(op, errors.Internal, fmt.Sprintf("cannot decode message: %v", err))
  1320  			}
  1321  			msgs = append(msgs, msg)
  1322  		}
  1323  	}
  1324  	return msgs, nil
  1325  }
  1326  
  1327  // ExtendLease extends the lease for the given tasks by LeaseDuration (30s).
  1328  // It returns a new expiration time if the operation was successful.
  1329  func (r *RDB) ExtendLease(qname string, ids ...string) (expirationTime time.Time, err error) {
  1330  	expireAt := r.clock.Now().Add(LeaseDuration)
  1331  	var zs []redis.Z
  1332  	for _, id := range ids {
  1333  		zs = append(zs, redis.Z{Member: id, Score: float64(expireAt.Unix())})
  1334  	}
  1335  	// Use XX option to only update elements that already exist; Don't add new elements
  1336  	// TODO: Consider adding GT option to ensure we only "extend" the lease. Ceveat is that GT is supported from redis v6.2.0 or above.
  1337  	err = r.client.ZAddXX(context.Background(), base.LeaseKey(qname), zs...).Err()
  1338  	if err != nil {
  1339  		return time.Time{}, err
  1340  	}
  1341  	return expireAt, nil
  1342  }
  1343  
  1344  // KEYS[1]  -> asynq:servers:{<host:pid:sid>}
  1345  // KEYS[2]  -> asynq:workers:{<host:pid:sid>}
  1346  // ARGV[1]  -> TTL in seconds
  1347  // ARGV[2]  -> server info
  1348  // ARGV[3:] -> alternate key-value pair of (worker id, worker data)
  1349  // Note: Add key to ZSET with expiration time as score.
  1350  // ref: https://github.com/antirez/redis/issues/135#issuecomment-2361996
  1351  var writeServerStateCmd = redis.NewScript(`
  1352  redis.call("SETEX", KEYS[1], ARGV[1], ARGV[2])
  1353  redis.call("DEL", KEYS[2])
  1354  for i = 3, table.getn(ARGV)-1, 2 do
  1355  	redis.call("HSET", KEYS[2], ARGV[i], ARGV[i+1])
  1356  end
  1357  redis.call("EXPIRE", KEYS[2], ARGV[1])
  1358  return redis.status_reply("OK")`)
  1359  
  1360  // WriteServerState writes server state data to redis with expiration set to the value ttl.
  1361  func (r *RDB) WriteServerState(info *base.ServerInfo, workers []*base.WorkerInfo, ttl time.Duration) error {
  1362  	var op errors.Op = "rdb.WriteServerState"
  1363  	ctx := context.Background()
  1364  	bytes, err := base.EncodeServerInfo(info)
  1365  	if err != nil {
  1366  		return errors.E(op, errors.Internal, fmt.Sprintf("cannot encode server info: %v", err))
  1367  	}
  1368  	exp := r.clock.Now().Add(ttl).UTC()
  1369  	args := []any{ttl.Seconds(), bytes} // args to the lua script
  1370  	for _, w := range workers {
  1371  		bytes, err := base.EncodeWorkerInfo(w)
  1372  		if err != nil {
  1373  			continue // skip bad data
  1374  		}
  1375  		args = append(args, w.ID, bytes)
  1376  	}
  1377  	skey := base.ServerInfoKey(info.Host, info.PID, info.ServerID)
  1378  	wkey := base.WorkersKey(info.Host, info.PID, info.ServerID)
  1379  	if err := r.client.ZAdd(ctx, base.AllServers, redis.Z{Score: float64(exp.Unix()), Member: skey}).Err(); err != nil {
  1380  		return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "sadd", Err: err})
  1381  	}
  1382  	if err := r.client.ZAdd(ctx, base.AllWorkers, redis.Z{Score: float64(exp.Unix()), Member: wkey}).Err(); err != nil {
  1383  		return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "zadd", Err: err})
  1384  	}
  1385  	return r.runScript(ctx, op, writeServerStateCmd, []string{skey, wkey}, args...)
  1386  }
  1387  
  1388  // KEYS[1] -> asynq:servers:{<host:pid:sid>}
  1389  // KEYS[2] -> asynq:workers:{<host:pid:sid>}
  1390  var clearServerStateCmd = redis.NewScript(`
  1391  redis.call("DEL", KEYS[1])
  1392  redis.call("DEL", KEYS[2])
  1393  return redis.status_reply("OK")`)
  1394  
  1395  // ClearServerState deletes server state data from redis.
  1396  func (r *RDB) ClearServerState(host string, pid int, serverID string) error {
  1397  	var op errors.Op = "rdb.ClearServerState"
  1398  	ctx := context.Background()
  1399  	skey := base.ServerInfoKey(host, pid, serverID)
  1400  	wkey := base.WorkersKey(host, pid, serverID)
  1401  	if err := r.client.ZRem(ctx, base.AllServers, skey).Err(); err != nil {
  1402  		return errors.E(op, errors.Internal, &errors.RedisCommandError{Command: "zrem", Err: err})
  1403  	}
  1404  	if err := r.client.ZRem(ctx, base.AllWorkers, wkey).Err(); err != nil {
  1405  		return errors.E(op, errors.Internal, &errors.RedisCommandError{Command: "zrem", Err: err})
  1406  	}
  1407  	return r.runScript(ctx, op, clearServerStateCmd, []string{skey, wkey})
  1408  }
  1409  
  1410  // KEYS[1]  -> asynq:schedulers:{<schedulerID>}
  1411  // ARGV[1]  -> TTL in seconds
  1412  // ARGV[2:] -> schedler entries
  1413  var writeSchedulerEntriesCmd = redis.NewScript(`
  1414  redis.call("DEL", KEYS[1])
  1415  for i = 2, #ARGV do
  1416  	redis.call("LPUSH", KEYS[1], ARGV[i])
  1417  end
  1418  redis.call("EXPIRE", KEYS[1], ARGV[1])
  1419  return redis.status_reply("OK")`)
  1420  
  1421  // WriteSchedulerEntries writes scheduler entries data to redis with expiration set to the value ttl.
  1422  func (r *RDB) WriteSchedulerEntries(schedulerID string, entries []*base.SchedulerEntry, ttl time.Duration) error {
  1423  	var op errors.Op = "rdb.WriteSchedulerEntries"
  1424  	ctx := context.Background()
  1425  	args := []any{ttl.Seconds()}
  1426  	for _, e := range entries {
  1427  		bytes, err := base.EncodeSchedulerEntry(e)
  1428  		if err != nil {
  1429  			continue // skip bad data
  1430  		}
  1431  		args = append(args, bytes)
  1432  	}
  1433  	exp := r.clock.Now().Add(ttl).UTC()
  1434  	key := base.SchedulerEntriesKey(schedulerID)
  1435  	err := r.client.ZAdd(ctx, base.AllSchedulers, redis.Z{Score: float64(exp.Unix()), Member: key}).Err()
  1436  	if err != nil {
  1437  		return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "zadd", Err: err})
  1438  	}
  1439  	return r.runScript(ctx, op, writeSchedulerEntriesCmd, []string{key}, args...)
  1440  }
  1441  
  1442  // ClearSchedulerEntries deletes scheduler entries data from redis.
  1443  func (r *RDB) ClearSchedulerEntries(scheduelrID string) error {
  1444  	var op errors.Op = "rdb.ClearSchedulerEntries"
  1445  	ctx := context.Background()
  1446  	key := base.SchedulerEntriesKey(scheduelrID)
  1447  	if err := r.client.ZRem(ctx, base.AllSchedulers, key).Err(); err != nil {
  1448  		return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "zrem", Err: err})
  1449  	}
  1450  	if err := r.client.Del(ctx, key).Err(); err != nil {
  1451  		return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "del", Err: err})
  1452  	}
  1453  	return nil
  1454  }
  1455  
  1456  // CancelationPubSub returns a pubsub for cancelation messages.
  1457  func (r *RDB) CancelationPubSub() (*redis.PubSub, error) {
  1458  	var op errors.Op = "rdb.CancelationPubSub"
  1459  	ctx := context.Background()
  1460  	pubsub := r.client.Subscribe(ctx, base.CancelChannel)
  1461  	_, err := pubsub.Receive(ctx)
  1462  	if err != nil {
  1463  		return nil, errors.E(op, errors.Unknown, fmt.Sprintf("redis pubsub receive error: %v", err))
  1464  	}
  1465  	return pubsub, nil
  1466  }
  1467  
  1468  // PublishCancelation publish cancelation message to all subscribers.
  1469  // The message is the ID for the task to be canceled.
  1470  func (r *RDB) PublishCancelation(id string) error {
  1471  	var op errors.Op = "rdb.PublishCancelation"
  1472  	ctx := context.Background()
  1473  	if err := r.client.Publish(ctx, base.CancelChannel, id).Err(); err != nil {
  1474  		return errors.E(op, errors.Unknown, fmt.Sprintf("redis pubsub publish error: %v", err))
  1475  	}
  1476  	return nil
  1477  }
  1478  
  1479  // KEYS[1] -> asynq:scheduler_history:<entryID>
  1480  // ARGV[1] -> enqueued_at timestamp
  1481  // ARGV[2] -> serialized SchedulerEnqueueEvent data
  1482  // ARGV[3] -> max number of events to be persisted
  1483  var recordSchedulerEnqueueEventCmd = redis.NewScript(`
  1484  redis.call("ZREMRANGEBYRANK", KEYS[1], 0, -ARGV[3])
  1485  redis.call("ZADD", KEYS[1], ARGV[1], ARGV[2])
  1486  return redis.status_reply("OK")`)
  1487  
  1488  // Maximum number of enqueue events to store per entry.
  1489  const maxEvents = 1000
  1490  
  1491  // RecordSchedulerEnqueueEvent records the time when the given task was enqueued.
  1492  func (r *RDB) RecordSchedulerEnqueueEvent(entryID string, event *base.SchedulerEnqueueEvent) error {
  1493  	var op errors.Op = "rdb.RecordSchedulerEnqueueEvent"
  1494  	ctx := context.Background()
  1495  	data, err := base.EncodeSchedulerEnqueueEvent(event)
  1496  	if err != nil {
  1497  		return errors.E(op, errors.Internal, fmt.Sprintf("cannot encode scheduler enqueue event: %v", err))
  1498  	}
  1499  	keys := []string{
  1500  		base.SchedulerHistoryKey(entryID),
  1501  	}
  1502  	argv := []any{
  1503  		event.EnqueuedAt.Unix(),
  1504  		data,
  1505  		maxEvents,
  1506  	}
  1507  	return r.runScript(ctx, op, recordSchedulerEnqueueEventCmd, keys, argv...)
  1508  }
  1509  
  1510  // ClearSchedulerHistory deletes the enqueue event history for the given scheduler entry.
  1511  func (r *RDB) ClearSchedulerHistory(entryID string) error {
  1512  	var op errors.Op = "rdb.ClearSchedulerHistory"
  1513  	ctx := context.Background()
  1514  	key := base.SchedulerHistoryKey(entryID)
  1515  	if err := r.client.Del(ctx, key).Err(); err != nil {
  1516  		return errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "del", Err: err})
  1517  	}
  1518  	return nil
  1519  }
  1520  
  1521  // WriteResult writes the given result data for the specified task.
  1522  func (r *RDB) WriteResult(qname, taskID string, data []byte) (int, error) {
  1523  	var op errors.Op = "rdb.WriteResult"
  1524  	ctx := context.Background()
  1525  	taskKey := base.TaskKey(qname, taskID)
  1526  	if err := r.client.HSet(ctx, taskKey, "result", data).Err(); err != nil {
  1527  		return 0, errors.E(op, errors.Unknown, &errors.RedisCommandError{Command: "hset", Err: err})
  1528  	}
  1529  	return len(data), nil
  1530  }