@@ -14,30 +14,33 @@ export interface RedisJobRequest<TPayload> extends BaseJobOptions, WithPriority,
1414export interface RedisClient {
1515 // Counter operations
1616 incr ( key : string ) : Promise < number > ;
17-
17+
1818 // Hash operations
1919 hSet ( key : string , field : string , value : string ) : Promise < number > ;
2020 hGet ( key : string , field : string ) : Promise < string | null > ;
2121 hDel ( key : string , fields : string [ ] ) : Promise < number > ;
2222 hIncrBy ( key : string , field : string , increment : number ) : Promise < number > ;
23-
23+
2424 // Sorted set operations
2525 zAdd ( key : string , members : { score : number ; value : string } ) : Promise < number > ;
2626 zRem ( key : string , members : string [ ] ) : Promise < number > ;
2727 zRangeByScore (
28- key : string ,
29- min : number | string ,
30- max : number | string ,
28+ key : string ,
29+ min : number | string ,
30+ max : number | string ,
3131 options ?: { REV ?: boolean ; LIMIT ?: { offset : number ; count : number } }
3232 ) : Promise < string [ ] > ;
3333 zRange (
34- key : string ,
35- start : number ,
36- stop : number ,
34+ key : string ,
35+ start : number ,
36+ stop : number ,
3737 options ?: { REV ?: boolean }
3838 ) : Promise < string [ ] > ;
3939 zScore ( key : string , member : string ) : Promise < number | null > ;
40-
40+
41+ // Script operations
42+ eval ( script : string , options : { keys : string [ ] ; arguments : string [ ] } ) : Promise < any > ;
43+
4144 // General operations
4245 del ( keys : string [ ] ) : Promise < number > ;
4346}
@@ -72,46 +75,86 @@ export class RedisQueue<TJobMap = Record<string, any>> extends Queue<TJobMap, Re
7275 }
7376
7477 protected async pushMessage ( payload : unknown , meta : JobMeta ) : Promise < string > {
75- // Check for idempotency
76- if ( meta . idempotencyKey ) {
77- const existingJobId = await this . redis . hGet ( this . idempotencyKey , meta . idempotencyKey ) ;
78- if ( existingJobId ) {
79- // Check if the job still exists
80- const existingMessage = await this . redis . hGet ( this . messagesKey , existingJobId ) ;
81- if ( existingMessage ) {
82- // Job is still waiting, delayed, or reserved - return existing ID
83- return existingJobId ;
84- }
85- // Job completed/failed, clean up the idempotency mapping
86- await this . redis . hDel ( this . idempotencyKey , [ meta . idempotencyKey ] ) ;
87- }
88- }
89-
90- const id = ( await this . redis . incr ( this . idKey ) ) . toString ( ) ;
9178 const now = Math . floor ( Date . now ( ) / 1000 ) ;
9279
80+ // Unified Lua script for atomic job creation (with or without idempotency)
81+ const script = `
82+ local idempotencyKey = KEYS[1]
83+ local messagesKey = KEYS[2]
84+ local idKey = KEYS[3]
85+ local queueKey = KEYS[4]
86+
87+ local idempotencyField = ARGV[1]
88+ local message = ARGV[2]
89+ local queueScore = ARGV[3]
90+
91+ -- Check if idempotency key is provided (non-empty string)
92+ if idempotencyField ~= "" then
93+ -- Check if idempotency key exists
94+ local existingJobId = redis.call('HGET', idempotencyKey, idempotencyField)
95+ if existingJobId then
96+ -- Check if the job still exists
97+ local existingMessage = redis.call('HGET', messagesKey, existingJobId)
98+ if existingMessage then
99+ -- Job is still active, return existing ID
100+ return existingJobId
101+ else
102+ -- Job completed/failed, clean up the idempotency mapping
103+ redis.call('HDEL', idempotencyKey, idempotencyField)
104+ end
105+ end
106+ end
107+
108+ -- Create new job
109+ local newId = tostring(redis.call('INCR', idKey))
110+
111+ -- Store job message
112+ redis.call('HSET', messagesKey, newId, message)
113+
114+ -- Store idempotency mapping if key is provided
115+ if idempotencyField ~= "" then
116+ redis.call('HSET', idempotencyKey, idempotencyField, newId)
117+ end
118+
119+ -- Add to appropriate queue
120+ redis.call('ZADD', queueKey, queueScore, newId)
121+
122+ return newId
123+ ` ;
124+
93125 const message = JSON . stringify ( { payload, meta } ) ;
94- await this . redis . hSet ( this . messagesKey , id , message ) ;
126+ const priority = meta . priority || 0 ;
127+ const delaySeconds = meta . delaySeconds || 0 ;
95128
96- // Store idempotency key mapping if provided
97- if ( meta . idempotencyKey ) {
98- await this . redis . hSet ( this . idempotencyKey , meta . idempotencyKey , id ) ;
99- }
129+ let queueKey : string ;
130+ let queueScore : string ;
100131
101- if ( meta . delaySeconds && meta . delaySeconds > 0 ) {
102- // Add to delayed set with execution time as score
103- const executeAt = now + meta . delaySeconds ;
104- await this . redis . zAdd ( this . delayedKey , { score : executeAt , value : id } ) ;
132+ if ( delaySeconds > 0 ) {
133+ // Delayed job
134+ queueKey = this . delayedKey ;
135+ queueScore = ( now + delaySeconds ) . toString ( ) ;
105136 } else {
106- // Add to waiting queue with priority support
107- // Use sorted set for priority, with higher priority = higher score
108- // For same priority, use job ID for FIFO (lower ID = added earlier = higher priority within same priority)
109- // Score = priority * 1000000000 + (1000000000 - jobId) for FIFO within same priority
110- const priority = meta . priority || 0 ;
137+ // Waiting job with priority
138+ queueKey = this . waitingKey ;
139+ // Note: we can't calculate the exact score without knowing the ID first
140+ // So we'll use a simpler approach: priority-based score only
141+ // The timePart will be handled separately after getting the ID
142+ queueScore = ( priority * 1000000000 ) . toString ( ) ;
143+ }
144+
145+ const result = await this . redis . eval ( script , {
146+ keys : [ this . idempotencyKey , this . messagesKey , this . idKey , queueKey ] ,
147+ arguments : [ meta . idempotencyKey || '' , message , queueScore ]
148+ } ) ;
149+
150+ const id = result . toString ( ) ;
151+
152+ // For non-delayed jobs, we need to update the score to include the FIFO timePart
153+ if ( delaySeconds === 0 ) {
111154 const jobIdNum = parseInt ( id ) ;
112- const timePart = 1000000000 - jobIdNum ; // Invert job ID for FIFO within same priority
113- const score = priority * 1000000000 + timePart ;
114- await this . redis . zAdd ( this . waitingKey , { score, value : id } ) ;
155+ const timePart = 1000000000 - jobIdNum ;
156+ const finalScore = priority * 1000000000 + timePart ;
157+ await this . redis . zAdd ( this . waitingKey , { score : finalScore , value : id } ) ;
115158 }
116159
117160 return id ;
0 commit comments