Unique(TTL < 1s) silently creates permanent lock due to int(ttl.Seconds()) truncation to 0
Bug: Unique(TTL < 1s) silently creates a permanent lock due to int(ttl.Seconds()) truncation
Related: #338 (closed as "documented", but no runtime validation was ever added)
Description
When Unique(ttl) is called with a TTL < 1 second (e.g. Unique(800 * time.Millisecond)), the TTL is silently truncated to 0 seconds via int(ttl.Seconds()) → int(0.8) → 0. This results in a Redis SET key val NX EX 0 command, which means the unique lock never expires — the task can never be enqueued again until the key is manually deleted or the Redis instance restarts.
Root Cause
Three Lua scripts in internal/rdb/rdb.go use EX (seconds):
local ok = redis.call("SET", KEYS[1], ARGV[1], "NX", "EX", ARGV[2])enqueueUniqueCmd— L158addToGroupUniqueCmd— L583scheduleUniqueCmd— L704
Three Go call sites pass int(ttl.Seconds()):
int(ttl.Seconds()),EnqueueUnique— L195AddToGroupUnique— L622ScheduleUnique— L740
No validation is performed in the Unique() function or anywhere in the enqueue path. The GoDoc says "must be >= 1s" (added after #338), but there is no runtime check.
Steps to Reproduce
client.Enqueue(task, asynq.Unique(800 * time.Millisecond))
// → internal call: int((800ms).Seconds()) → int(0.8) → 0
// → Redis: SET unique:key ID NX EX 0 ← permanent lockCheck the unique key in Redis:
redis> TTL asynq:{default}:unique:tasktype:hash
(integer) -1 ← never expiresFix
6 lines changed — backward compatible, all existing Unique(>=1s) usage behaves identically.
- Change
EXtoPXin all 3 Lua scripts - Change
int(ttl.Seconds())toint(ttl.Milliseconds())in all 3 Go call sites
--- a/internal/rdb/rdb.go
+++ b/internal/rdb/rdb.go
@@ -155,7 +155,7 @@
var enqueueUniqueCmd = redis.NewScript(`
-local ok = redis.call("SET", KEYS[1], ARGV[1], "NX", "EX", ARGV[2])
+local ok = redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
if not ok then
return -1
end
@@ -193,7 +193,7 @@
argv := []interface{}{
msg.ID,
- int(ttl.Seconds()),
+ int(ttl.Milliseconds()),
encoded,Repeat for addToGroupUniqueCmd (L582-597, L611-623) and scheduleUniqueCmd (L703-717, L733-743).
- Update the GoDoc in
client.go:
-// TTL duration must be greater than or equal to 1 second.
+// TTL duration must be greater than or equal to 1 millisecond.Backward Compatibility
| Usage | Before | After | Behavior |
|---|---|---|---|
Unique(5 * time.Second) |
EX 5 |
PX 5000 |
Equivalent (5s TTL) |
Unique(800 * time.Millisecond) |
EX 0 (broken) |
PX 800 |
Fixed (800ms TTL) |
Unique(1500 * time.Millisecond) |
EX 1 (wrong — 1s) |
PX 1500 |
Fixed (1.5s TTL) |
No existing correct usage (TTL >= 1s) is affected.
Source: hibiken/asynq