@platformatic/runtime 2.6.1 → 2.8.0-alpha.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.
- package/config.d.ts +3 -1
- package/eslint.config.js +1 -1
- package/lib/config.js +1 -1
- package/lib/dependencies.js +15 -13
- package/lib/errors.js +3 -1
- package/lib/logger.js +22 -7
- package/lib/management-api.js +1 -1
- package/lib/runtime.js +317 -204
- package/lib/schema.js +12 -0
- package/lib/start.js +15 -9
- package/lib/worker/app.js +16 -2
- package/lib/worker/itc.js +7 -2
- package/lib/worker/main.js +75 -7
- package/lib/worker/round-robin-map.js +61 -0
- package/lib/worker/symbols.js +6 -1
- package/package.json +16 -15
- package/schema.json +17 -1
package/lib/runtime.js
CHANGED
|
@@ -18,13 +18,25 @@ const { startManagementApi } = require('./management-api')
|
|
|
18
18
|
const { startPrometheusServer } = require('./prom-server')
|
|
19
19
|
const { getRuntimeTmpDir } = require('./utils')
|
|
20
20
|
const { sendViaITC, waitEventFromITC } = require('./worker/itc')
|
|
21
|
-
const {
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const { RoundRobinMap } = require('./worker/round-robin-map.js')
|
|
22
|
+
const {
|
|
23
|
+
kId,
|
|
24
|
+
kServiceId,
|
|
25
|
+
kWorkerId,
|
|
26
|
+
kITC,
|
|
27
|
+
kConfig,
|
|
28
|
+
kLoggerDestination,
|
|
29
|
+
kLoggingPort,
|
|
30
|
+
kWorkerStatus
|
|
31
|
+
} = require('./worker/symbols')
|
|
32
|
+
|
|
33
|
+
const fastify = require('fastify')
|
|
24
34
|
|
|
25
35
|
const platformaticVersion = require('../package.json').version
|
|
26
36
|
const kWorkerFile = join(__dirname, 'worker/main.js')
|
|
27
37
|
|
|
38
|
+
const kInspectorOptions = Symbol('plt.runtime.worker.inspectorOptions')
|
|
39
|
+
|
|
28
40
|
const MAX_LISTENERS_COUNT = 100
|
|
29
41
|
const MAX_METRICS_QUEUE_LENGTH = 5 * 60 // 5 minutes in seconds
|
|
30
42
|
const COLLECT_METRICS_TIMEOUT = 1000
|
|
@@ -33,12 +45,11 @@ const MAX_BOOTSTRAP_ATTEMPTS = 5
|
|
|
33
45
|
|
|
34
46
|
class Runtime extends EventEmitter {
|
|
35
47
|
#configManager
|
|
48
|
+
#isProduction
|
|
36
49
|
#runtimeTmpDir
|
|
37
50
|
#runtimeLogsDir
|
|
38
51
|
#env
|
|
39
|
-
#services
|
|
40
52
|
#servicesIds
|
|
41
|
-
#entrypoint
|
|
42
53
|
#entrypointId
|
|
43
54
|
#url
|
|
44
55
|
#loggerDestination
|
|
@@ -48,11 +59,9 @@ class Runtime extends EventEmitter {
|
|
|
48
59
|
#interceptor
|
|
49
60
|
#managementApi
|
|
50
61
|
#prometheusServer
|
|
51
|
-
#startedServices
|
|
52
|
-
#restartPromises
|
|
53
|
-
#bootstrapAttempts
|
|
54
|
-
#inspectors
|
|
55
62
|
#inspectorServer
|
|
63
|
+
#workers
|
|
64
|
+
#restartingWorkers
|
|
56
65
|
|
|
57
66
|
constructor (configManager, runtimeLogsDir, env) {
|
|
58
67
|
super()
|
|
@@ -62,16 +71,13 @@ class Runtime extends EventEmitter {
|
|
|
62
71
|
this.#runtimeTmpDir = getRuntimeTmpDir(configManager.dirname)
|
|
63
72
|
this.#runtimeLogsDir = runtimeLogsDir
|
|
64
73
|
this.#env = env
|
|
65
|
-
this.#
|
|
74
|
+
this.#workers = new RoundRobinMap()
|
|
66
75
|
this.#servicesIds = []
|
|
67
76
|
this.#url = undefined
|
|
68
77
|
// Note: nothing hits the main thread so there is no reason to set the globalDispatcher here
|
|
69
78
|
this.#interceptor = createThreadInterceptor({ domain: '.plt.local', timeout: true })
|
|
70
79
|
this.#status = undefined
|
|
71
|
-
this.#
|
|
72
|
-
this.#restartPromises = new Map()
|
|
73
|
-
this.#bootstrapAttempts = new Map()
|
|
74
|
-
this.#inspectors = []
|
|
80
|
+
this.#restartingWorkers = new Map()
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
async init () {
|
|
@@ -94,19 +100,21 @@ class Runtime extends EventEmitter {
|
|
|
94
100
|
this.logger = logger
|
|
95
101
|
this.#loggerDestination = destination
|
|
96
102
|
|
|
103
|
+
this.#isProduction = this.#configManager.args?.production ?? false
|
|
104
|
+
this.#servicesIds = config.services.map(service => service.id)
|
|
105
|
+
this.#workers.configure(config.services, this.#configManager.current.workers, this.#isProduction)
|
|
106
|
+
|
|
97
107
|
// Create all services, each in is own worker thread
|
|
98
108
|
for (const serviceConfig of config.services) {
|
|
99
|
-
// Setup forwarding of logs from the worker threads to the main thread
|
|
100
109
|
await this.#setupService(serviceConfig)
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
try {
|
|
104
113
|
// Make sure the list exists before computing the dependencies, otherwise some services might not be stopped
|
|
105
|
-
this.#servicesIds = config.services.map(service => service.id)
|
|
106
114
|
|
|
107
115
|
if (autoloadEnabled) {
|
|
108
116
|
checkDependencies(config.services)
|
|
109
|
-
this.#
|
|
117
|
+
this.#workers = topologicalSort(this.#workers, config)
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
// Recompute the list of services after sorting
|
|
@@ -119,7 +127,7 @@ class Runtime extends EventEmitter {
|
|
|
119
127
|
this.#updateStatus('init')
|
|
120
128
|
}
|
|
121
129
|
|
|
122
|
-
async start () {
|
|
130
|
+
async start (silent = false) {
|
|
123
131
|
if (typeof this.#configManager.current.entrypoint === 'undefined') {
|
|
124
132
|
throw new errors.MissingEntrypointError()
|
|
125
133
|
}
|
|
@@ -128,33 +136,41 @@ class Runtime extends EventEmitter {
|
|
|
128
136
|
// Important: do not use Promise.all here since it won't properly manage dependencies
|
|
129
137
|
try {
|
|
130
138
|
for (const service of this.#servicesIds) {
|
|
131
|
-
await this.startService(service)
|
|
139
|
+
await this.startService(service, silent)
|
|
132
140
|
}
|
|
133
141
|
|
|
134
142
|
if (this.#configManager.current.inspectorOptions) {
|
|
135
143
|
const { port } = this.#configManager.current.inspectorOptions
|
|
136
144
|
|
|
137
|
-
const server =
|
|
145
|
+
const server = fastify({
|
|
138
146
|
loggerInstance: this.logger.child({ name: 'inspector' }, { level: 'warn' })
|
|
139
147
|
})
|
|
140
148
|
|
|
141
|
-
const version = await fetch(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
const version = await fetch(
|
|
150
|
+
`http://127.0.0.1:${this.#configManager.current.inspectorOptions.port + 1}/json/version`
|
|
151
|
+
).then(res => res.json())
|
|
152
|
+
|
|
153
|
+
const data = await Promise.all(
|
|
154
|
+
Array.from(this.#workers.values()).map(async worker => {
|
|
155
|
+
const data = worker[kInspectorOptions]
|
|
156
|
+
|
|
157
|
+
const res = await fetch(`http://127.0.0.1:${data.port}/json/list`)
|
|
158
|
+
const details = await res.json()
|
|
159
|
+
return {
|
|
160
|
+
...details[0],
|
|
161
|
+
title: data.id
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
)
|
|
151
165
|
|
|
152
166
|
server.get('/json/list', () => data)
|
|
153
167
|
server.get('/json', () => data)
|
|
154
168
|
server.get('/json/version', () => version)
|
|
155
169
|
|
|
156
170
|
await server.listen({ port })
|
|
157
|
-
this.logger.info(
|
|
171
|
+
this.logger.info(
|
|
172
|
+
'The inspector server is now listening for all services. Open `chrome://inspect` in Google Chrome to connect.'
|
|
173
|
+
)
|
|
158
174
|
this.#inspectorServer = server
|
|
159
175
|
}
|
|
160
176
|
} catch (error) {
|
|
@@ -180,13 +196,14 @@ class Runtime extends EventEmitter {
|
|
|
180
196
|
}
|
|
181
197
|
|
|
182
198
|
this.#updateStatus('stopping')
|
|
183
|
-
this.#startedServices.clear()
|
|
184
199
|
|
|
185
200
|
if (this.#inspectorServer) {
|
|
186
201
|
await this.#inspectorServer.close()
|
|
187
202
|
}
|
|
188
203
|
|
|
189
|
-
|
|
204
|
+
for (const service of this.#servicesIds) {
|
|
205
|
+
await this.stopService(service, silent)
|
|
206
|
+
}
|
|
190
207
|
|
|
191
208
|
this.#updateStatus('stopped')
|
|
192
209
|
}
|
|
@@ -238,102 +255,45 @@ class Runtime extends EventEmitter {
|
|
|
238
255
|
this.#updateStatus('closed')
|
|
239
256
|
}
|
|
240
257
|
|
|
241
|
-
async startService (id) {
|
|
242
|
-
if
|
|
258
|
+
async startService (id, silent) {
|
|
259
|
+
// Since when a service is stopped the worker is deleted, we consider a service start if its first service
|
|
260
|
+
// is no longer in the init phase
|
|
261
|
+
const firstWorker = this.#workers.get(`${id}:0`)
|
|
262
|
+
if (firstWorker && firstWorker[kWorkerStatus] !== 'boot' && firstWorker[kWorkerStatus] !== 'init') {
|
|
243
263
|
throw new errors.ApplicationAlreadyStartedError()
|
|
244
264
|
}
|
|
245
265
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
let service = await this.#getServiceById(id, false, false)
|
|
250
|
-
|
|
251
|
-
// The service was stopped, recreate the thread
|
|
252
|
-
if (!service) {
|
|
253
|
-
const config = this.#configManager.current
|
|
254
|
-
const serviceConfig = config.services.find(s => s.id === id)
|
|
266
|
+
const config = this.#configManager.current
|
|
267
|
+
const serviceConfig = config.services.find(s => s.id === id)
|
|
255
268
|
|
|
256
|
-
|
|
257
|
-
|
|
269
|
+
if (!serviceConfig) {
|
|
270
|
+
throw new errors.ServiceNotFoundError(id, Array.from(this.#servicesIds).join(', '))
|
|
258
271
|
}
|
|
259
272
|
|
|
260
|
-
|
|
261
|
-
const serviceUrl = await sendViaITC(service, 'start')
|
|
262
|
-
if (serviceUrl) {
|
|
263
|
-
this.#url = serviceUrl
|
|
264
|
-
}
|
|
265
|
-
this.#bootstrapAttempts.set(id, 0)
|
|
266
|
-
} catch (error) {
|
|
267
|
-
// TODO: handle port allocation error here
|
|
268
|
-
if (error.code === 'EADDRINUSE') throw error
|
|
273
|
+
const workersCount = await this.#workers.getCount(serviceConfig.id)
|
|
269
274
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const config = this.#configManager.current
|
|
273
|
-
const restartOnError = config.restartOnError
|
|
274
|
-
|
|
275
|
-
if (!restartOnError) {
|
|
276
|
-
this.logger.error(`Failed to start service "${id}".`)
|
|
277
|
-
throw error
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
let bootstrapAttempt = this.#bootstrapAttempts.get(id)
|
|
281
|
-
if (bootstrapAttempt++ >= MAX_BOOTSTRAP_ATTEMPTS || restartOnError === 0) {
|
|
282
|
-
this.logger.error(`Failed to start service "${id}" after ${MAX_BOOTSTRAP_ATTEMPTS} attempts.`)
|
|
283
|
-
throw error
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
this.logger.warn(
|
|
287
|
-
`Starting a service "${id}" in ${restartOnError}ms. ` +
|
|
288
|
-
`Attempt ${bootstrapAttempt} of ${MAX_BOOTSTRAP_ATTEMPTS}...`
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
this.#bootstrapAttempts.set(id, bootstrapAttempt)
|
|
292
|
-
await this.#restartCrashedService(id)
|
|
275
|
+
for (let i = 0; i < workersCount; i++) {
|
|
276
|
+
await this.#startWorker(config, serviceConfig, workersCount, id, i, silent)
|
|
293
277
|
}
|
|
294
278
|
}
|
|
295
279
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
if (!service) {
|
|
301
|
-
return
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
this.#startedServices.set(id, false)
|
|
305
|
-
|
|
306
|
-
if (!silent) {
|
|
307
|
-
this.logger?.info(`Stopping service "${id}"...`)
|
|
308
|
-
}
|
|
280
|
+
async stopService (id, silent) {
|
|
281
|
+
const config = this.#configManager.current
|
|
282
|
+
const serviceConfig = config.services.find(s => s.id === id)
|
|
309
283
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
await executeWithTimeout(sendViaITC(service, 'stop'), 10000)
|
|
313
|
-
} catch (error) {
|
|
314
|
-
this.logger?.info(
|
|
315
|
-
{ error: ensureLoggableError(error) },
|
|
316
|
-
`Failed to stop service "${id}". Killing a worker thread.`
|
|
317
|
-
)
|
|
318
|
-
} finally {
|
|
319
|
-
service[kITC].close()
|
|
284
|
+
if (!serviceConfig) {
|
|
285
|
+
throw new errors.ServiceNotFoundError(id, Array.from(this.#servicesIds).join(', '))
|
|
320
286
|
}
|
|
321
287
|
|
|
322
|
-
|
|
323
|
-
const res = await executeWithTimeout(once(service, 'exit'), 10000)
|
|
288
|
+
const workersCount = await this.#workers.getCount(serviceConfig.id)
|
|
324
289
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
await service.terminate()
|
|
290
|
+
for (let i = 0; i < workersCount; i++) {
|
|
291
|
+
await this.#stopWorker(workersCount, id, i, silent)
|
|
328
292
|
}
|
|
329
293
|
}
|
|
330
294
|
|
|
331
295
|
async buildService (id) {
|
|
332
|
-
const service = this.#
|
|
333
|
-
|
|
334
|
-
if (!service) {
|
|
335
|
-
throw new errors.ServiceNotFoundError(id, Array.from(this.#services.keys()).join(', '))
|
|
336
|
-
}
|
|
296
|
+
const service = await this.#getServiceById(id)
|
|
337
297
|
|
|
338
298
|
try {
|
|
339
299
|
return await sendViaITC(service, 'build')
|
|
@@ -511,6 +471,7 @@ class Runtime extends EventEmitter {
|
|
|
511
471
|
async getServices () {
|
|
512
472
|
return {
|
|
513
473
|
entrypoint: this.#entrypointId,
|
|
474
|
+
production: this.#isProduction,
|
|
514
475
|
services: await Promise.all(this.#servicesIds.map(id => this.getServiceDetails(id)))
|
|
515
476
|
}
|
|
516
477
|
}
|
|
@@ -543,6 +504,10 @@ class Runtime extends EventEmitter {
|
|
|
543
504
|
dependencies
|
|
544
505
|
}
|
|
545
506
|
|
|
507
|
+
if (this.#isProduction) {
|
|
508
|
+
serviceDetails.workers = this.#workers.getCount(id)
|
|
509
|
+
}
|
|
510
|
+
|
|
546
511
|
if (entrypoint) {
|
|
547
512
|
serviceDetails.url = status === 'started' ? this.#url : null
|
|
548
513
|
}
|
|
@@ -581,16 +546,14 @@ class Runtime extends EventEmitter {
|
|
|
581
546
|
async getMetrics (format = 'json') {
|
|
582
547
|
let metrics = null
|
|
583
548
|
|
|
584
|
-
for (const
|
|
549
|
+
for (const worker of this.#workers.values()) {
|
|
585
550
|
try {
|
|
586
|
-
const service = await this.#getServiceById(id, true, false)
|
|
587
|
-
|
|
588
551
|
// The service might be temporarily unavailable
|
|
589
|
-
if (
|
|
552
|
+
if (worker[kWorkerStatus] !== 'started') {
|
|
590
553
|
continue
|
|
591
554
|
}
|
|
592
555
|
|
|
593
|
-
const serviceMetrics = await sendViaITC(
|
|
556
|
+
const serviceMetrics = await sendViaITC(worker, 'getMetrics', format)
|
|
594
557
|
if (serviceMetrics) {
|
|
595
558
|
if (metrics === null) {
|
|
596
559
|
metrics = format === 'json' ? [] : ''
|
|
@@ -699,11 +662,7 @@ class Runtime extends EventEmitter {
|
|
|
699
662
|
}
|
|
700
663
|
|
|
701
664
|
async getServiceMeta (id) {
|
|
702
|
-
const service = this.#
|
|
703
|
-
|
|
704
|
-
if (!service) {
|
|
705
|
-
throw new errors.ServiceNotFoundError(id, Array.from(this.#services.keys()).join(', '))
|
|
706
|
-
}
|
|
665
|
+
const service = await this.#getServiceById(id)
|
|
707
666
|
|
|
708
667
|
try {
|
|
709
668
|
return await sendViaITC(service, 'getServiceMeta')
|
|
@@ -768,16 +727,21 @@ class Runtime extends EventEmitter {
|
|
|
768
727
|
if (this.#status === 'stopping' || this.#status === 'closed') return
|
|
769
728
|
|
|
770
729
|
const config = this.#configManager.current
|
|
730
|
+
const workersCount = await this.#workers.getCount(serviceConfig.id)
|
|
731
|
+
const id = serviceConfig.id
|
|
732
|
+
|
|
733
|
+
for (let i = 0; i < workersCount; i++) {
|
|
734
|
+
await this.#setupWorker(config, serviceConfig, workersCount, id, i)
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
async #setupWorker (config, serviceConfig, workersCount, serviceId, index) {
|
|
771
739
|
const { autoload, restartOnError } = config
|
|
740
|
+
const workerId = `${serviceId}:${index}`
|
|
772
741
|
|
|
773
|
-
const id = serviceConfig.id
|
|
774
742
|
const { port1: loggerDestination, port2: loggingPort } = new MessageChannel()
|
|
775
743
|
loggerDestination.on('message', this.#forwardThreadLog.bind(this))
|
|
776
744
|
|
|
777
|
-
if (!this.#bootstrapAttempts.has(id)) {
|
|
778
|
-
this.#bootstrapAttempts.set(id, 0)
|
|
779
|
-
}
|
|
780
|
-
|
|
781
745
|
// Handle inspector
|
|
782
746
|
let inspectorOptions
|
|
783
747
|
|
|
@@ -786,23 +750,20 @@ class Runtime extends EventEmitter {
|
|
|
786
750
|
...this.#configManager.current.inspectorOptions
|
|
787
751
|
}
|
|
788
752
|
|
|
789
|
-
inspectorOptions.port = inspectorOptions.port + this.#
|
|
790
|
-
|
|
791
|
-
const inspectorData = {
|
|
792
|
-
port: inspectorOptions.port,
|
|
793
|
-
id,
|
|
794
|
-
dirname: this.#configManager.dirname
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
this.#inspectors.push(inspectorData)
|
|
753
|
+
inspectorOptions.port = inspectorOptions.port + this.#workers.size + 1
|
|
798
754
|
}
|
|
799
755
|
|
|
800
|
-
const
|
|
756
|
+
const worker = new Worker(kWorkerFile, {
|
|
801
757
|
workerData: {
|
|
802
758
|
config,
|
|
803
759
|
serviceConfig: {
|
|
804
760
|
...serviceConfig,
|
|
805
|
-
isProduction: this.#
|
|
761
|
+
isProduction: this.#isProduction
|
|
762
|
+
},
|
|
763
|
+
worker: {
|
|
764
|
+
id: workerId,
|
|
765
|
+
index,
|
|
766
|
+
count: workersCount
|
|
806
767
|
},
|
|
807
768
|
inspectorOptions,
|
|
808
769
|
dirname: this.#configManager.dirname,
|
|
@@ -824,89 +785,112 @@ class Runtime extends EventEmitter {
|
|
|
824
785
|
})
|
|
825
786
|
|
|
826
787
|
// Make sure the listener can handle a lot of API requests at once before raising a warning
|
|
827
|
-
|
|
788
|
+
worker.setMaxListeners(1e3)
|
|
828
789
|
|
|
829
790
|
// Track service exiting
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
791
|
+
worker.once('exit', code => {
|
|
792
|
+
if (worker[kWorkerStatus] === 'exited') {
|
|
793
|
+
return
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const started = worker[kWorkerStatus] === 'started'
|
|
797
|
+
worker[kWorkerStatus] = 'exited'
|
|
798
|
+
|
|
799
|
+
this.#cleanupWorker(workerId, worker)
|
|
836
800
|
|
|
837
|
-
if (this.#status === 'stopping')
|
|
801
|
+
if (this.#status === 'stopping') {
|
|
802
|
+
return
|
|
803
|
+
}
|
|
838
804
|
|
|
839
805
|
// Wait for the next tick so that crashed from the thread are logged first
|
|
840
806
|
setImmediate(() => {
|
|
841
|
-
|
|
842
|
-
|
|
807
|
+
const errorLabel = workersCount > 1 ? `worker ${index} of the service "${serviceId}"` : `service "${serviceId}"`
|
|
808
|
+
|
|
809
|
+
if (started && (!config.watch || code !== 0)) {
|
|
810
|
+
this.logger.warn(`The ${errorLabel} unexpectedly exited with code ${code}.`)
|
|
843
811
|
}
|
|
844
812
|
|
|
845
813
|
// Restart the service if it was started
|
|
846
814
|
if (started && this.#status === 'started') {
|
|
847
815
|
if (restartOnError > 0) {
|
|
848
|
-
this.logger.warn(`
|
|
849
|
-
this.#
|
|
850
|
-
this.logger.error({ err: ensureLoggableError(err) },
|
|
816
|
+
this.logger.warn(`The ${errorLabel} will be restarted in ${restartOnError}ms...`)
|
|
817
|
+
this.#restartCrashedWorker(config, serviceConfig, workersCount, serviceId, index, false, 0).catch(err => {
|
|
818
|
+
this.logger.error({ err: ensureLoggableError(err) }, `${errorLabel} could not be restarted.`)
|
|
851
819
|
})
|
|
852
820
|
} else {
|
|
853
|
-
this.logger.warn(`The
|
|
821
|
+
this.logger.warn(`The ${errorLabel} is no longer available.`)
|
|
854
822
|
}
|
|
855
823
|
}
|
|
856
824
|
})
|
|
857
825
|
})
|
|
858
826
|
|
|
859
|
-
|
|
860
|
-
|
|
827
|
+
worker[kId] = workersCount > 1 ? workerId : serviceId
|
|
828
|
+
worker[kServiceId] = serviceId
|
|
829
|
+
worker[kWorkerId] = workersCount > 1 ? index : undefined
|
|
830
|
+
worker[kConfig] = serviceConfig
|
|
831
|
+
worker[kLoggerDestination] = loggerDestination
|
|
832
|
+
worker[kLoggingPort] = loggingPort
|
|
833
|
+
|
|
834
|
+
if (inspectorOptions) {
|
|
835
|
+
worker[kInspectorOptions] = {
|
|
836
|
+
port: inspectorOptions.port,
|
|
837
|
+
id: serviceId,
|
|
838
|
+
dirname: this.#configManager.dirname
|
|
839
|
+
}
|
|
840
|
+
}
|
|
861
841
|
|
|
862
842
|
// Setup ITC
|
|
863
|
-
|
|
864
|
-
name:
|
|
865
|
-
port:
|
|
843
|
+
worker[kITC] = new ITC({
|
|
844
|
+
name: workerId + '-runtime',
|
|
845
|
+
port: worker,
|
|
866
846
|
handlers: {
|
|
867
847
|
getServiceMeta: this.getServiceMeta.bind(this),
|
|
868
|
-
|
|
848
|
+
listServices: () => {
|
|
849
|
+
return this.#servicesIds
|
|
850
|
+
}
|
|
869
851
|
}
|
|
870
852
|
})
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
//
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
853
|
+
worker[kITC].listen()
|
|
854
|
+
|
|
855
|
+
// Only activate watch for the first instance
|
|
856
|
+
if (index === 0) {
|
|
857
|
+
// Handle services changes
|
|
858
|
+
// This is not purposely activated on when this.#configManager.current.watch === true
|
|
859
|
+
// so that services can eventually manually trigger a restart. This mechanism is current
|
|
860
|
+
// used by the composer.
|
|
861
|
+
worker[kITC].on('changed', async () => {
|
|
862
|
+
try {
|
|
863
|
+
const wasStarted = worker[kWorkerStatus].startsWith('start')
|
|
864
|
+
await this.stopService(serviceId)
|
|
882
865
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
866
|
+
if (wasStarted) {
|
|
867
|
+
await this.startService(serviceId)
|
|
868
|
+
}
|
|
886
869
|
|
|
887
|
-
|
|
870
|
+
this.logger?.info(`Service "${serviceId}" has been successfully reloaded ...`)
|
|
888
871
|
|
|
889
|
-
|
|
890
|
-
|
|
872
|
+
if (serviceConfig.entrypoint) {
|
|
873
|
+
this.#showUrl()
|
|
874
|
+
}
|
|
875
|
+
} catch (e) {
|
|
876
|
+
this.logger?.error(e)
|
|
891
877
|
}
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
}
|
|
895
|
-
})
|
|
878
|
+
})
|
|
879
|
+
}
|
|
896
880
|
|
|
897
881
|
// Store locally
|
|
898
|
-
this.#
|
|
882
|
+
this.#workers.set(workerId, worker)
|
|
899
883
|
|
|
900
884
|
if (serviceConfig.entrypoint) {
|
|
901
|
-
this.#
|
|
902
|
-
this.#entrypointId = id
|
|
885
|
+
this.#entrypointId = serviceId
|
|
903
886
|
}
|
|
904
887
|
|
|
905
888
|
// Setup the interceptor
|
|
906
|
-
this.#interceptor.route(
|
|
889
|
+
this.#interceptor.route(serviceId, worker)
|
|
907
890
|
|
|
908
891
|
// Store dependencies
|
|
909
|
-
const [{ dependencies }] = await waitEventFromITC(
|
|
892
|
+
const [{ dependencies }] = await waitEventFromITC(worker, 'init')
|
|
893
|
+
worker[kWorkerStatus] = 'boot'
|
|
910
894
|
|
|
911
895
|
if (autoload) {
|
|
912
896
|
serviceConfig.dependencies = dependencies
|
|
@@ -918,11 +902,121 @@ class Runtime extends EventEmitter {
|
|
|
918
902
|
}
|
|
919
903
|
}
|
|
920
904
|
|
|
921
|
-
async #
|
|
922
|
-
const
|
|
923
|
-
const
|
|
905
|
+
async #startWorker (config, serviceConfig, workersCount, id, index, silent, bootstrapAttempt = 0) {
|
|
906
|
+
const workerId = `${id}:${index}`
|
|
907
|
+
const label = workersCount > 1 ? `worker ${index} of the service "${id}"` : `service "${id}"`
|
|
924
908
|
|
|
925
|
-
|
|
909
|
+
if (!silent) {
|
|
910
|
+
this.logger?.info(`Starting the ${label}...`)
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
let worker = await this.#getWorkerById(id, index, false, false)
|
|
914
|
+
|
|
915
|
+
// The service was stopped, recreate the thread
|
|
916
|
+
if (!worker) {
|
|
917
|
+
await this.#setupService(serviceConfig, index)
|
|
918
|
+
worker = await this.#getWorkerById(id, index)
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
worker[kWorkerStatus] = 'starting'
|
|
922
|
+
|
|
923
|
+
try {
|
|
924
|
+
const workerUrl = await sendViaITC(worker, 'start')
|
|
925
|
+
if (workerUrl) {
|
|
926
|
+
this.#url = workerUrl
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
worker[kWorkerStatus] = 'started'
|
|
930
|
+
|
|
931
|
+
if (!silent) {
|
|
932
|
+
this.logger?.info(`Started the ${label}...`)
|
|
933
|
+
}
|
|
934
|
+
} catch (error) {
|
|
935
|
+
// TODO: handle port allocation error here
|
|
936
|
+
if (error.code === 'EADDRINUSE') throw error
|
|
937
|
+
|
|
938
|
+
this.#cleanupWorker(workerId, worker)
|
|
939
|
+
|
|
940
|
+
if (worker[kWorkerStatus] !== 'exited') {
|
|
941
|
+
// This prevent the exit handler to restart service
|
|
942
|
+
worker[kWorkerStatus] = 'exited'
|
|
943
|
+
await worker.terminate()
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
this.logger.error({ err: ensureLoggableError(error) }, `Failed to start ${label}.`)
|
|
947
|
+
|
|
948
|
+
const restartOnError = config.restartOnError
|
|
949
|
+
|
|
950
|
+
if (!restartOnError) {
|
|
951
|
+
throw error
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (bootstrapAttempt++ >= MAX_BOOTSTRAP_ATTEMPTS || restartOnError === 0) {
|
|
955
|
+
this.logger.error(`Failed to start ${label} after ${MAX_BOOTSTRAP_ATTEMPTS} attempts.`)
|
|
956
|
+
throw error
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
this.logger.warn(
|
|
960
|
+
`Attempt ${bootstrapAttempt} of ${MAX_BOOTSTRAP_ATTEMPTS} to start the ${label} again will be performed in ${restartOnError}ms ...`
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
await this.#restartCrashedWorker(config, serviceConfig, workersCount, id, index, silent, bootstrapAttempt)
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
async #stopWorker (workersCount, id, index, silent) {
|
|
968
|
+
const worker = await this.#getWorkerById(id, index, false, false)
|
|
969
|
+
|
|
970
|
+
if (!worker) {
|
|
971
|
+
return
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
worker[kWorkerStatus] = 'stopping'
|
|
975
|
+
|
|
976
|
+
const label = workersCount > 1 ? `worker ${index} of the service "${id}"` : `service "${id}"`
|
|
977
|
+
|
|
978
|
+
if (!silent) {
|
|
979
|
+
this.logger?.info(`Stopping the ${label}...`)
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const exitPromise = once(worker, 'exit')
|
|
983
|
+
|
|
984
|
+
// Always send the stop message, it will shut down workers that only had ITC and interceptors setup
|
|
985
|
+
try {
|
|
986
|
+
await executeWithTimeout(sendViaITC(worker, 'stop'), 10000)
|
|
987
|
+
} catch (error) {
|
|
988
|
+
this.logger?.info({ error: ensureLoggableError(error) }, `Failed to stop ${label}. Killing a worker thread.`)
|
|
989
|
+
} finally {
|
|
990
|
+
worker[kITC].close()
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (!silent) {
|
|
994
|
+
this.logger?.info(`Stopped the ${label}...`)
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Wait for the worker thread to finish, we're going to create a new one if the service is ever restarted
|
|
998
|
+
const res = await executeWithTimeout(exitPromise, 10000)
|
|
999
|
+
|
|
1000
|
+
// If the worker didn't exit in time, kill it
|
|
1001
|
+
if (res === 'timeout') {
|
|
1002
|
+
await worker.terminate()
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
worker[kWorkerStatus] = 'stopped'
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
#cleanupWorker (workerId, worker) {
|
|
1009
|
+
this.#workers.delete(workerId)
|
|
1010
|
+
|
|
1011
|
+
worker[kITC].close()
|
|
1012
|
+
worker[kLoggerDestination].close()
|
|
1013
|
+
worker[kLoggingPort].close()
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
async #restartCrashedWorker (config, serviceConfig, workersCount, id, index, silent, bootstrapAttempt) {
|
|
1017
|
+
const workerId = `${id}:${index}`
|
|
1018
|
+
|
|
1019
|
+
let restartPromise = this.#restartingWorkers.get(workerId)
|
|
926
1020
|
if (restartPromise) {
|
|
927
1021
|
await restartPromise
|
|
928
1022
|
return
|
|
@@ -930,48 +1024,67 @@ class Runtime extends EventEmitter {
|
|
|
930
1024
|
|
|
931
1025
|
restartPromise = new Promise((resolve, reject) => {
|
|
932
1026
|
setTimeout(async () => {
|
|
933
|
-
this.#
|
|
1027
|
+
this.#restartingWorkers.delete(workerId)
|
|
934
1028
|
|
|
935
1029
|
try {
|
|
936
|
-
await this.#
|
|
937
|
-
|
|
938
|
-
const started = this.#startedServices.get(id)
|
|
939
|
-
if (started) {
|
|
940
|
-
this.#startedServices.set(id, false)
|
|
941
|
-
await this.startService(id)
|
|
942
|
-
}
|
|
1030
|
+
await this.#setupWorker(config, serviceConfig, workersCount, id, index)
|
|
1031
|
+
await this.#startWorker(config, serviceConfig, workersCount, id, index, silent, bootstrapAttempt)
|
|
943
1032
|
|
|
944
1033
|
resolve()
|
|
945
1034
|
} catch (err) {
|
|
1035
|
+
// The runtime was stopped while the restart was happening, ignore any error.
|
|
1036
|
+
if (!this.#status.startsWith('start')) {
|
|
1037
|
+
resolve()
|
|
1038
|
+
}
|
|
1039
|
+
|
|
946
1040
|
reject(err)
|
|
947
1041
|
}
|
|
948
1042
|
}, config.restartOnError)
|
|
949
1043
|
})
|
|
950
1044
|
|
|
951
|
-
this.#
|
|
1045
|
+
this.#restartingWorkers.set(workerId, restartPromise)
|
|
952
1046
|
await restartPromise
|
|
953
1047
|
}
|
|
954
1048
|
|
|
955
|
-
async #getServiceById (
|
|
956
|
-
|
|
1049
|
+
async #getServiceById (serviceId, ensureStarted = false, mustExist = true) {
|
|
1050
|
+
// If the serviceId includes the worker, properly split
|
|
1051
|
+
let workerId
|
|
1052
|
+
const matched = serviceId.match(/^(.+):(\d+)$/)
|
|
1053
|
+
|
|
1054
|
+
if (matched) {
|
|
1055
|
+
serviceId = matched[1]
|
|
1056
|
+
workerId = matched[2]
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
return this.#getWorkerById(serviceId, workerId, ensureStarted, mustExist)
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
async #getWorkerById (serviceId, workerId, ensureStarted = false, mustExist = true) {
|
|
1063
|
+
let worker
|
|
1064
|
+
|
|
1065
|
+
if (typeof workerId !== 'undefined') {
|
|
1066
|
+
worker = this.#workers.get(`${serviceId}:${workerId}`)
|
|
1067
|
+
} else {
|
|
1068
|
+
worker = this.#workers.next(serviceId)
|
|
1069
|
+
}
|
|
957
1070
|
|
|
958
|
-
if (!
|
|
959
|
-
if (!mustExist && this.#servicesIds.includes(
|
|
1071
|
+
if (!worker) {
|
|
1072
|
+
if (!mustExist && this.#servicesIds.includes(serviceId)) {
|
|
960
1073
|
return null
|
|
961
1074
|
}
|
|
962
1075
|
|
|
963
|
-
throw new errors.ServiceNotFoundError(
|
|
1076
|
+
throw new errors.ServiceNotFoundError(serviceId, Array.from(this.#servicesIds).join(', '))
|
|
964
1077
|
}
|
|
965
1078
|
|
|
966
1079
|
if (ensureStarted) {
|
|
967
|
-
const serviceStatus = await sendViaITC(
|
|
1080
|
+
const serviceStatus = await sendViaITC(worker, 'getStatus')
|
|
968
1081
|
|
|
969
1082
|
if (serviceStatus !== 'started') {
|
|
970
|
-
throw new errors.ServiceNotStartedError(
|
|
1083
|
+
throw new errors.ServiceNotStartedError(serviceId)
|
|
971
1084
|
}
|
|
972
1085
|
}
|
|
973
1086
|
|
|
974
|
-
return
|
|
1087
|
+
return worker
|
|
975
1088
|
}
|
|
976
1089
|
|
|
977
1090
|
async #getRuntimePackageJson () {
|