queue-jobs-worker 1.0.4 → 2.0.0

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.
Files changed (58) hide show
  1. package/README.md +754 -538
  2. package/dist/index.d.ts +546 -29
  3. package/dist/index.js +1279 -2410
  4. package/dist/scripts/claim-job.lua +117 -0
  5. package/dist/scripts/clear-queue.lua +42 -0
  6. package/dist/scripts/list-jobs.lua +50 -0
  7. package/dist/scripts/remove-job.lua +41 -0
  8. package/dist/scripts/save-job.lua +52 -0
  9. package/dist/scripts/update-job.lua +183 -0
  10. package/package.json +91 -110
  11. package/CHANGELOG.md +0 -160
  12. package/LICENSE +0 -21
  13. package/assets/queue-jobs-worker-demo.mp4 +0 -0
  14. package/assets/queue-jobs-worker-github.png +0 -0
  15. package/dist/core/backoff.d.ts +0 -24
  16. package/dist/core/backoff.d.ts.map +0 -1
  17. package/dist/core/client.d.ts +0 -95
  18. package/dist/core/client.d.ts.map +0 -1
  19. package/dist/core/id.d.ts +0 -9
  20. package/dist/core/id.d.ts.map +0 -1
  21. package/dist/core/index.d.ts +0 -7
  22. package/dist/core/index.d.ts.map +0 -1
  23. package/dist/core/job.d.ts +0 -75
  24. package/dist/core/job.d.ts.map +0 -1
  25. package/dist/core/queue.d.ts +0 -70
  26. package/dist/core/queue.d.ts.map +0 -1
  27. package/dist/core/worker.d.ts +0 -65
  28. package/dist/core/worker.d.ts.map +0 -1
  29. package/dist/events/emitter.d.ts +0 -24
  30. package/dist/events/emitter.d.ts.map +0 -1
  31. package/dist/index.cjs +0 -2529
  32. package/dist/index.cjs.map +0 -1
  33. package/dist/index.d.ts.map +0 -1
  34. package/dist/index.js.map +0 -1
  35. package/dist/storage/in-memory.adapter.d.ts +0 -33
  36. package/dist/storage/in-memory.adapter.d.ts.map +0 -1
  37. package/dist/storage/index.d.ts +0 -5
  38. package/dist/storage/index.d.ts.map +0 -1
  39. package/dist/storage/mysql.adapter.d.ts +0 -38
  40. package/dist/storage/mysql.adapter.d.ts.map +0 -1
  41. package/dist/storage/postgres.adapter.d.ts +0 -38
  42. package/dist/storage/postgres.adapter.d.ts.map +0 -1
  43. package/dist/storage/redis.adapter.d.ts +0 -45
  44. package/dist/storage/redis.adapter.d.ts.map +0 -1
  45. package/dist/types/client.types.d.ts +0 -41
  46. package/dist/types/client.types.d.ts.map +0 -1
  47. package/dist/types/events.types.d.ts +0 -22
  48. package/dist/types/events.types.d.ts.map +0 -1
  49. package/dist/types/index.d.ts +0 -10
  50. package/dist/types/index.d.ts.map +0 -1
  51. package/dist/types/job.types.d.ts +0 -97
  52. package/dist/types/job.types.d.ts.map +0 -1
  53. package/dist/types/queue.types.d.ts +0 -43
  54. package/dist/types/queue.types.d.ts.map +0 -1
  55. package/dist/types/storage.types.d.ts +0 -138
  56. package/dist/types/storage.types.d.ts.map +0 -1
  57. package/dist/types/worker.types.d.ts +0 -25
  58. package/dist/types/worker.types.d.ts.map +0 -1
@@ -0,0 +1,117 @@
1
+ --[[
2
+ claim-job.lua
3
+ Atomically claim the next eligible job from a queue.
4
+
5
+ This script runs server-side in a single round-trip so no two workers
6
+ can ever pick up the same job, even under high concurrency.
7
+
8
+ KEYS
9
+ [1] index key — sorted set "qjw:{queue}:index" (score = priority)
10
+ [2] waiting key — sorted set "qjw:{queue}:waiting" (score = runAt ms)
11
+
12
+ ARGV
13
+ [1] nowMs — current Unix timestamp in milliseconds (string)
14
+
15
+ Algorithm
16
+ 1. Scan the waiting set for members whose score (runAt) <= nowMs.
17
+ ZRANGEBYSCORE returns by runAt ASC — but two jobs with identical runAt
18
+ values would be served in arbitrary order, violating priority ordering.
19
+ 2. BUG FIX: re-sort candidates by their priority score from the index set
20
+ (lower score = higher priority) so high-priority jobs are always claimed
21
+ first even when multiple jobs become eligible at the same runAt.
22
+ 3. Iterate the priority-sorted list. For each candidate, atomically ZREM
23
+ it from the waiting set — only one worker can succeed (ZREM returns 1).
24
+ 4. Fetch the job JSON; skip if missing (external deletion race).
25
+ 5. Patch status → "active", bump updatedAt and processedAt in-place.
26
+ 6. Persist and return the updated JSON string.
27
+
28
+ Returns
29
+ Bulk string — updated job JSON, or nil if nothing is eligible.
30
+ --]]
31
+
32
+ local indexKey = KEYS[1] -- "qjw:{queue}:index"
33
+ local waitingKey = KEYS[2] -- "qjw:{queue}:waiting"
34
+ local nowMs = tonumber(ARGV[1])
35
+
36
+ -- ── 1. Fetch all eligible job IDs from the waiting set ──────────────────────
37
+ -- ZRANGEBYSCORE returns members ordered by score (runAt) ASC.
38
+ local candidates = redis.call("ZRANGEBYSCORE", waitingKey, "-inf", nowMs)
39
+
40
+ if #candidates == 0 then
41
+ return nil
42
+ end
43
+
44
+ -- ── 2. Re-sort by priority using scores from the index set ──────────────────
45
+ -- BUG FIX: the waiting set is ordered by runAt, not priority. When multiple
46
+ -- jobs are eligible at the same time (or have identical runAt values) they
47
+ -- should be dispatched in priority ASC order (lower score = higher priority).
48
+ -- We fetch each candidate's priority score and sort locally inside the script.
49
+ --
50
+ -- ZSCORE returns nil for members not in the index (should never happen in a
51
+ -- consistent store, but we default to 0 so such jobs sort at the front rather
52
+ -- than causing a Lua error).
53
+ local scored = {}
54
+ for _, jobId in ipairs(candidates) do
55
+ local score = redis.call("ZSCORE", indexKey, jobId)
56
+ scored[#scored + 1] = { id = jobId, pri = tonumber(score) or 0 }
57
+ end
58
+
59
+ -- Stable sort by priority ASC. Lua's table.sort is not guaranteed stable,
60
+ -- but for our purposes (priority is typically a small integer) this is fine —
61
+ -- ties within the same priority are already broken by runAt ASC order from
62
+ -- ZRANGEBYSCORE, and table.sort preserves relative input order for equal keys
63
+ -- in most Lua runtimes (including Redis's embedded LuaJIT).
64
+ table.sort(scored, function(a, b) return a.pri < b.pri end)
65
+
66
+ -- ── 3. Claim the first job we can atomically remove from the waiting set ─────
67
+ for _, entry in ipairs(scored) do
68
+ local jobId = entry.id
69
+
70
+ -- ZREM is atomic: returns 1 if we removed it, 0 if another worker beat us.
71
+ local removed = redis.call("ZREM", waitingKey, jobId)
72
+ if removed == 1 then
73
+ local hashKey = "qjw:job:" .. jobId
74
+ local raw = redis.call("GET", hashKey)
75
+
76
+ if raw then
77
+ -- ── 4. Patch status fields in-place ─────────────────────────────────
78
+ -- We use targeted string substitution rather than a full JSON parse
79
+ -- (Redis has no built-in JSON module by default). The job JSON is a
80
+ -- flat object — no nested status/updatedAt/processedAt fields exist
81
+ -- inside sub-objects, so string replacement is safe and unambiguous.
82
+ local nowStr = tostring(nowMs)
83
+
84
+ -- "status":"<anything>" → "status":"active"
85
+ raw = raw:gsub('"status":"[^"]*"', '"status":"active"', 1)
86
+
87
+ -- "updatedAt":<number> → current timestamp
88
+ raw = raw:gsub('"updatedAt":[%d%.%-]+', '"updatedAt":' .. nowStr, 1)
89
+
90
+ -- "processedAt": <string|number|null|absent> → current timestamp
91
+ -- Two-branch pattern handles both JSON string values and scalar values.
92
+ if raw:find('"processedAt":', 1, true) then
93
+ raw = raw:gsub(
94
+ '"processedAt":("([^"\\]|\\.)*"|[^,}]+)',
95
+ '"processedAt":' .. nowStr,
96
+ 1
97
+ )
98
+ else
99
+ -- Field absent — append before the closing brace.
100
+ raw = raw:gsub('}$', ',"processedAt":' .. nowStr .. '}')
101
+ end
102
+
103
+ -- ── 5. Persist and return ────────────────────────────────────────────
104
+ redis.call("SET", hashKey, raw)
105
+
106
+ -- Only remove from the *waiting* set, NOT from the index.
107
+ -- The index tracks ALL jobs regardless of status so listJobs("active")
108
+ -- continues to find this job. Index entries are removed only when the
109
+ -- job reaches a terminal state via update-job.lua or remove-job.lua.
110
+
111
+ return raw
112
+ end
113
+ -- JSON missing — job was deleted externally. Continue to next candidate.
114
+ end
115
+ end
116
+
117
+ return nil
@@ -0,0 +1,42 @@
1
+ --[[
2
+ clear-queue.lua
3
+ Atomically delete every job in a queue and clean up all sorted sets.
4
+
5
+ Without this script clearQueue requires:
6
+ 1 ZRANGE — fetch all IDs
7
+ N DEL — delete each job hash
8
+ 1 DEL — delete index
9
+ 1 DEL — delete waiting set
10
+ = N + 3 round-trips.
11
+
12
+ With this script it is a single round-trip regardless of queue size.
13
+
14
+ KEYS
15
+ [1] indexKey — "qjw:{queue}:index"
16
+ [2] waitingKey — "qjw:{queue}:waiting"
17
+
18
+ ARGV
19
+ (none)
20
+
21
+ Returns
22
+ Number of job hashes that were deleted.
23
+ --]]
24
+
25
+ local indexKey = KEYS[1]
26
+ local waitingKey = KEYS[2]
27
+
28
+ -- Collect every job ID tracked by the priority index.
29
+ -- ZRANGE 0 -1 returns all members in score order (ASC).
30
+ local ids = redis.call("ZRANGE", indexKey, 0, -1)
31
+ local deleted = 0
32
+
33
+ for _, jobId in ipairs(ids) do
34
+ local n = redis.call("DEL", "qjw:job:" .. jobId)
35
+ deleted = deleted + n
36
+ end
37
+
38
+ -- Drop both sorted sets.
39
+ redis.call("DEL", indexKey)
40
+ redis.call("DEL", waitingKey)
41
+
42
+ return deleted
@@ -0,0 +1,50 @@
1
+ --[[
2
+ list-jobs.lua
3
+ Fetch all jobs for a queue in a single server-side round-trip.
4
+
5
+ Without this script listJobs requires:
6
+ 1 ZRANGE — fetch all IDs
7
+ N GET — fetch each job hash individually ← N+1 problem
8
+ = N + 1 round-trips.
9
+
10
+ With this script it is always exactly 1 round-trip.
11
+
12
+ KEYS
13
+ [1] indexKey — "qjw:{queue}:index" (sorted by priority ASC)
14
+
15
+ ARGV
16
+ [1] statusFilter — status string to filter by, or "" for no filter
17
+
18
+ Algorithm
19
+ 1. ZRANGE the index to get all job IDs in priority order.
20
+ 2. GET each job hash in a loop (all inside the same script execution,
21
+ so it is pipelined on the Redis side with zero network overhead).
22
+ 3. Optionally filter by status using a simple string search.
23
+ 4. Return the matching JSON strings as a Redis array (multi-bulk reply).
24
+
25
+ Returns
26
+ Array of job JSON strings, sorted by priority ASC (then insertion order
27
+ within the same priority level, which matches createdAt ASC because jobs
28
+ are added with ZADD NX and the index preserves insertion order for ties).
29
+ --]]
30
+
31
+ local indexKey = KEYS[1]
32
+ local statusFilter = ARGV[1] -- e.g. "waiting", "failed", or "" for all
33
+
34
+ local ids = redis.call("ZRANGE", indexKey, 0, -1)
35
+ local result = {}
36
+
37
+ for _, jobId in ipairs(ids) do
38
+ local raw = redis.call("GET", "qjw:job:" .. jobId)
39
+ if raw then
40
+ -- Apply status filter if requested.
41
+ -- We do a simple substring search: '"status":"<filter>"'
42
+ -- This is safe because status values are simple ASCII words with no
43
+ -- special characters that would need escaping.
44
+ if statusFilter == "" or raw:find('"status":"' .. statusFilter .. '"', 1, true) then
45
+ result[#result + 1] = raw
46
+ end
47
+ end
48
+ end
49
+
50
+ return result
@@ -0,0 +1,41 @@
1
+ --[[
2
+ remove-job.lua
3
+ Atomically delete a job and remove it from every sorted set.
4
+
5
+ Replaces three separate round-trips (DEL + ZREM + ZREM) with a single
6
+ server-side script execution.
7
+
8
+ KEYS
9
+ [1] hashKey — "qjw:job:{jobId}"
10
+ [2] indexKey — "qjw:{queue}:index"
11
+ [3] waitingKey — "qjw:{queue}:waiting"
12
+
13
+ ARGV
14
+ [1] jobId — the job's id
15
+
16
+ Returns
17
+ 1 if the job existed and was deleted, 0 if it was not found.
18
+ --]]
19
+
20
+ local hashKey = KEYS[1]
21
+ local indexKey = KEYS[2]
22
+ local waitingKey = KEYS[3]
23
+
24
+ local jobId = ARGV[1]
25
+
26
+ -- Check existence first so we can return a meaningful result.
27
+ local existed = redis.call("EXISTS", hashKey)
28
+
29
+ if existed == 1 then
30
+ redis.call("DEL", hashKey)
31
+ redis.call("ZREM", indexKey, jobId)
32
+ redis.call("ZREM", waitingKey, jobId)
33
+ return 1
34
+ end
35
+
36
+ -- Still attempt the ZREM calls in case the hash was deleted externally
37
+ -- but the sorted-set entries were left behind (defensive cleanup).
38
+ redis.call("ZREM", indexKey, jobId)
39
+ redis.call("ZREM", waitingKey, jobId)
40
+
41
+ return 0
@@ -0,0 +1,52 @@
1
+ --[[
2
+ save-job.lua
3
+ Atomically persist a new job to Redis.
4
+
5
+ Replaces three separate round-trips (SET + ZADD + ZADD) with a single
6
+ server-side script execution.
7
+
8
+ KEYS
9
+ [1] hashKey — "qjw:job:{jobId}"
10
+ [2] indexKey — "qjw:{queue}:index" sorted set, score = priority
11
+ [3] waitingKey — "qjw:{queue}:waiting" sorted set, score = runAt ms
12
+
13
+ ARGV
14
+ [1] jobJson — full JSON string of the Job record
15
+ [2] jobId — the job's id field
16
+ [3] priority — numeric priority (string)
17
+ [4] runAt — runAt timestamp ms (string)
18
+ [5] eligible — "1" if this job should enter the waiting set now
19
+ (status is "waiting" or "retrying"), "0" otherwise
20
+
21
+ Returns
22
+ 1 always.
23
+ --]]
24
+
25
+ local hashKey = KEYS[1]
26
+ local indexKey = KEYS[2]
27
+ local waitingKey = KEYS[3]
28
+
29
+ local jobJson = ARGV[1]
30
+ local jobId = ARGV[2]
31
+ local priority = ARGV[3]
32
+ local runAt = ARGV[4]
33
+ local eligible = ARGV[5]
34
+
35
+ -- Store the full job JSON.
36
+ redis.call("SET", hashKey, jobJson)
37
+
38
+ -- Always add to the priority index so listJobs / countJobs work.
39
+ -- BUG FIX: use ZADD NX so re-saving an existing job (e.g. after a crash
40
+ -- recovery re-enqueue) does not silently overwrite the priority score that
41
+ -- update-job.lua may have set. saveJob is a new-job operation; priority
42
+ -- updates go through updateJob / update-job.lua exclusively.
43
+ redis.call("ZADD", indexKey, "NX", priority, jobId)
44
+
45
+ -- Only add to the waiting set if the job is immediately pick-up-eligible.
46
+ -- Use plain ZADD (not NX) so a re-save after a crash can correct the runAt
47
+ -- score; the waiting set score must always reflect the true scheduled time.
48
+ if eligible == "1" then
49
+ redis.call("ZADD", waitingKey, runAt, jobId)
50
+ end
51
+
52
+ return 1
@@ -0,0 +1,183 @@
1
+ --[[
2
+ update-job.lua v6
3
+ Atomically read, patch, and re-persist a job — entirely server-side.
4
+
5
+ One Redis round-trip: GET + targeted string replacement + SET.
6
+
7
+ ── KEYS ──────────────────────────────────────────────────────────────────────
8
+ [1] hashKey — "qjw:job:{jobId}"
9
+ [2] indexKey — "qjw:{queue}:index" sorted set, score = priority
10
+ [3] waitingKey — "qjw:{queue}:waiting" sorted set, score = runAt ms
11
+
12
+ ── ARGV ──────────────────────────────────────────────────────────────────────
13
+ [1] newStatus — target status string (always patched)
14
+ [2] nowMs — current Unix timestamp ms (always written as updatedAt)
15
+ [3] runAt — runAt ms for "retrying" re-queue, or "-1" as sentinel
16
+ meaning "do not touch runAt / skip ZADD waitingKey".
17
+ [4] priority — job priority integer, or "-1" sentinel ("keep existing").
18
+ [5] strFields — RS-delimited "key\x1evalue" pairs for string fields.
19
+ [6] numFields — RS-delimited "key\x1evalue" pairs for numeric fields.
20
+ [7] nullFields — RS-delimited field names to set to JSON null.
21
+ [8] boolFields — RS-delimited "key\x1evalue" pairs for boolean fields
22
+ ("true" / "false" JSON literals).
23
+
24
+ ── Returns ───────────────────────────────────────────────────────────────────
25
+ 1 on success, 0 if the job does not exist.
26
+ --]]
27
+
28
+ local hashKey = KEYS[1]
29
+ local indexKey = KEYS[2]
30
+ local waitingKey = KEYS[3]
31
+
32
+ local newStatus = ARGV[1]
33
+ local nowMs = ARGV[2]
34
+ local runAt = ARGV[3]
35
+ local priority = ARGV[4]
36
+ local strFields = ARGV[5]
37
+ local numFields = ARGV[6]
38
+ local nullFields = ARGV[7]
39
+ local boolFields = ARGV[8] or ""
40
+
41
+ local RS = "\x1e"
42
+
43
+ -- ── 1. Read current job JSON ──────────────────────────────────────────────────
44
+ local raw = redis.call("GET", hashKey)
45
+ if not raw then return 0 end
46
+
47
+ -- ── helpers ───────────────────────────────────────────────────────────────────
48
+
49
+ -- Escape for Lua gsub() PATTERN (not replacement).
50
+ local function escPat(s)
51
+ return s:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1")
52
+ end
53
+
54
+ -- Escape for Lua gsub() REPLACEMENT string.
55
+ local function escRepl(s)
56
+ return s:gsub("%%", "%%%%")
57
+ end
58
+
59
+ -- Split RS-delimited string into array.
60
+ local function splitRS(s)
61
+ local parts = {}
62
+ for part in (s .. RS):gmatch("([^" .. RS .. "]*)" .. RS) do
63
+ parts[#parts + 1] = part
64
+ end
65
+ return parts
66
+ end
67
+
68
+ -- BUG FIX (v6): existence check uses plain literal search (4th arg = true).
69
+ -- Previously `find('"' .. escPat(key) .. '":', 1, true)` was used, but the
70
+ -- escPat() output contains % escapes that are meaningless in plain-text mode
71
+ -- and could corrupt the search string if the key contained literal %.
72
+ -- Using the raw key with plain=true is both correct and simpler.
73
+ local function hasField(json, key)
74
+ return json:find('"' .. key .. '":', 1, true) ~= nil
75
+ end
76
+
77
+ -- ── 2. Patch: status (always) ─────────────────────────────────────────────────
78
+ raw = raw:gsub('"status":"[^"]*"', '"status":"' .. newStatus .. '"', 1)
79
+
80
+ -- ── 3. Patch: updatedAt (always, numeric) ────────────────────────────────────
81
+ raw = raw:gsub('"updatedAt":[%d%.%-]+', '"updatedAt":' .. nowMs, 1)
82
+
83
+ -- ── 4. Patch: string fields ───────────────────────────────────────────────────
84
+ if strFields ~= "" then
85
+ local parts = splitRS(strFields)
86
+ local i = 1
87
+ while i < #parts do
88
+ local key = parts[i]
89
+ local val = parts[i + 1]
90
+ i = i + 2
91
+
92
+ if key ~= "" then
93
+ local pat = '"' .. escPat(key) .. '":"([^"\\\\]|\\\\.)*"'
94
+ local repl = '"' .. key .. '":"' .. escRepl(val) .. '"'
95
+
96
+ if hasField(raw, key) then
97
+ raw = raw:gsub(pat, repl, 1)
98
+ else
99
+ raw = raw:gsub('}$', ',"' .. key .. '":"' .. escRepl(val) .. '"}')
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ -- ── 5. Patch: numeric fields ──────────────────────────────────────────────────
106
+ if numFields ~= "" then
107
+ local parts = splitRS(numFields)
108
+ local i = 1
109
+ while i < #parts do
110
+ local key = parts[i]
111
+ local val = parts[i + 1]
112
+ i = i + 2
113
+
114
+ if key ~= "" then
115
+ local pat = '"' .. escPat(key) .. '":[%d%.%-]+'
116
+ local repl = '"' .. key .. '":' .. val
117
+
118
+ if hasField(raw, key) then
119
+ raw = raw:gsub(pat, repl, 1)
120
+ else
121
+ raw = raw:gsub('}$', ',"' .. key .. '":' .. val .. '}')
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ -- ── 6. Patch: boolean fields ──────────────────────────────────────────────────
128
+ if boolFields ~= "" then
129
+ local parts = splitRS(boolFields)
130
+ local i = 1
131
+ while i < #parts do
132
+ local key = parts[i]
133
+ local val = parts[i + 1] -- "true" or "false"
134
+ i = i + 2
135
+
136
+ if key ~= "" then
137
+ local pat = '"' .. escPat(key) .. '":(true|false)'
138
+ local repl = '"' .. key .. '":' .. val
139
+
140
+ if hasField(raw, key) then
141
+ raw = raw:gsub(pat, repl, 1)
142
+ else
143
+ raw = raw:gsub('}$', ',"' .. key .. '":' .. val .. '}')
144
+ end
145
+ end
146
+ end
147
+ end
148
+
149
+ -- ── 7. Patch: null fields ─────────────────────────────────────────────────────
150
+ if nullFields ~= "" then
151
+ local parts = splitRS(nullFields)
152
+ for _, key in ipairs(parts) do
153
+ if key ~= "" and hasField(raw, key) then
154
+ local pat = '"' .. escPat(key) .. '":("([^"\\\\]|\\\\.)*"|[^,}]+)'
155
+ raw = raw:gsub(pat, '"' .. key .. '":null', 1)
156
+ end
157
+ end
158
+ end
159
+
160
+ -- ── 8. Persist updated JSON ───────────────────────────────────────────────────
161
+ redis.call("SET", hashKey, raw)
162
+
163
+ -- ── 9. Manage sorted-set membership ──────────────────────────────────────────
164
+ local jobId = hashKey:match("qjw:job:(.+)")
165
+
166
+ if newStatus == "retrying" then
167
+ if runAt ~= "-1" then
168
+ redis.call("ZADD", waitingKey, runAt, jobId)
169
+ end
170
+ if priority ~= "-1" then
171
+ redis.call("ZADD", indexKey, priority, jobId)
172
+ end
173
+
174
+ elseif newStatus == "completed" or newStatus == "failed" then
175
+ redis.call("ZREM", waitingKey, jobId)
176
+ redis.call("ZREM", indexKey, jobId)
177
+
178
+ elseif newStatus == "active" then
179
+ redis.call("ZREM", waitingKey, jobId)
180
+
181
+ end
182
+
183
+ return 1