@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,119 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.registerJobsExecuteRoute = void 0;
|
|
51
|
+
var api_1 = require("@terreno/api");
|
|
52
|
+
var jobExecutor_1 = require("../jobExecutor");
|
|
53
|
+
var JOB_NOT_FOUND_TITLE = "Job not found";
|
|
54
|
+
var readJobId = function (body) {
|
|
55
|
+
if (!body || typeof body !== "object" || !("jobId" in body)) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
var jobId = body.jobId;
|
|
59
|
+
if (typeof jobId !== "string") {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
var trimmed = jobId.trim();
|
|
63
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
64
|
+
};
|
|
65
|
+
var serializeJob = function (job) {
|
|
66
|
+
var json = job.toJSON();
|
|
67
|
+
if (json._id && typeof json._id === "object" && "toString" in json._id) {
|
|
68
|
+
return __assign(__assign({}, json), { id: json._id.toString() });
|
|
69
|
+
}
|
|
70
|
+
return json;
|
|
71
|
+
};
|
|
72
|
+
var registerJobsExecuteRoute = function (_a) {
|
|
73
|
+
var app = _a.app, options = _a.options;
|
|
74
|
+
var path = "".concat(options.basePath, "/execute");
|
|
75
|
+
app.post(path, (0, api_1.asyncHandler)(function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
|
|
76
|
+
var authorized, _a, _b, jobId, outcome;
|
|
77
|
+
return __generator(this, function (_c) {
|
|
78
|
+
switch (_c.label) {
|
|
79
|
+
case 0:
|
|
80
|
+
authorized = false;
|
|
81
|
+
_c.label = 1;
|
|
82
|
+
case 1:
|
|
83
|
+
_c.trys.push([1, 3, , 4]);
|
|
84
|
+
_a = Boolean;
|
|
85
|
+
return [4 /*yield*/, options.executeAuth(req)];
|
|
86
|
+
case 2:
|
|
87
|
+
authorized = _a.apply(void 0, [_c.sent()]);
|
|
88
|
+
return [3 /*break*/, 4];
|
|
89
|
+
case 3:
|
|
90
|
+
_b = _c.sent();
|
|
91
|
+
authorized = false;
|
|
92
|
+
return [3 /*break*/, 4];
|
|
93
|
+
case 4:
|
|
94
|
+
if (!authorized) {
|
|
95
|
+
throw new api_1.UnauthorizedError("Unauthorized");
|
|
96
|
+
}
|
|
97
|
+
jobId = readJobId(req.body);
|
|
98
|
+
if (!jobId) {
|
|
99
|
+
throw new api_1.APIError({ status: 400, title: "jobId is required" });
|
|
100
|
+
}
|
|
101
|
+
return [4 /*yield*/, (0, jobExecutor_1.executeJobById)({
|
|
102
|
+
host: options.host,
|
|
103
|
+
jobId: jobId,
|
|
104
|
+
signal: AbortSignal.timeout(30 * 60 * 1000),
|
|
105
|
+
})];
|
|
106
|
+
case 5:
|
|
107
|
+
outcome = _c.sent();
|
|
108
|
+
if (outcome.kind === "not_found") {
|
|
109
|
+
throw new api_1.NotFoundError(JOB_NOT_FOUND_TITLE);
|
|
110
|
+
}
|
|
111
|
+
if (outcome.kind === "conflict") {
|
|
112
|
+
throw new api_1.ConflictError(outcome.reason === "not_due" ? "Job is not due yet" : "Job is already running");
|
|
113
|
+
}
|
|
114
|
+
return [2 /*return*/, res.status(200).json({ data: serializeJob(outcome.job) })];
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}); }));
|
|
118
|
+
};
|
|
119
|
+
exports.registerJobsExecuteRoute = registerJobsExecuteRoute;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { JobDocument } from "../modelTypes";
|
|
2
|
+
import type { JobRunner, JobRunnerStartOptions } from "../types";
|
|
3
|
+
export interface GcpTimestamp {
|
|
4
|
+
nanos?: number;
|
|
5
|
+
seconds: number;
|
|
6
|
+
}
|
|
7
|
+
/** protobuf Duration (`google.protobuf.Duration`) — used by `@google-cloud/tasks`. */
|
|
8
|
+
export interface GcpDuration {
|
|
9
|
+
nanos?: number;
|
|
10
|
+
seconds: number;
|
|
11
|
+
}
|
|
12
|
+
export interface GcpHttpRequest {
|
|
13
|
+
body?: string;
|
|
14
|
+
headers?: Record<string, string>;
|
|
15
|
+
httpMethod: "POST";
|
|
16
|
+
oidcToken?: {
|
|
17
|
+
audience?: string;
|
|
18
|
+
serviceAccountEmail: string;
|
|
19
|
+
};
|
|
20
|
+
url: string;
|
|
21
|
+
}
|
|
22
|
+
export interface GcpTask {
|
|
23
|
+
dispatchDeadline?: GcpDuration;
|
|
24
|
+
httpRequest?: GcpHttpRequest;
|
|
25
|
+
scheduleTime?: GcpTimestamp;
|
|
26
|
+
}
|
|
27
|
+
export interface GcpCreateTaskRequest {
|
|
28
|
+
parent: string;
|
|
29
|
+
task: GcpTask;
|
|
30
|
+
}
|
|
31
|
+
export interface GcpCreateTaskResponse {
|
|
32
|
+
name?: string;
|
|
33
|
+
}
|
|
34
|
+
/** Narrow Cloud Tasks client seam — inject in tests; production loads `@google-cloud/tasks`. */
|
|
35
|
+
export interface GcpCloudTasksClient {
|
|
36
|
+
createTask(request: GcpCreateTaskRequest): Promise<[GcpCreateTaskResponse]>;
|
|
37
|
+
queuePath(project: string, location: string, queue: string): string;
|
|
38
|
+
}
|
|
39
|
+
/** Cloud Tasks HTTP `dispatchDeadline` bounds (15s–30min). Default 30min matches Cloud Run. */
|
|
40
|
+
export declare const GCP_HTTP_TASK_MIN_DISPATCH_DEADLINE_SECONDS = 15;
|
|
41
|
+
export declare const GCP_HTTP_TASK_MAX_DISPATCH_DEADLINE_SECONDS = 1800;
|
|
42
|
+
export declare const GCP_HTTP_TASK_DEFAULT_DISPATCH_DEADLINE_SECONDS = 1800;
|
|
43
|
+
export interface GcpCloudTasksRunnerConfig {
|
|
44
|
+
basePath?: string;
|
|
45
|
+
/** Injected client for tests. Production wiring loads `@google-cloud/tasks`. */
|
|
46
|
+
client?: GcpCloudTasksClient;
|
|
47
|
+
/**
|
|
48
|
+
* Seconds Cloud Tasks waits for POST /jobs/execute before retrying.
|
|
49
|
+
* HTTP range is 15–1800. Default 1800 so a 30-minute handler is not retried at 10 minutes.
|
|
50
|
+
*/
|
|
51
|
+
dispatchDeadlineSeconds?: number;
|
|
52
|
+
location: string;
|
|
53
|
+
oidcAudience?: string;
|
|
54
|
+
project: string;
|
|
55
|
+
publicUrl: string;
|
|
56
|
+
queue: string;
|
|
57
|
+
serviceAccountEmail: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Dispatch-only runner that enqueues Cloud Tasks HTTP POST callbacks to
|
|
61
|
+
* `{publicUrl}{basePath}/execute` with an OIDC token.
|
|
62
|
+
*
|
|
63
|
+
* `start()` ticks Mongo schedules so cron rows enqueue Cloud Tasks. It does not
|
|
64
|
+
* claim job rows — execution happens on POST /jobs/execute.
|
|
65
|
+
*/
|
|
66
|
+
export declare class GcpCloudTasksRunner implements JobRunner {
|
|
67
|
+
readonly id = "gcp-cloud-tasks";
|
|
68
|
+
readonly requiresExecuteRoute = true;
|
|
69
|
+
private readonly client;
|
|
70
|
+
private readonly config;
|
|
71
|
+
private pollIntervalMs;
|
|
72
|
+
private running;
|
|
73
|
+
private stopResolve;
|
|
74
|
+
constructor(config: GcpCloudTasksRunnerConfig);
|
|
75
|
+
enqueue(job: JobDocument): Promise<void>;
|
|
76
|
+
start(options: JobRunnerStartOptions): Promise<void>;
|
|
77
|
+
stop(): Promise<void>;
|
|
78
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
+
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);
|
|
13
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
+
function step(op) {
|
|
16
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
18
|
+
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;
|
|
19
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
+
switch (op[0]) {
|
|
21
|
+
case 0: case 1: t = op; break;
|
|
22
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
+
default:
|
|
26
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
+
if (t[2]) _.ops.pop();
|
|
31
|
+
_.trys.pop(); continue;
|
|
32
|
+
}
|
|
33
|
+
op = body.call(thisArg, _);
|
|
34
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.GcpCloudTasksRunner = exports.GCP_HTTP_TASK_DEFAULT_DISPATCH_DEADLINE_SECONDS = exports.GCP_HTTP_TASK_MAX_DISPATCH_DEADLINE_SECONDS = exports.GCP_HTTP_TASK_MIN_DISPATCH_DEADLINE_SECONDS = void 0;
|
|
40
|
+
var node_module_1 = require("node:module");
|
|
41
|
+
var luxon_1 = require("luxon");
|
|
42
|
+
/** Cloud Tasks HTTP `dispatchDeadline` bounds (15s–30min). Default 30min matches Cloud Run. */
|
|
43
|
+
exports.GCP_HTTP_TASK_MIN_DISPATCH_DEADLINE_SECONDS = 15;
|
|
44
|
+
exports.GCP_HTTP_TASK_MAX_DISPATCH_DEADLINE_SECONDS = 1800;
|
|
45
|
+
exports.GCP_HTTP_TASK_DEFAULT_DISPATCH_DEADLINE_SECONDS = 1800;
|
|
46
|
+
var nodeRequire = (0, node_module_1.createRequire)(__filename);
|
|
47
|
+
var requireNonEmpty = function (value, label) {
|
|
48
|
+
var trimmed = value === null || value === void 0 ? void 0 : value.trim();
|
|
49
|
+
if (!trimmed) {
|
|
50
|
+
throw new Error("GcpCloudTasksRunner requires ".concat(label));
|
|
51
|
+
}
|
|
52
|
+
return trimmed;
|
|
53
|
+
};
|
|
54
|
+
var normalizePublicUrl = function (publicUrl) {
|
|
55
|
+
var parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = new URL(publicUrl);
|
|
58
|
+
}
|
|
59
|
+
catch (_a) {
|
|
60
|
+
throw new Error("GcpCloudTasksRunner publicUrl must be an absolute http(s) URL, received: ".concat(publicUrl));
|
|
61
|
+
}
|
|
62
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
63
|
+
throw new Error("GcpCloudTasksRunner publicUrl must use http or https, received: ".concat(publicUrl));
|
|
64
|
+
}
|
|
65
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
66
|
+
};
|
|
67
|
+
var normalizeBasePath = function (basePath) {
|
|
68
|
+
var trimmed = (basePath !== null && basePath !== void 0 ? basePath : "/jobs").trim();
|
|
69
|
+
if (!trimmed.startsWith("/")) {
|
|
70
|
+
throw new Error("GcpCloudTasksRunner basePath must start with \"/\", received: ".concat(basePath));
|
|
71
|
+
}
|
|
72
|
+
return trimmed.replace(/\/+$/, "") || "/";
|
|
73
|
+
};
|
|
74
|
+
var validateDispatchDeadlineSeconds = function (dispatchDeadlineSeconds) {
|
|
75
|
+
if (dispatchDeadlineSeconds === undefined) {
|
|
76
|
+
return exports.GCP_HTTP_TASK_DEFAULT_DISPATCH_DEADLINE_SECONDS;
|
|
77
|
+
}
|
|
78
|
+
if (!Number.isFinite(dispatchDeadlineSeconds) ||
|
|
79
|
+
dispatchDeadlineSeconds < exports.GCP_HTTP_TASK_MIN_DISPATCH_DEADLINE_SECONDS ||
|
|
80
|
+
dispatchDeadlineSeconds > exports.GCP_HTTP_TASK_MAX_DISPATCH_DEADLINE_SECONDS) {
|
|
81
|
+
throw new Error("GcpCloudTasksRunner dispatchDeadlineSeconds must be between ".concat(exports.GCP_HTTP_TASK_MIN_DISPATCH_DEADLINE_SECONDS, " and ").concat(exports.GCP_HTTP_TASK_MAX_DISPATCH_DEADLINE_SECONDS, " seconds"));
|
|
82
|
+
}
|
|
83
|
+
return dispatchDeadlineSeconds;
|
|
84
|
+
};
|
|
85
|
+
var toProtobufDuration = function (dispatchDeadlineSeconds) { return ({
|
|
86
|
+
seconds: dispatchDeadlineSeconds,
|
|
87
|
+
}); };
|
|
88
|
+
var resolveConfig = function (config) {
|
|
89
|
+
var _a;
|
|
90
|
+
return ({
|
|
91
|
+
basePath: normalizeBasePath(config.basePath),
|
|
92
|
+
dispatchDeadline: toProtobufDuration(validateDispatchDeadlineSeconds(config.dispatchDeadlineSeconds)),
|
|
93
|
+
location: requireNonEmpty(config.location, "location"),
|
|
94
|
+
oidcAudience: ((_a = config.oidcAudience) === null || _a === void 0 ? void 0 : _a.trim()) || undefined,
|
|
95
|
+
project: requireNonEmpty(config.project, "project"),
|
|
96
|
+
publicUrl: normalizePublicUrl(requireNonEmpty(config.publicUrl, "publicUrl")),
|
|
97
|
+
queue: requireNonEmpty(config.queue, "queue"),
|
|
98
|
+
serviceAccountEmail: requireNonEmpty(config.serviceAccountEmail, "serviceAccountEmail"),
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
var loadCloudTasksClient = function () {
|
|
102
|
+
try {
|
|
103
|
+
var module_1 = nodeRequire("@google-cloud/tasks");
|
|
104
|
+
return module_1.CloudTasksClient;
|
|
105
|
+
}
|
|
106
|
+
catch (_a) {
|
|
107
|
+
throw new Error("GcpCloudTasksRunner requires optional peer dependency @google-cloud/tasks. " +
|
|
108
|
+
"Install it with: bun add @google-cloud/tasks");
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
var buildExecuteUrl = function (publicUrl, basePath) {
|
|
112
|
+
return "".concat(publicUrl).concat(basePath, "/execute");
|
|
113
|
+
};
|
|
114
|
+
var buildScheduleTime = function (runAt) {
|
|
115
|
+
var scheduled = luxon_1.DateTime.fromJSDate(runAt, { zone: "utc" });
|
|
116
|
+
var now = luxon_1.DateTime.utc();
|
|
117
|
+
if (scheduled <= now) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
nanos: (scheduled.toMillis() % 1000) * 1000000,
|
|
122
|
+
seconds: Math.floor(scheduled.toSeconds()),
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
var waitForAbortOrTimeout = function (signal, timeoutMs) {
|
|
126
|
+
return new Promise(function (resolve) {
|
|
127
|
+
if (signal.aborted) {
|
|
128
|
+
resolve();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
var timer = setTimeout(function () {
|
|
132
|
+
signal.removeEventListener("abort", onAbort);
|
|
133
|
+
resolve();
|
|
134
|
+
}, timeoutMs);
|
|
135
|
+
var onAbort = function () {
|
|
136
|
+
clearTimeout(timer);
|
|
137
|
+
resolve();
|
|
138
|
+
};
|
|
139
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
var DEFAULT_SCHEDULE_POLL_INTERVAL_MS = 1000;
|
|
143
|
+
/**
|
|
144
|
+
* Dispatch-only runner that enqueues Cloud Tasks HTTP POST callbacks to
|
|
145
|
+
* `{publicUrl}{basePath}/execute` with an OIDC token.
|
|
146
|
+
*
|
|
147
|
+
* `start()` ticks Mongo schedules so cron rows enqueue Cloud Tasks. It does not
|
|
148
|
+
* claim job rows — execution happens on POST /jobs/execute.
|
|
149
|
+
*/
|
|
150
|
+
var GcpCloudTasksRunner = /** @class */ (function () {
|
|
151
|
+
function GcpCloudTasksRunner(config) {
|
|
152
|
+
var _a;
|
|
153
|
+
this.id = "gcp-cloud-tasks";
|
|
154
|
+
this.requiresExecuteRoute = true;
|
|
155
|
+
this.pollIntervalMs = DEFAULT_SCHEDULE_POLL_INTERVAL_MS;
|
|
156
|
+
this.running = false;
|
|
157
|
+
this.config = resolveConfig(config);
|
|
158
|
+
this.client = (_a = config.client) !== null && _a !== void 0 ? _a : new (loadCloudTasksClient())();
|
|
159
|
+
}
|
|
160
|
+
GcpCloudTasksRunner.prototype.enqueue = function (job) {
|
|
161
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
162
|
+
var executeUrl, body, task, scheduleTime, parent;
|
|
163
|
+
var _a;
|
|
164
|
+
return __generator(this, function (_b) {
|
|
165
|
+
switch (_b.label) {
|
|
166
|
+
case 0:
|
|
167
|
+
executeUrl = buildExecuteUrl(this.config.publicUrl, this.config.basePath);
|
|
168
|
+
body = Buffer.from(JSON.stringify({ jobId: job._id.toString() })).toString("base64");
|
|
169
|
+
task = {
|
|
170
|
+
dispatchDeadline: this.config.dispatchDeadline,
|
|
171
|
+
httpRequest: {
|
|
172
|
+
body: body,
|
|
173
|
+
headers: {
|
|
174
|
+
"Content-Type": "application/json",
|
|
175
|
+
},
|
|
176
|
+
httpMethod: "POST",
|
|
177
|
+
oidcToken: {
|
|
178
|
+
audience: (_a = this.config.oidcAudience) !== null && _a !== void 0 ? _a : executeUrl,
|
|
179
|
+
serviceAccountEmail: this.config.serviceAccountEmail,
|
|
180
|
+
},
|
|
181
|
+
url: executeUrl,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
scheduleTime = buildScheduleTime(job.runAt);
|
|
185
|
+
if (scheduleTime) {
|
|
186
|
+
task.scheduleTime = scheduleTime;
|
|
187
|
+
}
|
|
188
|
+
parent = this.client.queuePath(this.config.project, this.config.location, this.config.queue);
|
|
189
|
+
return [4 /*yield*/, this.client.createTask({ parent: parent, task: task })];
|
|
190
|
+
case 1:
|
|
191
|
+
_b.sent();
|
|
192
|
+
return [2 /*return*/];
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
GcpCloudTasksRunner.prototype.start = function (options) {
|
|
198
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
199
|
+
var _a, _b;
|
|
200
|
+
return __generator(this, function (_c) {
|
|
201
|
+
switch (_c.label) {
|
|
202
|
+
case 0:
|
|
203
|
+
this.pollIntervalMs = (_a = options.pollIntervalMs) !== null && _a !== void 0 ? _a : DEFAULT_SCHEDULE_POLL_INTERVAL_MS;
|
|
204
|
+
this.running = true;
|
|
205
|
+
_c.label = 1;
|
|
206
|
+
case 1:
|
|
207
|
+
_c.trys.push([1, , 7, 8]);
|
|
208
|
+
_c.label = 2;
|
|
209
|
+
case 2:
|
|
210
|
+
if (!!options.signal.aborted) return [3 /*break*/, 6];
|
|
211
|
+
return [4 /*yield*/, options.jobs.reconcileSchedulesIfDirty()];
|
|
212
|
+
case 3:
|
|
213
|
+
_c.sent();
|
|
214
|
+
return [4 /*yield*/, options.jobs.tickSchedules()];
|
|
215
|
+
case 4:
|
|
216
|
+
_c.sent();
|
|
217
|
+
if (options.signal.aborted) {
|
|
218
|
+
return [3 /*break*/, 6];
|
|
219
|
+
}
|
|
220
|
+
return [4 /*yield*/, waitForAbortOrTimeout(options.signal, this.pollIntervalMs)];
|
|
221
|
+
case 5:
|
|
222
|
+
_c.sent();
|
|
223
|
+
return [3 /*break*/, 2];
|
|
224
|
+
case 6: return [3 /*break*/, 8];
|
|
225
|
+
case 7:
|
|
226
|
+
this.running = false;
|
|
227
|
+
(_b = this.stopResolve) === null || _b === void 0 ? void 0 : _b.call(this);
|
|
228
|
+
this.stopResolve = undefined;
|
|
229
|
+
return [7 /*endfinally*/];
|
|
230
|
+
case 8: return [2 /*return*/];
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
};
|
|
235
|
+
GcpCloudTasksRunner.prototype.stop = function () {
|
|
236
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
237
|
+
var _this = this;
|
|
238
|
+
return __generator(this, function (_a) {
|
|
239
|
+
switch (_a.label) {
|
|
240
|
+
case 0:
|
|
241
|
+
if (!this.running) {
|
|
242
|
+
return [2 /*return*/];
|
|
243
|
+
}
|
|
244
|
+
return [4 /*yield*/, new Promise(function (resolve) {
|
|
245
|
+
_this.stopResolve = resolve;
|
|
246
|
+
})];
|
|
247
|
+
case 1:
|
|
248
|
+
_a.sent();
|
|
249
|
+
return [2 /*return*/];
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
return GcpCloudTasksRunner;
|
|
255
|
+
}());
|
|
256
|
+
exports.GcpCloudTasksRunner = GcpCloudTasksRunner;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { JobDocument } from "../modelTypes";
|
|
2
|
+
import type { JobRunner, JobRunnerStartOptions } from "../types";
|
|
3
|
+
/** @internal */
|
|
4
|
+
export { getWorkerId, hasWorkerIdPrefix } from "../claimLock";
|
|
5
|
+
export declare class MongoJobRunner implements JobRunner {
|
|
6
|
+
readonly id = "mongo";
|
|
7
|
+
private pollIntervalMs;
|
|
8
|
+
private running;
|
|
9
|
+
private stopResolve;
|
|
10
|
+
enqueue(_job: JobDocument): Promise<void>;
|
|
11
|
+
start(options: JobRunnerStartOptions): Promise<void>;
|
|
12
|
+
stop(): Promise<void>;
|
|
13
|
+
private processNext;
|
|
14
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
+
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);
|
|
13
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
+
function step(op) {
|
|
16
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
18
|
+
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;
|
|
19
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
+
switch (op[0]) {
|
|
21
|
+
case 0: case 1: t = op; break;
|
|
22
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
+
default:
|
|
26
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
+
if (t[2]) _.ops.pop();
|
|
31
|
+
_.trys.pop(); continue;
|
|
32
|
+
}
|
|
33
|
+
op = body.call(thisArg, _);
|
|
34
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.MongoJobRunner = exports.hasWorkerIdPrefix = exports.getWorkerId = void 0;
|
|
40
|
+
var luxon_1 = require("luxon");
|
|
41
|
+
var claimLock_1 = require("../claimLock");
|
|
42
|
+
var jobExecutor_1 = require("../jobExecutor");
|
|
43
|
+
var job_1 = require("../models/job");
|
|
44
|
+
var DEFAULT_POLL_INTERVAL_MS = 1000;
|
|
45
|
+
/** @internal */
|
|
46
|
+
var claimLock_2 = require("../claimLock");
|
|
47
|
+
Object.defineProperty(exports, "getWorkerId", { enumerable: true, get: function () { return claimLock_2.getWorkerId; } });
|
|
48
|
+
Object.defineProperty(exports, "hasWorkerIdPrefix", { enumerable: true, get: function () { return claimLock_2.hasWorkerIdPrefix; } });
|
|
49
|
+
var buildLockExpiry = function (lockTtlMs) {
|
|
50
|
+
return luxon_1.DateTime.utc().minus({ milliseconds: lockTtlMs }).toJSDate();
|
|
51
|
+
};
|
|
52
|
+
var waitForAbortOrTimeout = function (signal, timeoutMs) {
|
|
53
|
+
return new Promise(function (resolve) {
|
|
54
|
+
if (signal.aborted) {
|
|
55
|
+
resolve();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
var timer = setTimeout(function () {
|
|
59
|
+
signal.removeEventListener("abort", onAbort);
|
|
60
|
+
resolve();
|
|
61
|
+
}, timeoutMs);
|
|
62
|
+
var onAbort = function () {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
resolve();
|
|
65
|
+
};
|
|
66
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
var readClaimLock = function (job) {
|
|
70
|
+
if (!job.lockedAt || !job.lockedBy) {
|
|
71
|
+
throw new Error("Job ".concat(job._id.toString(), " is missing claim lock metadata"));
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
lockedAt: job.lockedAt,
|
|
75
|
+
lockedBy: job.lockedBy,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
var MongoJobRunner = /** @class */ (function () {
|
|
79
|
+
function MongoJobRunner() {
|
|
80
|
+
this.id = "mongo";
|
|
81
|
+
this.pollIntervalMs = DEFAULT_POLL_INTERVAL_MS;
|
|
82
|
+
this.running = false;
|
|
83
|
+
}
|
|
84
|
+
MongoJobRunner.prototype.enqueue = function (_job) {
|
|
85
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
86
|
+
return __generator(this, function (_a) {
|
|
87
|
+
return [2 /*return*/];
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
MongoJobRunner.prototype.start = function (options) {
|
|
92
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
93
|
+
var processedAny, processed;
|
|
94
|
+
var _a, _b;
|
|
95
|
+
return __generator(this, function (_c) {
|
|
96
|
+
switch (_c.label) {
|
|
97
|
+
case 0:
|
|
98
|
+
this.pollIntervalMs = (_a = options.pollIntervalMs) !== null && _a !== void 0 ? _a : DEFAULT_POLL_INTERVAL_MS;
|
|
99
|
+
this.running = true;
|
|
100
|
+
_c.label = 1;
|
|
101
|
+
case 1:
|
|
102
|
+
_c.trys.push([1, , 11, 12]);
|
|
103
|
+
_c.label = 2;
|
|
104
|
+
case 2:
|
|
105
|
+
if (!!options.signal.aborted) return [3 /*break*/, 10];
|
|
106
|
+
return [4 /*yield*/, options.jobs.reconcileSchedulesIfDirty()];
|
|
107
|
+
case 3:
|
|
108
|
+
_c.sent();
|
|
109
|
+
return [4 /*yield*/, options.jobs.tickSchedules()];
|
|
110
|
+
case 4:
|
|
111
|
+
_c.sent();
|
|
112
|
+
processedAny = false;
|
|
113
|
+
_c.label = 5;
|
|
114
|
+
case 5:
|
|
115
|
+
if (!!options.signal.aborted) return [3 /*break*/, 7];
|
|
116
|
+
return [4 /*yield*/, this.processNext(options)];
|
|
117
|
+
case 6:
|
|
118
|
+
processed = _c.sent();
|
|
119
|
+
if (!processed) {
|
|
120
|
+
return [3 /*break*/, 7];
|
|
121
|
+
}
|
|
122
|
+
processedAny = true;
|
|
123
|
+
return [3 /*break*/, 5];
|
|
124
|
+
case 7:
|
|
125
|
+
if (options.signal.aborted) {
|
|
126
|
+
return [3 /*break*/, 10];
|
|
127
|
+
}
|
|
128
|
+
if (!!processedAny) return [3 /*break*/, 9];
|
|
129
|
+
return [4 /*yield*/, waitForAbortOrTimeout(options.signal, this.pollIntervalMs)];
|
|
130
|
+
case 8:
|
|
131
|
+
_c.sent();
|
|
132
|
+
_c.label = 9;
|
|
133
|
+
case 9: return [3 /*break*/, 2];
|
|
134
|
+
case 10: return [3 /*break*/, 12];
|
|
135
|
+
case 11:
|
|
136
|
+
this.running = false;
|
|
137
|
+
(_b = this.stopResolve) === null || _b === void 0 ? void 0 : _b.call(this);
|
|
138
|
+
this.stopResolve = undefined;
|
|
139
|
+
return [7 /*endfinally*/];
|
|
140
|
+
case 12: return [2 /*return*/];
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
MongoJobRunner.prototype.stop = function () {
|
|
146
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
147
|
+
var _this = this;
|
|
148
|
+
return __generator(this, function (_a) {
|
|
149
|
+
switch (_a.label) {
|
|
150
|
+
case 0:
|
|
151
|
+
if (!this.running) {
|
|
152
|
+
return [2 /*return*/];
|
|
153
|
+
}
|
|
154
|
+
return [4 /*yield*/, new Promise(function (resolve) {
|
|
155
|
+
_this.stopResolve = resolve;
|
|
156
|
+
})];
|
|
157
|
+
case 1:
|
|
158
|
+
_a.sent();
|
|
159
|
+
return [2 /*return*/];
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
MongoJobRunner.prototype.processNext = function (options) {
|
|
165
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
166
|
+
var now, lockExpiry, claim, claimed;
|
|
167
|
+
return __generator(this, function (_a) {
|
|
168
|
+
switch (_a.label) {
|
|
169
|
+
case 0:
|
|
170
|
+
now = luxon_1.DateTime.utc().toJSDate();
|
|
171
|
+
lockExpiry = buildLockExpiry(options.jobs.getLockTtlMs());
|
|
172
|
+
claim = (0, claimLock_1.createClaimLock)(now);
|
|
173
|
+
return [4 /*yield*/, job_1.Job.findOneAndUpdate({
|
|
174
|
+
$or: [{ lockedAt: { $exists: false } }, { lockedAt: null }, { lockedAt: { $lte: lockExpiry } }],
|
|
175
|
+
runAt: { $lte: now },
|
|
176
|
+
status: { $in: ["pending", "running", "scheduled"] },
|
|
177
|
+
}, {
|
|
178
|
+
$set: {
|
|
179
|
+
lockedAt: claim.lockedAt,
|
|
180
|
+
lockedBy: claim.lockedBy,
|
|
181
|
+
status: "running",
|
|
182
|
+
},
|
|
183
|
+
}, { returnDocument: "after", sort: { runAt: 1 } })];
|
|
184
|
+
case 1:
|
|
185
|
+
claimed = _a.sent();
|
|
186
|
+
if (!claimed) {
|
|
187
|
+
return [2 /*return*/, false];
|
|
188
|
+
}
|
|
189
|
+
return [4 /*yield*/, (0, jobExecutor_1.runClaimedJob)({
|
|
190
|
+
claim: readClaimLock(claimed),
|
|
191
|
+
host: options.jobs,
|
|
192
|
+
job: claimed,
|
|
193
|
+
signal: options.signal,
|
|
194
|
+
})];
|
|
195
|
+
case 2:
|
|
196
|
+
_a.sent();
|
|
197
|
+
return [2 /*return*/, true];
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
return MongoJobRunner;
|
|
203
|
+
}());
|
|
204
|
+
exports.MongoJobRunner = MongoJobRunner;
|