@terreno/jobs 57.4.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.
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/biome.jsonc +20 -0
- package/bunfig.toml +5 -0
- package/dist/activeJobExecution.d.ts +3 -0
- package/dist/activeJobExecution.js +29 -0
- package/dist/claimLock.d.ts +16 -0
- package/dist/claimLock.js +34 -0
- package/dist/dispatchCompensation.d.ts +3 -0
- package/dist/dispatchCompensation.js +89 -0
- package/dist/dispatchError.d.ts +10 -0
- package/dist/dispatchError.js +33 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +15 -0
- package/dist/jobExecutor.d.ts +34 -0
- package/dist/jobExecutor.js +328 -0
- package/dist/jobsApp.d.ts +61 -0
- package/dist/jobsApp.js +308 -0
- package/dist/jobsService.d.ts +38 -0
- package/dist/jobsService.js +505 -0
- package/dist/modelTypes.d.ts +52 -0
- package/dist/modelTypes.js +4 -0
- package/dist/models/job.d.ts +2 -0
- package/dist/models/job.js +124 -0
- package/dist/models/jobSchedule.d.ts +2 -0
- package/dist/models/jobSchedule.js +48 -0
- package/dist/retryBackoff.d.ts +17 -0
- package/dist/retryBackoff.js +22 -0
- package/dist/routes/jobsAdmin.d.ts +15 -0
- package/dist/routes/jobsAdmin.js +596 -0
- package/dist/routes/jobsExecute.d.ts +12 -0
- package/dist/routes/jobsExecute.js +119 -0
- package/dist/runners/gcpCloudTasks.d.ts +78 -0
- package/dist/runners/gcpCloudTasks.js +256 -0
- package/dist/runners/mongoRunner.d.ts +14 -0
- package/dist/runners/mongoRunner.js +204 -0
- package/dist/runners/vercelQueues.d.ts +111 -0
- package/dist/runners/vercelQueues.js +326 -0
- package/dist/scheduleCron.d.ts +15 -0
- package/dist/scheduleCron.js +61 -0
- package/dist/scheduler.d.ts +8 -0
- package/dist/scheduler.js +178 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.js +2 -0
- package/package.json +82 -0
- package/src/activeJobExecution.ts +28 -0
- package/src/claimLock.ts +37 -0
- package/src/dispatchCompensation.ts +39 -0
- package/src/dispatchError.ts +14 -0
- package/src/index.ts +24 -0
- package/src/jobExecutor.ts +333 -0
- package/src/jobsApp.ts +255 -0
- package/src/jobsService.ts +395 -0
- package/src/modelTypes.ts +78 -0
- package/src/models/job.ts +129 -0
- package/src/models/jobSchedule.ts +57 -0
- package/src/retryBackoff.ts +34 -0
- package/src/routes/jobsAdmin.ts +562 -0
- package/src/routes/jobsExecute.ts +95 -0
- package/src/runners/gcpCloudTasks.ts +290 -0
- package/src/runners/mongoRunner.ts +135 -0
- package/src/runners/vercelQueues.ts +394 -0
- package/src/scheduleCron.ts +65 -0
- package/src/scheduler.ts +112 -0
- package/src/tests/bunSetup.ts +9 -0
- package/src/tests/gcpCloudTasksRunner.test.ts +349 -0
- package/src/tests/jobModel.test.ts +62 -0
- package/src/tests/jobScheduleModel.test.ts +44 -0
- package/src/tests/jobsAdmin.test.ts +728 -0
- package/src/tests/jobsApp.test.ts +113 -0
- package/src/tests/jobsCustomRunner.test.ts +367 -0
- package/src/tests/jobsDelayIdempotency.test.ts +255 -0
- package/src/tests/jobsExecute.test.ts +640 -0
- package/src/tests/jobsRetry.test.ts +420 -0
- package/src/tests/jobsSchedule.test.ts +505 -0
- package/src/tests/jobsTracer.test.ts +165 -0
- package/src/tests/jobsWorkerLifecycle.test.ts +509 -0
- package/src/tests/packageExports.test.ts +24 -0
- package/src/tests/retryBackoff.test.ts +76 -0
- package/src/tests/scheduleCron.test.ts +87 -0
- package/src/tests/vercelQueuesRunner.test.ts +524 -0
- package/src/types.ts +74 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
13
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
14
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
15
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
16
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
17
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
18
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
19
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
23
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
24
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
25
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
26
|
+
function step(op) {
|
|
27
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
28
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
29
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
30
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
31
|
+
switch (op[0]) {
|
|
32
|
+
case 0: case 1: t = op; break;
|
|
33
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
34
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
35
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
36
|
+
default:
|
|
37
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
38
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
39
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
40
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
41
|
+
if (t[2]) _.ops.pop();
|
|
42
|
+
_.trys.pop(); continue;
|
|
43
|
+
}
|
|
44
|
+
op = body.call(thisArg, _);
|
|
45
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
46
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
50
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
51
|
+
};
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.executeJobById = exports.runClaimedJob = void 0;
|
|
54
|
+
var api_1 = require("@terreno/api");
|
|
55
|
+
var luxon_1 = require("luxon");
|
|
56
|
+
var mongoose_1 = __importDefault(require("mongoose"));
|
|
57
|
+
var activeJobExecution_1 = require("./activeJobExecution");
|
|
58
|
+
var claimLock_1 = require("./claimLock");
|
|
59
|
+
var job_1 = require("./models/job");
|
|
60
|
+
var retryBackoff_1 = require("./retryBackoff");
|
|
61
|
+
var TERMINAL_NOOP_STATUSES = new Set(["cancelled", "completed", "dead", "failed"]);
|
|
62
|
+
var buildLockExpiry = function (lockTtlMs, now) {
|
|
63
|
+
return luxon_1.DateTime.fromJSDate(now, { zone: "utc" }).minus({ milliseconds: lockTtlMs }).toJSDate();
|
|
64
|
+
};
|
|
65
|
+
var isLiveLock = function (lockedAt, lockExpiry) {
|
|
66
|
+
return Boolean(lockedAt && lockedAt > lockExpiry);
|
|
67
|
+
};
|
|
68
|
+
var readClaimLock = function (job) {
|
|
69
|
+
if (!job.lockedAt || !job.lockedBy) {
|
|
70
|
+
throw new Error("Job ".concat(job._id.toString(), " is missing claim lock metadata"));
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
lockedAt: job.lockedAt,
|
|
74
|
+
lockedBy: job.lockedBy,
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
var classifyConflict = function (job, lockExpiry, now) {
|
|
78
|
+
if (job.runAt > now) {
|
|
79
|
+
return { kind: "conflict", reason: "not_due", runAt: job.runAt };
|
|
80
|
+
}
|
|
81
|
+
if (job.status === "running" && isLiveLock(job.lockedAt, lockExpiry)) {
|
|
82
|
+
return { kind: "conflict", reason: "locked" };
|
|
83
|
+
}
|
|
84
|
+
return { kind: "conflict", reason: "locked" };
|
|
85
|
+
};
|
|
86
|
+
var recordHandlerSuccess = function (jobId, claim) { return __awaiter(void 0, void 0, void 0, function () {
|
|
87
|
+
var updated, current;
|
|
88
|
+
return __generator(this, function (_a) {
|
|
89
|
+
switch (_a.label) {
|
|
90
|
+
case 0: return [4 /*yield*/, job_1.Job.findOneAndUpdate(__assign(__assign({}, (0, claimLock_1.buildClaimOwnershipFilter)(jobId, claim)), { status: "running" }), {
|
|
91
|
+
$set: { status: "completed" },
|
|
92
|
+
$unset: { lastError: "" },
|
|
93
|
+
}, { returnDocument: "after" })];
|
|
94
|
+
case 1:
|
|
95
|
+
updated = _a.sent();
|
|
96
|
+
if (!!updated) return [3 /*break*/, 3];
|
|
97
|
+
return [4 /*yield*/, job_1.Job.findOneOrNone({ _id: jobId })];
|
|
98
|
+
case 2:
|
|
99
|
+
current = _a.sent();
|
|
100
|
+
if ((current === null || current === void 0 ? void 0 : current.status) === "cancelled") {
|
|
101
|
+
return [2 /*return*/, current];
|
|
102
|
+
}
|
|
103
|
+
throw new Error("Failed to record success for job ".concat(jobId.toString()));
|
|
104
|
+
case 3: return [2 /*return*/, updated];
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}); };
|
|
108
|
+
var recordMissingHandlerFailure = function (job, message, claim) { return __awaiter(void 0, void 0, void 0, function () {
|
|
109
|
+
var updated;
|
|
110
|
+
return __generator(this, function (_a) {
|
|
111
|
+
switch (_a.label) {
|
|
112
|
+
case 0: return [4 /*yield*/, job_1.Job.findOneAndUpdate((0, claimLock_1.buildClaimOwnershipFilter)(job._id, claim), {
|
|
113
|
+
$set: {
|
|
114
|
+
lastError: message,
|
|
115
|
+
status: "failed",
|
|
116
|
+
},
|
|
117
|
+
}, { returnDocument: "after" })];
|
|
118
|
+
case 1:
|
|
119
|
+
updated = _a.sent();
|
|
120
|
+
if (!updated) {
|
|
121
|
+
throw new Error("Failed to record missing handler for job ".concat(job._id.toString()));
|
|
122
|
+
}
|
|
123
|
+
return [2 /*return*/, updated];
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}); };
|
|
127
|
+
var recordHandlerFailure = function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
|
|
128
|
+
var current, released, errorMessage, errorClass, attemptedAt, attemptEntry, nextAttemptCount, ownershipFilter, dead, runAt, pending;
|
|
129
|
+
var claim = _b.claim, error = _b.error, job = _b.job, signal = _b.signal;
|
|
130
|
+
return __generator(this, function (_c) {
|
|
131
|
+
switch (_c.label) {
|
|
132
|
+
case 0:
|
|
133
|
+
if (!(signal.aborted && (0, claimLock_1.isAbortError)(error))) return [3 /*break*/, 3];
|
|
134
|
+
return [4 /*yield*/, job_1.Job.findOneOrNone({ _id: job._id })];
|
|
135
|
+
case 1:
|
|
136
|
+
current = _c.sent();
|
|
137
|
+
if ((current === null || current === void 0 ? void 0 : current.status) === "cancelled") {
|
|
138
|
+
return [2 /*return*/, current];
|
|
139
|
+
}
|
|
140
|
+
return [4 /*yield*/, job_1.Job.findOneAndUpdate((0, claimLock_1.buildClaimOwnershipFilter)(job._id, claim), {
|
|
141
|
+
$set: { status: "pending" },
|
|
142
|
+
$unset: { lockedAt: "", lockedBy: "" },
|
|
143
|
+
}, { returnDocument: "after" })];
|
|
144
|
+
case 2:
|
|
145
|
+
released = _c.sent();
|
|
146
|
+
if (!released) {
|
|
147
|
+
throw new Error("Failed to release lock for job ".concat(job._id.toString()));
|
|
148
|
+
}
|
|
149
|
+
return [2 /*return*/, released];
|
|
150
|
+
case 3:
|
|
151
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
152
|
+
errorClass = error instanceof Error ? error.name : "Error";
|
|
153
|
+
attemptedAt = luxon_1.DateTime.utc().toJSDate();
|
|
154
|
+
attemptEntry = {
|
|
155
|
+
at: attemptedAt,
|
|
156
|
+
error: errorMessage,
|
|
157
|
+
errorClass: errorClass,
|
|
158
|
+
};
|
|
159
|
+
nextAttemptCount = job.attemptCount + 1;
|
|
160
|
+
ownershipFilter = (0, claimLock_1.buildClaimOwnershipFilter)(job._id, claim);
|
|
161
|
+
if (!(nextAttemptCount >= job.maxAttempts)) return [3 /*break*/, 5];
|
|
162
|
+
return [4 /*yield*/, job_1.Job.findOneAndUpdate(ownershipFilter, {
|
|
163
|
+
$push: { attempts: attemptEntry },
|
|
164
|
+
$set: {
|
|
165
|
+
attemptCount: nextAttemptCount,
|
|
166
|
+
lastError: errorMessage,
|
|
167
|
+
status: "dead",
|
|
168
|
+
},
|
|
169
|
+
$unset: { lockedAt: "", lockedBy: "" },
|
|
170
|
+
}, { returnDocument: "after" })];
|
|
171
|
+
case 4:
|
|
172
|
+
dead = _c.sent();
|
|
173
|
+
if (!dead) {
|
|
174
|
+
throw new Error("Failed to record dead status for job ".concat(job._id.toString()));
|
|
175
|
+
}
|
|
176
|
+
return [2 /*return*/, dead];
|
|
177
|
+
case 5:
|
|
178
|
+
runAt = (0, retryBackoff_1.computeRetryRunAt)({
|
|
179
|
+
attemptCount: nextAttemptCount,
|
|
180
|
+
backoffMs: job.backoffMs,
|
|
181
|
+
from: luxon_1.DateTime.utc(),
|
|
182
|
+
maxBackoffMs: job.maxBackoffMs,
|
|
183
|
+
}).toJSDate();
|
|
184
|
+
return [4 /*yield*/, job_1.Job.findOneAndUpdate(ownershipFilter, {
|
|
185
|
+
$push: { attempts: attemptEntry },
|
|
186
|
+
$set: {
|
|
187
|
+
attemptCount: nextAttemptCount,
|
|
188
|
+
lastError: errorMessage,
|
|
189
|
+
runAt: runAt,
|
|
190
|
+
status: "pending",
|
|
191
|
+
},
|
|
192
|
+
$unset: { lockedAt: "", lockedBy: "" },
|
|
193
|
+
}, { returnDocument: "after" })];
|
|
194
|
+
case 6:
|
|
195
|
+
pending = _c.sent();
|
|
196
|
+
if (!pending) {
|
|
197
|
+
throw new Error("Failed to record retry for job ".concat(job._id.toString()));
|
|
198
|
+
}
|
|
199
|
+
return [2 /*return*/, pending];
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}); };
|
|
203
|
+
var runClaimedJob = function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
|
|
204
|
+
var definition, message, log, executionSignal, error_1;
|
|
205
|
+
var claim = _b.claim, host = _b.host, job = _b.job, signal = _b.signal;
|
|
206
|
+
return __generator(this, function (_c) {
|
|
207
|
+
switch (_c.label) {
|
|
208
|
+
case 0:
|
|
209
|
+
definition = host.getDefinition(job.name);
|
|
210
|
+
if (!definition) {
|
|
211
|
+
message = "No handler registered for job name \"".concat(job.name, "\"");
|
|
212
|
+
return [2 /*return*/, recordMissingHandlerFailure(job, message, claim)];
|
|
213
|
+
}
|
|
214
|
+
log = (0, api_1.createScopedLogger)({
|
|
215
|
+
labels: { jobId: job._id.toString(), jobName: job.name },
|
|
216
|
+
prefix: "[Job]",
|
|
217
|
+
});
|
|
218
|
+
executionSignal = (0, activeJobExecution_1.beginActiveJobExecution)(job._id.toString(), signal);
|
|
219
|
+
_c.label = 1;
|
|
220
|
+
case 1:
|
|
221
|
+
_c.trys.push([1, 3, 4, 5]);
|
|
222
|
+
return [4 /*yield*/, (0, api_1.runWithRequestContext)({ jobId: job._id.toString() }, function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
223
|
+
return __generator(this, function (_a) {
|
|
224
|
+
switch (_a.label) {
|
|
225
|
+
case 0: return [4 /*yield*/, definition.handler(job.payload, {
|
|
226
|
+
jobId: job._id.toString(),
|
|
227
|
+
log: log,
|
|
228
|
+
signal: executionSignal,
|
|
229
|
+
})];
|
|
230
|
+
case 1:
|
|
231
|
+
_a.sent();
|
|
232
|
+
return [2 /*return*/];
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}); })];
|
|
236
|
+
case 2:
|
|
237
|
+
_c.sent();
|
|
238
|
+
return [2 /*return*/, recordHandlerSuccess(job._id, claim)];
|
|
239
|
+
case 3:
|
|
240
|
+
error_1 = _c.sent();
|
|
241
|
+
return [2 /*return*/, recordHandlerFailure({ claim: claim, error: error_1, job: job, signal: executionSignal })];
|
|
242
|
+
case 4:
|
|
243
|
+
(0, activeJobExecution_1.endActiveJobExecution)(job._id.toString());
|
|
244
|
+
return [7 /*endfinally*/];
|
|
245
|
+
case 5: return [2 /*return*/];
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}); };
|
|
249
|
+
exports.runClaimedJob = runClaimedJob;
|
|
250
|
+
var claimJobById = function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
|
|
251
|
+
var lockExpiry, claim, claimed;
|
|
252
|
+
var host = _b.host, jobId = _b.jobId, _c = _b.now, now = _c === void 0 ? luxon_1.DateTime.utc().toJSDate() : _c;
|
|
253
|
+
return __generator(this, function (_d) {
|
|
254
|
+
switch (_d.label) {
|
|
255
|
+
case 0:
|
|
256
|
+
lockExpiry = buildLockExpiry(host.getLockTtlMs(), now);
|
|
257
|
+
claim = (0, claimLock_1.createClaimLock)(now);
|
|
258
|
+
return [4 /*yield*/, job_1.Job.findOneAndUpdate({
|
|
259
|
+
_id: jobId,
|
|
260
|
+
$or: [{ lockedAt: { $exists: false } }, { lockedAt: null }, { lockedAt: { $lte: lockExpiry } }],
|
|
261
|
+
runAt: { $lte: now },
|
|
262
|
+
status: { $in: ["pending", "running", "scheduled"] },
|
|
263
|
+
}, {
|
|
264
|
+
$set: {
|
|
265
|
+
lockedAt: claim.lockedAt,
|
|
266
|
+
lockedBy: claim.lockedBy,
|
|
267
|
+
status: "running",
|
|
268
|
+
},
|
|
269
|
+
}, { returnDocument: "after" })];
|
|
270
|
+
case 1:
|
|
271
|
+
claimed = _d.sent();
|
|
272
|
+
return [2 /*return*/, claimed !== null && claimed !== void 0 ? claimed : undefined];
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}); };
|
|
276
|
+
var executeJobById = function (_a) { return __awaiter(void 0, [_a], void 0, function (_b) {
|
|
277
|
+
var existing, now, lockExpiry, claimed, refreshed, executed;
|
|
278
|
+
var host = _b.host, jobId = _b.jobId, signal = _b.signal;
|
|
279
|
+
return __generator(this, function (_c) {
|
|
280
|
+
switch (_c.label) {
|
|
281
|
+
case 0:
|
|
282
|
+
if (!mongoose_1.default.isValidObjectId(jobId)) {
|
|
283
|
+
return [2 /*return*/, { kind: "not_found" }];
|
|
284
|
+
}
|
|
285
|
+
return [4 /*yield*/, job_1.Job.findOneOrNone({ _id: jobId })];
|
|
286
|
+
case 1:
|
|
287
|
+
existing = _c.sent();
|
|
288
|
+
if (!existing) {
|
|
289
|
+
return [2 /*return*/, { kind: "not_found" }];
|
|
290
|
+
}
|
|
291
|
+
now = luxon_1.DateTime.utc().toJSDate();
|
|
292
|
+
lockExpiry = buildLockExpiry(host.getLockTtlMs(), now);
|
|
293
|
+
if (TERMINAL_NOOP_STATUSES.has(existing.status)) {
|
|
294
|
+
return [2 /*return*/, { job: existing, kind: "noop" }];
|
|
295
|
+
}
|
|
296
|
+
if (existing.runAt > now) {
|
|
297
|
+
return [2 /*return*/, { kind: "conflict", reason: "not_due", runAt: existing.runAt }];
|
|
298
|
+
}
|
|
299
|
+
if (existing.status === "running" && isLiveLock(existing.lockedAt, lockExpiry)) {
|
|
300
|
+
return [2 /*return*/, { kind: "conflict", reason: "locked" }];
|
|
301
|
+
}
|
|
302
|
+
return [4 /*yield*/, claimJobById({ host: host, jobId: jobId, now: now })];
|
|
303
|
+
case 2:
|
|
304
|
+
claimed = _c.sent();
|
|
305
|
+
if (!!claimed) return [3 /*break*/, 4];
|
|
306
|
+
return [4 /*yield*/, job_1.Job.findOneOrNone({ _id: jobId })];
|
|
307
|
+
case 3:
|
|
308
|
+
refreshed = _c.sent();
|
|
309
|
+
if (!refreshed) {
|
|
310
|
+
return [2 /*return*/, { kind: "not_found" }];
|
|
311
|
+
}
|
|
312
|
+
if (TERMINAL_NOOP_STATUSES.has(refreshed.status)) {
|
|
313
|
+
return [2 /*return*/, { job: refreshed, kind: "noop" }];
|
|
314
|
+
}
|
|
315
|
+
return [2 /*return*/, classifyConflict(refreshed, lockExpiry, now)];
|
|
316
|
+
case 4: return [4 /*yield*/, (0, exports.runClaimedJob)({
|
|
317
|
+
claim: readClaimLock(claimed),
|
|
318
|
+
host: host,
|
|
319
|
+
job: claimed,
|
|
320
|
+
signal: signal,
|
|
321
|
+
})];
|
|
322
|
+
case 5:
|
|
323
|
+
executed = _c.sent();
|
|
324
|
+
return [2 /*return*/, { job: executed, kind: "executed" }];
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}); };
|
|
328
|
+
exports.executeJobById = executeJobById;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type AdminContribution, type AnyTerrenoAccess, type TerrenoPlugin } from "@terreno/api";
|
|
2
|
+
import type express from "express";
|
|
3
|
+
import { type ExecuteJobOutcome } from "./jobExecutor";
|
|
4
|
+
import { type ExecuteAuthVerifier } from "./routes/jobsExecute";
|
|
5
|
+
import type { JobDefinition, JobRunner, JobsAdminRedactPayload, JobsRunnerHost } from "./types";
|
|
6
|
+
export interface JobsAppOptions {
|
|
7
|
+
accessControl?: AnyTerrenoAccess;
|
|
8
|
+
basePath?: string;
|
|
9
|
+
/** Verifies inbound execute HTTP requests (required when the execute route is mounted). */
|
|
10
|
+
executeAuth?: ExecuteAuthVerifier;
|
|
11
|
+
lockTtlMs?: number;
|
|
12
|
+
/** Mount internal `POST {basePath}/execute` for cloud/custom dispatch. */
|
|
13
|
+
mountExecuteRoute?: boolean;
|
|
14
|
+
pollIntervalMs?: number;
|
|
15
|
+
/** Admin list/detail payload projection. Default omits payload; identity hook may expose raw payload. */
|
|
16
|
+
redactPayload?: JobsAdminRedactPayload;
|
|
17
|
+
runner?: JobRunner;
|
|
18
|
+
timezone?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* TerrenoPlugin for durable background jobs.
|
|
22
|
+
*
|
|
23
|
+
* Registering `JobsApp` wires the plugin into the Express app but does **not**
|
|
24
|
+
* start a worker poll loop. Call {@link JobsApp.startWorker} explicitly from the
|
|
25
|
+
* API process or a dedicated worker entrypoint.
|
|
26
|
+
*/
|
|
27
|
+
export declare class JobsApp implements JobsRunnerHost, TerrenoPlugin {
|
|
28
|
+
private readonly options;
|
|
29
|
+
private readonly runner;
|
|
30
|
+
private readonly service;
|
|
31
|
+
private abortController;
|
|
32
|
+
private workerActive;
|
|
33
|
+
private workerPromise;
|
|
34
|
+
private workerStartPromise;
|
|
35
|
+
constructor(options?: JobsAppOptions);
|
|
36
|
+
define(name: string, definition: JobDefinition): this;
|
|
37
|
+
getBasePath(): string;
|
|
38
|
+
getDefinition(name: string): JobDefinition | undefined;
|
|
39
|
+
getDefaultTimezone(): string;
|
|
40
|
+
getLockTtlMs(): number;
|
|
41
|
+
/** Execute a persisted job by id from an external dispatch surface (queue consumer, execute HTTP). */
|
|
42
|
+
executeQueuedJob(jobId: string, signal?: AbortSignal): Promise<ExecuteJobOutcome>;
|
|
43
|
+
getPollIntervalMs(): number;
|
|
44
|
+
reconcileSchedules(): Promise<void>;
|
|
45
|
+
reconcileSchedulesIfDirty(): Promise<void>;
|
|
46
|
+
tickSchedules(now?: Date): Promise<void>;
|
|
47
|
+
register(app: express.Application, openApi?: unknown): void;
|
|
48
|
+
adminContribution(): AdminContribution;
|
|
49
|
+
private shouldMountExecuteRoute;
|
|
50
|
+
isWorkerActive(): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Starts the background worker poll loop without awaiting it.
|
|
53
|
+
*
|
|
54
|
+
* Fire-and-forget: resolves once the runner has been scheduled. Runner errors are
|
|
55
|
+
* logged and worker state is reset; they are not thrown from this method.
|
|
56
|
+
*/
|
|
57
|
+
startWorker(): Promise<void>;
|
|
58
|
+
stopWorker(): Promise<void>;
|
|
59
|
+
private resetWorkerState;
|
|
60
|
+
private beginWorkerStart;
|
|
61
|
+
}
|