@platformatic/runtime 2.53.2 → 2.54.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/config.d.ts +18 -1
- package/lib/runtime.js +11 -0
- package/lib/scheduler.js +121 -0
- package/lib/schema.js +45 -0
- package/package.json +15 -14
- package/schema.json +67 -1
package/config.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* and run json-schema-to-typescript to regenerate this file.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
export type
|
|
8
|
+
export type HttpsSchemasPlatformaticDevPlatformaticRuntime2540Json = {
|
|
9
9
|
[k: string]: unknown;
|
|
10
10
|
} & {
|
|
11
11
|
$schema?: string;
|
|
@@ -216,6 +216,23 @@ export type HttpsSchemasPlatformaticDevPlatformaticRuntime2532Json = {
|
|
|
216
216
|
[k: string]: string;
|
|
217
217
|
};
|
|
218
218
|
sourceMaps?: boolean;
|
|
219
|
+
scheduler?: {
|
|
220
|
+
enabled?: boolean | string;
|
|
221
|
+
name: string;
|
|
222
|
+
cron: string;
|
|
223
|
+
callbackUrl: string;
|
|
224
|
+
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
225
|
+
headers?: {
|
|
226
|
+
[k: string]: string;
|
|
227
|
+
};
|
|
228
|
+
body?:
|
|
229
|
+
| string
|
|
230
|
+
| {
|
|
231
|
+
[k: string]: unknown;
|
|
232
|
+
};
|
|
233
|
+
maxRetries?: number;
|
|
234
|
+
[k: string]: unknown;
|
|
235
|
+
}[];
|
|
219
236
|
};
|
|
220
237
|
|
|
221
238
|
export interface UndiciInterceptor {
|
package/lib/runtime.js
CHANGED
|
@@ -18,6 +18,7 @@ const errors = require('./errors')
|
|
|
18
18
|
const { createLogger } = require('./logger')
|
|
19
19
|
const { startManagementApi } = require('./management-api')
|
|
20
20
|
const { startPrometheusServer } = require('./prom-server')
|
|
21
|
+
const { startScheduler } = require('./scheduler')
|
|
21
22
|
const { createSharedStore } = require('./shared-http-cache')
|
|
22
23
|
const { getRuntimeTmpDir } = require('./utils')
|
|
23
24
|
const { sendViaITC, waitEventFromITC } = require('./worker/itc')
|
|
@@ -74,6 +75,7 @@ class Runtime extends EventEmitter {
|
|
|
74
75
|
#restartingWorkers
|
|
75
76
|
#sharedHttpCache
|
|
76
77
|
servicesConfigsPatches
|
|
78
|
+
#scheduler
|
|
77
79
|
|
|
78
80
|
constructor (configManager, runtimeLogsDir, env) {
|
|
79
81
|
super()
|
|
@@ -199,6 +201,10 @@ class Runtime extends EventEmitter {
|
|
|
199
201
|
|
|
200
202
|
this.#dispatcher = new Agent(dispatcherOpts).compose(interceptors)
|
|
201
203
|
|
|
204
|
+
if (config.scheduler) {
|
|
205
|
+
this.#scheduler = startScheduler(config.scheduler, this.#dispatcher, logger)
|
|
206
|
+
}
|
|
207
|
+
|
|
202
208
|
this.#updateStatus('init')
|
|
203
209
|
}
|
|
204
210
|
|
|
@@ -263,6 +269,10 @@ class Runtime extends EventEmitter {
|
|
|
263
269
|
}
|
|
264
270
|
|
|
265
271
|
async stop (silent = false) {
|
|
272
|
+
if (this.#scheduler) {
|
|
273
|
+
await this.#scheduler.stop()
|
|
274
|
+
}
|
|
275
|
+
|
|
266
276
|
if (this.#status === 'starting') {
|
|
267
277
|
await once(this, 'started')
|
|
268
278
|
}
|
|
@@ -289,6 +299,7 @@ class Runtime extends EventEmitter {
|
|
|
289
299
|
}
|
|
290
300
|
|
|
291
301
|
await this.#meshInterceptor.close()
|
|
302
|
+
|
|
292
303
|
this.#updateStatus('stopped')
|
|
293
304
|
}
|
|
294
305
|
|
package/lib/scheduler.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { CronJob, validateCronExpression } = require('cron')
|
|
4
|
+
const { setTimeout } = require('node:timers/promises')
|
|
5
|
+
const { request } = require('undici')
|
|
6
|
+
|
|
7
|
+
class SchedulerService {
|
|
8
|
+
constructor (schedulerConfig, dispatcher, logger) {
|
|
9
|
+
this.logger = logger
|
|
10
|
+
this.jobsConfig = []
|
|
11
|
+
this.cronJobs = []
|
|
12
|
+
this.dispatcher = dispatcher
|
|
13
|
+
this.validateCronSchedulers(schedulerConfig)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
validateCronSchedulers (schedulerConfig) {
|
|
17
|
+
for (const config of schedulerConfig) {
|
|
18
|
+
// Skip disabled schedulers
|
|
19
|
+
if (config.enabled === false) {
|
|
20
|
+
continue
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Validate cron expression
|
|
24
|
+
const validation = validateCronExpression(config.cron)
|
|
25
|
+
if (!validation.valid) {
|
|
26
|
+
throw new Error(`Invalid cron expression "${config.cron}" for scheduler "${config.name}"`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Set defaults for optional fields
|
|
30
|
+
const job = {
|
|
31
|
+
...config,
|
|
32
|
+
headers: config.headers || {},
|
|
33
|
+
body: config.body || {},
|
|
34
|
+
maxRetries: config.maxRetries || 3
|
|
35
|
+
}
|
|
36
|
+
this.jobsConfig.push(job)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
start () {
|
|
41
|
+
for (const job of this.jobsConfig) {
|
|
42
|
+
this.logger.info(`Configuring scheduler "${job.name}" with cron "${job.cron}"`)
|
|
43
|
+
const cronJob = CronJob.from({
|
|
44
|
+
cronTime: job.cron,
|
|
45
|
+
onTick: async () => {
|
|
46
|
+
this.logger.info(`Executing scheduler "${job.name}"`)
|
|
47
|
+
// This cannot throw, the try/catch is inside
|
|
48
|
+
await this.executeCallback(job)
|
|
49
|
+
},
|
|
50
|
+
start: true,
|
|
51
|
+
timeZone: 'UTC',
|
|
52
|
+
waitForCompletion: true,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
this.cronJobs.push(cronJob)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async stop () {
|
|
60
|
+
for (const job of this.cronJobs) {
|
|
61
|
+
await job.stop()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async executeCallback (scheduler) {
|
|
66
|
+
let attempt = 0
|
|
67
|
+
let success = false
|
|
68
|
+
|
|
69
|
+
while (!success && attempt < scheduler.maxRetries) {
|
|
70
|
+
try {
|
|
71
|
+
const delay = attempt > 0 ? 100 * Math.pow(2, attempt) : 0
|
|
72
|
+
|
|
73
|
+
if (delay > 0) {
|
|
74
|
+
this.logger.info(`Retrying scheduler "${scheduler.name}" in ${delay}ms (attempt ${attempt + 1}/${scheduler.maxRetries})`)
|
|
75
|
+
await setTimeout(delay)
|
|
76
|
+
}
|
|
77
|
+
const headers = {
|
|
78
|
+
'x-retry-attempt': attempt + 1,
|
|
79
|
+
...scheduler.headers
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const bodyString = typeof scheduler.body === 'string' ? scheduler.body : JSON.stringify(scheduler.body)
|
|
83
|
+
const response = await request(scheduler.callbackUrl, {
|
|
84
|
+
method: scheduler.method,
|
|
85
|
+
headers,
|
|
86
|
+
body: bodyString,
|
|
87
|
+
dispatcher: this.dispatcher
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Consumes the body, but we are not interested in the body content,
|
|
91
|
+
// we don't save it anywere, so we just dump it
|
|
92
|
+
await response.body.dump()
|
|
93
|
+
|
|
94
|
+
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
95
|
+
this.logger.info(`Scheduler "${scheduler.name}" executed successfully`)
|
|
96
|
+
success = true
|
|
97
|
+
} else {
|
|
98
|
+
throw new Error(`HTTP error ${response.statusCode}`)
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
this.logger.error(`Error executing scheduler "${scheduler.name}" (attempt ${attempt + 1}/${scheduler.maxRetries}):`, error.message)
|
|
102
|
+
attempt++
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!success) {
|
|
107
|
+
this.logger.error(`Scheduler "${scheduler.name}" failed after ${scheduler.maxRetries} attempts`)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const startScheduler = (config, interceptors, logger) => {
|
|
113
|
+
const schedulerService = new SchedulerService(config, interceptors, logger)
|
|
114
|
+
schedulerService.start()
|
|
115
|
+
return schedulerService
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = {
|
|
119
|
+
startScheduler,
|
|
120
|
+
SchedulerService
|
|
121
|
+
}
|
package/lib/schema.js
CHANGED
|
@@ -478,6 +478,51 @@ const platformaticRuntimeSchema = {
|
|
|
478
478
|
sourceMaps: {
|
|
479
479
|
type: 'boolean',
|
|
480
480
|
default: false
|
|
481
|
+
},
|
|
482
|
+
scheduler: {
|
|
483
|
+
type: 'array',
|
|
484
|
+
items: {
|
|
485
|
+
type: 'object',
|
|
486
|
+
properties: {
|
|
487
|
+
enabled: {
|
|
488
|
+
anyOf: [{
|
|
489
|
+
type: 'boolean'
|
|
490
|
+
}, {
|
|
491
|
+
type: 'string'
|
|
492
|
+
}],
|
|
493
|
+
default: true
|
|
494
|
+
},
|
|
495
|
+
name: {
|
|
496
|
+
type: 'string'
|
|
497
|
+
},
|
|
498
|
+
cron: {
|
|
499
|
+
type: 'string'
|
|
500
|
+
},
|
|
501
|
+
callbackUrl: {
|
|
502
|
+
type: 'string'
|
|
503
|
+
},
|
|
504
|
+
method: {
|
|
505
|
+
type: 'string',
|
|
506
|
+
enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
|
507
|
+
default: 'GET'
|
|
508
|
+
},
|
|
509
|
+
headers: {
|
|
510
|
+
type: 'object',
|
|
511
|
+
additionalProperties: {
|
|
512
|
+
type: 'string'
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
body: {
|
|
516
|
+
anyOf: [{ type: 'string' }, { type: 'object', additionalProperties: true }]
|
|
517
|
+
},
|
|
518
|
+
maxRetries: {
|
|
519
|
+
type: 'number',
|
|
520
|
+
minimum: 0,
|
|
521
|
+
default: 3
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
required: ['name', 'cron', 'callbackUrl']
|
|
525
|
+
}
|
|
481
526
|
}
|
|
482
527
|
},
|
|
483
528
|
anyOf: [{ required: ['autoload'] }, { required: ['services'] }, { required: ['web'] }],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/runtime",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.54.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -37,12 +37,12 @@
|
|
|
37
37
|
"typescript": "^5.5.4",
|
|
38
38
|
"undici-oidc-interceptor": "^0.5.0",
|
|
39
39
|
"why-is-node-running": "^2.2.2",
|
|
40
|
-
"@platformatic/
|
|
41
|
-
"@platformatic/
|
|
42
|
-
"@platformatic/node": "2.
|
|
43
|
-
"@platformatic/
|
|
44
|
-
"@platformatic/sql-
|
|
45
|
-
"@platformatic/
|
|
40
|
+
"@platformatic/db": "2.54.0",
|
|
41
|
+
"@platformatic/composer": "2.54.0",
|
|
42
|
+
"@platformatic/node": "2.54.0",
|
|
43
|
+
"@platformatic/service": "2.54.0",
|
|
44
|
+
"@platformatic/sql-graphql": "2.54.0",
|
|
45
|
+
"@platformatic/sql-mapper": "2.54.0"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@fastify/accepts": "^5.0.0",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"change-case-all": "^2.1.0",
|
|
57
57
|
"close-with-grace": "^2.0.0",
|
|
58
58
|
"commist": "^3.2.0",
|
|
59
|
+
"cron": "^4.1.0",
|
|
59
60
|
"debounce": "^2.0.0",
|
|
60
61
|
"desm": "^1.3.1",
|
|
61
62
|
"dotenv": "^16.4.5",
|
|
@@ -76,13 +77,13 @@
|
|
|
76
77
|
"undici": "^7.0.0",
|
|
77
78
|
"undici-thread-interceptor": "^0.13.1",
|
|
78
79
|
"ws": "^8.16.0",
|
|
79
|
-
"@platformatic/basic": "2.
|
|
80
|
-
"@platformatic/
|
|
81
|
-
"@platformatic/itc": "2.
|
|
82
|
-
"@platformatic/
|
|
83
|
-
"@platformatic/
|
|
84
|
-
"@platformatic/
|
|
85
|
-
"@platformatic/
|
|
80
|
+
"@platformatic/basic": "2.54.0",
|
|
81
|
+
"@platformatic/generators": "2.54.0",
|
|
82
|
+
"@platformatic/itc": "2.54.0",
|
|
83
|
+
"@platformatic/config": "2.54.0",
|
|
84
|
+
"@platformatic/telemetry": "2.54.0",
|
|
85
|
+
"@platformatic/ts-compiler": "2.54.0",
|
|
86
|
+
"@platformatic/utils": "2.54.0"
|
|
86
87
|
},
|
|
87
88
|
"scripts": {
|
|
88
89
|
"test": "npm run lint && borp --concurrency=1 --timeout=300000 && tsd",
|
package/schema.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.
|
|
2
|
+
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.54.0.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1412,6 +1412,72 @@
|
|
|
1412
1412
|
"sourceMaps": {
|
|
1413
1413
|
"type": "boolean",
|
|
1414
1414
|
"default": false
|
|
1415
|
+
},
|
|
1416
|
+
"scheduler": {
|
|
1417
|
+
"type": "array",
|
|
1418
|
+
"items": {
|
|
1419
|
+
"type": "object",
|
|
1420
|
+
"properties": {
|
|
1421
|
+
"enabled": {
|
|
1422
|
+
"anyOf": [
|
|
1423
|
+
{
|
|
1424
|
+
"type": "boolean"
|
|
1425
|
+
},
|
|
1426
|
+
{
|
|
1427
|
+
"type": "string"
|
|
1428
|
+
}
|
|
1429
|
+
],
|
|
1430
|
+
"default": true
|
|
1431
|
+
},
|
|
1432
|
+
"name": {
|
|
1433
|
+
"type": "string"
|
|
1434
|
+
},
|
|
1435
|
+
"cron": {
|
|
1436
|
+
"type": "string"
|
|
1437
|
+
},
|
|
1438
|
+
"callbackUrl": {
|
|
1439
|
+
"type": "string"
|
|
1440
|
+
},
|
|
1441
|
+
"method": {
|
|
1442
|
+
"type": "string",
|
|
1443
|
+
"enum": [
|
|
1444
|
+
"GET",
|
|
1445
|
+
"POST",
|
|
1446
|
+
"PUT",
|
|
1447
|
+
"PATCH",
|
|
1448
|
+
"DELETE"
|
|
1449
|
+
],
|
|
1450
|
+
"default": "GET"
|
|
1451
|
+
},
|
|
1452
|
+
"headers": {
|
|
1453
|
+
"type": "object",
|
|
1454
|
+
"additionalProperties": {
|
|
1455
|
+
"type": "string"
|
|
1456
|
+
}
|
|
1457
|
+
},
|
|
1458
|
+
"body": {
|
|
1459
|
+
"anyOf": [
|
|
1460
|
+
{
|
|
1461
|
+
"type": "string"
|
|
1462
|
+
},
|
|
1463
|
+
{
|
|
1464
|
+
"type": "object",
|
|
1465
|
+
"additionalProperties": true
|
|
1466
|
+
}
|
|
1467
|
+
]
|
|
1468
|
+
},
|
|
1469
|
+
"maxRetries": {
|
|
1470
|
+
"type": "number",
|
|
1471
|
+
"minimum": 0,
|
|
1472
|
+
"default": 3
|
|
1473
|
+
}
|
|
1474
|
+
},
|
|
1475
|
+
"required": [
|
|
1476
|
+
"name",
|
|
1477
|
+
"cron",
|
|
1478
|
+
"callbackUrl"
|
|
1479
|
+
]
|
|
1480
|
+
}
|
|
1415
1481
|
}
|
|
1416
1482
|
},
|
|
1417
1483
|
"anyOf": [
|