@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 CHANGED
@@ -5,7 +5,7 @@
5
5
  * and run json-schema-to-typescript to regenerate this file.
6
6
  */
7
7
 
8
- export type HttpsSchemasPlatformaticDevPlatformaticRuntime2532Json = {
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: health.maxHeapTotal
1116
+ maxOldGenerationSizeMb,
1117
+ maxYoungGenerationSizeMb
1101
1118
  },
1102
1119
  stdout: true,
1103
1120
  stderr: true
@@ -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.53.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.53.2",
41
- "@platformatic/db": "2.53.2",
42
- "@platformatic/node": "2.53.2",
43
- "@platformatic/sql-graphql": "2.53.2",
44
- "@platformatic/sql-mapper": "2.53.2",
45
- "@platformatic/service": "2.53.2"
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.53.2",
80
- "@platformatic/config": "2.53.2",
81
- "@platformatic/itc": "2.53.2",
82
- "@platformatic/telemetry": "2.53.2",
83
- "@platformatic/ts-compiler": "2.53.2",
84
- "@platformatic/utils": "2.53.2",
85
- "@platformatic/generators": "2.53.2"
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.53.2.json",
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": [