@platformatic/runtime 3.63.0 → 3.65.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/index.d.ts +131 -4
- package/index.js +2 -0
- package/lib/errors.js +12 -1
- package/lib/management-api.js +32 -2
- package/lib/management-handlers.js +20 -0
- package/lib/runtime.js +333 -50
- package/lib/scaling-algorithm.js +3 -3
- package/lib/scheduler.js +192 -60
- package/lib/worker/controller.js +2 -2
- package/lib/worker/itc.js +34 -4
- package/lib/worker/main.js +16 -1
- package/lib/worker/management.js +16 -0
- package/package.json +16 -16
- package/schema.json +1 -1
package/lib/runtime.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
executeInParallel,
|
|
6
6
|
executeWithTimeout,
|
|
7
7
|
features,
|
|
8
|
+
kEnvFileFallbackKeys,
|
|
8
9
|
kMetadata,
|
|
9
10
|
kTimeout,
|
|
10
11
|
parseMemorySize
|
|
@@ -93,6 +94,7 @@ const kInspectorOptions = Symbol('plt.runtime.worker.inspectorOptions')
|
|
|
93
94
|
const kHeapCheckCounter = Symbol('plt.runtime.worker.heapCheckCounter')
|
|
94
95
|
const kLastHeapStats = Symbol('plt.runtime.worker.lastHeapStats')
|
|
95
96
|
const kProfilingELUGates = Symbol('plt.runtime.worker.profilingELUGates')
|
|
97
|
+
const kWorkerScheduledTasks = Symbol('plt.runtime.worker.scheduledTasks')
|
|
96
98
|
const kHealthITCTimeoutMs = 5000
|
|
97
99
|
const kProfilingELUHysteresis = 0.1
|
|
98
100
|
const kLastProfileTimeoutMs = 10000
|
|
@@ -116,6 +118,10 @@ const kApplicationRestartsMetricHelp = 'Total number of restarts triggered by th
|
|
|
116
118
|
|
|
117
119
|
const MAX_LISTENERS_COUNT = 100
|
|
118
120
|
|
|
121
|
+
function hasWorkerIndex (applicationId) {
|
|
122
|
+
return /^.+:\d+$/.test(applicationId)
|
|
123
|
+
}
|
|
124
|
+
|
|
119
125
|
function parseOrigins (origins) {
|
|
120
126
|
if (!origins) return undefined
|
|
121
127
|
|
|
@@ -141,6 +147,24 @@ function formatMetricValue (value) {
|
|
|
141
147
|
return `${value}`
|
|
142
148
|
}
|
|
143
149
|
|
|
150
|
+
// Resolves the setup function of a runtime extension out of its module namespace.
|
|
151
|
+
//
|
|
152
|
+
// The canonical form is a default export, but transpilers and bundlers emit
|
|
153
|
+
// faux ESM modules, in which the real exports are properties of `module.exports`
|
|
154
|
+
// and therefore only reachable via an additional `default` hop. The setup
|
|
155
|
+
// function can also be exported as a named `setup` export, at both levels.
|
|
156
|
+
function resolveExtensionSetup (imported) {
|
|
157
|
+
const candidates = [imported?.default, imported?.setup, imported?.default?.default, imported?.default?.setup]
|
|
158
|
+
|
|
159
|
+
for (const candidate of candidates) {
|
|
160
|
+
if (typeof candidate === 'function') {
|
|
161
|
+
return candidate
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
|
|
144
168
|
// Always run operations in parallel to avoid deadlocks when services have dependencies
|
|
145
169
|
const DEFAULT_CONCURRENCY = availableParallelism() * 2
|
|
146
170
|
const MAX_BOOTSTRAP_ATTEMPTS = 5
|
|
@@ -352,8 +376,8 @@ export class Runtime extends EventEmitter {
|
|
|
352
376
|
await this.addApplications(this.#config.applications)
|
|
353
377
|
await this.#setDispatcher(config.undici)
|
|
354
378
|
|
|
355
|
-
if (
|
|
356
|
-
this.#scheduler = startScheduler(config.scheduler, this.#dispatcher, logger)
|
|
379
|
+
if (!this.#context.build) {
|
|
380
|
+
this.#scheduler = startScheduler(config.scheduler ?? [], this.#dispatcher, logger)
|
|
357
381
|
}
|
|
358
382
|
|
|
359
383
|
this.#updateStatus('init')
|
|
@@ -708,6 +732,7 @@ export class Runtime extends EventEmitter {
|
|
|
708
732
|
|
|
709
733
|
for (const application of applications) {
|
|
710
734
|
this.#dynamicWorkersScaler?.remove(application)
|
|
735
|
+
await this.#scheduler?.removeApplicationJobs(application)
|
|
711
736
|
this.#applications.delete(application)
|
|
712
737
|
this.#applicationRestartCounts.delete(application)
|
|
713
738
|
}
|
|
@@ -816,6 +841,8 @@ export class Runtime extends EventEmitter {
|
|
|
816
841
|
await this.#startWorker(config, applicationConfig, workers, id, i, silent)
|
|
817
842
|
}
|
|
818
843
|
|
|
844
|
+
await this.#registerApplicationSchedulerJobs(id)
|
|
845
|
+
|
|
819
846
|
this.emitAndNotify('application:started', id)
|
|
820
847
|
await this.#dynamicWorkersScaler?.applyPendingUpdate(id)
|
|
821
848
|
}
|
|
@@ -829,6 +856,7 @@ export class Runtime extends EventEmitter {
|
|
|
829
856
|
const workersCount = workersIds.length
|
|
830
857
|
|
|
831
858
|
this.emitAndNotify('application:stopping', id)
|
|
859
|
+
await this.#scheduler?.stopApplicationJobs(id)
|
|
832
860
|
|
|
833
861
|
if (typeof workersCount === 'number') {
|
|
834
862
|
const stopInvocations = []
|
|
@@ -882,6 +910,7 @@ export class Runtime extends EventEmitter {
|
|
|
882
910
|
await this.#replaceWorker(config, applicationConfig, workersCount, id, workerIndex, worker, true)
|
|
883
911
|
}
|
|
884
912
|
|
|
913
|
+
await this.#registerApplicationSchedulerJobs(id)
|
|
885
914
|
this.#incrementApplicationRestartCount(id)
|
|
886
915
|
this.emitAndNotify('application:restarted', id)
|
|
887
916
|
} finally {
|
|
@@ -907,17 +936,68 @@ export class Runtime extends EventEmitter {
|
|
|
907
936
|
}
|
|
908
937
|
|
|
909
938
|
async startApplicationProfiling (id, options = {}, ensureStarted = true) {
|
|
910
|
-
const service = await this.#getApplicationById(id, ensureStarted)
|
|
911
939
|
this.#validatePprofCapturePreload()
|
|
912
940
|
|
|
913
|
-
|
|
941
|
+
const { allWorkers, ...profilingOptions } = options
|
|
942
|
+
|
|
943
|
+
if (!allWorkers || hasWorkerIndex(id)) {
|
|
944
|
+
const service = await this.#getApplicationWorkerForProfiling(id, ensureStarted)
|
|
945
|
+
return sendViaITC(service, 'startProfiling', profilingOptions)
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const started = []
|
|
949
|
+
const alreadyProfiling = []
|
|
950
|
+
let firstError
|
|
951
|
+
|
|
952
|
+
for (const { workerIndex, worker } of await this.#getApplicationWorkersForProfiling(id, ensureStarted)) {
|
|
953
|
+
try {
|
|
954
|
+
await sendViaITC(worker, 'startProfiling', profilingOptions)
|
|
955
|
+
started.push(workerIndex)
|
|
956
|
+
} catch (error) {
|
|
957
|
+
// A worker which is already being profiled is considered covered, but
|
|
958
|
+
// if no other worker could be started the error is still reported.
|
|
959
|
+
if (error.code === 'PLT_PPROF_PROFILING_ALREADY_STARTED') {
|
|
960
|
+
alreadyProfiling.push(workerIndex)
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
firstError ??= error
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if (started.length === 0 && firstError) {
|
|
968
|
+
throw firstError
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
return { workers: started.concat(alreadyProfiling).sort((a, b) => a - b) }
|
|
914
972
|
}
|
|
915
973
|
|
|
916
974
|
async stopApplicationProfiling (id, options = {}, ensureStarted = true) {
|
|
917
|
-
const service = await this.#getApplicationById(id, ensureStarted)
|
|
918
975
|
this.#validatePprofCapturePreload()
|
|
919
976
|
|
|
920
|
-
|
|
977
|
+
const { allWorkers, ...profilingOptions } = options
|
|
978
|
+
|
|
979
|
+
if (!allWorkers || hasWorkerIndex(id)) {
|
|
980
|
+
const service = await this.#getApplicationWorkerForProfiling(id, ensureStarted)
|
|
981
|
+
return sendViaITC(service, 'stopProfiling', profilingOptions)
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const profiles = []
|
|
985
|
+
let firstError
|
|
986
|
+
|
|
987
|
+
for (const { workerIndex, worker } of await this.#getApplicationWorkersForProfiling(id, ensureStarted)) {
|
|
988
|
+
try {
|
|
989
|
+
const profile = await sendViaITC(worker, 'stopProfiling', profilingOptions)
|
|
990
|
+
profiles.push({ workerIndex, profile })
|
|
991
|
+
} catch (error) {
|
|
992
|
+
firstError ??= error
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
if (profiles.length === 0 && firstError) {
|
|
997
|
+
throw firstError
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return profiles
|
|
921
1001
|
}
|
|
922
1002
|
|
|
923
1003
|
async getApplicationLastProfile (id, options = {}, ensureStarted = true) {
|
|
@@ -929,39 +1009,45 @@ export class Runtime extends EventEmitter {
|
|
|
929
1009
|
|
|
930
1010
|
try {
|
|
931
1011
|
// Bound the whole retrieval with a single timeout budget: resolving the
|
|
932
|
-
//
|
|
933
|
-
//
|
|
1012
|
+
// workers round-trips to them when ensureStarted is set, and the profile
|
|
1013
|
+
// pulls do too — both hang if a worker event loop is blocked. Attach
|
|
934
1014
|
// a noop handler so that a late settlement after the timeout does not
|
|
935
1015
|
// surface as an unhandled rejection.
|
|
936
|
-
const pull = (
|
|
937
|
-
const service = await this.#getApplicationById(id, ensureStarted)
|
|
938
|
-
const result = await sendViaITC(service, 'getLastProfile', { ...options, includeTimestamp: true })
|
|
939
|
-
return { service, result }
|
|
940
|
-
})()
|
|
1016
|
+
const pull = this.#pullLastProfiles(id, options, ensureStarted)
|
|
941
1017
|
pull.catch(() => {})
|
|
942
1018
|
|
|
943
1019
|
const outcome = await executeWithTimeout(pull, timeout, kTimeout)
|
|
944
1020
|
|
|
945
1021
|
if (outcome !== kTimeout) {
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1022
|
+
let best = null
|
|
1023
|
+
|
|
1024
|
+
for (const { service, result } of outcome) {
|
|
1025
|
+
// An older capture module which does not support includeTimestamp
|
|
1026
|
+
// returns the raw profile.
|
|
1027
|
+
const value = result instanceof Uint8Array
|
|
1028
|
+
? { profile: result, timestamp: null, sampleCount: null }
|
|
1029
|
+
: { sampleCount: null, ...result }
|
|
1030
|
+
|
|
1031
|
+
// A strictly newer live window supersedes the preserved overload
|
|
1032
|
+
// profile: prune it so the preserved copy naturally expires once the
|
|
1033
|
+
// worker is healthy again and its profiles are being consumed.
|
|
1034
|
+
if (value.timestamp != null) {
|
|
1035
|
+
const key = `${service[kApplicationId]}:${service[kWorkerId]}:${type}`
|
|
1036
|
+
const preserved = this.#lastOverloadProfiles.get(key)
|
|
1037
|
+
|
|
1038
|
+
if (preserved && preserved.timestamp < value.timestamp) {
|
|
1039
|
+
this.#lastOverloadProfiles.delete(key)
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
958
1042
|
|
|
959
|
-
|
|
960
|
-
|
|
1043
|
+
// For an application-level id the newest window across the workers
|
|
1044
|
+
// wins, mirroring the preserved overload profile fallback below.
|
|
1045
|
+
if (!best || (value.timestamp != null && (best.timestamp == null || value.timestamp > best.timestamp))) {
|
|
1046
|
+
best = value
|
|
961
1047
|
}
|
|
962
1048
|
}
|
|
963
1049
|
|
|
964
|
-
return { ...
|
|
1050
|
+
return { ...best, preserved: false }
|
|
965
1051
|
}
|
|
966
1052
|
|
|
967
1053
|
// The worker event loop is not responding (e.g. it is hard-blocked).
|
|
@@ -979,12 +1065,73 @@ export class Runtime extends EventEmitter {
|
|
|
979
1065
|
if (preserved) {
|
|
980
1066
|
// The preserved flag lets consumers distinguish post-mortem evidence
|
|
981
1067
|
// from a live window and judge it together with the timestamp.
|
|
982
|
-
return {
|
|
1068
|
+
return {
|
|
1069
|
+
profile: preserved.profile,
|
|
1070
|
+
timestamp: preserved.timestamp,
|
|
1071
|
+
sampleCount: preserved.sampleCount,
|
|
1072
|
+
preserved: true
|
|
1073
|
+
}
|
|
983
1074
|
}
|
|
984
1075
|
|
|
985
1076
|
throw error
|
|
986
1077
|
}
|
|
987
1078
|
|
|
1079
|
+
// Pulls the last profile from the addressed worker, or from every worker of
|
|
1080
|
+
// the application when no explicit worker index is given: the application
|
|
1081
|
+
// "last profile" is the newest window among its workers, so one arbitrary
|
|
1082
|
+
// worker cannot answer for all of them. Per-worker failures with fallback
|
|
1083
|
+
// codes are ignored as long as at least one worker yields a profile.
|
|
1084
|
+
async #pullLastProfiles (id, options, ensureStarted) {
|
|
1085
|
+
const pullOptions = { ...options, includeTimestamp: true, includeSampleCount: true }
|
|
1086
|
+
|
|
1087
|
+
const pullWorker = async service => {
|
|
1088
|
+
const result = await sendViaITC(service, 'getLastProfile', pullOptions)
|
|
1089
|
+
return { service, result }
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
if (/^.+:\d+$/.test(id)) {
|
|
1093
|
+
return [await pullWorker(await this.#getApplicationById(id, ensureStarted))]
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
if (!this.#applications.has(id)) {
|
|
1097
|
+
throw new ApplicationNotFoundError(id, this.getApplicationsIds().join(', '))
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const keys = this.#workers.getKeys(id)
|
|
1101
|
+
|
|
1102
|
+
// No worker is currently registered: resolve the id as usual so that the
|
|
1103
|
+
// canonical error is raised.
|
|
1104
|
+
if (keys.length === 0) {
|
|
1105
|
+
return [await pullWorker(await this.#getApplicationById(id, ensureStarted))]
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const settled = await Promise.allSettled(
|
|
1109
|
+
keys.map(async key => {
|
|
1110
|
+
const service = await this.#getWorkerByIdOrNext(id, key.split(':')[1], ensureStarted)
|
|
1111
|
+
return pullWorker(service)
|
|
1112
|
+
})
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
const profiles = []
|
|
1116
|
+
let firstError
|
|
1117
|
+
|
|
1118
|
+
for (const outcome of settled) {
|
|
1119
|
+
if (outcome.status === 'fulfilled') {
|
|
1120
|
+
profiles.push(outcome.value)
|
|
1121
|
+
} else if (!kLastProfileFallbackCodes.has(outcome.reason?.code)) {
|
|
1122
|
+
throw outcome.reason
|
|
1123
|
+
} else {
|
|
1124
|
+
firstError ??= outcome.reason
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
if (profiles.length === 0) {
|
|
1129
|
+
throw firstError
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
return profiles
|
|
1133
|
+
}
|
|
1134
|
+
|
|
988
1135
|
// The final profile of an overload pause is pushed by the worker and
|
|
989
1136
|
// preserved in the main thread (see the profile:overload listener), so that
|
|
990
1137
|
// the evidence of what saturated a worker survives the worker being blocked
|
|
@@ -1888,17 +2035,27 @@ export class Runtime extends EventEmitter {
|
|
|
1888
2035
|
}
|
|
1889
2036
|
|
|
1890
2037
|
async getApplicationMeta (id) {
|
|
1891
|
-
const
|
|
2038
|
+
const hasWorkerId = /^.+:\d+$/.test(id)
|
|
2039
|
+
const attempts = hasWorkerId ? 1 : Math.max(1, this.#workers.getKeys(id).length)
|
|
1892
2040
|
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
} catch (e) {
|
|
1896
|
-
// The application exports no meta, return an empty object
|
|
1897
|
-
if (e.code === 'PLT_ITC_HANDLER_NOT_FOUND') {
|
|
1898
|
-
return {}
|
|
1899
|
-
}
|
|
2041
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
2042
|
+
const application = await this.#getApplicationById(id)
|
|
1900
2043
|
|
|
1901
|
-
|
|
2044
|
+
try {
|
|
2045
|
+
return await sendViaITC(application, 'getApplicationMeta')
|
|
2046
|
+
} catch (e) {
|
|
2047
|
+
// The application exports no meta, return an empty object
|
|
2048
|
+
if (e.code === 'PLT_ITC_HANDLER_NOT_FOUND') {
|
|
2049
|
+
return {}
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
// A parallel restart can stop the selected worker while metadata is
|
|
2053
|
+
// being retrieved. Retry another worker unless one was requested
|
|
2054
|
+
// explicitly or every worker available at the start has been tried.
|
|
2055
|
+
if (e.code !== 'PLT_RUNTIME_APPLICATION_WORKER_EXIT' || attempt === attempts - 1) {
|
|
2056
|
+
throw e
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
1902
2059
|
}
|
|
1903
2060
|
}
|
|
1904
2061
|
|
|
@@ -1973,6 +2130,79 @@ export class Runtime extends EventEmitter {
|
|
|
1973
2130
|
return sendViaITC(application, 'getApplicationGraphQLSchema')
|
|
1974
2131
|
}
|
|
1975
2132
|
|
|
2133
|
+
async getApplicationScheduledTasks (id) {
|
|
2134
|
+
const application = await this.#getApplicationById(id, true)
|
|
2135
|
+
|
|
2136
|
+
return sendViaITC(application, 'getApplicationScheduledTasks')
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
async runApplicationScheduledTasks (id, scheduleId, scheduledTime) {
|
|
2140
|
+
const application = await this.#getApplicationById(id, true)
|
|
2141
|
+
|
|
2142
|
+
return sendViaITC(application, 'runApplicationScheduledTasks', { scheduleId, scheduledTime })
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
getSchedulerJobs () {
|
|
2146
|
+
return this.#scheduler?.getJobs() ?? []
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
pauseSchedulerJob (name) {
|
|
2150
|
+
return this.#schedulerOrThrow().pauseJob(name)
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
resumeSchedulerJob (name) {
|
|
2154
|
+
return this.#schedulerOrThrow().resumeJob(name)
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
runSchedulerJob (name) {
|
|
2158
|
+
return this.#schedulerOrThrow().runJob(name)
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
#schedulerOrThrow () {
|
|
2162
|
+
if (!this.#scheduler) {
|
|
2163
|
+
throw new Error('The scheduler is not configured')
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
return this.#scheduler
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
async #registerApplicationSchedulerJobs (id) {
|
|
2170
|
+
if (!this.#scheduler) {
|
|
2171
|
+
return
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
const pausedJobs = new Set(
|
|
2175
|
+
this.#scheduler
|
|
2176
|
+
.getJobs()
|
|
2177
|
+
.filter(job => job.applicationId === id && job.paused)
|
|
2178
|
+
.map(job => job.name)
|
|
2179
|
+
)
|
|
2180
|
+
|
|
2181
|
+
await this.#scheduler.removeApplicationJobs(id)
|
|
2182
|
+
|
|
2183
|
+
const workerId = this.#workers.getKeys(id)[0]
|
|
2184
|
+
const schedules = this.#workers.get(workerId)?.[kWorkerScheduledTasks] ?? []
|
|
2185
|
+
for (const schedule of schedules) {
|
|
2186
|
+
const name = `${id}:${schedule.id}`
|
|
2187
|
+
this.#scheduler.addJob(
|
|
2188
|
+
{
|
|
2189
|
+
name,
|
|
2190
|
+
cron: schedule.cron,
|
|
2191
|
+
source: 'application',
|
|
2192
|
+
applicationId: id,
|
|
2193
|
+
scheduleId: schedule.id,
|
|
2194
|
+
tasks: schedule.tasks,
|
|
2195
|
+
maxRetries: 3
|
|
2196
|
+
},
|
|
2197
|
+
({ scheduledTime }) => this.runApplicationScheduledTasks(id, schedule.id, scheduledTime)
|
|
2198
|
+
)
|
|
2199
|
+
|
|
2200
|
+
if (pausedJobs.has(name)) {
|
|
2201
|
+
await this.#scheduler.pauseJob(name)
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
|
|
1976
2206
|
async getWorkers (includeRaw = false) {
|
|
1977
2207
|
const status = {}
|
|
1978
2208
|
|
|
@@ -2278,7 +2508,10 @@ export class Runtime extends EventEmitter {
|
|
|
2278
2508
|
codeRangeSizeMb
|
|
2279
2509
|
},
|
|
2280
2510
|
inspectorOptions,
|
|
2281
|
-
dirname: this.#root
|
|
2511
|
+
dirname: this.#root,
|
|
2512
|
+
// Keys of the worker environment which only come from an env file of the runtime:
|
|
2513
|
+
// the env file of the application is allowed to override those.
|
|
2514
|
+
envFileFallbackKeys: this.#env[kEnvFileFallbackKeys] ?? []
|
|
2282
2515
|
},
|
|
2283
2516
|
argv: applicationConfig.arguments,
|
|
2284
2517
|
execArgv,
|
|
@@ -2414,7 +2647,7 @@ export class Runtime extends EventEmitter {
|
|
|
2414
2647
|
// The event only carries metadata: the profile can be retrieved on demand
|
|
2415
2648
|
// via getApplicationLastProfile. We use emit instead of emitAndNotify since
|
|
2416
2649
|
// other workers are not interested in this event.
|
|
2417
|
-
worker[kITC].on('profile:captured', ({ type, timestamp }) => {
|
|
2650
|
+
worker[kITC].on('profile:captured', ({ type, timestamp, sampleCount }) => {
|
|
2418
2651
|
// A strictly newer completed window supersedes the preserved overload
|
|
2419
2652
|
// profile: once the worker is past the overload and producing windows
|
|
2420
2653
|
// again, its old evidence must not be served anymore.
|
|
@@ -2430,7 +2663,8 @@ export class Runtime extends EventEmitter {
|
|
|
2430
2663
|
application: applicationId,
|
|
2431
2664
|
worker: index,
|
|
2432
2665
|
type,
|
|
2433
|
-
timestamp
|
|
2666
|
+
timestamp,
|
|
2667
|
+
sampleCount: sampleCount ?? null
|
|
2434
2668
|
})
|
|
2435
2669
|
})
|
|
2436
2670
|
|
|
@@ -2492,8 +2726,12 @@ export class Runtime extends EventEmitter {
|
|
|
2492
2726
|
// the worker can be retrieved (see getApplicationLastProfile) even while
|
|
2493
2727
|
// the worker event loop is blocked, and it survives the worker being
|
|
2494
2728
|
// replaced by the health checks.
|
|
2495
|
-
worker[kITC].on('profile:overload', ({ type, timestamp, profile }) => {
|
|
2496
|
-
this.#lastOverloadProfiles.set(`${workerId}:${type}`, {
|
|
2729
|
+
worker[kITC].on('profile:overload', ({ type, timestamp, profile, sampleCount }) => {
|
|
2730
|
+
this.#lastOverloadProfiles.set(`${workerId}:${type}`, {
|
|
2731
|
+
profile,
|
|
2732
|
+
timestamp,
|
|
2733
|
+
sampleCount: sampleCount ?? null
|
|
2734
|
+
})
|
|
2497
2735
|
})
|
|
2498
2736
|
|
|
2499
2737
|
// Preserved overload profiles outlive their worker only for a grace
|
|
@@ -3000,22 +3238,24 @@ export class Runtime extends EventEmitter {
|
|
|
3000
3238
|
this.emitAndNotify('application:worker:starting', eventPayload)
|
|
3001
3239
|
|
|
3002
3240
|
try {
|
|
3003
|
-
let
|
|
3241
|
+
let workerStartResult
|
|
3004
3242
|
if (config.startTimeout > 0) {
|
|
3005
|
-
|
|
3243
|
+
workerStartResult = await executeWithTimeout(sendViaITC(worker, 'start'), config.startTimeout)
|
|
3006
3244
|
|
|
3007
|
-
if (
|
|
3245
|
+
if (workerStartResult === kTimeout) {
|
|
3008
3246
|
this.emitAndNotify('application:worker:startTimeout', eventPayload)
|
|
3009
3247
|
this.logger.error(`The ${label} failed to start in ${config.startTimeout}ms. Forcefully killing the thread.`)
|
|
3010
3248
|
worker.terminate()
|
|
3011
3249
|
throw new ApplicationStartTimeoutError(id, config.startTimeout)
|
|
3012
3250
|
}
|
|
3013
3251
|
} else {
|
|
3014
|
-
|
|
3252
|
+
workerStartResult = await sendViaITC(worker, 'start')
|
|
3015
3253
|
}
|
|
3016
3254
|
|
|
3017
3255
|
await this.#avoidOutOfOrderThreadLogs()
|
|
3018
3256
|
|
|
3257
|
+
const { url: workerUrl, scheduledTasks } = workerStartResult
|
|
3258
|
+
worker[kWorkerScheduledTasks] = scheduledTasks
|
|
3019
3259
|
if (workerUrl) {
|
|
3020
3260
|
this.#url = workerUrl
|
|
3021
3261
|
|
|
@@ -3338,7 +3578,17 @@ export class Runtime extends EventEmitter {
|
|
|
3338
3578
|
this.#workers.set(newWorkerId, newWorker)
|
|
3339
3579
|
|
|
3340
3580
|
// Add the worker to the mesh
|
|
3341
|
-
await this.#startWorker(
|
|
3581
|
+
await this.#startWorker(
|
|
3582
|
+
configForNewWorker,
|
|
3583
|
+
applicationConfig,
|
|
3584
|
+
workersCount,
|
|
3585
|
+
applicationId,
|
|
3586
|
+
newIndex,
|
|
3587
|
+
false,
|
|
3588
|
+
0,
|
|
3589
|
+
newWorker,
|
|
3590
|
+
true
|
|
3591
|
+
)
|
|
3342
3592
|
|
|
3343
3593
|
// Make sure the runtime hasn't been stopped in the meanwhile
|
|
3344
3594
|
if (this.#status !== 'started') {
|
|
@@ -3388,6 +3638,37 @@ export class Runtime extends EventEmitter {
|
|
|
3388
3638
|
return this.#getWorkerByIdOrNext(applicationId, workerId, ensureStarted, mustExist)
|
|
3389
3639
|
}
|
|
3390
3640
|
|
|
3641
|
+
// Profiling start and stop must address the same worker: resolve an id
|
|
3642
|
+
// without an explicit worker index to the first worker deterministically,
|
|
3643
|
+
// as the round-robin used by #getApplicationById would rotate to a
|
|
3644
|
+
// different worker between the two calls.
|
|
3645
|
+
async #getApplicationWorkerForProfiling (applicationId, ensureStarted) {
|
|
3646
|
+
if (hasWorkerIndex(applicationId)) {
|
|
3647
|
+
return this.#getApplicationById(applicationId, ensureStarted)
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
if (!this.#applications.has(applicationId)) {
|
|
3651
|
+
throw new ApplicationNotFoundError(applicationId, this.getApplicationsIds().join(', '))
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
const [firstWorker] = this.#workers.getKeys(applicationId)
|
|
3655
|
+
return this.#getWorkerByIdOrNext(applicationId, firstWorker?.split(':')[1], ensureStarted)
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
async #getApplicationWorkersForProfiling (applicationId, ensureStarted) {
|
|
3659
|
+
if (!this.#applications.has(applicationId)) {
|
|
3660
|
+
throw new ApplicationNotFoundError(applicationId, this.getApplicationsIds().join(', '))
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
const workers = []
|
|
3664
|
+
for (const key of this.#workers.getKeys(applicationId)) {
|
|
3665
|
+
const workerIndex = parseInt(key.split(':')[1], 10)
|
|
3666
|
+
workers.push({ workerIndex, worker: await this.#getWorkerByIdOrNext(applicationId, workerIndex, ensureStarted) })
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
return workers
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3391
3672
|
// This method can work in two modes: when workerId is provided, it will return the specific worker
|
|
3392
3673
|
// otherwise it will return the next available worker for the application.
|
|
3393
3674
|
async #getWorkerByIdOrNext (applicationId, workerId, ensureStarted = false, mustExist = true) {
|
|
@@ -4073,13 +4354,15 @@ export class Runtime extends EventEmitter {
|
|
|
4073
4354
|
for (const extension of extensions) {
|
|
4074
4355
|
const { path, options } = typeof extension === 'string' ? { path: extension } : extension
|
|
4075
4356
|
|
|
4076
|
-
let
|
|
4357
|
+
let imported
|
|
4077
4358
|
try {
|
|
4078
|
-
|
|
4359
|
+
imported = await import(pathToFileURL(path))
|
|
4079
4360
|
} catch (e) {
|
|
4080
4361
|
throw new FailedToLoadExtensionError(path, e.message, { cause: e })
|
|
4081
4362
|
}
|
|
4082
4363
|
|
|
4364
|
+
const setup = resolveExtensionSetup(imported)
|
|
4365
|
+
|
|
4083
4366
|
if (typeof setup !== 'function') {
|
|
4084
4367
|
throw new InvalidExtensionError(path)
|
|
4085
4368
|
}
|
package/lib/scaling-algorithm.js
CHANGED
|
@@ -83,7 +83,7 @@ export class ScalingAlgorithm {
|
|
|
83
83
|
|
|
84
84
|
const newWorkersCount = appMinWorkers - workersCount
|
|
85
85
|
totalWorkersCount += newWorkersCount
|
|
86
|
-
totalAvailableMemory
|
|
86
|
+
totalAvailableMemory -= newWorkersCount * avgHeapUsed
|
|
87
87
|
continue
|
|
88
88
|
}
|
|
89
89
|
|
|
@@ -96,7 +96,7 @@ export class ScalingAlgorithm {
|
|
|
96
96
|
|
|
97
97
|
const removedWorkersCount = workersCount - appMaxWorkers
|
|
98
98
|
totalWorkersCount -= removedWorkersCount
|
|
99
|
-
totalAvailableMemory
|
|
99
|
+
totalAvailableMemory += removedWorkersCount * avgHeapUsed
|
|
100
100
|
continue
|
|
101
101
|
}
|
|
102
102
|
|
|
@@ -111,7 +111,7 @@ export class ScalingAlgorithm {
|
|
|
111
111
|
|
|
112
112
|
const removedWorkersCount = 1
|
|
113
113
|
totalWorkersCount -= removedWorkersCount
|
|
114
|
-
totalAvailableMemory
|
|
114
|
+
totalAvailableMemory += removedWorkersCount * avgHeapUsed
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
}
|