@platformatic/runtime 2.53.2 → 2.55.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 +20 -1
- package/lib/runtime.js +18 -1
- package/lib/scheduler.js +121 -0
- package/lib/schema.js +45 -0
- package/package.json +15 -14
- package/schema.json +83 -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 HttpsSchemasPlatformaticDevPlatformaticRuntime2550Json = {
|
|
9
9
|
[k: string]: unknown;
|
|
10
10
|
} & {
|
|
11
11
|
$schema?: string;
|
|
@@ -29,6 +29,7 @@ export type HttpsSchemasPlatformaticDevPlatformaticRuntime2532Json = {
|
|
|
29
29
|
maxELU?: number | string;
|
|
30
30
|
maxHeapUsed?: number | string;
|
|
31
31
|
maxHeapTotal?: number | string;
|
|
32
|
+
maxYoungGeneration?: number;
|
|
32
33
|
};
|
|
33
34
|
preload?: string | string[];
|
|
34
35
|
arguments?: string[];
|
|
@@ -121,6 +122,7 @@ export type HttpsSchemasPlatformaticDevPlatformaticRuntime2532Json = {
|
|
|
121
122
|
maxELU?: number | string;
|
|
122
123
|
maxHeapUsed?: number | string;
|
|
123
124
|
maxHeapTotal?: number | string;
|
|
125
|
+
maxYoungGeneration?: number;
|
|
124
126
|
};
|
|
125
127
|
undici?: {
|
|
126
128
|
agentOptions?: {
|
|
@@ -216,6 +218,23 @@ export type HttpsSchemasPlatformaticDevPlatformaticRuntime2532Json = {
|
|
|
216
218
|
[k: string]: string;
|
|
217
219
|
};
|
|
218
220
|
sourceMaps?: boolean;
|
|
221
|
+
scheduler?: {
|
|
222
|
+
enabled?: boolean | string;
|
|
223
|
+
name: string;
|
|
224
|
+
cron: string;
|
|
225
|
+
callbackUrl: string;
|
|
226
|
+
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
227
|
+
headers?: {
|
|
228
|
+
[k: string]: string;
|
|
229
|
+
};
|
|
230
|
+
body?:
|
|
231
|
+
| string
|
|
232
|
+
| {
|
|
233
|
+
[k: string]: unknown;
|
|
234
|
+
};
|
|
235
|
+
maxRetries?: number;
|
|
236
|
+
[k: string]: unknown;
|
|
237
|
+
}[];
|
|
219
238
|
};
|
|
220
239
|
|
|
221
240
|
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
|
|
|
@@ -1076,6 +1087,11 @@ class Runtime extends EventEmitter {
|
|
|
1076
1087
|
workerEnv['NODE_OPTIONS'] = `${originalNodeOptions} ${serviceConfig.nodeOptions}`.trim()
|
|
1077
1088
|
}
|
|
1078
1089
|
|
|
1090
|
+
const maxOldGenerationSizeMb = Math.floor(
|
|
1091
|
+
(health.maxYoungGeneration > 0 ? health.maxHeapTotal - health.maxYoungGeneration : health.maxHeapTotal) / (1024 * 1024)
|
|
1092
|
+
)
|
|
1093
|
+
const maxYoungGenerationSizeMb = health.maxYoungGeneration ? Math.floor(health.maxYoungGeneration / (1024 * 1024)) : undefined
|
|
1094
|
+
|
|
1079
1095
|
const worker = new Worker(kWorkerFile, {
|
|
1080
1096
|
workerData: {
|
|
1081
1097
|
config,
|
|
@@ -1097,7 +1113,8 @@ class Runtime extends EventEmitter {
|
|
|
1097
1113
|
execArgv,
|
|
1098
1114
|
env: workerEnv,
|
|
1099
1115
|
resourceLimits: {
|
|
1100
|
-
maxOldGenerationSizeMb
|
|
1116
|
+
maxOldGenerationSizeMb,
|
|
1117
|
+
maxYoungGenerationSizeMb
|
|
1101
1118
|
},
|
|
1102
1119
|
stdout: true,
|
|
1103
1120
|
stderr: true
|
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.55.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/composer": "2.
|
|
41
|
-
"@platformatic/db": "2.
|
|
42
|
-
"@platformatic/node": "2.
|
|
43
|
-
"@platformatic/sql-graphql": "2.
|
|
44
|
-
"@platformatic/
|
|
45
|
-
"@platformatic/
|
|
40
|
+
"@platformatic/composer": "2.55.0",
|
|
41
|
+
"@platformatic/db": "2.55.0",
|
|
42
|
+
"@platformatic/node": "2.55.0",
|
|
43
|
+
"@platformatic/sql-graphql": "2.55.0",
|
|
44
|
+
"@platformatic/service": "2.55.0",
|
|
45
|
+
"@platformatic/sql-mapper": "2.55.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/
|
|
82
|
-
"@platformatic/
|
|
83
|
-
"@platformatic/
|
|
84
|
-
"@platformatic/
|
|
85
|
-
"@platformatic/
|
|
80
|
+
"@platformatic/basic": "2.55.0",
|
|
81
|
+
"@platformatic/itc": "2.55.0",
|
|
82
|
+
"@platformatic/config": "2.55.0",
|
|
83
|
+
"@platformatic/generators": "2.55.0",
|
|
84
|
+
"@platformatic/telemetry": "2.55.0",
|
|
85
|
+
"@platformatic/ts-compiler": "2.55.0",
|
|
86
|
+
"@platformatic/utils": "2.55.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.55.0.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -154,6 +154,10 @@
|
|
|
154
154
|
"type": "string"
|
|
155
155
|
}
|
|
156
156
|
]
|
|
157
|
+
},
|
|
158
|
+
"maxYoungGeneration": {
|
|
159
|
+
"type": "number",
|
|
160
|
+
"minimum": 0
|
|
157
161
|
}
|
|
158
162
|
},
|
|
159
163
|
"additionalProperties": false
|
|
@@ -318,6 +322,10 @@
|
|
|
318
322
|
"type": "string"
|
|
319
323
|
}
|
|
320
324
|
]
|
|
325
|
+
},
|
|
326
|
+
"maxYoungGeneration": {
|
|
327
|
+
"type": "number",
|
|
328
|
+
"minimum": 0
|
|
321
329
|
}
|
|
322
330
|
},
|
|
323
331
|
"additionalProperties": false
|
|
@@ -547,6 +555,10 @@
|
|
|
547
555
|
"type": "string"
|
|
548
556
|
}
|
|
549
557
|
]
|
|
558
|
+
},
|
|
559
|
+
"maxYoungGeneration": {
|
|
560
|
+
"type": "number",
|
|
561
|
+
"minimum": 0
|
|
550
562
|
}
|
|
551
563
|
},
|
|
552
564
|
"additionalProperties": false
|
|
@@ -976,6 +988,10 @@
|
|
|
976
988
|
"type": "string"
|
|
977
989
|
}
|
|
978
990
|
]
|
|
991
|
+
},
|
|
992
|
+
"maxYoungGeneration": {
|
|
993
|
+
"type": "number",
|
|
994
|
+
"minimum": 0
|
|
979
995
|
}
|
|
980
996
|
},
|
|
981
997
|
"additionalProperties": false
|
|
@@ -1412,6 +1428,72 @@
|
|
|
1412
1428
|
"sourceMaps": {
|
|
1413
1429
|
"type": "boolean",
|
|
1414
1430
|
"default": false
|
|
1431
|
+
},
|
|
1432
|
+
"scheduler": {
|
|
1433
|
+
"type": "array",
|
|
1434
|
+
"items": {
|
|
1435
|
+
"type": "object",
|
|
1436
|
+
"properties": {
|
|
1437
|
+
"enabled": {
|
|
1438
|
+
"anyOf": [
|
|
1439
|
+
{
|
|
1440
|
+
"type": "boolean"
|
|
1441
|
+
},
|
|
1442
|
+
{
|
|
1443
|
+
"type": "string"
|
|
1444
|
+
}
|
|
1445
|
+
],
|
|
1446
|
+
"default": true
|
|
1447
|
+
},
|
|
1448
|
+
"name": {
|
|
1449
|
+
"type": "string"
|
|
1450
|
+
},
|
|
1451
|
+
"cron": {
|
|
1452
|
+
"type": "string"
|
|
1453
|
+
},
|
|
1454
|
+
"callbackUrl": {
|
|
1455
|
+
"type": "string"
|
|
1456
|
+
},
|
|
1457
|
+
"method": {
|
|
1458
|
+
"type": "string",
|
|
1459
|
+
"enum": [
|
|
1460
|
+
"GET",
|
|
1461
|
+
"POST",
|
|
1462
|
+
"PUT",
|
|
1463
|
+
"PATCH",
|
|
1464
|
+
"DELETE"
|
|
1465
|
+
],
|
|
1466
|
+
"default": "GET"
|
|
1467
|
+
},
|
|
1468
|
+
"headers": {
|
|
1469
|
+
"type": "object",
|
|
1470
|
+
"additionalProperties": {
|
|
1471
|
+
"type": "string"
|
|
1472
|
+
}
|
|
1473
|
+
},
|
|
1474
|
+
"body": {
|
|
1475
|
+
"anyOf": [
|
|
1476
|
+
{
|
|
1477
|
+
"type": "string"
|
|
1478
|
+
},
|
|
1479
|
+
{
|
|
1480
|
+
"type": "object",
|
|
1481
|
+
"additionalProperties": true
|
|
1482
|
+
}
|
|
1483
|
+
]
|
|
1484
|
+
},
|
|
1485
|
+
"maxRetries": {
|
|
1486
|
+
"type": "number",
|
|
1487
|
+
"minimum": 0,
|
|
1488
|
+
"default": 3
|
|
1489
|
+
}
|
|
1490
|
+
},
|
|
1491
|
+
"required": [
|
|
1492
|
+
"name",
|
|
1493
|
+
"cron",
|
|
1494
|
+
"callbackUrl"
|
|
1495
|
+
]
|
|
1496
|
+
}
|
|
1415
1497
|
}
|
|
1416
1498
|
},
|
|
1417
1499
|
"anyOf": [
|