@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/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 (config.scheduler && !this.#context.build) {
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
- return sendViaITC(service, 'startProfiling', options)
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
- return sendViaITC(service, 'stopProfiling', options)
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
- // worker round-trips to it when ensureStarted is set, and the profile
933
- // pull does too — both hang if the worker event loop is blocked. Attach
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 = (async () => {
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
- const { service, result } = outcome
947
-
948
- // An older capture module which does not support includeTimestamp
949
- // returns the raw profile.
950
- const value = result instanceof Uint8Array ? { profile: result, timestamp: null } : result
951
-
952
- // A strictly newer live window supersedes the preserved overload
953
- // profile: prune it so the preserved copy naturally expires once the
954
- // worker is healthy again and its profiles are being consumed.
955
- if (value.timestamp != null) {
956
- const key = `${service[kApplicationId]}:${service[kWorkerId]}:${type}`
957
- const preserved = this.#lastOverloadProfiles.get(key)
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
- if (preserved && preserved.timestamp < value.timestamp) {
960
- this.#lastOverloadProfiles.delete(key)
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 { ...value, preserved: false }
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 { profile: preserved.profile, timestamp: preserved.timestamp, preserved: true }
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 application = await this.#getApplicationById(id)
2038
+ const hasWorkerId = /^.+:\d+$/.test(id)
2039
+ const attempts = hasWorkerId ? 1 : Math.max(1, this.#workers.getKeys(id).length)
1892
2040
 
1893
- try {
1894
- return await sendViaITC(application, 'getApplicationMeta')
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
- throw e
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}`, { profile, timestamp })
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 workerUrl
3241
+ let workerStartResult
3004
3242
  if (config.startTimeout > 0) {
3005
- workerUrl = await executeWithTimeout(sendViaITC(worker, 'start'), config.startTimeout)
3243
+ workerStartResult = await executeWithTimeout(sendViaITC(worker, 'start'), config.startTimeout)
3006
3244
 
3007
- if (workerUrl === kTimeout) {
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
- workerUrl = await sendViaITC(worker, 'start')
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(configForNewWorker, applicationConfig, workersCount, applicationId, newIndex, false, 0, newWorker, true)
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 setup
4357
+ let imported
4077
4358
  try {
4078
- setup = (await import(pathToFileURL(path))).default
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
  }
@@ -83,7 +83,7 @@ export class ScalingAlgorithm {
83
83
 
84
84
  const newWorkersCount = appMinWorkers - workersCount
85
85
  totalWorkersCount += newWorkersCount
86
- totalAvailableMemory += newWorkersCount * avgHeapUsed
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 -= removedWorkersCount * avgHeapUsed
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 -= removedWorkersCount * avgHeapUsed
114
+ totalAvailableMemory += removedWorkersCount * avgHeapUsed
115
115
  }
116
116
  }
117
117
  }