@balena/pinejs 16.1.0-build-joshbwlng-tasks-82a48d2f7c281892020e59c514b2775690dcabed-1 → 16.1.0-build-partial-unique-index-constraints-c664148d85c67b645954adc929c778a7ce81115f-2
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/.pinejs-cache.json +1 -1
- package/.versionbot/CHANGELOG.yml +175 -6
- package/CHANGELOG.md +67 -2
- package/out/config-loader/env.d.ts +0 -4
- package/out/config-loader/env.js +1 -5
- package/out/config-loader/env.js.map +1 -1
- package/out/migrator/async.js +5 -4
- package/out/migrator/async.js.map +1 -1
- package/out/migrator/utils.d.ts +4 -4
- package/out/migrator/utils.js +9 -1
- package/out/migrator/utils.js.map +1 -1
- package/out/sbvr-api/sbvr-utils.d.ts +0 -1
- package/out/sbvr-api/sbvr-utils.js +37 -24
- package/out/sbvr-api/sbvr-utils.js.map +1 -1
- package/out/server-glue/module.d.ts +0 -1
- package/out/server-glue/module.js +1 -2
- package/out/server-glue/module.js.map +1 -1
- package/package.json +4 -7
- package/src/config-loader/env.ts +1 -6
- package/src/migrator/async.ts +5 -4
- package/src/migrator/utils.ts +26 -5
- package/src/sbvr-api/sbvr-utils.ts +53 -27
- package/src/server-glue/module.ts +0 -1
- package/out/sbvr-api/tasks.d.ts +0 -44
- package/out/sbvr-api/tasks.js +0 -245
- package/out/sbvr-api/tasks.js.map +0 -1
- package/out/sbvr-api/tasks.sbvr +0 -56
- package/src/sbvr-api/tasks.sbvr +0 -56
- package/src/sbvr-api/tasks.ts +0 -378
package/src/sbvr-api/tasks.ts
DELETED
@@ -1,378 +0,0 @@
|
|
1
|
-
import Ajv from 'ajv';
|
2
|
-
import type { Schema, ValidateFunction } from 'ajv';
|
3
|
-
import * as cronParser from 'cron-parser';
|
4
|
-
import type { AnyObject } from 'pinejs-client-core';
|
5
|
-
|
6
|
-
import { tasks as tasksEnv } from '../config-loader/env';
|
7
|
-
import type { Tx } from '../database-layer/db';
|
8
|
-
import { BadRequestError } from './errors';
|
9
|
-
import { addPureHook } from './hooks';
|
10
|
-
import * as permissions from './permissions';
|
11
|
-
import type { ExecutableModel } from './sbvr-utils';
|
12
|
-
import { PinejsClient } from './sbvr-utils';
|
13
|
-
import { sbvrUtils } from '../server-glue/module';
|
14
|
-
|
15
|
-
export const apiRoot = 'tasks';
|
16
|
-
|
17
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
18
|
-
const modelText: string = require(`./${apiRoot}.sbvr`);
|
19
|
-
|
20
|
-
const handlers: {
|
21
|
-
[name: string]: TaskHandler;
|
22
|
-
} = {};
|
23
|
-
|
24
|
-
export const taskStatuses = ['pending', 'cancelled', 'success', 'failed'];
|
25
|
-
export interface Task {
|
26
|
-
id: number;
|
27
|
-
created_at: Date;
|
28
|
-
modified_at: Date;
|
29
|
-
is_created_by__actor: number;
|
30
|
-
is_executed_by__handler: string;
|
31
|
-
is_executed_with__parameter_set: object | null;
|
32
|
-
is_scheduled_with__cron_expression: string | null;
|
33
|
-
is_scheduled_to_execute_on__time: Date | null;
|
34
|
-
priority: number;
|
35
|
-
status: (typeof taskStatuses)[number];
|
36
|
-
started_on__time: Date | null;
|
37
|
-
ended_on__time: Date | null;
|
38
|
-
error_message: string | null;
|
39
|
-
attempt_count: number;
|
40
|
-
attempt_limit: number;
|
41
|
-
}
|
42
|
-
|
43
|
-
type PartialTask = Pick<
|
44
|
-
Task,
|
45
|
-
| 'id'
|
46
|
-
| 'is_created_by__actor'
|
47
|
-
| 'is_executed_by__handler'
|
48
|
-
| 'is_executed_with__parameter_set'
|
49
|
-
| 'is_scheduled_with__cron_expression'
|
50
|
-
| 'priority'
|
51
|
-
| 'attempt_count'
|
52
|
-
| 'attempt_limit'
|
53
|
-
>;
|
54
|
-
|
55
|
-
interface TaskArgs {
|
56
|
-
api: PinejsClient;
|
57
|
-
params: AnyObject;
|
58
|
-
tx: Tx;
|
59
|
-
}
|
60
|
-
|
61
|
-
type TaskResponse = Promise<{
|
62
|
-
status: (typeof taskStatuses)[number];
|
63
|
-
error?: string;
|
64
|
-
}>;
|
65
|
-
|
66
|
-
export interface TaskHandler {
|
67
|
-
name: string;
|
68
|
-
fn: (options: TaskArgs) => TaskResponse;
|
69
|
-
validate?: ValidateFunction;
|
70
|
-
}
|
71
|
-
|
72
|
-
// Parse a cron expression
|
73
|
-
function parseCron(cron: string): Date {
|
74
|
-
return cronParser.parseExpression(cron).next().toDate();
|
75
|
-
}
|
76
|
-
|
77
|
-
export const config = {
|
78
|
-
models: [
|
79
|
-
{
|
80
|
-
apiRoot,
|
81
|
-
modelText,
|
82
|
-
customServerCode: exports,
|
83
|
-
migrations: {},
|
84
|
-
},
|
85
|
-
] as ExecutableModel[],
|
86
|
-
};
|
87
|
-
|
88
|
-
// Setting inlineRefs=false as without it we run into a
|
89
|
-
// "Maximum call stack size exceeded" error apprarently caused
|
90
|
-
// by String.prototype._uncountable_words being set in sbvr-parser?
|
91
|
-
const ajv = new Ajv({
|
92
|
-
inlineRefs: false,
|
93
|
-
});
|
94
|
-
|
95
|
-
export const setup = async () => {
|
96
|
-
addPureHook('POST', apiRoot, 'task', {
|
97
|
-
POSTPARSE: async ({ req, request }) => {
|
98
|
-
// Set the actor
|
99
|
-
request.values.is_created_by__actor =
|
100
|
-
req.user?.actor ?? req.apiKey?.actor;
|
101
|
-
if (request.values.is_created_by__actor == null) {
|
102
|
-
throw new BadRequestError(
|
103
|
-
'Creating tasks with missing actor on req is not allowed',
|
104
|
-
);
|
105
|
-
}
|
106
|
-
|
107
|
-
// Set defaults
|
108
|
-
request.values.status = 'pending';
|
109
|
-
request.values.attempt_count = 0;
|
110
|
-
request.values.priority ??= 1;
|
111
|
-
request.values.attempt_limit ??= 1;
|
112
|
-
|
113
|
-
// Set scheduled start time using cron expression if provided
|
114
|
-
if (
|
115
|
-
request.values.is_scheduled_with__cron_expression != null &&
|
116
|
-
request.values.is_scheduled_to_execute_on__time == null
|
117
|
-
) {
|
118
|
-
try {
|
119
|
-
request.values.is_scheduled_to_execute_on__time = parseCron(
|
120
|
-
request.values.is_scheduled_with__cron_expression,
|
121
|
-
).toISOString();
|
122
|
-
} catch (_) {
|
123
|
-
throw new BadRequestError(
|
124
|
-
`Invalid cron expression: ${request.values.is_scheduled_with__cron_expression}`,
|
125
|
-
);
|
126
|
-
}
|
127
|
-
}
|
128
|
-
|
129
|
-
// Assert that the provided start time is far enough in the future
|
130
|
-
if (request.values.is_scheduled_to_execute_on__time != null) {
|
131
|
-
const now = new Date(new Date().getTime() + tasksEnv.queueIntervalMS);
|
132
|
-
const startTime = new Date(
|
133
|
-
request.values.is_scheduled_to_execute_on__time,
|
134
|
-
);
|
135
|
-
if (startTime < now) {
|
136
|
-
throw new BadRequestError(
|
137
|
-
`Task scheduled start time must be greater than ${tasksEnv.queueIntervalMS} milliseconds in the future`,
|
138
|
-
);
|
139
|
-
}
|
140
|
-
}
|
141
|
-
|
142
|
-
// Assert that the requested handler exists
|
143
|
-
const handlerName = request.values.is_executed_by__handler;
|
144
|
-
if (handlerName == null) {
|
145
|
-
throw new BadRequestError(`Must specify a task handler to execute`);
|
146
|
-
}
|
147
|
-
const handler = handlers[handlerName];
|
148
|
-
if (handler == null) {
|
149
|
-
throw new BadRequestError(
|
150
|
-
`No task handler with name '${handlerName}' registered`,
|
151
|
-
);
|
152
|
-
}
|
153
|
-
|
154
|
-
// Assert that the provided parameter set is valid
|
155
|
-
if (handler.validate != null) {
|
156
|
-
if (!handler.validate(request.values.is_executed_with__parameter_set)) {
|
157
|
-
throw new BadRequestError(
|
158
|
-
`Invalid parameter set: ${ajv.errorsText(handler.validate.errors)}`,
|
159
|
-
);
|
160
|
-
}
|
161
|
-
}
|
162
|
-
},
|
163
|
-
});
|
164
|
-
|
165
|
-
// Start the worker if possible
|
166
|
-
if (tasksEnv.queueConcurrency > 0 && tasksEnv.queueIntervalMS >= 1000) {
|
167
|
-
watch();
|
168
|
-
}
|
169
|
-
};
|
170
|
-
|
171
|
-
// Register a task handler
|
172
|
-
export function addTaskHandler(
|
173
|
-
name: string,
|
174
|
-
fn: TaskHandler['fn'],
|
175
|
-
schema?: Schema,
|
176
|
-
): void {
|
177
|
-
if (handlers[name] != null) {
|
178
|
-
throw new Error(`Task handler with name '${name}' already registered`);
|
179
|
-
}
|
180
|
-
handlers[name] = {
|
181
|
-
name,
|
182
|
-
fn,
|
183
|
-
validate: schema != null ? ajv.compile(schema) : undefined,
|
184
|
-
};
|
185
|
-
}
|
186
|
-
|
187
|
-
// Calculate next attempt datetime for a task that has failed using exponential backoff
|
188
|
-
function getNextAttemptTime(attempt: number): Date | null {
|
189
|
-
const millisecondsInFuture =
|
190
|
-
Math.ceil(Math.exp(Math.min(10, attempt))) * 1000;
|
191
|
-
return new Date(Date.now() + millisecondsInFuture);
|
192
|
-
}
|
193
|
-
|
194
|
-
// Watch for new tasks to execute
|
195
|
-
let executing = 0;
|
196
|
-
function watch(): void {
|
197
|
-
const client = new PinejsClient({
|
198
|
-
apiPrefix: `/${apiRoot}/`,
|
199
|
-
});
|
200
|
-
|
201
|
-
setInterval(async () => {
|
202
|
-
// Do nothing if there are no handlers or if we are already at the concurrency limit
|
203
|
-
if (
|
204
|
-
Object.keys(handlers).length === 0 ||
|
205
|
-
executing >= tasksEnv.queueConcurrency
|
206
|
-
) {
|
207
|
-
return;
|
208
|
-
}
|
209
|
-
|
210
|
-
try {
|
211
|
-
await sbvrUtils.db.transaction(async (tx) => {
|
212
|
-
const names = Object.keys(handlers);
|
213
|
-
const binds = names.map((_, index) => `$${index + 1}`).join(', ');
|
214
|
-
const result = await tx.executeSql(
|
215
|
-
`
|
216
|
-
SELECT
|
217
|
-
t."id",
|
218
|
-
t."is executed by-handler" AS is_executed_by__handler,
|
219
|
-
t."is executed with-parameter set" AS is_executed_with__parameter_set,
|
220
|
-
t."is scheduled with-cron expression" AS is_scheduled_with__cron_expression,
|
221
|
-
t."attempt count" AS attempt_count,
|
222
|
-
t."attempt limit" AS attempt_limit,
|
223
|
-
t."priority" AS priority,
|
224
|
-
t."is created by-actor" AS is_created_by__actor
|
225
|
-
FROM
|
226
|
-
task AS t
|
227
|
-
WHERE
|
228
|
-
t."is executed by-handler" IN (${binds}) AND
|
229
|
-
t."status" = 'pending' AND
|
230
|
-
t."attempt count" <= t."attempt limit" AND
|
231
|
-
(
|
232
|
-
t."is scheduled to execute on-time" IS NULL OR
|
233
|
-
t."is scheduled to execute on-time" <= CURRENT_TIMESTAMP + INTERVAL '${Math.ceil(tasksEnv.queueIntervalMS / 1000)} second'
|
234
|
-
)
|
235
|
-
ORDER BY
|
236
|
-
t."is scheduled to execute on-time" ASC,
|
237
|
-
t."priority" DESC,
|
238
|
-
t."id" ASC
|
239
|
-
LIMIT 1
|
240
|
-
FOR UPDATE
|
241
|
-
SKIP LOCKED
|
242
|
-
`,
|
243
|
-
names,
|
244
|
-
);
|
245
|
-
if (result.rows.length > 0) {
|
246
|
-
executing++;
|
247
|
-
await execute(client, result.rows[0] as PartialTask, tx);
|
248
|
-
executing--;
|
249
|
-
}
|
250
|
-
});
|
251
|
-
} catch (err: unknown) {
|
252
|
-
console.error('Failed polling for tasks:', err);
|
253
|
-
}
|
254
|
-
}, tasksEnv.queueIntervalMS);
|
255
|
-
}
|
256
|
-
|
257
|
-
// Execute a task
|
258
|
-
async function execute(
|
259
|
-
client: PinejsClient,
|
260
|
-
task: PartialTask,
|
261
|
-
tx: Tx,
|
262
|
-
): Promise<void> {
|
263
|
-
try {
|
264
|
-
// Get the handler
|
265
|
-
const handler = handlers[task.is_executed_by__handler];
|
266
|
-
const startedOnTime = new Date();
|
267
|
-
if (handler == null) {
|
268
|
-
await update(
|
269
|
-
client,
|
270
|
-
tx,
|
271
|
-
task,
|
272
|
-
startedOnTime,
|
273
|
-
'failed',
|
274
|
-
'Matching task handler not found',
|
275
|
-
);
|
276
|
-
return;
|
277
|
-
}
|
278
|
-
|
279
|
-
// Validate parameters before execution so we can fail early if
|
280
|
-
// the parameter set is invalid. This can happen if the handler
|
281
|
-
// definition changes after a task is added to the queue.
|
282
|
-
if (
|
283
|
-
handler.validate != null &&
|
284
|
-
!handler.validate(task.is_executed_with__parameter_set)
|
285
|
-
) {
|
286
|
-
await update(
|
287
|
-
client,
|
288
|
-
tx,
|
289
|
-
task,
|
290
|
-
startedOnTime,
|
291
|
-
'failed',
|
292
|
-
`Invalid parameter set: ${ajv.errorsText(handler.validate.errors)}`,
|
293
|
-
);
|
294
|
-
return;
|
295
|
-
}
|
296
|
-
|
297
|
-
// Execute the handler
|
298
|
-
const result = await handler.fn({
|
299
|
-
api: new PinejsClient({
|
300
|
-
passthrough: {
|
301
|
-
tx,
|
302
|
-
},
|
303
|
-
}),
|
304
|
-
params: task.is_executed_with__parameter_set ?? {},
|
305
|
-
tx,
|
306
|
-
});
|
307
|
-
|
308
|
-
// Update the task with the results
|
309
|
-
await update(client, tx, task, startedOnTime, result.status, result.error);
|
310
|
-
} catch (err: unknown) {
|
311
|
-
// This shouldn't normally happen, but if it does, we want to log it and kill the process
|
312
|
-
console.error('Task execution failed:', err);
|
313
|
-
process.exit(1);
|
314
|
-
}
|
315
|
-
}
|
316
|
-
|
317
|
-
// Update a task
|
318
|
-
async function update(
|
319
|
-
client: PinejsClient,
|
320
|
-
tx: Tx,
|
321
|
-
task: PartialTask,
|
322
|
-
startedOnTime: Date,
|
323
|
-
status: string,
|
324
|
-
errorMessage?: string,
|
325
|
-
): Promise<void> {
|
326
|
-
const attemptCount = task.attempt_count + 1;
|
327
|
-
const body: AnyObject = {
|
328
|
-
started_on__time: startedOnTime,
|
329
|
-
ended_on__time: new Date(),
|
330
|
-
status,
|
331
|
-
attempt_count: attemptCount,
|
332
|
-
...(errorMessage != null && { error_message: errorMessage }),
|
333
|
-
};
|
334
|
-
|
335
|
-
// Re-enqueue if the task failed but has retries left, remember that
|
336
|
-
// executionCount includes the initial attempt while retryLimit does not
|
337
|
-
if (status === 'failed' && attemptCount < task.attempt_limit) {
|
338
|
-
body.status = 'pending';
|
339
|
-
|
340
|
-
// Schedule next attempt using exponential backoff
|
341
|
-
body.is_scheduled_to_execute_on__time = getNextAttemptTime(attemptCount);
|
342
|
-
}
|
343
|
-
|
344
|
-
// Patch current task
|
345
|
-
await client.patch({
|
346
|
-
resource: 'task',
|
347
|
-
passthrough: {
|
348
|
-
tx,
|
349
|
-
req: permissions.root,
|
350
|
-
},
|
351
|
-
id: task.id,
|
352
|
-
body,
|
353
|
-
});
|
354
|
-
|
355
|
-
// Create new task with same configuration if previous
|
356
|
-
// iteration completed and has a cron expression
|
357
|
-
if (
|
358
|
-
['failed', 'success'].includes(body.status) &&
|
359
|
-
task.is_scheduled_with__cron_expression != null
|
360
|
-
) {
|
361
|
-
await client.post({
|
362
|
-
resource: 'task',
|
363
|
-
passthrough: {
|
364
|
-
tx,
|
365
|
-
req: permissions.root,
|
366
|
-
},
|
367
|
-
body: {
|
368
|
-
attempt_limit: task.attempt_limit,
|
369
|
-
is_created_by__actor: task.is_created_by__actor,
|
370
|
-
is_executed_by__handler: task.is_executed_by__handler,
|
371
|
-
is_executed_with__parameter_set: task.is_executed_with__parameter_set,
|
372
|
-
is_scheduled_with__cron_expression:
|
373
|
-
task.is_scheduled_with__cron_expression,
|
374
|
-
priority: task.priority,
|
375
|
-
},
|
376
|
-
});
|
377
|
-
}
|
378
|
-
}
|