@platformatic/runtime 3.61.1 → 3.62.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.
@@ -1,4 +1,5 @@
1
1
  import { getITC, updateGlobals } from '@platformatic/globals'
2
+ import { monitorEventLoopDelay } from 'node:perf_hooks'
2
3
  import {
3
4
  HealthSignalMustBeObjectError,
4
5
  HealthSignalTypeMustBeStringError
@@ -79,4 +80,37 @@ export function initHealthSignalsApi (options = {}) {
79
80
  }
80
81
 
81
82
  updateGlobals({ sendHealthSignal })
83
+
84
+ return sendHealthSignal
85
+ }
86
+
87
+ // Samples the event loop delay and reports it as an eventLoopDelay health
88
+ // signal once per second, in milliseconds. The measurement must run inside
89
+ // the worker (monitorEventLoopDelay is per-isolate, there is no cross-thread
90
+ // equivalent): during a hard block nothing is reported and a sample with a
91
+ // large max arrives once the loop unblocks — full blocks are already caught
92
+ // by the main-thread measured ELU, this signal catches long individual
93
+ // stalls at low average utilization, which are invisible to any ELU
94
+ // threshold.
95
+ export function startEventLoopDelayMonitor (sendHealthSignal, options = {}) {
96
+ const histogram = monitorEventLoopDelay({ resolution: options.resolution ?? 20 })
97
+ histogram.enable()
98
+
99
+ const interval = setInterval(() => {
100
+ // With one-second windows the p99 is close to the max (few dozen samples
101
+ // per window), but it smooths single outliers on consumers that prefer it
102
+ const max = histogram.max / 1e6
103
+ const mean = histogram.mean / 1e6
104
+ const p99 = histogram.percentile(99) / 1e6
105
+ histogram.reset()
106
+
107
+ // Signals are delivered on a best-effort basis
108
+ sendHealthSignal({ type: 'eventLoopDelay', max, mean, p99 }).catch(() => {})
109
+ }, options.interval ?? 1000)
110
+ interval.unref()
111
+
112
+ return function stopEventLoopDelayMonitor () {
113
+ clearInterval(interval)
114
+ histogram.disable()
115
+ }
82
116
  }
@@ -22,7 +22,7 @@ import pino from 'pino'
22
22
  import { install as installUndiciGlobals } from 'undici'
23
23
  import { exitCodes } from '../errors.js'
24
24
  import { Controller } from './controller.js'
25
- import { initHealthSignalsApi } from './health-signals.js'
25
+ import { initHealthSignalsApi, startEventLoopDelayMonitor } from './health-signals.js'
26
26
  import { setDispatcher } from './interceptors.js'
27
27
  import { setupITC } from './itc.js'
28
28
  import { SharedContext } from './shared-context.js'
@@ -338,11 +338,27 @@ async function main () {
338
338
  }
339
339
  }
340
340
 
341
- initHealthSignalsApi({
341
+ const sendHealthSignal = initHealthSignalsApi({
342
342
  workerId: workerData.worker.id,
343
343
  applicationId: applicationConfig.id
344
344
  })
345
345
 
346
+ // When health.maxEventLoopDelay or health.maxEventLoopDelayP99 are
347
+ // configured, sample the event loop delay and report it as a health
348
+ // signal: the main thread evaluates it as part of the health checks and it
349
+ // is exposed on the health metrics event.
350
+ const maxEventLoopDelay = Number(applicationConfig.health?.maxEventLoopDelay ?? runtimeConfig.health?.maxEventLoopDelay)
351
+ const maxEventLoopDelayP99 = Number(
352
+ applicationConfig.health?.maxEventLoopDelayP99 ?? runtimeConfig.health?.maxEventLoopDelayP99
353
+ )
354
+
355
+ if (
356
+ (Number.isFinite(maxEventLoopDelay) && maxEventLoopDelay > 0) ||
357
+ (Number.isFinite(maxEventLoopDelayP99) && maxEventLoopDelayP99 > 0)
358
+ ) {
359
+ startEventLoopDelayMonitor(sendHealthSignal)
360
+ }
361
+
346
362
  itc.notify('init')
347
363
  }
348
364
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "3.61.1",
4
- "description": "",
3
+ "version": "3.62.0",
4
+ "description": "Platformatic runtime",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "author": "Platformatic Inc. <oss@platformatic.dev> (https://platformatic.dev)",
@@ -36,17 +36,17 @@
36
36
  "pino-sentry-transport": "^1.6.0",
37
37
  "split2": "^4.2.0",
38
38
  "tstyche": "^6.2.0",
39
- "typescript": "^5.5.4",
39
+ "typescript": "^5.9.3",
40
40
  "undici-oidc-interceptor": "^0.9.0",
41
41
  "why-is-node-running": "^2.2.2",
42
- "@platformatic/db": "3.61.1",
43
- "@platformatic/gateway": "3.61.1",
44
- "@platformatic/service": "3.61.1",
45
- "@platformatic/node": "3.61.1",
46
- "@platformatic/composer": "3.61.1",
47
- "@platformatic/sql-graphql": "3.61.1",
48
- "@platformatic/wattpm-pprof-capture": "3.61.1",
49
- "@platformatic/sql-mapper": "3.61.1"
42
+ "@platformatic/db": "3.62.0",
43
+ "@platformatic/composer": "3.62.0",
44
+ "@platformatic/node": "3.62.0",
45
+ "@platformatic/service": "3.62.0",
46
+ "@platformatic/sql-mapper": "3.62.0",
47
+ "@platformatic/wattpm-pprof-capture": "3.62.0",
48
+ "@platformatic/sql-graphql": "3.62.0",
49
+ "@platformatic/gateway": "3.62.0"
50
50
  },
51
51
  "dependencies": {
52
52
  "@fastify/accepts": "^5.0.0",
@@ -77,13 +77,13 @@
77
77
  "undici": "^7.27.2",
78
78
  "undici-thread-interceptor": "^1.3.1",
79
79
  "ws": "^8.16.0",
80
- "@platformatic/basic": "3.61.1",
81
- "@platformatic/foundation": "3.61.1",
82
- "@platformatic/globals": "3.61.1",
83
- "@platformatic/itc": "3.61.1",
84
- "@platformatic/metrics": "3.61.1",
85
- "@platformatic/telemetry": "3.61.1",
86
- "@platformatic/generators": "3.61.1"
80
+ "@platformatic/basic": "3.62.0",
81
+ "@platformatic/foundation": "3.62.0",
82
+ "@platformatic/generators": "3.62.0",
83
+ "@platformatic/globals": "3.62.0",
84
+ "@platformatic/itc": "3.62.0",
85
+ "@platformatic/metrics": "3.62.0",
86
+ "@platformatic/telemetry": "3.62.0"
87
87
  },
88
88
  "engines": {
89
89
  "node": ">=22.19.0"
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.61.1.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.62.0.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Runtime Config",
5
5
  "type": "object",
@@ -22,6 +22,59 @@
22
22
  }
23
23
  ]
24
24
  },
25
+ "extensions": {
26
+ "anyOf": [
27
+ {
28
+ "type": "string",
29
+ "resolvePath": true
30
+ },
31
+ {
32
+ "type": "object",
33
+ "additionalProperties": false,
34
+ "required": [
35
+ "path"
36
+ ],
37
+ "properties": {
38
+ "path": {
39
+ "type": "string",
40
+ "resolvePath": true
41
+ },
42
+ "options": {
43
+ "type": "object",
44
+ "additionalProperties": true
45
+ }
46
+ }
47
+ },
48
+ {
49
+ "type": "array",
50
+ "items": {
51
+ "anyOf": [
52
+ {
53
+ "type": "string",
54
+ "resolvePath": true
55
+ },
56
+ {
57
+ "type": "object",
58
+ "additionalProperties": false,
59
+ "required": [
60
+ "path"
61
+ ],
62
+ "properties": {
63
+ "path": {
64
+ "type": "string",
65
+ "resolvePath": true
66
+ },
67
+ "options": {
68
+ "type": "object",
69
+ "additionalProperties": true
70
+ }
71
+ }
72
+ }
73
+ ]
74
+ }
75
+ }
76
+ ]
77
+ },
25
78
  "entrypoint": {
26
79
  "type": "string"
27
80
  },
@@ -181,6 +234,28 @@
181
234
  }
182
235
  ]
183
236
  },
237
+ "maxEventLoopDelay": {
238
+ "anyOf": [
239
+ {
240
+ "type": "number",
241
+ "minimum": 0
242
+ },
243
+ {
244
+ "type": "string"
245
+ }
246
+ ]
247
+ },
248
+ "maxEventLoopDelayP99": {
249
+ "anyOf": [
250
+ {
251
+ "type": "number",
252
+ "minimum": 0
253
+ },
254
+ {
255
+ "type": "string"
256
+ }
257
+ ]
258
+ },
184
259
  "maxHeapUsed": {
185
260
  "anyOf": [
186
261
  {
@@ -251,6 +326,18 @@
251
326
  },
252
327
  "additionalProperties": false
253
328
  },
329
+ "restartOnError": {
330
+ "description": "Overrides the runtime-level restartOnError for this application. Set to false or 0 to never restart the application when it crashes, a positive number to wait that amount of milliseconds between restarts, or true to use the default delay.",
331
+ "anyOf": [
332
+ {
333
+ "type": "boolean"
334
+ },
335
+ {
336
+ "type": "number",
337
+ "minimum": 0
338
+ }
339
+ ]
340
+ },
254
341
  "dependencies": {
255
342
  "type": "array",
256
343
  "items": {
@@ -579,6 +666,28 @@
579
666
  }
580
667
  ]
581
668
  },
669
+ "maxEventLoopDelay": {
670
+ "anyOf": [
671
+ {
672
+ "type": "number",
673
+ "minimum": 0
674
+ },
675
+ {
676
+ "type": "string"
677
+ }
678
+ ]
679
+ },
680
+ "maxEventLoopDelayP99": {
681
+ "anyOf": [
682
+ {
683
+ "type": "number",
684
+ "minimum": 0
685
+ },
686
+ {
687
+ "type": "string"
688
+ }
689
+ ]
690
+ },
582
691
  "maxHeapUsed": {
583
692
  "anyOf": [
584
693
  {
@@ -649,6 +758,18 @@
649
758
  },
650
759
  "additionalProperties": false
651
760
  },
761
+ "restartOnError": {
762
+ "description": "Overrides the runtime-level restartOnError for this application. Set to false or 0 to never restart the application when it crashes, a positive number to wait that amount of milliseconds between restarts, or true to use the default delay.",
763
+ "anyOf": [
764
+ {
765
+ "type": "boolean"
766
+ },
767
+ {
768
+ "type": "number",
769
+ "minimum": 0
770
+ }
771
+ ]
772
+ },
652
773
  "dependencies": {
653
774
  "type": "array",
654
775
  "items": {
@@ -975,6 +1096,28 @@
975
1096
  }
976
1097
  ]
977
1098
  },
1099
+ "maxEventLoopDelay": {
1100
+ "anyOf": [
1101
+ {
1102
+ "type": "number",
1103
+ "minimum": 0
1104
+ },
1105
+ {
1106
+ "type": "string"
1107
+ }
1108
+ ]
1109
+ },
1110
+ "maxEventLoopDelayP99": {
1111
+ "anyOf": [
1112
+ {
1113
+ "type": "number",
1114
+ "minimum": 0
1115
+ },
1116
+ {
1117
+ "type": "string"
1118
+ }
1119
+ ]
1120
+ },
978
1121
  "maxHeapUsed": {
979
1122
  "anyOf": [
980
1123
  {
@@ -1045,6 +1188,18 @@
1045
1188
  },
1046
1189
  "additionalProperties": false
1047
1190
  },
1191
+ "restartOnError": {
1192
+ "description": "Overrides the runtime-level restartOnError for this application. Set to false or 0 to never restart the application when it crashes, a positive number to wait that amount of milliseconds between restarts, or true to use the default delay.",
1193
+ "anyOf": [
1194
+ {
1195
+ "type": "boolean"
1196
+ },
1197
+ {
1198
+ "type": "number",
1199
+ "minimum": 0
1200
+ }
1201
+ ]
1202
+ },
1048
1203
  "dependencies": {
1049
1204
  "type": "array",
1050
1205
  "items": {
@@ -1371,6 +1526,28 @@
1371
1526
  }
1372
1527
  ]
1373
1528
  },
1529
+ "maxEventLoopDelay": {
1530
+ "anyOf": [
1531
+ {
1532
+ "type": "number",
1533
+ "minimum": 0
1534
+ },
1535
+ {
1536
+ "type": "string"
1537
+ }
1538
+ ]
1539
+ },
1540
+ "maxEventLoopDelayP99": {
1541
+ "anyOf": [
1542
+ {
1543
+ "type": "number",
1544
+ "minimum": 0
1545
+ },
1546
+ {
1547
+ "type": "string"
1548
+ }
1549
+ ]
1550
+ },
1374
1551
  "maxHeapUsed": {
1375
1552
  "anyOf": [
1376
1553
  {
@@ -1441,6 +1618,18 @@
1441
1618
  },
1442
1619
  "additionalProperties": false
1443
1620
  },
1621
+ "restartOnError": {
1622
+ "description": "Overrides the runtime-level restartOnError for this application. Set to false or 0 to never restart the application when it crashes, a positive number to wait that amount of milliseconds between restarts, or true to use the default delay.",
1623
+ "anyOf": [
1624
+ {
1625
+ "type": "boolean"
1626
+ },
1627
+ {
1628
+ "type": "number",
1629
+ "minimum": 0
1630
+ }
1631
+ ]
1632
+ },
1444
1633
  "dependencies": {
1445
1634
  "type": "array",
1446
1635
  "items": {
@@ -2136,6 +2325,28 @@
2136
2325
  ],
2137
2326
  "default": 0.99
2138
2327
  },
2328
+ "maxEventLoopDelay": {
2329
+ "anyOf": [
2330
+ {
2331
+ "type": "number",
2332
+ "minimum": 0
2333
+ },
2334
+ {
2335
+ "type": "string"
2336
+ }
2337
+ ]
2338
+ },
2339
+ "maxEventLoopDelayP99": {
2340
+ "anyOf": [
2341
+ {
2342
+ "type": "number",
2343
+ "minimum": 0
2344
+ },
2345
+ {
2346
+ "type": "string"
2347
+ }
2348
+ ]
2349
+ },
2139
2350
  "maxHeapUsed": {
2140
2351
  "anyOf": [
2141
2352
  {