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 }