@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,505 @@
|
|
|
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 __values = (this && this.__values) || function(o) {
|
|
50
|
+
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
|
|
51
|
+
if (m) return m.call(o);
|
|
52
|
+
if (o && typeof o.length === "number") return {
|
|
53
|
+
next: function () {
|
|
54
|
+
if (o && i >= o.length) o = void 0;
|
|
55
|
+
return { value: o && o[i++], done: !o };
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
|
59
|
+
};
|
|
60
|
+
var __read = (this && this.__read) || function (o, n) {
|
|
61
|
+
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
|
62
|
+
if (!m) return o;
|
|
63
|
+
var i = m.call(o), r, ar = [], e;
|
|
64
|
+
try {
|
|
65
|
+
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
|
|
66
|
+
}
|
|
67
|
+
catch (error) { e = { error: error }; }
|
|
68
|
+
finally {
|
|
69
|
+
try {
|
|
70
|
+
if (r && !r.done && (m = i["return"])) m.call(i);
|
|
71
|
+
}
|
|
72
|
+
finally { if (e) throw e.error; }
|
|
73
|
+
}
|
|
74
|
+
return ar;
|
|
75
|
+
};
|
|
76
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
77
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
78
|
+
};
|
|
79
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
80
|
+
exports.unregisterJobsService = exports.registerJobsService = exports.tryGetJobsService = exports.getJobsService = exports.JobsService = void 0;
|
|
81
|
+
var api_1 = require("@terreno/api");
|
|
82
|
+
var luxon_1 = require("luxon");
|
|
83
|
+
var mongoose_1 = __importDefault(require("mongoose"));
|
|
84
|
+
var activeJobExecution_1 = require("./activeJobExecution");
|
|
85
|
+
var dispatchCompensation_1 = require("./dispatchCompensation");
|
|
86
|
+
var job_1 = require("./models/job");
|
|
87
|
+
var jobSchedule_1 = require("./models/jobSchedule");
|
|
88
|
+
var retryBackoff_1 = require("./retryBackoff");
|
|
89
|
+
var scheduleCron_1 = require("./scheduleCron");
|
|
90
|
+
var scheduler_1 = require("./scheduler");
|
|
91
|
+
var DEFAULT_MAX_ATTEMPTS = 5;
|
|
92
|
+
var DEFAULT_TIMEZONE = "UTC";
|
|
93
|
+
var UNKNOWN_JOB_NAME_TITLE = "Unknown job name";
|
|
94
|
+
var isDuplicateKeyError = function (error) {
|
|
95
|
+
return Boolean(error &&
|
|
96
|
+
typeof error === "object" &&
|
|
97
|
+
"code" in error &&
|
|
98
|
+
error.code === 11000);
|
|
99
|
+
};
|
|
100
|
+
var JobsService = /** @class */ (function () {
|
|
101
|
+
function JobsService(options) {
|
|
102
|
+
var _a, _b;
|
|
103
|
+
this.definitions = new Map();
|
|
104
|
+
this.schedulesDirty = false;
|
|
105
|
+
this.defaultTimezone = (_a = options === null || options === void 0 ? void 0 : options.defaultTimezone) !== null && _a !== void 0 ? _a : DEFAULT_TIMEZONE;
|
|
106
|
+
this.lockTtlMs = (_b = options === null || options === void 0 ? void 0 : options.lockTtlMs) !== null && _b !== void 0 ? _b : 15 * 60 * 1000;
|
|
107
|
+
this.runner = options === null || options === void 0 ? void 0 : options.runner;
|
|
108
|
+
}
|
|
109
|
+
JobsService.prototype.define = function (name, definition) {
|
|
110
|
+
if (definition.schedule) {
|
|
111
|
+
(0, scheduleCron_1.validateScheduleDefinition)({
|
|
112
|
+
cron: definition.schedule.cron,
|
|
113
|
+
defaultTimezone: this.defaultTimezone,
|
|
114
|
+
scheduleTimezone: definition.schedule.timezone,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
this.definitions.set(name, definition);
|
|
118
|
+
};
|
|
119
|
+
JobsService.prototype.getDefinition = function (name) {
|
|
120
|
+
return this.definitions.get(name);
|
|
121
|
+
};
|
|
122
|
+
JobsService.prototype.getDefaultTimezone = function () {
|
|
123
|
+
return this.defaultTimezone;
|
|
124
|
+
};
|
|
125
|
+
JobsService.prototype.getLockTtlMs = function () {
|
|
126
|
+
return this.lockTtlMs;
|
|
127
|
+
};
|
|
128
|
+
JobsService.prototype.isSchedulesDirty = function () {
|
|
129
|
+
return this.schedulesDirty;
|
|
130
|
+
};
|
|
131
|
+
JobsService.prototype.markSchedulesDirty = function () {
|
|
132
|
+
this.schedulesDirty = true;
|
|
133
|
+
};
|
|
134
|
+
JobsService.prototype.reconcileSchedulesIfDirty = function () {
|
|
135
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
136
|
+
return __generator(this, function (_a) {
|
|
137
|
+
switch (_a.label) {
|
|
138
|
+
case 0:
|
|
139
|
+
if (!this.schedulesDirty) {
|
|
140
|
+
return [2 /*return*/];
|
|
141
|
+
}
|
|
142
|
+
return [4 /*yield*/, this.reconcileSchedules()];
|
|
143
|
+
case 1:
|
|
144
|
+
_a.sent();
|
|
145
|
+
return [2 /*return*/];
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
JobsService.prototype.reconcileSchedules = function () {
|
|
151
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
152
|
+
var _a, _b, _c, name_1, definition, timezone, cron, existing, cronTimezoneChanged, update, e_1_1;
|
|
153
|
+
var e_1, _d;
|
|
154
|
+
return __generator(this, function (_e) {
|
|
155
|
+
switch (_e.label) {
|
|
156
|
+
case 0:
|
|
157
|
+
_e.trys.push([0, 8, 9, 10]);
|
|
158
|
+
_a = __values(this.definitions), _b = _a.next();
|
|
159
|
+
_e.label = 1;
|
|
160
|
+
case 1:
|
|
161
|
+
if (!!_b.done) return [3 /*break*/, 7];
|
|
162
|
+
_c = __read(_b.value, 2), name_1 = _c[0], definition = _c[1];
|
|
163
|
+
if (!definition.schedule) {
|
|
164
|
+
return [3 /*break*/, 6];
|
|
165
|
+
}
|
|
166
|
+
timezone = (0, scheduleCron_1.resolveScheduleTimezone)({
|
|
167
|
+
defaultTimezone: this.defaultTimezone,
|
|
168
|
+
scheduleTimezone: definition.schedule.timezone,
|
|
169
|
+
});
|
|
170
|
+
cron = definition.schedule.cron;
|
|
171
|
+
return [4 /*yield*/, jobSchedule_1.JobSchedule.findOneOrNone({ name: name_1 })];
|
|
172
|
+
case 2:
|
|
173
|
+
existing = _e.sent();
|
|
174
|
+
if (!!existing) return [3 /*break*/, 4];
|
|
175
|
+
return [4 /*yield*/, jobSchedule_1.JobSchedule.findOneAndUpdate({ name: name_1 }, {
|
|
176
|
+
$setOnInsert: {
|
|
177
|
+
cron: cron,
|
|
178
|
+
enabled: true,
|
|
179
|
+
handlerName: name_1,
|
|
180
|
+
nextRunAt: (0, scheduleCron_1.computeInitialNextRunAt)(cron, timezone),
|
|
181
|
+
timezone: timezone,
|
|
182
|
+
},
|
|
183
|
+
}, { upsert: true })];
|
|
184
|
+
case 3:
|
|
185
|
+
_e.sent();
|
|
186
|
+
return [3 /*break*/, 6];
|
|
187
|
+
case 4:
|
|
188
|
+
cronTimezoneChanged = existing.cron !== cron || existing.timezone !== timezone;
|
|
189
|
+
update = {
|
|
190
|
+
cron: cron,
|
|
191
|
+
handlerName: name_1,
|
|
192
|
+
timezone: timezone,
|
|
193
|
+
};
|
|
194
|
+
if (cronTimezoneChanged) {
|
|
195
|
+
update.nextRunAt = (0, scheduleCron_1.computeInitialNextRunAt)(cron, timezone);
|
|
196
|
+
}
|
|
197
|
+
return [4 /*yield*/, jobSchedule_1.JobSchedule.updateOne({ _id: existing._id }, { $set: update })];
|
|
198
|
+
case 5:
|
|
199
|
+
_e.sent();
|
|
200
|
+
_e.label = 6;
|
|
201
|
+
case 6:
|
|
202
|
+
_b = _a.next();
|
|
203
|
+
return [3 /*break*/, 1];
|
|
204
|
+
case 7: return [3 /*break*/, 10];
|
|
205
|
+
case 8:
|
|
206
|
+
e_1_1 = _e.sent();
|
|
207
|
+
e_1 = { error: e_1_1 };
|
|
208
|
+
return [3 /*break*/, 10];
|
|
209
|
+
case 9:
|
|
210
|
+
try {
|
|
211
|
+
if (_b && !_b.done && (_d = _a.return)) _d.call(_a);
|
|
212
|
+
}
|
|
213
|
+
finally { if (e_1) throw e_1.error; }
|
|
214
|
+
return [7 /*endfinally*/];
|
|
215
|
+
case 10:
|
|
216
|
+
this.schedulesDirty = false;
|
|
217
|
+
return [2 /*return*/];
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
JobsService.prototype.tickSchedules = function (now) {
|
|
223
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
224
|
+
return __generator(this, function (_a) {
|
|
225
|
+
switch (_a.label) {
|
|
226
|
+
case 0: return [4 /*yield*/, (0, scheduler_1.tickDueSchedules)({
|
|
227
|
+
jobsService: this,
|
|
228
|
+
now: now,
|
|
229
|
+
})];
|
|
230
|
+
case 1:
|
|
231
|
+
_a.sent();
|
|
232
|
+
return [2 /*return*/];
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
JobsService.prototype.enqueue = function (params) {
|
|
238
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
239
|
+
var definition, runAt, maxAttempts, backoffMs, maxBackoffMs, idempotencyKey, existing, createPayload, job, error_1;
|
|
240
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
241
|
+
return __generator(this, function (_j) {
|
|
242
|
+
switch (_j.label) {
|
|
243
|
+
case 0:
|
|
244
|
+
definition = this.definitions.get(params.name);
|
|
245
|
+
if (!definition) {
|
|
246
|
+
throw new api_1.APIError({ status: 400, title: UNKNOWN_JOB_NAME_TITLE });
|
|
247
|
+
}
|
|
248
|
+
runAt = (_a = params.runAt) !== null && _a !== void 0 ? _a : luxon_1.DateTime.utc().toJSDate();
|
|
249
|
+
maxAttempts = (_c = (_b = definition.retry) === null || _b === void 0 ? void 0 : _b.maxAttempts) !== null && _c !== void 0 ? _c : DEFAULT_MAX_ATTEMPTS;
|
|
250
|
+
backoffMs = (_e = (_d = definition.retry) === null || _d === void 0 ? void 0 : _d.backoffMs) !== null && _e !== void 0 ? _e : retryBackoff_1.DEFAULT_BACKOFF_MS;
|
|
251
|
+
maxBackoffMs = (_g = (_f = definition.retry) === null || _f === void 0 ? void 0 : _f.maxBackoffMs) !== null && _g !== void 0 ? _g : retryBackoff_1.DEFAULT_MAX_BACKOFF_MS;
|
|
252
|
+
idempotencyKey = (_h = params.idempotencyKey) === null || _h === void 0 ? void 0 : _h.trim();
|
|
253
|
+
if (!idempotencyKey) return [3 /*break*/, 2];
|
|
254
|
+
return [4 /*yield*/, job_1.Job.findOneOrNone({
|
|
255
|
+
idempotencyKey: idempotencyKey,
|
|
256
|
+
name: params.name,
|
|
257
|
+
})];
|
|
258
|
+
case 1:
|
|
259
|
+
existing = _j.sent();
|
|
260
|
+
if (existing) {
|
|
261
|
+
return [2 /*return*/, existing];
|
|
262
|
+
}
|
|
263
|
+
_j.label = 2;
|
|
264
|
+
case 2:
|
|
265
|
+
createPayload = __assign(__assign({ attemptCount: 0, backoffMs: backoffMs, maxAttempts: maxAttempts, maxBackoffMs: maxBackoffMs, name: params.name, payload: params.payload, payloadRedacted: false, runAt: runAt, status: "pending" }, (idempotencyKey ? { idempotencyKey: idempotencyKey } : {})), (params.scheduleId ? { scheduleId: new mongoose_1.default.Types.ObjectId(params.scheduleId) } : {}));
|
|
266
|
+
_j.label = 3;
|
|
267
|
+
case 3:
|
|
268
|
+
_j.trys.push([3, 5, , 6]);
|
|
269
|
+
return [4 /*yield*/, job_1.Job.create(createPayload)];
|
|
270
|
+
case 4:
|
|
271
|
+
job = _j.sent();
|
|
272
|
+
return [3 /*break*/, 6];
|
|
273
|
+
case 5:
|
|
274
|
+
error_1 = _j.sent();
|
|
275
|
+
if (!idempotencyKey || !isDuplicateKeyError(error_1)) {
|
|
276
|
+
throw error_1;
|
|
277
|
+
}
|
|
278
|
+
return [2 /*return*/, job_1.Job.findExactlyOne({
|
|
279
|
+
idempotencyKey: idempotencyKey,
|
|
280
|
+
name: params.name,
|
|
281
|
+
})];
|
|
282
|
+
case 6:
|
|
283
|
+
if (!this.runner) return [3 /*break*/, 8];
|
|
284
|
+
return [4 /*yield*/, (0, dispatchCompensation_1.dispatchJobToRunner)(this.runner, job)];
|
|
285
|
+
case 7:
|
|
286
|
+
_j.sent();
|
|
287
|
+
_j.label = 8;
|
|
288
|
+
case 8: return [2 /*return*/, job];
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
JobsService.prototype.adminRetryJob = function (jobId) {
|
|
294
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
295
|
+
var original, definition, now, retry, linked, current;
|
|
296
|
+
return __generator(this, function (_a) {
|
|
297
|
+
switch (_a.label) {
|
|
298
|
+
case 0:
|
|
299
|
+
if (!mongoose_1.default.isValidObjectId(jobId)) {
|
|
300
|
+
throw new api_1.NotFoundError("Job not found");
|
|
301
|
+
}
|
|
302
|
+
return [4 /*yield*/, job_1.Job.findOneOrNone({ _id: jobId })];
|
|
303
|
+
case 1:
|
|
304
|
+
original = _a.sent();
|
|
305
|
+
if (!original) {
|
|
306
|
+
throw new api_1.NotFoundError("Job not found");
|
|
307
|
+
}
|
|
308
|
+
if (original.retriedById) {
|
|
309
|
+
throw new api_1.APIError({ status: 409, title: "Job already retried" });
|
|
310
|
+
}
|
|
311
|
+
if (original.status !== "dead" && original.status !== "failed") {
|
|
312
|
+
throw new api_1.APIError({
|
|
313
|
+
detail: "Status is ".concat(original.status),
|
|
314
|
+
status: 400,
|
|
315
|
+
title: "Job cannot be retried",
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
definition = this.definitions.get(original.name);
|
|
319
|
+
if (!definition) {
|
|
320
|
+
throw new api_1.APIError({ status: 400, title: UNKNOWN_JOB_NAME_TITLE });
|
|
321
|
+
}
|
|
322
|
+
now = luxon_1.DateTime.utc().toJSDate();
|
|
323
|
+
return [4 /*yield*/, job_1.Job.create({
|
|
324
|
+
attemptCount: 0,
|
|
325
|
+
attempts: [],
|
|
326
|
+
backoffMs: original.backoffMs,
|
|
327
|
+
maxAttempts: original.maxAttempts,
|
|
328
|
+
maxBackoffMs: original.maxBackoffMs,
|
|
329
|
+
name: original.name,
|
|
330
|
+
payload: original.payload,
|
|
331
|
+
payloadRedacted: original.payloadRedacted,
|
|
332
|
+
retriedFromId: original._id,
|
|
333
|
+
runAt: now,
|
|
334
|
+
scheduleId: original.scheduleId,
|
|
335
|
+
status: "pending",
|
|
336
|
+
})];
|
|
337
|
+
case 2:
|
|
338
|
+
retry = _a.sent();
|
|
339
|
+
return [4 /*yield*/, job_1.Job.findOneAndUpdate({
|
|
340
|
+
_id: original._id,
|
|
341
|
+
retriedById: { $exists: false },
|
|
342
|
+
status: { $in: ["dead", "failed"] },
|
|
343
|
+
}, { $set: { retriedById: retry._id } }, { returnDocument: "after" })];
|
|
344
|
+
case 3:
|
|
345
|
+
linked = _a.sent();
|
|
346
|
+
if (!!linked) return [3 /*break*/, 6];
|
|
347
|
+
return [4 /*yield*/, job_1.Job.deleteOne({ _id: retry._id })];
|
|
348
|
+
case 4:
|
|
349
|
+
_a.sent();
|
|
350
|
+
return [4 /*yield*/, job_1.Job.findOneOrNone({ _id: original._id })];
|
|
351
|
+
case 5:
|
|
352
|
+
current = _a.sent();
|
|
353
|
+
if (current === null || current === void 0 ? void 0 : current.retriedById) {
|
|
354
|
+
throw new api_1.APIError({ status: 409, title: "Job already retried" });
|
|
355
|
+
}
|
|
356
|
+
throw new api_1.APIError({ status: 409, title: "Job state changed" });
|
|
357
|
+
case 6:
|
|
358
|
+
if (!this.runner) return [3 /*break*/, 8];
|
|
359
|
+
return [4 /*yield*/, (0, dispatchCompensation_1.dispatchJobToRunner)(this.runner, retry)];
|
|
360
|
+
case 7:
|
|
361
|
+
_a.sent();
|
|
362
|
+
_a.label = 8;
|
|
363
|
+
case 8: return [2 /*return*/, { original: linked, retry: retry }];
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
};
|
|
368
|
+
JobsService.prototype.adminRequeueJob = function (jobId) {
|
|
369
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
370
|
+
var now, requeued, existing;
|
|
371
|
+
return __generator(this, function (_a) {
|
|
372
|
+
switch (_a.label) {
|
|
373
|
+
case 0:
|
|
374
|
+
if (!mongoose_1.default.isValidObjectId(jobId)) {
|
|
375
|
+
throw new api_1.NotFoundError("Job not found");
|
|
376
|
+
}
|
|
377
|
+
now = luxon_1.DateTime.utc().toJSDate();
|
|
378
|
+
return [4 /*yield*/, job_1.Job.findOneAndUpdate({
|
|
379
|
+
_id: jobId,
|
|
380
|
+
retriedById: { $exists: false },
|
|
381
|
+
status: { $in: ["cancelled", "dead", "failed"] },
|
|
382
|
+
}, {
|
|
383
|
+
$set: { runAt: now, status: "pending" },
|
|
384
|
+
$unset: { lockedAt: "", lockedBy: "" },
|
|
385
|
+
}, { returnDocument: "after" })];
|
|
386
|
+
case 1:
|
|
387
|
+
requeued = _a.sent();
|
|
388
|
+
if (!!requeued) return [3 /*break*/, 3];
|
|
389
|
+
return [4 /*yield*/, job_1.Job.findOneOrNone({ _id: jobId })];
|
|
390
|
+
case 2:
|
|
391
|
+
existing = _a.sent();
|
|
392
|
+
if (!existing) {
|
|
393
|
+
throw new api_1.NotFoundError("Job not found");
|
|
394
|
+
}
|
|
395
|
+
if (existing.retriedById) {
|
|
396
|
+
throw new api_1.APIError({ status: 409, title: "Job already retried" });
|
|
397
|
+
}
|
|
398
|
+
throw new api_1.APIError({
|
|
399
|
+
detail: "Status is ".concat(existing.status),
|
|
400
|
+
status: 400,
|
|
401
|
+
title: "Job cannot be requeued",
|
|
402
|
+
});
|
|
403
|
+
case 3:
|
|
404
|
+
if (!this.runner) return [3 /*break*/, 5];
|
|
405
|
+
return [4 /*yield*/, (0, dispatchCompensation_1.dispatchJobToRunner)(this.runner, requeued)];
|
|
406
|
+
case 4:
|
|
407
|
+
_a.sent();
|
|
408
|
+
_a.label = 5;
|
|
409
|
+
case 5: return [2 /*return*/, requeued];
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
};
|
|
414
|
+
JobsService.prototype.adminCancelJob = function (jobId) {
|
|
415
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
416
|
+
var cancelled, existing;
|
|
417
|
+
return __generator(this, function (_a) {
|
|
418
|
+
switch (_a.label) {
|
|
419
|
+
case 0:
|
|
420
|
+
if (!mongoose_1.default.isValidObjectId(jobId)) {
|
|
421
|
+
throw new api_1.NotFoundError("Job not found");
|
|
422
|
+
}
|
|
423
|
+
return [4 /*yield*/, job_1.Job.findOneAndUpdate({
|
|
424
|
+
_id: jobId,
|
|
425
|
+
status: { $in: ["pending", "running", "scheduled"] },
|
|
426
|
+
}, {
|
|
427
|
+
$set: { status: "cancelled" },
|
|
428
|
+
$unset: { lockedAt: "", lockedBy: "" },
|
|
429
|
+
}, { returnDocument: "after" })];
|
|
430
|
+
case 1:
|
|
431
|
+
cancelled = _a.sent();
|
|
432
|
+
if (!!cancelled) return [3 /*break*/, 3];
|
|
433
|
+
return [4 /*yield*/, job_1.Job.findOneOrNone({ _id: jobId })];
|
|
434
|
+
case 2:
|
|
435
|
+
existing = _a.sent();
|
|
436
|
+
if (!existing) {
|
|
437
|
+
throw new api_1.NotFoundError("Job not found");
|
|
438
|
+
}
|
|
439
|
+
throw new api_1.APIError({
|
|
440
|
+
detail: "Status is ".concat(existing.status),
|
|
441
|
+
status: 400,
|
|
442
|
+
title: "Job cannot be cancelled",
|
|
443
|
+
});
|
|
444
|
+
case 3:
|
|
445
|
+
(0, activeJobExecution_1.abortActiveJobExecution)(jobId);
|
|
446
|
+
return [2 /*return*/, cancelled];
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
};
|
|
451
|
+
JobsService.prototype.pauseSchedule = function (name) {
|
|
452
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
453
|
+
var updated;
|
|
454
|
+
return __generator(this, function (_a) {
|
|
455
|
+
switch (_a.label) {
|
|
456
|
+
case 0: return [4 /*yield*/, jobSchedule_1.JobSchedule.findOneAndUpdate({ name: name }, { $set: { enabled: false } }, { returnDocument: "after" })];
|
|
457
|
+
case 1:
|
|
458
|
+
updated = _a.sent();
|
|
459
|
+
if (!updated) {
|
|
460
|
+
throw new api_1.NotFoundError("Schedule not found");
|
|
461
|
+
}
|
|
462
|
+
return [2 /*return*/, updated];
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
};
|
|
467
|
+
JobsService.prototype.resumeSchedule = function (name) {
|
|
468
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
469
|
+
var updated;
|
|
470
|
+
return __generator(this, function (_a) {
|
|
471
|
+
switch (_a.label) {
|
|
472
|
+
case 0: return [4 /*yield*/, jobSchedule_1.JobSchedule.findOneAndUpdate({ name: name }, { $set: { enabled: true } }, { returnDocument: "after" })];
|
|
473
|
+
case 1:
|
|
474
|
+
updated = _a.sent();
|
|
475
|
+
if (!updated) {
|
|
476
|
+
throw new api_1.NotFoundError("Schedule not found");
|
|
477
|
+
}
|
|
478
|
+
return [2 /*return*/, updated];
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
};
|
|
483
|
+
return JobsService;
|
|
484
|
+
}());
|
|
485
|
+
exports.JobsService = JobsService;
|
|
486
|
+
var registeredJobsService;
|
|
487
|
+
var getJobsService = function () {
|
|
488
|
+
if (!registeredJobsService) {
|
|
489
|
+
throw new api_1.APIError({ status: 500, title: "JobsApp is not registered" });
|
|
490
|
+
}
|
|
491
|
+
return registeredJobsService;
|
|
492
|
+
};
|
|
493
|
+
exports.getJobsService = getJobsService;
|
|
494
|
+
/** Returns the registered jobs service when `JobsApp` is mounted; otherwise `undefined`. */
|
|
495
|
+
var tryGetJobsService = function () { return registeredJobsService; };
|
|
496
|
+
exports.tryGetJobsService = tryGetJobsService;
|
|
497
|
+
var registerJobsService = function (service) {
|
|
498
|
+
registeredJobsService = service;
|
|
499
|
+
};
|
|
500
|
+
exports.registerJobsService = registerJobsService;
|
|
501
|
+
/** Clears the process-wide jobs service. Tests use this so AdminApp falls back to in-process runs. */
|
|
502
|
+
var unregisterJobsService = function () {
|
|
503
|
+
registeredJobsService = undefined;
|
|
504
|
+
};
|
|
505
|
+
exports.unregisterJobsService = unregisterJobsService;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { FindExactlyOnePlugin, FindOneOrNonePlugin } from "@terreno/api";
|
|
2
|
+
import type mongoose from "mongoose";
|
|
3
|
+
export type JobStatus = "cancelled" | "completed" | "dead" | "failed" | "pending" | "running" | "scheduled";
|
|
4
|
+
export declare const IN_FLIGHT_JOB_STATUSES: readonly JobStatus[];
|
|
5
|
+
export interface JobAttempt {
|
|
6
|
+
at: Date;
|
|
7
|
+
error?: string;
|
|
8
|
+
errorClass?: string;
|
|
9
|
+
}
|
|
10
|
+
export type JobMethods = Record<never, never>;
|
|
11
|
+
export interface JobDocument extends mongoose.Document<mongoose.Types.ObjectId>, JobMethods {
|
|
12
|
+
attemptCount: number;
|
|
13
|
+
attempts: JobAttempt[];
|
|
14
|
+
backoffMs: number;
|
|
15
|
+
created: Date;
|
|
16
|
+
idempotencyKey?: string;
|
|
17
|
+
lastError?: string;
|
|
18
|
+
lockedAt?: Date;
|
|
19
|
+
lockedBy?: string;
|
|
20
|
+
maxAttempts: number;
|
|
21
|
+
maxBackoffMs: number;
|
|
22
|
+
name: string;
|
|
23
|
+
payload?: unknown;
|
|
24
|
+
payloadRedacted: boolean;
|
|
25
|
+
retriedById?: mongoose.Types.ObjectId;
|
|
26
|
+
retriedFromId?: mongoose.Types.ObjectId;
|
|
27
|
+
runAt: Date;
|
|
28
|
+
scheduleId?: mongoose.Types.ObjectId;
|
|
29
|
+
status: JobStatus;
|
|
30
|
+
updated: Date;
|
|
31
|
+
}
|
|
32
|
+
export interface JobStatics extends FindExactlyOnePlugin<JobDocument>, FindOneOrNonePlugin<JobDocument> {
|
|
33
|
+
}
|
|
34
|
+
export interface JobModel extends mongoose.Model<JobDocument>, JobStatics {
|
|
35
|
+
}
|
|
36
|
+
export type JobSchema = mongoose.Schema<JobDocument, JobModel, JobMethods>;
|
|
37
|
+
export type JobScheduleMethods = Record<never, never>;
|
|
38
|
+
export interface JobScheduleDocument extends mongoose.Document<mongoose.Types.ObjectId>, JobScheduleMethods {
|
|
39
|
+
created: Date;
|
|
40
|
+
cron: string;
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
handlerName: string;
|
|
43
|
+
name: string;
|
|
44
|
+
nextRunAt: Date;
|
|
45
|
+
timezone: string;
|
|
46
|
+
updated: Date;
|
|
47
|
+
}
|
|
48
|
+
export interface JobScheduleStatics extends FindExactlyOnePlugin<JobScheduleDocument>, FindOneOrNonePlugin<JobScheduleDocument> {
|
|
49
|
+
}
|
|
50
|
+
export interface JobScheduleModel extends mongoose.Model<JobScheduleDocument>, JobScheduleStatics {
|
|
51
|
+
}
|
|
52
|
+
export type JobScheduleSchema = mongoose.Schema<JobScheduleDocument, JobScheduleModel, JobScheduleMethods>;
|