fix(redis): keep queues working with payloads cjson rejects · boringnode/queue@c33ae0b · GitHub
Skip to content

Commit c33ae0b

Browse files
committed
fix(redis): keep queues working with payloads cjson rejects
Lua cjson rejects JSON that JSON.parse accepts, such as a lone surrogate or nesting deeper than 1000 levels. The acquire script decoded the whole job only to read its priority, so one such payload made every popFrom() on its queue fail. The retry script removed the job from the active hash before its decode threw, leaving the job in no state, and stalled recovery failed the same way. Scripts now read job fields through a guarded decode that falls back to the defaults (priority 5), and the retry script reads everything before its first write.
1 parent cdac0c0 commit c33ae0b

4 files changed

Lines changed: 82 additions & 9 deletions

File tree

Lines changed: 17 additions & 0 deletions

‎src/drivers/redis_job_storage.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ export function encodeRedisJobPayloadOverlay(payload: JobData['payload']): [stri
3636
}
3737

3838
export const REDIS_JOB_STORAGE_LUA = `
39+
-- Read the fields a script needs from the original job JSON. cjson rejects
40+
-- some JSON that JSON.parse accepts, such as a lone surrogate or more than
41+
-- 1000 levels of nesting in the payload: such a job gets the defaults
42+
-- (priority 5, attempts 0) instead of failing every script that touches it.
43+
local function read_job_fields(job_data)
44+
local ok, job = pcall(cjson.decode, job_data)
45+
if ok and type(job) == 'table' then
46+
return job
47+
end
48+
return {}
49+
end
50+
3951
local function read_job_overlay(overlay_key, job_id)
4052
local overlay_json = redis.call('HGET', overlay_key, job_id)
4153
if overlay_json then

‎src/drivers/redis_scripts.ts‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,7 @@ ${REDIS_JOB_STORAGE_LUA}
128128
local job_id = ready_job_ids[i]
129129
local job_data = redis.call('HGET', data_key, job_id)
130130
if job_data then
131-
local job = cjson.decode(job_data)
132-
local priority = job.priority or 5
131+
local priority = read_job_fields(job_data).priority or 5
133132
local score = priority * 10000000000000 + now
134133
redis.call('ZADD', pending_key, score, job_id)
135134
redis.call('ZREM', delayed_key, job_id)
@@ -198,8 +197,8 @@ ${REDIS_LEASE_LUA}
198197
-- Read job data to extract dedup.id before deleting
199198
local job_data = redis.call('HGET', data_key, job_id)
200199
if job_data then
201-
local ok, job = pcall(cjson.decode, job_data)
202-
if ok and job and job.dedup and job.dedup.id then
200+
local job = read_job_fields(job_data)
201+
if job.dedup and job.dedup.id then
203202
local dkey = dedup_prefix .. job.dedup.id
204203
if redis.call('GET', dkey) == job_id then
205204
redis.call('DEL', dkey)
@@ -258,8 +257,8 @@ ${REDIS_LEASE_LUA}
258257
local id = ids[i]
259258
local d = redis.call('HGET', data_key, id)
260259
if d then
261-
local ok, job = pcall(cjson.decode, d)
262-
if ok and job and job.dedup and job.dedup.id then
260+
local job = read_job_fields(d)
261+
if job.dedup and job.dedup.id then
263262
local dkey = dedup_prefix .. job.dedup.id
264263
if redis.call('GET', dkey) == id then
265264
redis.call('DEL', dkey)
@@ -331,12 +330,15 @@ ${REDIS_LEASE_LUA}
331330
return 0
332331
end
333332
333+
-- Read everything before the first write: an error past it would leave
334+
-- the job in no state, since a script does not roll back its writes.
335+
local job = read_job_fields(job_data)
336+
local overlay = read_job_overlay(overlay_key, job_id)
337+
334338
-- Remove from active
335339
redis.call('HDEL', active_key, job_id)
336340
337341
-- Increment attempts without rewriting opaque job JSON.
338-
local job = cjson.decode(job_data)
339-
local overlay = read_job_overlay(overlay_key, job_id)
340342
overlay.attempts = (overlay.attempts or job.attempts or 0) + 1
341343
write_job_overlay(overlay_key, job_id, overlay)
342344
@@ -395,7 +397,7 @@ ${REDIS_JOB_STORAGE_LUA}
395397
if active.acquiredAt < stalled_cutoff then
396398
local job_data = redis.call('HGET', data_key, job_id)
397399
if job_data then
398-
local job = cjson.decode(job_data)
400+
local job = read_job_fields(job_data)
399401
local overlay = read_job_overlay(overlay_key, job_id)
400402
local current_stalled_count = overlay.stalledCount or job.stalledCount or 0
401403

‎tests/_utils/register_driver_test_suite.ts‎

Lines changed: 42 additions & 0 deletions

0 commit comments

Comments
 (0)