bullmq 5.81.0 → 5.81.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -389,8 +389,13 @@ class QueueGetters extends queue_base_1.QueueBase {
389
389
  */
390
390
  async getJobs(types, start = 0, end = -1, asc = false) {
391
391
  const currentTypes = this.sanitizeJobTypes(types);
392
- const jobIds = await this.getRanges(currentTypes, start, end, asc);
393
- return Promise.all(jobIds.map(jobId => this.Job.fromId(this, jobId)));
392
+ const jobDataByType = await this.scripts.getJobs(currentTypes, start, end, asc);
393
+ return jobDataByType.reduce((jobs, jobData) => {
394
+ for (const [jobId, jobHashFields] of jobData || []) {
395
+ jobs.push(this.Job.fromJSON(this, (0, utils_1.array2obj)(jobHashFields), jobId));
396
+ }
397
+ return jobs;
398
+ }, []);
394
399
  }
395
400
  /**
396
401
  * Returns the logs for a given Job.
@@ -15,6 +15,13 @@ const enums_1 = require("../enums");
15
15
  const utils_1 = require("../utils");
16
16
  const version_1 = require("../version");
17
17
  const errors_1 = require("./errors");
18
+ /**
19
+ * Upper bound on the number of forward backfill iterations the `getJobs` Lua
20
+ * script performs to replace skipped ids (missing job hashes) within a bounded
21
+ * range. It caps the work done per call so a range full of missing jobs cannot
22
+ * scan the whole state unboundedly.
23
+ */
24
+ const GET_JOBS_MAX_BACKFILL_ITERATIONS = 5;
18
25
  class Scripts {
19
26
  constructor(queue) {
20
27
  this.queue = queue;
@@ -612,6 +619,32 @@ class Scripts {
612
619
  const args = this.getRangesArgs(types, start, end, asc);
613
620
  return await this.execCommand(client, 'getRanges', args);
614
621
  }
622
+ getJobsArgs(types, start, end, asc) {
623
+ const queueKeys = this.queue.keys;
624
+ const transformedTypes = [
625
+ ...new Set(types.map(type => (type === 'waiting' ? 'wait' : type))),
626
+ ];
627
+ const keys = [queueKeys['']];
628
+ const args = [
629
+ start,
630
+ end,
631
+ asc ? '1' : '0',
632
+ GET_JOBS_MAX_BACKFILL_ITERATIONS,
633
+ ...transformedTypes,
634
+ ];
635
+ return keys.concat(args);
636
+ }
637
+ /**
638
+ * Fetches job ids and their job hashes for the provided states in a single
639
+ * script, skipping ids whose job hash is missing (for example the deprecated
640
+ * wait list marker or jobs removed after their id was read). Each returned
641
+ * entry is a `[jobId, jobHashFields]` tuple grouped per requested type.
642
+ */
643
+ async getJobs(types, start = 0, end = -1, asc = false) {
644
+ const client = await this.queue.client;
645
+ const args = this.getJobsArgs(types, start, end, asc);
646
+ return await this.execCommand(client, 'getJobs', args);
647
+ }
615
648
  getCountsArgs(types) {
616
649
  const queueKeys = this.queue.keys;
617
650
  const transformedTypes = types.map(type => {
@@ -0,0 +1,151 @@
1
+ --[[
2
+ Get jobs (id + data) for the provided states.
3
+
4
+ Job ids and their hashes are read in the same script so that ids whose hash
5
+ disappears after the id is read (but before the job is loaded) do not appear
6
+ in the result set. Ids without a job hash (for example the deprecated wait
7
+ list marker entry stored in the list) are skipped. For bounded ranges the
8
+ script iterates forward using the range offset as a cursor to backfill
9
+ skipped ids, preserving the requested page size when possible.
10
+
11
+ Input:
12
+ KEYS[1] 'prefix'
13
+
14
+ ARGV[1] start
15
+ ARGV[2] end
16
+ ARGV[3] asc ('1' | '0')
17
+ ARGV[4] max iterations (backfill bound)
18
+ ARGV[5...] types
19
+
20
+ Output:
21
+ results grouped per requested type; each entry is a
22
+ {jobId, {field, value, ...}} tuple
23
+ ]]
24
+ local rcall = redis.call
25
+ local prefix = KEYS[1]
26
+ local rangeStart = tonumber(ARGV[1])
27
+ local rangeEnd = tonumber(ARGV[2])
28
+ local asc = ARGV[3] == "1"
29
+ local max_iterations = tonumber(ARGV[4])
30
+ local results = {}
31
+
32
+ local function isListType(stateType)
33
+ return stateType == "wait" or stateType == "paused" or stateType == "active"
34
+ end
35
+
36
+ -- Fetch a slice of ids for the given state respecting the requested order.
37
+ local function fetchIds(stateKey, stateType, sliceStart, sliceEnd, listLength)
38
+ if isListType(stateType) then
39
+ if asc then
40
+ local modifiedRangeStart
41
+ local modifiedRangeEnd
42
+ if sliceStart == -1 then
43
+ modifiedRangeStart = 0
44
+ else
45
+ modifiedRangeStart = -(sliceStart + 1)
46
+ end
47
+
48
+ if sliceEnd == -1 then
49
+ modifiedRangeEnd = 0
50
+ else
51
+ modifiedRangeEnd = -(sliceEnd + 1)
52
+ end
53
+
54
+ -- Ascending list slices use negative indexes. When the whole window is
55
+ -- beyond the list length Redis clamps both indexes to 0 and LRANGE would
56
+ -- return the head element, so guard against out-of-range slices with LLEN.
57
+ if listLength ~= nil and sliceStart >= 0 and sliceEnd >= 0 and sliceStart >= listLength then
58
+ return {}
59
+ end
60
+
61
+ local ids = rcall("LRANGE", stateKey, modifiedRangeEnd, modifiedRangeStart)
62
+ local reversed = {}
63
+ for i = #ids, 1, -1 do
64
+ reversed[#reversed + 1] = ids[i]
65
+ end
66
+ return reversed
67
+ else
68
+ return rcall("LRANGE", stateKey, sliceStart, sliceEnd)
69
+ end
70
+ else
71
+ if asc then
72
+ return rcall("ZRANGE", stateKey, sliceStart, sliceEnd)
73
+ else
74
+ return rcall("ZREVRANGE", stateKey, sliceStart, sliceEnd)
75
+ end
76
+ end
77
+ end
78
+
79
+ -- Fetch the job hash for an id and append it when present.
80
+ local function appendJob(entries, jobId)
81
+ local jobData = rcall("HGETALL", prefix .. jobId)
82
+ if #jobData > 0 then
83
+ entries[#entries + 1] = {jobId, jobData}
84
+ end
85
+ end
86
+
87
+ local function cleanupDeprecatedMarker(stateKey, stateType)
88
+ if stateType == "wait" or stateType == "paused" then
89
+ local marker = rcall("LINDEX", stateKey, -1)
90
+ if marker and string.sub(marker, 1, 2) == "0:" then
91
+ local count = rcall("LLEN", stateKey)
92
+ if count > 1 then
93
+ rcall("RPOP", stateKey)
94
+ return count - 1
95
+ end
96
+ return 0
97
+ end
98
+ end
99
+ end
100
+
101
+ local function collectJobs(stateKey, stateType)
102
+ local entries = {}
103
+ local listLength
104
+ local cleanedListLength = cleanupDeprecatedMarker(stateKey, stateType)
105
+
106
+ if asc and isListType(stateType) and rangeStart >= 0 and rangeEnd >= 0 then
107
+ if cleanedListLength ~= nil then
108
+ listLength = cleanedListLength
109
+ else
110
+ listLength = rcall("LLEN", stateKey)
111
+ end
112
+ end
113
+
114
+ -- Unbounded or negative ranges: fetch the exact window and skip missing ids.
115
+ if rangeStart < 0 or rangeEnd < 0 then
116
+ local ids = fetchIds(stateKey, stateType, rangeStart, rangeEnd, listLength)
117
+ for i = 1, #ids do
118
+ appendJob(entries, ids[i])
119
+ end
120
+ return entries
121
+ end
122
+
123
+ -- Bounded range: iterate forward to backfill skipped ids.
124
+ local needed = rangeEnd - rangeStart + 1
125
+ local cursor = rangeStart
126
+ local iterations = 0
127
+ while #entries < needed and iterations < max_iterations do
128
+ local ids = fetchIds(stateKey, stateType, cursor, cursor + needed - 1, listLength)
129
+ if #ids == 0 then
130
+ break
131
+ end
132
+ for i = 1, #ids do
133
+ if #entries >= needed then
134
+ break
135
+ end
136
+ appendJob(entries, ids[i])
137
+ end
138
+ cursor = cursor + #ids
139
+ iterations = iterations + 1
140
+ end
141
+
142
+ return entries
143
+ end
144
+
145
+ for i = 5, #ARGV do
146
+ local stateType = ARGV[i]
147
+ local stateKey = prefix .. stateType
148
+ results[#results + 1] = collectJobs(stateKey, stateType)
149
+ end
150
+
151
+ return results
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getJobs = void 0;
4
+ const content = `--[[
5
+ Get jobs (id + data) for the provided states.
6
+ Job ids and their hashes are read in the same script so that ids whose hash
7
+ disappears after the id is read (but before the job is loaded) do not appear
8
+ in the result set. Ids without a job hash (for example the deprecated wait
9
+ list marker entry stored in the list) are skipped. For bounded ranges the
10
+ script iterates forward using the range offset as a cursor to backfill
11
+ skipped ids, preserving the requested page size when possible.
12
+ Input:
13
+ KEYS[1] 'prefix'
14
+ ARGV[1] start
15
+ ARGV[2] end
16
+ ARGV[3] asc ('1' | '0')
17
+ ARGV[4] max iterations (backfill bound)
18
+ ARGV[5...] types
19
+ Output:
20
+ results grouped per requested type; each entry is a
21
+ {jobId, {field, value, ...}} tuple
22
+ ]]
23
+ local rcall = redis.call
24
+ local prefix = KEYS[1]
25
+ local rangeStart = tonumber(ARGV[1])
26
+ local rangeEnd = tonumber(ARGV[2])
27
+ local asc = ARGV[3] == "1"
28
+ local max_iterations = tonumber(ARGV[4])
29
+ local results = {}
30
+ local function isListType(stateType)
31
+ return stateType == "wait" or stateType == "paused" or stateType == "active"
32
+ end
33
+ -- Fetch a slice of ids for the given state respecting the requested order.
34
+ local function fetchIds(stateKey, stateType, sliceStart, sliceEnd, listLength)
35
+ if isListType(stateType) then
36
+ if asc then
37
+ local modifiedRangeStart
38
+ local modifiedRangeEnd
39
+ if sliceStart == -1 then
40
+ modifiedRangeStart = 0
41
+ else
42
+ modifiedRangeStart = -(sliceStart + 1)
43
+ end
44
+ if sliceEnd == -1 then
45
+ modifiedRangeEnd = 0
46
+ else
47
+ modifiedRangeEnd = -(sliceEnd + 1)
48
+ end
49
+ -- Ascending list slices use negative indexes. When the whole window is
50
+ -- beyond the list length Redis clamps both indexes to 0 and LRANGE would
51
+ -- return the head element, so guard against out-of-range slices with LLEN.
52
+ if listLength ~= nil and sliceStart >= 0 and sliceEnd >= 0 and sliceStart >= listLength then
53
+ return {}
54
+ end
55
+ local ids = rcall("LRANGE", stateKey, modifiedRangeEnd, modifiedRangeStart)
56
+ local reversed = {}
57
+ for i = #ids, 1, -1 do
58
+ reversed[#reversed + 1] = ids[i]
59
+ end
60
+ return reversed
61
+ else
62
+ return rcall("LRANGE", stateKey, sliceStart, sliceEnd)
63
+ end
64
+ else
65
+ if asc then
66
+ return rcall("ZRANGE", stateKey, sliceStart, sliceEnd)
67
+ else
68
+ return rcall("ZREVRANGE", stateKey, sliceStart, sliceEnd)
69
+ end
70
+ end
71
+ end
72
+ -- Fetch the job hash for an id and append it when present.
73
+ local function appendJob(entries, jobId)
74
+ local jobData = rcall("HGETALL", prefix .. jobId)
75
+ if #jobData > 0 then
76
+ entries[#entries + 1] = {jobId, jobData}
77
+ end
78
+ end
79
+ local function cleanupDeprecatedMarker(stateKey, stateType)
80
+ if stateType == "wait" or stateType == "paused" then
81
+ local marker = rcall("LINDEX", stateKey, -1)
82
+ if marker and string.sub(marker, 1, 2) == "0:" then
83
+ local count = rcall("LLEN", stateKey)
84
+ if count > 1 then
85
+ rcall("RPOP", stateKey)
86
+ return count - 1
87
+ end
88
+ return 0
89
+ end
90
+ end
91
+ end
92
+ local function collectJobs(stateKey, stateType)
93
+ local entries = {}
94
+ local listLength
95
+ local cleanedListLength = cleanupDeprecatedMarker(stateKey, stateType)
96
+ if asc and isListType(stateType) and rangeStart >= 0 and rangeEnd >= 0 then
97
+ if cleanedListLength ~= nil then
98
+ listLength = cleanedListLength
99
+ else
100
+ listLength = rcall("LLEN", stateKey)
101
+ end
102
+ end
103
+ -- Unbounded or negative ranges: fetch the exact window and skip missing ids.
104
+ if rangeStart < 0 or rangeEnd < 0 then
105
+ local ids = fetchIds(stateKey, stateType, rangeStart, rangeEnd, listLength)
106
+ for i = 1, #ids do
107
+ appendJob(entries, ids[i])
108
+ end
109
+ return entries
110
+ end
111
+ -- Bounded range: iterate forward to backfill skipped ids.
112
+ local needed = rangeEnd - rangeStart + 1
113
+ local cursor = rangeStart
114
+ local iterations = 0
115
+ while #entries < needed and iterations < max_iterations do
116
+ local ids = fetchIds(stateKey, stateType, cursor, cursor + needed - 1, listLength)
117
+ if #ids == 0 then
118
+ break
119
+ end
120
+ for i = 1, #ids do
121
+ if #entries >= needed then
122
+ break
123
+ end
124
+ appendJob(entries, ids[i])
125
+ end
126
+ cursor = cursor + #ids
127
+ iterations = iterations + 1
128
+ end
129
+ return entries
130
+ end
131
+ for i = 5, #ARGV do
132
+ local stateType = ARGV[i]
133
+ local stateKey = prefix .. stateType
134
+ results[#results + 1] = collectJobs(stateKey, stateType)
135
+ end
136
+ return results
137
+ `;
138
+ exports.getJobs = {
139
+ name: 'getJobs',
140
+ content,
141
+ keys: 1,
142
+ };
@@ -18,6 +18,7 @@ tslib_1.__exportStar(require("./getCounts-1"), exports);
18
18
  tslib_1.__exportStar(require("./getCountsPerPriority-4"), exports);
19
19
  tslib_1.__exportStar(require("./getDependencyCounts-4"), exports);
20
20
  tslib_1.__exportStar(require("./getJobScheduler-1"), exports);
21
+ tslib_1.__exportStar(require("./getJobs-1"), exports);
21
22
  tslib_1.__exportStar(require("./getMetrics-2"), exports);
22
23
  tslib_1.__exportStar(require("./getRanges-1"), exports);
23
24
  tslib_1.__exportStar(require("./getRateLimitTtl-2"), exports);