@platformatic/runtime 2.69.0 → 2.70.1

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.
@@ -164,14 +164,16 @@ async function getDispatcherOpts (undiciConfig) {
164
164
 
165
165
  function createThreadInterceptor (runtimeConfig) {
166
166
  const telemetry = runtimeConfig.telemetry
167
- const hooks = telemetry ? createTelemetryThreadInterceptorHooks() : {}
167
+
168
+ const telemetryHooks = telemetry ? createTelemetryThreadInterceptorHooks() : {}
169
+
168
170
  const threadDispatcher = wire({
169
171
  // Specifying the domain is critical to avoid flooding the DNS
170
172
  // with requests for a domain that's never going to exist.
171
173
  domain: '.plt.local',
172
174
  port: parentPort,
173
175
  timeout: runtimeConfig.serviceTimeout,
174
- ...hooks
176
+ ...telemetryHooks,
175
177
  })
176
178
  return threadDispatcher
177
179
  }
package/lib/worker/itc.js CHANGED
@@ -9,6 +9,7 @@ const { Unpromise } = require('@watchable/unpromise')
9
9
  const errors = require('../errors')
10
10
  const { updateUndiciInterceptors } = require('./interceptors')
11
11
  const { kITC, kId, kServiceId, kWorkerId } = require('./symbols')
12
+ const { MessagingITC } = require('./messaging')
12
13
 
13
14
  async function safeHandleInITC (worker, fn) {
14
15
  try {
@@ -47,8 +48,8 @@ async function safeHandleInITC (worker, fn) {
47
48
  }
48
49
  }
49
50
 
50
- async function sendViaITC (worker, name, message) {
51
- return safeHandleInITC(worker, () => worker[kITC].send(name, message))
51
+ async function sendViaITC (worker, name, message, transferList) {
52
+ return safeHandleInITC(worker, () => worker[kITC].send(name, message, { transferList }))
52
53
  }
53
54
 
54
55
  async function waitEventFromITC (worker, event) {
@@ -56,6 +57,15 @@ async function waitEventFromITC (worker, event) {
56
57
  }
57
58
 
58
59
  function setupITC (app, service, dispatcher) {
60
+ const messaging = new MessagingITC(app.appConfig.id, workerData.config)
61
+
62
+ Object.assign(globalThis.platformatic ?? {}, {
63
+ messaging: {
64
+ handle: messaging.handle.bind(messaging),
65
+ send: messaging.send.bind(messaging)
66
+ }
67
+ })
68
+
59
69
  const itc = new ITC({
60
70
  name: app.appConfig.id + '-worker',
61
71
  port: parentPort,
@@ -96,6 +106,7 @@ function setupITC (app, service, dispatcher) {
96
106
 
97
107
  await dispatcher.interceptor.close()
98
108
  itc.close()
109
+ messaging.close()
99
110
  },
100
111
 
101
112
  async build () {
@@ -116,8 +127,10 @@ function setupITC (app, service, dispatcher) {
116
127
 
117
128
  async updateWorkersCount (data) {
118
129
  const { serviceId, workers } = data
119
- const w = workerData.config.serviceMap.get(serviceId)
120
- if (w) { w.workers = workers }
130
+ const worker = workerData.config.serviceMap.get(serviceId)
131
+ if (worker) {
132
+ worker.workers = workers
133
+ }
121
134
  workerData.serviceConfig.workers = workers
122
135
  workerData.worker.count = workers
123
136
  },
@@ -195,6 +208,10 @@ function setupITC (app, service, dispatcher) {
195
208
  } catch (err) {
196
209
  throw new errors.FailedToPerformCustomReadinessCheckError(service.id, err.message)
197
210
  }
211
+ },
212
+
213
+ saveMessagingChannel (channel) {
214
+ messaging.addSource(channel)
198
215
  }
199
216
  }
200
217
  })
@@ -0,0 +1,186 @@
1
+ 'use strict'
2
+
3
+ const { withResolvers, executeWithTimeout, kTimeout } = require('@platformatic/utils')
4
+ const { ITC, generateResponse, sanitize } = require('@platformatic/itc')
5
+ const errors = require('../errors')
6
+ const { RoundRobinMap } = require('./round-robin-map')
7
+ const { kWorkersBroadcast, kITC } = require('./symbols')
8
+
9
+ const kPendingResponses = Symbol('plt.messaging.pendingResponses')
10
+
11
+ class MessagingITC extends ITC {
12
+ #timeout
13
+ #listener
14
+ #closeResolvers
15
+ #broadcastChannel
16
+ #workers
17
+ #sources
18
+
19
+ constructor (id, runtimeConfig) {
20
+ super({
21
+ throwOnMissingHandler: true,
22
+ name: `${id}-messaging`
23
+ })
24
+
25
+ this.#timeout = runtimeConfig.messagingTimeout
26
+ this.#workers = new RoundRobinMap()
27
+ this.#sources = new Set()
28
+
29
+ // Start listening on the BroadcastChannel for the list of services
30
+ this.#broadcastChannel = new BroadcastChannel(kWorkersBroadcast)
31
+ this.#broadcastChannel.onmessage = this.#updateWorkers.bind(this)
32
+
33
+ this.listen()
34
+ }
35
+
36
+ _setupListener (listener) {
37
+ this.#listener = listener
38
+ }
39
+
40
+ handle (message, handler) {
41
+ if (typeof message === 'object') {
42
+ for (const [name, fn] of Object.entries(message)) {
43
+ super.handle(name, fn)
44
+ }
45
+ } else {
46
+ super.handle(message, handler)
47
+ }
48
+ }
49
+
50
+ async send (service, name, message, options) {
51
+ // Get the next worker for the service
52
+ const worker = this.#workers.next(service)
53
+
54
+ if (!worker) {
55
+ throw new errors.MessagingError(service, 'No workers available')
56
+ }
57
+
58
+ if (!worker.channel) {
59
+ // Use twice the value here as a fallback measure. The target handler in the main thread is forwarding
60
+ // the request to the worker, using executeWithTimeout with the user set timeout value.
61
+ const channel = await executeWithTimeout(
62
+ globalThis[kITC].send('getWorkerMessagingChannel', { service: worker.service, worker: worker.worker }),
63
+ this.#timeout * 2
64
+ )
65
+
66
+ /* c8 ignore next 3 - Hard to test */
67
+ if (channel === kTimeout) {
68
+ throw new errors.MessagingError(service, 'Timeout while waiting for a communication channel.')
69
+ }
70
+
71
+ worker.channel = channel
72
+ this.#setupChannel(channel)
73
+
74
+ channel[kPendingResponses] = new Map()
75
+ channel.on('close', this.#handlePendingResponse.bind(this, channel))
76
+ }
77
+
78
+ const context = { ...options }
79
+ context.channel = worker.channel
80
+ context.service = worker.service
81
+ context.trackResponse = true
82
+
83
+ const response = await executeWithTimeout(super.send(name, message, context), this.#timeout)
84
+
85
+ if (response === kTimeout) {
86
+ throw new errors.MessagingError(service, 'Timeout while waiting for a response.')
87
+ }
88
+
89
+ return response
90
+ }
91
+
92
+ async addSource (channel) {
93
+ this.#sources.add(channel)
94
+ this.#setupChannel(channel)
95
+
96
+ // This has been closed on the other side.
97
+ // Pending messages will be silently discarded by Node (as postMessage does not throw) so we don't need to handle them.
98
+ channel.on('close', () => {
99
+ this.#sources.delete(channel)
100
+ })
101
+ }
102
+
103
+ _send (request, context) {
104
+ const { channel, transferList } = context
105
+
106
+ if (context.trackResponse) {
107
+ const service = context.service
108
+ channel[kPendingResponses].set(request.reqId, { service, request })
109
+ }
110
+
111
+ channel.postMessage(sanitize(request, transferList), { transferList })
112
+ }
113
+
114
+ _createClosePromise () {
115
+ const { promise, resolve, reject } = withResolvers()
116
+ this.#closeResolvers = { resolve, reject }
117
+ return promise
118
+ }
119
+
120
+ _close () {
121
+ this.#closeResolvers.resolve()
122
+ this.#broadcastChannel.close()
123
+
124
+ for (const source of this.#sources) {
125
+ source.close()
126
+ }
127
+
128
+ for (const worker of this.#workers.values()) {
129
+ worker.channel?.close()
130
+ }
131
+
132
+ this.#sources.clear()
133
+ }
134
+
135
+ #setupChannel (channel) {
136
+ // Setup the message for processing
137
+ channel.on('message', event => {
138
+ this.#listener(event, { channel })
139
+ })
140
+ }
141
+
142
+ #updateWorkers (event) {
143
+ // Gather all existing channels by thread, it will make them reusable
144
+ const existingChannels = new Map()
145
+ for (const source of this.#workers.values()) {
146
+ existingChannels.set(source.thread, source.channel)
147
+ }
148
+
149
+ // Create a brand new map
150
+ this.#workers = new RoundRobinMap()
151
+
152
+ const instances = []
153
+ for (const [service, workers] of event.data) {
154
+ const count = workers.length
155
+ const next = Math.floor(Math.random() * count)
156
+
157
+ instances.push({ id: service, next, workers: count })
158
+
159
+ for (let i = 0; i < count; i++) {
160
+ const worker = workers[i]
161
+ const channel = existingChannels.get(worker.thread)
162
+
163
+ // Note i is not the worker index as in runtime, but the index in the list of current alive workers for the service
164
+ this.#workers.set(`${service}:${i}`, { ...worker, channel })
165
+ }
166
+ }
167
+
168
+ this.#workers.configure(instances)
169
+ }
170
+
171
+ #handlePendingResponse (channel) {
172
+ for (const { service, request } of channel[kPendingResponses].values()) {
173
+ this._emitResponse(
174
+ generateResponse(
175
+ request,
176
+ new errors.MessagingError(service, 'The communication channel was closed before receiving a response.'),
177
+ null
178
+ )
179
+ )
180
+ }
181
+
182
+ channel[kPendingResponses].clear()
183
+ }
184
+ }
185
+
186
+ module.exports = { MessagingITC }
@@ -16,7 +16,7 @@ class RoundRobinMap extends Map {
16
16
  this.#instances = {}
17
17
 
18
18
  for (const service of services) {
19
- this.#instances[service.id] = { next: 0, count: service.workers }
19
+ this.#instances[service.id] = { next: service.next ?? 0, count: service.workers }
20
20
  }
21
21
  }
22
22
 
@@ -9,10 +9,14 @@ const kITC = Symbol.for('plt.runtime.itc')
9
9
  const kHealthCheckTimer = Symbol.for('plt.runtime.worker.healthCheckTimer')
10
10
  const kWorkerStatus = Symbol('plt.runtime.worker.status')
11
11
  const kInterceptors = Symbol.for('plt.runtime.worker.interceptors')
12
+ const kLastELU = Symbol.for('plt.runtime.worker.lastELU')
12
13
 
13
14
  // This string marker should be safe to use since it belongs to Unicode private area
14
15
  const kStderrMarker = '\ue002'
15
16
 
17
+ // Note that this is used to create a BroadcastChannel so it must be a string
18
+ const kWorkersBroadcast = 'plt.runtime.workers'
19
+
16
20
  module.exports = {
17
21
  kConfig,
18
22
  kId,
@@ -21,7 +25,9 @@ module.exports = {
21
25
  kWorkerId,
22
26
  kITC,
23
27
  kHealthCheckTimer,
28
+ kLastELU,
24
29
  kWorkerStatus,
25
30
  kStderrMarker,
26
- kInterceptors
31
+ kInterceptors,
32
+ kWorkersBroadcast
27
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "2.69.0",
3
+ "version": "2.70.1",
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.69.0",
41
- "@platformatic/db": "2.69.0",
42
- "@platformatic/node": "2.69.0",
43
- "@platformatic/service": "2.69.0",
44
- "@platformatic/sql-graphql": "2.69.0",
45
- "@platformatic/sql-mapper": "2.69.0"
40
+ "@platformatic/composer": "2.70.1",
41
+ "@platformatic/node": "2.70.1",
42
+ "@platformatic/service": "2.70.1",
43
+ "@platformatic/sql-graphql": "2.70.1",
44
+ "@platformatic/sql-mapper": "2.70.1",
45
+ "@platformatic/db": "2.70.1"
46
46
  },
47
47
  "dependencies": {
48
48
  "@fastify/accepts": "^5.0.0",
@@ -50,7 +50,6 @@
50
50
  "@fastify/websocket": "^11.0.0",
51
51
  "@hapi/topo": "^6.0.2",
52
52
  "@opentelemetry/api": "^1.8.0",
53
- "@platformatic/http-metrics": "^0.2.1",
54
53
  "@platformatic/undici-cache-memory": "^0.8.1",
55
54
  "@watchable/unpromise": "^1.0.2",
56
55
  "change-case-all": "^2.1.0",
@@ -75,25 +74,26 @@
75
74
  "sonic-boom": "^4.2.0",
76
75
  "tail-file-stream": "^0.2.0",
77
76
  "undici": "^7.0.0",
78
- "undici-thread-interceptor": "^0.13.1",
77
+ "undici-thread-interceptor": "^0.14.0",
79
78
  "ws": "^8.16.0",
80
- "@platformatic/basic": "2.69.0",
81
- "@platformatic/config": "2.69.0",
82
- "@platformatic/generators": "2.69.0",
83
- "@platformatic/itc": "2.69.0",
84
- "@platformatic/telemetry": "2.69.0",
85
- "@platformatic/ts-compiler": "2.69.0",
86
- "@platformatic/utils": "2.69.0"
79
+ "@platformatic/config": "2.70.1",
80
+ "@platformatic/basic": "2.70.1",
81
+ "@platformatic/generators": "2.70.1",
82
+ "@platformatic/itc": "2.70.1",
83
+ "@platformatic/metrics": "2.70.1",
84
+ "@platformatic/telemetry": "2.70.1",
85
+ "@platformatic/ts-compiler": "2.70.1",
86
+ "@platformatic/utils": "2.70.1"
87
87
  },
88
88
  "scripts": {
89
- "test": "pnpm run lint && borp --concurrency=1 --timeout=600000 && tsd",
90
- "test:main": "borp --concurrency=1 --timeout=300000 test/*.test.js test/*.test.mjs test/versions/*.test.js test/versions/*.test.mjs",
91
- "test:api": "borp --concurrency=1 --timeout=300000 test/api/*.test.js test/api/*.test.mjs test/management-api/*.test.js test/management-api/*.test.mjs",
92
- "test:cli": "borp --concurrency=1 --timeout=300000 test/cli/*.test.js test/cli/*.test.mjs test/cli/**/*.test.js test/cli/**/*.test.mjs",
93
- "test:start": "borp --concurrency=1 --timeout=300000 test/start/*.test.js test/start/*.test.mjs",
94
- "test:multiple-workers": "borp --concurrency=1 --timeout=600000 test/multiple-workers/*.test.js test/multiple-workers/*.test.mjs",
89
+ "test": "pnpm run lint && borp --concurrency=1 --timeout=1200000 && tsd",
90
+ "test:main": "borp --concurrency=1 --timeout=1200000 test/*.test.js test/*.test.mjs test/versions/*.test.js test/versions/*.test.mjs",
91
+ "test:api": "borp --concurrency=1 --timeout=1200000 test/api/*.test.js test/api/*.test.mjs test/management-api/*.test.js test/management-api/*.test.mjs",
92
+ "test:cli": "borp --concurrency=1 --timeout=1200000 test/cli/*.test.js test/cli/*.test.mjs test/cli/**/*.test.js test/cli/**/*.test.mjs",
93
+ "test:start": "borp --concurrency=1 --timeout=1200000 test/start/*.test.js test/start/*.test.mjs",
94
+ "test:multiple-workers": "borp --concurrency=1 --timeout=1200000 test/multiple-workers/*.test.js test/multiple-workers/*.test.mjs",
95
95
  "test:types": "tsd",
96
- "coverage": "pnpm run lint && borp -X fixtures -X test -C --concurrency=1 --timeout=300000 && tsd",
96
+ "coverage": "pnpm run lint && borp -X fixtures -X test -C --concurrency=1 --timeout=1200000 && tsd",
97
97
  "gen-schema": "node lib/schema.js > schema.json",
98
98
  "gen-types": "json2ts > config.d.ts < schema.json",
99
99
  "build": "pnpm run gen-schema && pnpm run gen-types",
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.69.0.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/runtime/2.70.1.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "type": "object",
5
5
  "properties": {
@@ -76,6 +76,7 @@
76
76
  },
77
77
  "health": {
78
78
  "type": "object",
79
+ "default": {},
79
80
  "properties": {
80
81
  "enabled": {
81
82
  "anyOf": [
@@ -156,8 +157,15 @@
156
157
  ]
157
158
  },
158
159
  "maxYoungGeneration": {
159
- "type": "number",
160
- "minimum": 0
160
+ "anyOf": [
161
+ {
162
+ "type": "number",
163
+ "minimum": 0
164
+ },
165
+ {
166
+ "type": "string"
167
+ }
168
+ ]
161
169
  }
162
170
  },
163
171
  "additionalProperties": false
@@ -244,6 +252,7 @@
244
252
  },
245
253
  "health": {
246
254
  "type": "object",
255
+ "default": {},
247
256
  "properties": {
248
257
  "enabled": {
249
258
  "anyOf": [
@@ -324,8 +333,15 @@
324
333
  ]
325
334
  },
326
335
  "maxYoungGeneration": {
327
- "type": "number",
328
- "minimum": 0
336
+ "anyOf": [
337
+ {
338
+ "type": "number",
339
+ "minimum": 0
340
+ },
341
+ {
342
+ "type": "string"
343
+ }
344
+ ]
329
345
  }
330
346
  },
331
347
  "additionalProperties": false
@@ -477,6 +493,7 @@
477
493
  },
478
494
  "health": {
479
495
  "type": "object",
496
+ "default": {},
480
497
  "properties": {
481
498
  "enabled": {
482
499
  "anyOf": [
@@ -557,8 +574,15 @@
557
574
  ]
558
575
  },
559
576
  "maxYoungGeneration": {
560
- "type": "number",
561
- "minimum": 0
577
+ "anyOf": [
578
+ {
579
+ "type": "number",
580
+ "minimum": 0
581
+ },
582
+ {
583
+ "type": "string"
584
+ }
585
+ ]
562
586
  }
563
587
  },
564
588
  "additionalProperties": false
@@ -976,7 +1000,6 @@
976
1000
  "default": {},
977
1001
  "properties": {
978
1002
  "enabled": {
979
- "default": true,
980
1003
  "anyOf": [
981
1004
  {
982
1005
  "type": "boolean"
@@ -984,10 +1007,10 @@
984
1007
  {
985
1008
  "type": "string"
986
1009
  }
987
- ]
1010
+ ],
1011
+ "default": true
988
1012
  },
989
1013
  "interval": {
990
- "default": 30000,
991
1014
  "anyOf": [
992
1015
  {
993
1016
  "type": "number",
@@ -996,10 +1019,10 @@
996
1019
  {
997
1020
  "type": "string"
998
1021
  }
999
- ]
1022
+ ],
1023
+ "default": 30000
1000
1024
  },
1001
1025
  "gracePeriod": {
1002
- "default": 30000,
1003
1026
  "anyOf": [
1004
1027
  {
1005
1028
  "type": "number",
@@ -1008,10 +1031,10 @@
1008
1031
  {
1009
1032
  "type": "string"
1010
1033
  }
1011
- ]
1034
+ ],
1035
+ "default": 30000
1012
1036
  },
1013
1037
  "maxUnhealthyChecks": {
1014
- "default": 10,
1015
1038
  "anyOf": [
1016
1039
  {
1017
1040
  "type": "number",
@@ -1020,10 +1043,10 @@
1020
1043
  {
1021
1044
  "type": "string"
1022
1045
  }
1023
- ]
1046
+ ],
1047
+ "default": 10
1024
1048
  },
1025
1049
  "maxELU": {
1026
- "default": 0.99,
1027
1050
  "anyOf": [
1028
1051
  {
1029
1052
  "type": "number",
@@ -1033,10 +1056,10 @@
1033
1056
  {
1034
1057
  "type": "string"
1035
1058
  }
1036
- ]
1059
+ ],
1060
+ "default": 0.99
1037
1061
  },
1038
1062
  "maxHeapUsed": {
1039
- "default": 0.99,
1040
1063
  "anyOf": [
1041
1064
  {
1042
1065
  "type": "number",
@@ -1046,10 +1069,10 @@
1046
1069
  {
1047
1070
  "type": "string"
1048
1071
  }
1049
- ]
1072
+ ],
1073
+ "default": 0.99
1050
1074
  },
1051
1075
  "maxHeapTotal": {
1052
- "default": 4294967296,
1053
1076
  "anyOf": [
1054
1077
  {
1055
1078
  "type": "number",
@@ -1058,11 +1081,19 @@
1058
1081
  {
1059
1082
  "type": "string"
1060
1083
  }
1061
- ]
1084
+ ],
1085
+ "default": 4294967296
1062
1086
  },
1063
1087
  "maxYoungGeneration": {
1064
- "type": "number",
1065
- "minimum": 0
1088
+ "anyOf": [
1089
+ {
1090
+ "type": "number",
1091
+ "minimum": 0
1092
+ },
1093
+ {
1094
+ "type": "string"
1095
+ }
1096
+ ]
1066
1097
  }
1067
1098
  },
1068
1099
  "additionalProperties": false
@@ -1537,6 +1568,18 @@
1537
1568
  ],
1538
1569
  "default": 300000
1539
1570
  },
1571
+ "messagingTimeout": {
1572
+ "anyOf": [
1573
+ {
1574
+ "type": "number",
1575
+ "minimum": 1
1576
+ },
1577
+ {
1578
+ "type": "string"
1579
+ }
1580
+ ],
1581
+ "default": 30000
1582
+ },
1540
1583
  "resolvedServicesBasePath": {
1541
1584
  "type": "string",
1542
1585
  "default": "external"