@platformatic/runtime 3.71.1-alpha.0 → 4.0.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 +602 -673
- package/index.d.ts +22 -27
- package/index.js +290 -30
- package/lib/config.js +270 -367
- package/lib/errors.js +27 -13
- package/lib/generator.js +249 -127
- package/lib/logger.js +3 -3
- package/lib/management-api.js +22 -65
- package/lib/management-handlers.js +3 -5
- package/lib/memory.js +29 -0
- package/lib/policies.js +27 -0
- package/lib/predictive-scaling.js +910 -0
- package/lib/predictive-worker-scaler.js +378 -0
- package/lib/runtime.js +1081 -572
- package/lib/scheduler.js +6 -41
- package/lib/schema.js +77 -15
- package/lib/upgrade.js +16 -1
- package/lib/versions/v4.0.0.js +31 -0
- package/lib/worker/controller.js +89 -109
- package/lib/worker/interceptors.js +34 -34
- package/lib/worker/itc.js +248 -66
- package/lib/worker/main.js +50 -98
- package/lib/worker/messaging.js +42 -1
- package/lib/worker/round-robin-map.js +10 -3
- package/lib/worker/symbols.js +6 -2
- package/package.json +29 -20
- package/schema.json +252 -433
- package/lib/scaling-algorithm.js +0 -256
- package/lib/worker-scaler.js +0 -249
package/lib/scheduler.js
CHANGED
|
@@ -1,37 +1,24 @@
|
|
|
1
1
|
import { CronJob, validateCronExpression } from 'cron'
|
|
2
2
|
import { setTimeout } from 'node:timers/promises'
|
|
3
|
-
import {
|
|
4
|
-
import { DuplicateSchedulerJobError, SchedulerJobNotFoundError } from './errors.js'
|
|
3
|
+
import { DuplicateSchedulerJobError, InvalidSchedulerCronError, SchedulerJobNotFoundError } from './errors.js'
|
|
5
4
|
|
|
6
5
|
export class SchedulerService {
|
|
7
|
-
constructor (
|
|
6
|
+
constructor (logger) {
|
|
8
7
|
this.logger = logger
|
|
9
|
-
this.dispatcher = dispatcher
|
|
10
8
|
this.jobs = new Map()
|
|
11
9
|
this.started = false
|
|
12
|
-
|
|
13
|
-
for (const config of schedulerConfig) {
|
|
14
|
-
this.addJob({ ...config, source: 'config' })
|
|
15
|
-
}
|
|
16
10
|
}
|
|
17
11
|
|
|
18
12
|
addJob (config, executor) {
|
|
19
|
-
// Skip disabled schedulers
|
|
20
|
-
if (config.enabled === false) {
|
|
21
|
-
return null
|
|
22
|
-
}
|
|
23
|
-
|
|
24
13
|
// Validate cron expression
|
|
25
14
|
const validation = validateCronExpression(config.cron)
|
|
26
15
|
if (!validation.valid) {
|
|
27
|
-
throw new
|
|
16
|
+
throw new InvalidSchedulerCronError(config.cron, config.name)
|
|
28
17
|
}
|
|
29
18
|
|
|
30
19
|
// Set defaults for optional fields
|
|
31
20
|
const job = {
|
|
32
21
|
...config,
|
|
33
|
-
headers: config.headers || {},
|
|
34
|
-
body: config.body || {},
|
|
35
22
|
maxRetries: config.maxRetries ?? 3,
|
|
36
23
|
source: config.source ?? 'application',
|
|
37
24
|
paused: false,
|
|
@@ -182,29 +169,7 @@ export class SchedulerService {
|
|
|
182
169
|
)
|
|
183
170
|
await setTimeout(delay)
|
|
184
171
|
}
|
|
185
|
-
|
|
186
|
-
await scheduler.executor({ scheduledTime })
|
|
187
|
-
} else {
|
|
188
|
-
const headers = {
|
|
189
|
-
'x-retry-attempt': attempt + 1,
|
|
190
|
-
...scheduler.headers
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const bodyString = typeof scheduler.body === 'string' ? scheduler.body : JSON.stringify(scheduler.body)
|
|
194
|
-
const response = await request(scheduler.callbackUrl, {
|
|
195
|
-
method: scheduler.method,
|
|
196
|
-
headers,
|
|
197
|
-
body: bodyString,
|
|
198
|
-
dispatcher: this.dispatcher
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
// Consume the body to release the connection.
|
|
202
|
-
await response.body.dump()
|
|
203
|
-
|
|
204
|
-
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
205
|
-
throw new Error(`HTTP error ${response.statusCode}`)
|
|
206
|
-
}
|
|
207
|
-
}
|
|
172
|
+
await scheduler.executor({ scheduledTime })
|
|
208
173
|
|
|
209
174
|
this.logger.info(`Scheduler "${scheduler.name}" executed successfully`)
|
|
210
175
|
success = true
|
|
@@ -244,8 +209,8 @@ export class SchedulerService {
|
|
|
244
209
|
}
|
|
245
210
|
}
|
|
246
211
|
|
|
247
|
-
export function startScheduler (
|
|
248
|
-
const schedulerService = new SchedulerService(
|
|
212
|
+
export function startScheduler (logger) {
|
|
213
|
+
const schedulerService = new SchedulerService(logger)
|
|
249
214
|
schedulerService.start()
|
|
250
215
|
return schedulerService
|
|
251
216
|
}
|
package/lib/schema.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { schemaComponents } from '@platformatic/foundation'
|
|
3
|
+
import { schemaComponents } from '@platformatic/foundation/schema'
|
|
4
4
|
import { version } from './version.js'
|
|
5
5
|
|
|
6
6
|
const runtimeLogger = {
|
|
@@ -33,21 +33,13 @@ const runtimeLogger = {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
/*
|
|
37
|
+
The root logger is not the application one: it carries `captureStdio` and the `pino` key mapping,
|
|
38
|
+
which only the runtime reads. Naming them apart is what lets a person write the type of either.
|
|
39
|
+
*/
|
|
40
|
+
runtimeLogger.title = 'RuntimeLoggerOptions'
|
|
37
41
|
|
|
38
|
-
schemaComponents.runtimeProperties.
|
|
39
|
-
type: 'object',
|
|
40
|
-
additionalProperties: {
|
|
41
|
-
type: 'object',
|
|
42
|
-
properties: {
|
|
43
|
-
minWorkers: { type: 'number', minimum: 1 },
|
|
44
|
-
maxWorkers: { type: 'number', minimum: 1 },
|
|
45
|
-
scaleUpELU: { type: 'number', minimum: 0, maximum: 1 },
|
|
46
|
-
scaleDownELU: { type: 'number', minimum: 0, maximum: 1 }
|
|
47
|
-
},
|
|
48
|
-
additionalProperties: false
|
|
49
|
-
}
|
|
50
|
-
}
|
|
42
|
+
schemaComponents.runtimeProperties.logger = runtimeLogger
|
|
51
43
|
|
|
52
44
|
const platformaticRuntimeSchema = {
|
|
53
45
|
$id: `https://schemas.platformatic.dev/@platformatic/runtime/${version}.json`,
|
|
@@ -71,6 +63,76 @@ const platformaticRuntimeSchema = {
|
|
|
71
63
|
|
|
72
64
|
export const schema = platformaticRuntimeSchema
|
|
73
65
|
|
|
66
|
+
/*
|
|
67
|
+
The keys this schema does not implement. Validating one of these configurations against the legacy
|
|
68
|
+
schema accepted them and then ignored them, which is the worst of both: `envfile: './deploy.env'`
|
|
69
|
+
at the root looked like it was doing something. They survive on the legacy schema, which still
|
|
70
|
+
serves legacy projects, and inside migrate's legacy reader.
|
|
71
|
+
|
|
72
|
+
Root `envfile` is removed, not renamed — an entry may still declare one, and that property lives
|
|
73
|
+
on the application schema rather than here.
|
|
74
|
+
|
|
75
|
+
`$schema` is different in kind: machine writers of the plain-object form still stamp it, and the
|
|
76
|
+
loader reads it for version detection and strips it before validation. The schema refusing it is
|
|
77
|
+
what makes the strip load-bearing rather than decorative — a stamp that reached AJV would mean the
|
|
78
|
+
loader had skipped the step that checks the file is not a legacy one.
|
|
79
|
+
|
|
80
|
+
`verticalScaler` is the deprecated spelling of `workers`, kept on the legacy schema with a transform
|
|
81
|
+
that migrates it. There is now one spelling: a configuration that still says `verticalScaler` is told
|
|
82
|
+
so by the schema rather than being quietly rewritten, which is the only way the two cannot
|
|
83
|
+
disagree about which of them a project meant.
|
|
84
|
+
|
|
85
|
+
`services` and `web` are the same story: the legacy spellings of `applications`, folded by the shared
|
|
86
|
+
transform. Alone they fail the pipeline's topology check by name; beside an `applications` key
|
|
87
|
+
they would validate and be folded silently, so the schema refuses them instead.
|
|
88
|
+
*/
|
|
89
|
+
const removedKeys = ['envfile', 'strictEnv', '$schema', 'verticalScaler', 'services', 'web']
|
|
90
|
+
|
|
91
|
+
const { ...configurationProperties } = platformaticRuntimeSchema.properties
|
|
92
|
+
|
|
93
|
+
for (const key of removedKeys) {
|
|
94
|
+
delete configurationProperties[key]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/*
|
|
98
|
+
The legacy entry anyOf required an id beside the path or url, because ids were derived only for
|
|
99
|
+
autoloaded applications. Ids are now derived at every position -- explicit entries included, from the
|
|
100
|
+
package name and then the directory -- so requiring one would refuse an entry the loader handles.
|
|
101
|
+
What an entry genuinely needs is a place: a path or a url.
|
|
102
|
+
|
|
103
|
+
Copied rather than edited, because `applications.items` is shared with the legacy schema, where the
|
|
104
|
+
requirement still holds.
|
|
105
|
+
*/
|
|
106
|
+
const applicationEntryItems = {
|
|
107
|
+
...configurationProperties.applications.items,
|
|
108
|
+
anyOf: [{ required: ['path'] }, { required: ['url'] }]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
configurationProperties.applications = { ...configurationProperties.applications, items: applicationEntryItems }
|
|
112
|
+
|
|
113
|
+
/*
|
|
114
|
+
The singular shorthand: one application with runtime options, Level 1b. It is an application
|
|
115
|
+
entry whose identity and place the loader both supply -- the id derives from the package name
|
|
116
|
+
and the path defaults to the configuration file's own directory -- so it keeps the entry's
|
|
117
|
+
properties and drops the anyOf entirely.
|
|
118
|
+
*/
|
|
119
|
+
const { anyOf: _entryRequirements, ...applicationShorthand } = applicationEntryItems
|
|
120
|
+
|
|
121
|
+
configurationProperties.application = applicationShorthand
|
|
122
|
+
|
|
123
|
+
export const configurationSchema = {
|
|
124
|
+
...platformaticRuntimeSchema,
|
|
125
|
+
$id: `https://schemas.platformatic.dev/@platformatic/runtime/${version}-strict.json`,
|
|
126
|
+
properties: configurationProperties,
|
|
127
|
+
/*
|
|
128
|
+
The legacy anyOf listed the removed spellings, and it is decorative here anyway -- by the
|
|
129
|
+
time either validation pass runs, normalization has already defaulted `applications`. The gate
|
|
130
|
+
that actually asks the question is the pipeline's topology check, before that default; this
|
|
131
|
+
matches it so the schema and the loader tell the same story.
|
|
132
|
+
*/
|
|
133
|
+
anyOf: [{ required: ['autoload'] }, { required: ['applications'] }, { required: ['application'] }]
|
|
134
|
+
}
|
|
135
|
+
|
|
74
136
|
if (import.meta.main) {
|
|
75
137
|
console.log(JSON.stringify(platformaticRuntimeSchema, null, 2))
|
|
76
138
|
}
|
package/lib/upgrade.js
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
import { abstractLogger } from '@platformatic/foundation'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
+
import { lt } from 'semver'
|
|
3
4
|
import { semgrator } from 'semgrator'
|
|
4
5
|
|
|
5
6
|
export async function upgrade (logger, config, version) {
|
|
7
|
+
const runtimeLogger = logger?.child({ name: '@platformatic/runtime' }) ?? abstractLogger
|
|
8
|
+
|
|
9
|
+
const schema = config.$schema
|
|
10
|
+
const isRuntimeSchema =
|
|
11
|
+
!schema ||
|
|
12
|
+
/(?:^|\/)(?:@platformatic\/)?runtime(?:\/|$)/.test(schema) ||
|
|
13
|
+
/(?:^|\/)wattpm(?:\/|$)/.test(schema)
|
|
14
|
+
|
|
15
|
+
if (isRuntimeSchema && config.server && lt(version, '4.0.0')) {
|
|
16
|
+
runtimeLogger.warn(
|
|
17
|
+
'This runtime no longer supports a root server configuration. Move it into the configuration of the capability that owns the listener.'
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
6
21
|
const iterator = semgrator({
|
|
7
22
|
version,
|
|
8
23
|
path: join(import.meta.dirname, 'versions'),
|
|
9
24
|
input: config,
|
|
10
|
-
logger:
|
|
25
|
+
logger: runtimeLogger
|
|
11
26
|
})
|
|
12
27
|
|
|
13
28
|
let result
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
version: '3.99.0',
|
|
3
|
+
up (config) {
|
|
4
|
+
const schema = config.$schema
|
|
5
|
+
const isRuntimeSchema =
|
|
6
|
+
!schema ||
|
|
7
|
+
/(?:^|\/)(?:@platformatic\/)?runtime(?:\/|$)/.test(schema) ||
|
|
8
|
+
/(?:^|\/)wattpm(?:\/|$)/.test(schema)
|
|
9
|
+
|
|
10
|
+
// Standalone application configurations are wrapped by Runtime and pass
|
|
11
|
+
// through this upgrade chain. Their capability-owned server must survive.
|
|
12
|
+
if (!isRuntimeSchema) {
|
|
13
|
+
return config
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
for (const section of ['applications', 'services', 'web']) {
|
|
17
|
+
for (const application of config[section] ?? []) {
|
|
18
|
+
delete application.server
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (const application of Object.values(config.autoload?.mappings ?? {})) {
|
|
23
|
+
delete application.server
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
delete config.entrypoint
|
|
27
|
+
delete config.server
|
|
28
|
+
|
|
29
|
+
return config
|
|
30
|
+
}
|
|
31
|
+
}
|
package/lib/worker/controller.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
+
import { configurationFileNames } from '@platformatic/foundation/loader'
|
|
1
2
|
import {
|
|
2
|
-
convertApplicationNameToPrefix,
|
|
3
3
|
ensureLoggableError,
|
|
4
4
|
FileWatcher,
|
|
5
5
|
kHandledError,
|
|
6
|
-
|
|
7
|
-
loadConfiguration,
|
|
8
|
-
loadConfigurationModule,
|
|
9
|
-
mirrorGlobalDispatcherForBuiltinFetch
|
|
6
|
+
loadConfigurationModule
|
|
10
7
|
} from '@platformatic/foundation'
|
|
11
8
|
import {
|
|
12
9
|
getLogger,
|
|
@@ -18,38 +15,33 @@ import {
|
|
|
18
15
|
getOnHttpStatsRunning,
|
|
19
16
|
getOnHttpStatsSize
|
|
20
17
|
} from '@platformatic/globals'
|
|
18
|
+
import { importCapabilityPackage } from '@platformatic/basic'
|
|
21
19
|
import debounce from 'debounce'
|
|
22
20
|
import { EventEmitter } from 'node:events'
|
|
23
|
-
import {
|
|
21
|
+
import { getCompileCacheDir } from 'node:module'
|
|
24
22
|
import { resolve } from 'node:path'
|
|
25
23
|
import { getActiveResourcesInfo } from 'node:process'
|
|
26
24
|
import { workerData } from 'node:worker_threads'
|
|
27
|
-
import { getGlobalDispatcher
|
|
28
|
-
import {
|
|
25
|
+
import { getGlobalDispatcher } from 'undici'
|
|
26
|
+
import {
|
|
27
|
+
ApplicationAlreadyStartedError,
|
|
28
|
+
exitCodes,
|
|
29
|
+
InvalidApplicationModuleError,
|
|
30
|
+
RuntimeNotStartedError
|
|
31
|
+
} from '../errors.js'
|
|
29
32
|
import { getApplicationUrl } from '../utils.js'
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
* because the id to prefix conversion is not reversible ("with-logger" and "with_logger" share a
|
|
39
|
-
* prefix) and because plenty of _URL variables do not name an application at all: a connection string
|
|
40
|
-
* fragment like {VALKEY_URL} or a DSN like {PLT_DATABASE_URL} must keep resolving to nothing rather
|
|
41
|
-
* than silently becoming an HTTP URL.
|
|
42
|
-
*/
|
|
43
|
-
function buildApplicationUrlResolver (applications) {
|
|
44
|
-
const urls = new Map()
|
|
45
|
-
|
|
46
|
-
for (const { id } of applications ?? []) {
|
|
47
|
-
if (id) {
|
|
48
|
-
urls.set(`PLT_${convertApplicationNameToPrefix(id)}_URL`, getApplicationUrl(id))
|
|
33
|
+
import { installGlobalDispatcher, refreshGlobalDispatcher } from './interceptors.js'
|
|
34
|
+
|
|
35
|
+
function fetchApplicationUrl (applications, key) {
|
|
36
|
+
// Only named application placeholders may fall back to a mesh URL.
|
|
37
|
+
for (const application of applications) {
|
|
38
|
+
const name = application.id.toUpperCase().replaceAll(/[^A-Z0-9_]/g, '_')
|
|
39
|
+
if (key === `PLT_${name}_URL`) {
|
|
40
|
+
return getApplicationUrl(application.id)
|
|
49
41
|
}
|
|
50
42
|
}
|
|
51
43
|
|
|
52
|
-
return
|
|
44
|
+
return null
|
|
53
45
|
}
|
|
54
46
|
|
|
55
47
|
function handleUnhandled (app, event, listeners, timeout, err, ...args) {
|
|
@@ -69,23 +61,20 @@ function handleUnhandled (app, event, listeners, timeout, err, ...args) {
|
|
|
69
61
|
}
|
|
70
62
|
}
|
|
71
63
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
app.stop().catch(stopError => {
|
|
75
|
-
logger.debug({ err: ensureLoggableError(stopError) }, `Stopping the ${label} after the ${event} event failed.`)
|
|
64
|
+
app.stop().catch(err => {
|
|
65
|
+
logger.debug({ err: ensureLoggableError(err) }, `Stopping the ${label} after the ${event} event failed.`)
|
|
76
66
|
})
|
|
77
67
|
}
|
|
78
68
|
|
|
79
69
|
export class Controller extends EventEmitter {
|
|
80
70
|
#starting
|
|
81
71
|
#started
|
|
82
|
-
#listening
|
|
83
72
|
#watch
|
|
84
73
|
#fileWatcher
|
|
85
74
|
#debouncedRestart
|
|
86
75
|
#context
|
|
87
76
|
|
|
88
|
-
constructor (runtimeConfig, applicationConfig, workerId,
|
|
77
|
+
constructor (runtimeConfig, applicationConfig, workerId, metricsConfig) {
|
|
89
78
|
super()
|
|
90
79
|
this.runtimeConfig = runtimeConfig
|
|
91
80
|
this.applicationConfig = applicationConfig
|
|
@@ -94,12 +83,9 @@ export class Controller extends EventEmitter {
|
|
|
94
83
|
this.#watch = !!runtimeConfig.watch
|
|
95
84
|
this.#starting = false
|
|
96
85
|
this.#started = false
|
|
97
|
-
this.#listening = false
|
|
98
86
|
this.capability = null
|
|
99
87
|
this.#fileWatcher = null
|
|
100
88
|
|
|
101
|
-
const onMissingEnv = buildApplicationUrlResolver(runtimeConfig.applications)
|
|
102
|
-
|
|
103
89
|
this.#context = {
|
|
104
90
|
controller: this,
|
|
105
91
|
runtimeConfig: this.runtimeConfig,
|
|
@@ -107,20 +93,16 @@ export class Controller extends EventEmitter {
|
|
|
107
93
|
applicationId: this.applicationId,
|
|
108
94
|
workerId: this.workerId,
|
|
109
95
|
directory: this.applicationConfig.path,
|
|
96
|
+
sourcePath: this.applicationConfig.sourcePath,
|
|
110
97
|
dependencies: this.applicationConfig.dependencies,
|
|
111
|
-
isEntrypoint: this.applicationConfig.entrypoint,
|
|
112
98
|
isProduction: this.applicationConfig.isProduction,
|
|
113
|
-
|
|
99
|
+
tracingConfig: this.applicationConfig.tracing,
|
|
114
100
|
loggerConfig: runtimeConfig.logger,
|
|
115
101
|
metricsConfig,
|
|
116
|
-
serverConfig,
|
|
117
102
|
worker: workerData?.worker,
|
|
118
103
|
resourceLimits: workerData?.resourceLimits,
|
|
119
104
|
hasManagementApi: !!runtimeConfig.managementApi,
|
|
120
|
-
|
|
121
|
-
// Capabilities spread the whole context into their loadConfiguration call, which is what makes
|
|
122
|
-
// the configuration they load resolve application references like the throwaway load below.
|
|
123
|
-
onMissingEnv,
|
|
105
|
+
onMissingEnv: fetchApplicationUrl.bind(null, runtimeConfig.applications ?? [applicationConfig]),
|
|
124
106
|
strictEnv: runtimeConfig.strictEnv
|
|
125
107
|
}
|
|
126
108
|
}
|
|
@@ -154,31 +136,45 @@ export class Controller extends EventEmitter {
|
|
|
154
136
|
process.env.NODE_ENV = 'production'
|
|
155
137
|
}
|
|
156
138
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
139
|
+
/*
|
|
140
|
+
The configuration was evaluated exactly once, main-side, and this worker receives the
|
|
141
|
+
validated capability payload as data. There is no file to re-read and no schema to
|
|
142
|
+
rediscover — which is the whole point, since re-parsing per worker meant an application
|
|
143
|
+
with workers: 4 evaluated user code five times and could reach five different answers.
|
|
144
|
+
|
|
145
|
+
The capability is imported through the canonical resolution order, application-scoped first,
|
|
146
|
+
so the copy that runs here is the copy whose schema validated the payload main-side. When the
|
|
147
|
+
entry names an npm module, that module is the capability, resolved the same way.
|
|
148
|
+
*/
|
|
149
|
+
if (appConfig.resolvedConfig) {
|
|
150
|
+
const pkg = await importCapabilityPackage(appConfig.path, appConfig.module, {
|
|
151
|
+
runtimeScope: import.meta.filename
|
|
152
|
+
})
|
|
161
153
|
|
|
162
|
-
|
|
163
|
-
|
|
154
|
+
// A module application names its capability directly. If that package exports no `create`,
|
|
155
|
+
// name it rather than letting the call below throw a bare "pkg.create is not a function".
|
|
156
|
+
if (appConfig.module && typeof pkg.create !== 'function') {
|
|
157
|
+
throw new InvalidApplicationModuleError(appConfig.module)
|
|
164
158
|
}
|
|
165
|
-
}
|
|
166
159
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
strictEnv: false
|
|
160
|
+
/*
|
|
161
|
+
`resolved` says this object has already been through the loader: its environment was
|
|
162
|
+
layered main-side, it holds no placeholders, and it has been validated against this
|
|
163
|
+
capability's schema. It is what tells the capability apart from an embedder handing over
|
|
164
|
+
an object nobody has checked, which still gets validated on the way in.
|
|
165
|
+
*/
|
|
166
|
+
this.capability = await pkg.create(appConfig.path, appConfig.resolvedConfig, {
|
|
167
|
+
...this.#context,
|
|
168
|
+
resolved: true
|
|
177
169
|
})
|
|
178
|
-
const pkg = await loadConfigurationModule(appConfig.path, unvalidatedConfig)
|
|
179
|
-
this.capability = await pkg.create(appConfig.path, appConfig.config, this.#context)
|
|
180
|
-
// We could not find a configuration file, we use the bundle @platformatic/basic with the runtime to load it
|
|
181
170
|
} else {
|
|
171
|
+
/*
|
|
172
|
+
No payload at all, which the loader does not produce -- `prepareRuntimeApplication` gives
|
|
173
|
+
every entry a `resolvedConfig`, an empty object where there is nothing to say. What is
|
|
174
|
+
left here is an embedder constructing a Controller by hand, and the bundled base
|
|
175
|
+
capability is the answer for that: there is no configuration file to look for, because the
|
|
176
|
+
runtime decides that main-side and hands the result over.
|
|
177
|
+
*/
|
|
182
178
|
const pkg = await loadConfigurationModule(resolve(import.meta.dirname, '../..'), {}, '@platformatic/basic')
|
|
183
179
|
this.capability = await pkg.create(appConfig.path, {}, this.#context)
|
|
184
180
|
}
|
|
@@ -219,6 +215,7 @@ export class Controller extends EventEmitter {
|
|
|
219
215
|
|
|
220
216
|
try {
|
|
221
217
|
await this.capability.init?.()
|
|
218
|
+
|
|
222
219
|
this.emit('init')
|
|
223
220
|
} catch (err) {
|
|
224
221
|
this.#logAndThrow(err)
|
|
@@ -228,7 +225,6 @@ export class Controller extends EventEmitter {
|
|
|
228
225
|
return
|
|
229
226
|
}
|
|
230
227
|
|
|
231
|
-
this.#updateCapabilityStatus('starting')
|
|
232
228
|
this.emit('starting')
|
|
233
229
|
|
|
234
230
|
if (this.#watch) {
|
|
@@ -245,17 +241,13 @@ export class Controller extends EventEmitter {
|
|
|
245
241
|
}
|
|
246
242
|
}
|
|
247
243
|
|
|
248
|
-
const listen = !!(this.applicationConfig.useHttp || this.applicationConfig.websocket)
|
|
249
|
-
|
|
250
244
|
try {
|
|
251
|
-
await this.capability.start(
|
|
245
|
+
await this.capability.start()
|
|
252
246
|
if (refreshGlobalDispatcher()) {
|
|
253
247
|
this.#updateDispatcher()
|
|
254
248
|
}
|
|
255
|
-
this.#listening = listen
|
|
256
249
|
/* c8 ignore next 5 */
|
|
257
250
|
} catch (err) {
|
|
258
|
-
this.#updateCapabilityStatus('start:error')
|
|
259
251
|
this.emit('start:error', err)
|
|
260
252
|
|
|
261
253
|
this.capability.log({ message: err.message, level: 'debug' })
|
|
@@ -266,53 +258,36 @@ export class Controller extends EventEmitter {
|
|
|
266
258
|
this.#started = true
|
|
267
259
|
this.#starting = false
|
|
268
260
|
|
|
269
|
-
this.#updateCapabilityStatus('started')
|
|
270
261
|
this.emit('started')
|
|
271
262
|
}
|
|
272
263
|
|
|
273
|
-
|
|
264
|
+
getUrl () {
|
|
265
|
+
return this.capability.getUrl()
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async stop (force = false, dependents = [], shutdownTimeout) {
|
|
274
269
|
if (!force && (!this.#started || this.#starting)) {
|
|
275
270
|
throw new RuntimeNotStartedError()
|
|
276
271
|
}
|
|
277
272
|
|
|
278
273
|
this.emit('stopping')
|
|
279
|
-
// Do not update status of the capability to "stopping" here otherwise
|
|
280
|
-
// if stop is called before start is finished, the capability will not
|
|
281
|
-
// be able to wait for start to finish and it will create a race condition.
|
|
282
274
|
|
|
283
275
|
await this.#stopFileWatching()
|
|
284
276
|
await this.capability.waitForDependentsStop(dependents)
|
|
277
|
+
this.capability.shutdownTimeout = shutdownTimeout
|
|
285
278
|
await this.capability.stop()
|
|
286
279
|
|
|
287
280
|
this.#started = false
|
|
288
281
|
this.#starting = false
|
|
289
|
-
this.#listening = false
|
|
290
282
|
|
|
291
|
-
this.#updateCapabilityStatus('stopped')
|
|
292
283
|
this.emit('stopped')
|
|
293
284
|
}
|
|
294
285
|
|
|
295
|
-
async listen () {
|
|
296
|
-
// This server is not an entrypoint or already listened in start. Behave as no-op.
|
|
297
|
-
if (
|
|
298
|
-
!this.applicationConfig.entrypoint ||
|
|
299
|
-
this.applicationConfig.useHttp ||
|
|
300
|
-
this.applicationConfig.websocket ||
|
|
301
|
-
this.#listening
|
|
302
|
-
) {
|
|
303
|
-
return
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
await this.capability.start({ listen: true })
|
|
307
|
-
}
|
|
308
|
-
|
|
309
286
|
async getMetrics ({ format }) {
|
|
310
287
|
const dispatcher = getGlobalDispatcher()
|
|
311
288
|
const onHttpStatsFree = getOnHttpStatsFree({ throwOnMissing: false })
|
|
312
289
|
|
|
313
290
|
if (onHttpStatsFree && dispatcher?.stats) {
|
|
314
|
-
// The capability might come from an older version of @platformatic/basic
|
|
315
|
-
// which registered these globals without the fields tracking, so never throw.
|
|
316
291
|
const onHttpStatsConnected = getOnHttpStatsConnected({ throwOnMissing: false })
|
|
317
292
|
const onHttpStatsPending = getOnHttpStatsPending({ throwOnMissing: false })
|
|
318
293
|
const onHttpStatsQueued = getOnHttpStatsQueued({ throwOnMissing: false })
|
|
@@ -352,11 +327,28 @@ export class Controller extends EventEmitter {
|
|
|
352
327
|
return
|
|
353
328
|
}
|
|
354
329
|
|
|
330
|
+
/*
|
|
331
|
+
The application's own configuration file is not the worker's to watch. The runtime evaluates
|
|
332
|
+
configuration once, main-side, and watches every file that evaluation read -- so a change to
|
|
333
|
+
one is a configuration change and the runtime reloads for it.
|
|
334
|
+
|
|
335
|
+
Watching it here as well meant both fired for a single edit: the runtime tore itself down and
|
|
336
|
+
rebuilt while this worker restarted the same application, and the rebuilt runtime could not
|
|
337
|
+
bind its management socket because the one it replaced had not released it yet.
|
|
338
|
+
*/
|
|
355
339
|
const fileWatcher = new FileWatcher({
|
|
356
340
|
path: watch.path,
|
|
357
341
|
/* c8 ignore next 2 */
|
|
358
342
|
allowToWatch: watch?.allow,
|
|
359
|
-
watchIgnore: watch?.ignore || []
|
|
343
|
+
watchIgnore: [...(watch?.ignore || []), ...configurationFileNames],
|
|
344
|
+
// Cache flushes are runtime output, not source edits. Cover both worker and command caches,
|
|
345
|
+
// including an already-enabled cache inherited from the environment.
|
|
346
|
+
watchIgnorePaths: [
|
|
347
|
+
getCompileCacheDir(),
|
|
348
|
+
resolve(this.applicationConfig.path, '.plt', 'compile-cache'),
|
|
349
|
+
this.applicationConfig.compileCache?.directory ?? this.runtimeConfig.compileCache?.directory,
|
|
350
|
+
this.capability.config?.compileCache?.directory
|
|
351
|
+
].filter(Boolean)
|
|
360
352
|
})
|
|
361
353
|
|
|
362
354
|
fileWatcher.on('update', this.#debouncedRestart)
|
|
@@ -387,8 +379,8 @@ export class Controller extends EventEmitter {
|
|
|
387
379
|
}
|
|
388
380
|
|
|
389
381
|
#updateDispatcher () {
|
|
390
|
-
const
|
|
391
|
-
const telemetryId =
|
|
382
|
+
const tracingConfig = this.#context.tracingConfig
|
|
383
|
+
const telemetryId = tracingConfig?.applicationName
|
|
392
384
|
|
|
393
385
|
const interceptor = dispatch => {
|
|
394
386
|
return function InterceptedDispatch (opts, handler) {
|
|
@@ -404,9 +396,7 @@ export class Controller extends EventEmitter {
|
|
|
404
396
|
|
|
405
397
|
const dispatcher = getGlobalDispatcher().compose(interceptor)
|
|
406
398
|
|
|
407
|
-
|
|
408
|
-
setGlobalDispatcher(dispatcher)
|
|
409
|
-
mirrorGlobalDispatcherForBuiltinFetch(dispatcher)
|
|
399
|
+
installGlobalDispatcher(dispatcher)
|
|
410
400
|
}
|
|
411
401
|
|
|
412
402
|
#setupHandlers (timeout) {
|
|
@@ -431,14 +421,4 @@ export class Controller extends EventEmitter {
|
|
|
431
421
|
}
|
|
432
422
|
})
|
|
433
423
|
}
|
|
434
|
-
|
|
435
|
-
#updateCapabilityStatus (status) {
|
|
436
|
-
if (typeof this.capability.updateStatus === 'function') {
|
|
437
|
-
this.capability.updateStatus(status)
|
|
438
|
-
} else {
|
|
439
|
-
// This is horrible but needed for backward compatibility
|
|
440
|
-
this.capability.status = status
|
|
441
|
-
this.capability.emit(status)
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
424
|
}
|