@intelligems/sst 2.49.6-ig.9 → 2.49.8-ig.2

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.
Files changed (44) hide show
  1. package/cli/commands/dev.js +10 -6
  2. package/constructs/AstroSite.d.ts +1 -1
  3. package/constructs/EdgeFunction.d.ts +1 -1
  4. package/constructs/EdgeFunction.js +8 -6
  5. package/constructs/Function.d.ts +3 -2
  6. package/constructs/Function.js +2 -1
  7. package/constructs/Job.d.ts +2 -2
  8. package/constructs/Job.js +4 -2
  9. package/constructs/NextjsSite.d.ts +1 -1
  10. package/constructs/NextjsSite.js +2 -2
  11. package/constructs/RemixSite.d.ts +1 -1
  12. package/constructs/SolidStartSite.d.ts +1 -1
  13. package/constructs/SsrFunction.d.ts +2 -2
  14. package/constructs/SsrFunction.js +7 -5
  15. package/constructs/SsrSite.d.ts +2 -2
  16. package/constructs/SsrSite.js +1 -1
  17. package/constructs/Stack.d.ts +1 -1
  18. package/constructs/Stack.js +1 -1
  19. package/constructs/SvelteKitSite.d.ts +1 -1
  20. package/constructs/deprecated/NextjsSite.d.ts +3 -3
  21. package/constructs/deprecated/NextjsSite.js +4 -1
  22. package/constructs/deprecated/cross-region-helper.js +3 -3
  23. package/package.json +3 -3
  24. package/runtime/handlers/node.js +24 -2
  25. package/runtime/handlers.d.ts +4 -0
  26. package/runtime/memory-logging.d.ts +26 -0
  27. package/runtime/memory-logging.js +112 -0
  28. package/runtime/mono-build-config.d.ts +6 -3
  29. package/runtime/mono-build-config.js +34 -10
  30. package/runtime/server.js +35 -44
  31. package/runtime/stdout-attribution.d.ts +18 -0
  32. package/runtime/stdout-attribution.js +43 -0
  33. package/runtime/worker-config.d.ts +19 -0
  34. package/runtime/worker-config.js +26 -0
  35. package/runtime/worker-pool-logging.d.ts +2 -2
  36. package/runtime/worker-pool-logging.js +5 -5
  37. package/runtime/worker-pool.d.ts +77 -0
  38. package/runtime/worker-pool.js +162 -0
  39. package/runtime/workers.d.ts +33 -11
  40. package/runtime/workers.js +405 -513
  41. package/support/nodejs-runtime/index.mjs +214 -100
  42. package/watcher.js +2 -0
  43. package/README.md +0 -43
  44. package/package.json.bak +0 -156
@@ -7,13 +7,15 @@ import { useRuntimeServerConfig, useRuntimeServer } from "./server.js";
7
7
  import { useFunctions } from "../constructs/Function.js";
8
8
  import { lazy } from "../util/lazy.js";
9
9
  import { Logger } from "../logger.js";
10
- import { POOL_SIZE, IDLE_TIMEOUT, logPool, logInvokeTrace, trackRequestStart, trackRequestEnd, setFunctionNameResolver, writeSessionEndSummary, } from "./worker-pool-logging.js";
10
+ import { logPool, logInvokeTrace, trackRequestStart, trackRequestEnd, setFunctionNameResolver, writeSessionEndSummary, } from "./worker-pool-logging.js";
11
+ import { POOL_SIZE, IDLE_TIMEOUT, WORKER_CONCURRENCY, DEBUG_MEMORY, } from "./worker-config.js";
12
+ import { WorkerPool } from "./worker-pool.js";
13
+ import { splitAttributed } from "./stdout-attribution.js";
14
+ import { startMemorySampling } from "./memory-logging.js";
11
15
  import { useMonoBuildConfig, isMonoBuildPath } from "./mono-build-config.js";
12
16
  import { getRequestPath, getCorrelationId, getApiGatewayRequestId } from "./request-utils.js";
13
17
  import { logWorkers } from "./debug-bridge-logging.js";
14
18
  import { logEventTrace } from "./event-trace-logging.js";
15
- // Track workers marked as stale (should not return to pool after completion)
16
- const staleWorkers = new Set();
17
19
  const bundleMtimes = new Map();
18
20
  const bundleWatchers = new Map();
19
21
  // Clean up watchers on exit
@@ -76,7 +78,6 @@ function getBundleMtime(buildOut) {
76
78
  // Helper: Get pool key for worker lookup
77
79
  // For mono-build, uses shared key so any warm worker can serve any handler
78
80
  function getPoolKey(functionID, runtime, buildOut) {
79
- // Use the global mono build config for pool key calculation
80
81
  return useMonoBuildConfig().getPoolKey(functionID, runtime, buildOut);
81
82
  }
82
83
  // Extract readable function name from handler path
@@ -100,6 +101,7 @@ const POOLABLE_RUNTIMES = new Set([
100
101
  "nodejs18.x",
101
102
  "nodejs20.x",
102
103
  "nodejs22.x",
104
+ "nodejs24.x",
103
105
  // Python - has while True loop in python-runtime/runtime.py
104
106
  "python",
105
107
  "python3.7",
@@ -135,25 +137,35 @@ function isPoolableRuntime(runtime, env) {
135
137
  return (POOLABLE_RUNTIMES.has(runtime) ||
136
138
  [...POOLABLE_RUNTIMES].some((r) => runtime.startsWith(r)));
137
139
  }
140
+ /** Only the Node runtime shim knows how to run invocations side by side. */
141
+ function concurrencyFor(runtime) {
142
+ return runtime.startsWith("nodejs") ? WORKER_CONCURRENCY : 1;
143
+ }
138
144
  export const useRuntimeWorkers = lazy(async () => {
139
145
  // Set up function name resolver for logging module
140
146
  setFunctionNameResolver(getFunctionName);
141
147
  // Non-pooled workers (legacy behavior)
142
148
  const workers = new Map();
143
- // Worker pool data structures
144
- const workerPool = new Map();
145
- const activeWorkers = new Map();
146
- const workerIDMapping = new Map(); // awsWorkerIDpooledWorkerID
147
- const reverseMapping = new Map(); // pooledWorkerID → awsWorkerID
148
- const startedWorkers = new Set(); // Track started pooledWorkerIDs
149
+ // Pooled workers and the invocations they hold
150
+ const requests = new Map(); // requestID → request
151
+ const lastRequestId = new Map(); // workerID → most recent requestID
152
+ const waitQueues = new Map(); // poolKeywaiting
149
153
  const bus = useBus();
150
154
  const handlers = useRuntimeHandlers();
151
155
  const builder = useFunctionBuilder();
152
156
  const serverConfig = await useRuntimeServerConfig();
157
+ const pool = new WorkerPool({
158
+ maxWorkers: POOL_SIZE,
159
+ idleTimeoutMs: IDLE_TIMEOUT,
160
+ onTerminate: (worker, reason) => {
161
+ void stopPooledWorker(worker, reason);
162
+ },
163
+ });
153
164
  // Log pool configuration on startup
154
165
  logPool("INIT", {
155
166
  poolSize: POOL_SIZE,
156
167
  idleTimeoutMs: IDLE_TIMEOUT,
168
+ concurrency: WORKER_CONCURRENCY,
157
169
  poolableRuntimes: [...POOLABLE_RUNTIMES].length,
158
170
  });
159
171
  // Lazy getter for server to avoid circular initialization
@@ -164,219 +176,270 @@ export const useRuntimeWorkers = lazy(async () => {
164
176
  }
165
177
  return _server;
166
178
  }
167
- // Helper: Terminate a pooled worker
168
- async function terminatePooledWorker(pooledWorkerID, reason) {
169
- const worker = activeWorkers.get(pooledWorkerID) ||
170
- [...workerPool.values()]
171
- .flat()
172
- .find((w) => w.pooledWorkerID === pooledWorkerID);
173
- if (!worker)
174
- return;
175
- const props = useFunctions().fromID(worker.functionID);
176
- if (!props)
177
- return;
178
- const uptime = Date.now() - worker.createdAt;
179
+ function queuedCount(poolKey) {
180
+ if (poolKey)
181
+ return waitQueues.get(poolKey)?.length ?? 0;
182
+ let total = 0;
183
+ for (const q of waitQueues.values())
184
+ total += q.length;
185
+ return total;
186
+ }
187
+ // Helper: stop a pooled worker that the pool has already forgotten
188
+ async function stopPooledWorker(worker, reason) {
179
189
  logPool("TERMINATE", {
180
- pooledWorkerID: pooledWorkerID.slice(0, 8),
190
+ pooledWorkerID: worker.id.slice(0, 8),
181
191
  functionID: worker.functionID,
182
- reason: reason || "unknown",
183
- uptimeMs: uptime,
192
+ reason,
193
+ uptimeMs: Date.now() - worker.createdAt,
194
+ inFlight: worker.inFlight,
184
195
  });
185
- const handler = handlers.for(props.runtime);
186
- await handler?.stopWorker(pooledWorkerID);
187
- // Clean up mappings
188
- activeWorkers.delete(pooledWorkerID);
189
- staleWorkers.delete(pooledWorkerID);
190
- const awsWorkerID = reverseMapping.get(pooledWorkerID);
191
- if (awsWorkerID) {
192
- workerIDMapping.delete(awsWorkerID);
196
+ try {
197
+ const handler = handlers.for(worker.runtime);
198
+ await handler?.stopWorker(worker.id);
199
+ }
200
+ catch (ex) {
201
+ Logger.debug("Failed to stop pooled worker", worker.id, ex);
193
202
  }
194
- reverseMapping.delete(pooledWorkerID);
195
- startedWorkers.delete(pooledWorkerID);
196
- lastRequestId.delete(pooledWorkerID);
197
- Logger.debug("Terminated pooled worker", pooledWorkerID);
203
+ lastRequestId.delete(worker.id);
204
+ Logger.debug("Terminated pooled worker", worker.id);
205
+ // A slot opened up
206
+ void drain(worker.poolKey);
198
207
  }
199
- // Helper: Get idle worker from pool
200
- // Uses poolKey for lookup (shared key for mono-build)
201
- function getIdleWorker(poolKey, functionID, buildOut) {
202
- const pool = workerPool.get(poolKey);
203
- if (!pool || pool.length === 0) {
204
- logPool("POOL_MISS", {
205
- functionID,
206
- poolKey: poolKey.slice(0, 30),
207
- poolSize: 0,
208
+ // Helper: publish an error for every invocation a worker was holding
209
+ function failRequestsOn(pooledWorkerID, errorType, errorMessage) {
210
+ for (const req of [...requests.values()]) {
211
+ if (req.pooledWorkerID !== pooledWorkerID)
212
+ continue;
213
+ requests.delete(req.requestID);
214
+ trackRequestEnd(req.functionID);
215
+ bus.publish("function.error", {
216
+ workerID: req.awsWorkerID,
217
+ functionID: req.functionID,
218
+ requestID: req.requestID,
219
+ errorType,
220
+ errorMessage,
221
+ trace: [],
208
222
  });
209
- return undefined;
210
223
  }
211
- // Check current bundle mtime for staleness detection
212
- const currentMtime = getBundleMtime(buildOut);
213
- // Try to find a non-stale worker
214
- while (pool.length > 0) {
215
- const worker = pool.pop();
216
- if (!worker)
217
- break;
218
- clearTimeout(worker.idleTimer);
219
- // Check if worker is stale (bundle was modified since worker started)
220
- if (currentMtime && worker.bundleMtime && currentMtime > worker.bundleMtime) {
221
- logPool("STALE_MTIME", {
222
- pooledWorkerID: worker.pooledWorkerID.slice(0, 8),
223
- functionID,
224
- workerMtime: worker.bundleMtime,
225
- currentMtime,
226
- });
227
- terminatePooledWorker(worker.pooledWorkerID, "stale_mtime");
228
- continue; // Try next worker
229
- }
230
- worker.state = "busy";
231
- const age = Date.now() - worker.createdAt;
232
- const crossFunction = worker.functionID !== functionID;
224
+ }
225
+ // Helper: hand an invocation to a worker (new or reused)
226
+ async function assign(worker, pending, reused) {
227
+ const { evt } = pending;
228
+ const { workerID: awsWorkerID, functionID, requestID, event } = evt.properties;
229
+ const requestPath = getRequestPath(event);
230
+ pool.checkout(worker, functionID);
231
+ requests.set(requestID, {
232
+ requestID,
233
+ awsWorkerID,
234
+ functionID,
235
+ pooledWorkerID: worker.id,
236
+ startedAt: Date.now(),
237
+ });
238
+ lastRequestId.set(worker.id, requestID);
239
+ trackRequestStart(functionID, reused);
240
+ if (reused) {
233
241
  logPool("REUSE", {
234
- pooledWorkerID: worker.pooledWorkerID.slice(0, 8),
242
+ pooledWorkerID: worker.id.slice(0, 8),
243
+ functionID,
244
+ inFlight: worker.inFlight,
245
+ workerAgeMs: Date.now() - worker.createdAt,
246
+ waitedMs: Date.now() - pending.queuedAt,
247
+ });
248
+ logInvokeTrace("WORKER_REUSE", requestID, `pooled=${worker.id.slice(0, 8)}`);
249
+ logEventTrace("WORKER_START", {
250
+ requestID,
251
+ functionID,
252
+ workerID: worker.id,
253
+ path: requestPath,
254
+ correlationId: getCorrelationId(event),
255
+ apiGwReqId: getApiGatewayRequestId(event),
256
+ reused: true,
257
+ });
258
+ bus.publish("worker.reused", {
259
+ workerID: awsWorkerID,
235
260
  functionID,
236
- originalFunctionID: crossFunction ? worker.functionID : undefined,
237
- poolSizeAfter: pool.length,
238
- workerAgeMs: age,
239
- crossFunction,
261
+ pooledWorkerID: worker.id,
240
262
  });
241
- Logger.debug("Reusing pooled worker", worker.pooledWorkerID, "for", functionID, crossFunction ? "(cross-function reuse)" : "");
242
- return worker;
243
263
  }
244
- // All workers were stale
245
- logPool("POOL_MISS", {
264
+ const server = await getServer();
265
+ logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Routing invocation to worker ${worker.id.slice(0, 8)} inFlight=${worker.inFlight}`);
266
+ logInvokeTrace("ROUTE_INVOCATION", requestID);
267
+ server.routeInvocation(worker.id, evt.properties);
268
+ }
269
+ // Helper: create a worker for an invocation. The worker is registered (and
270
+ // counted against the cap) before the thread starts, so concurrent
271
+ // dispatches cannot overshoot the pool size.
272
+ async function createAndAssign(pending) {
273
+ const { evt, props, handler, build, poolKey, isShared } = pending;
274
+ const { workerID: awsWorkerID, functionID, requestID, env, event } = evt.properties;
275
+ const requestPath = getRequestPath(event);
276
+ const worker = {
277
+ id: crypto.randomBytes(16).toString("hex"),
278
+ poolKey,
246
279
  functionID,
280
+ runtime: props.runtime,
281
+ inFlight: 0,
282
+ maxConcurrency: concurrencyFor(props.runtime),
283
+ stale: false,
284
+ createdAt: Date.now(),
285
+ bundlePath: build.out,
286
+ bundleMtime: getBundleMtime(build.out),
287
+ isSharedPool: isShared,
288
+ };
289
+ pool.add(worker);
290
+ logPool("CREATE", {
291
+ pooledWorkerID: worker.id.slice(0, 8),
292
+ functionID,
293
+ runtime: props.runtime,
294
+ requestID: requestID.slice(0, 8),
247
295
  poolKey: poolKey.slice(0, 30),
248
- poolSize: 0,
249
- reason: "all_stale",
296
+ isSharedPool: isShared,
297
+ liveWorkers: pool.liveCount(poolKey),
298
+ waitedMs: Date.now() - pending.queuedAt,
250
299
  });
251
- return undefined;
300
+ // Route first so the invocation is queued at the server by the time the
301
+ // worker asks for it.
302
+ await assign(worker, pending, false);
303
+ logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Starting worker ${worker.id.slice(0, 8)}...`);
304
+ logInvokeTrace("WORKER_START", requestID, `pooled=${worker.id.slice(0, 8)}`);
305
+ const workerStartTime = Date.now();
306
+ try {
307
+ await handler.startWorker({
308
+ ...build,
309
+ workerID: worker.id,
310
+ functionID,
311
+ environment: env,
312
+ url: `${serverConfig.url}/${worker.id}/${serverConfig.API_VERSION}`,
313
+ runtime: props.runtime,
314
+ isMonoBuild: isShared,
315
+ concurrency: worker.maxConcurrency,
316
+ debugMemory: DEBUG_MEMORY,
317
+ });
318
+ }
319
+ catch (ex) {
320
+ logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ERROR: Failed to start worker: ${ex.message}`);
321
+ Logger.debug("Failed to start pooled worker", ex);
322
+ pool.remove(worker);
323
+ failRequestsOn(worker.id, "WorkerStartFailed", `Failed to start pooled worker: ${ex.message}`);
324
+ lastRequestId.delete(worker.id);
325
+ void drain(poolKey);
326
+ return;
327
+ }
328
+ const workerStartElapsed = Date.now() - workerStartTime;
329
+ logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Worker started in ${workerStartElapsed}ms`);
330
+ logInvokeTrace("WORKER_STARTED", requestID);
331
+ logEventTrace("WORKER_START", {
332
+ requestID,
333
+ functionID,
334
+ workerID: worker.id,
335
+ path: requestPath,
336
+ correlationId: getCorrelationId(event),
337
+ apiGwReqId: getApiGatewayRequestId(event),
338
+ elapsed: workerStartElapsed,
339
+ reused: false,
340
+ });
341
+ bus.publish("worker.started", { workerID: awsWorkerID, functionID });
252
342
  }
253
- // Helper: Return worker to pool
254
- // Uses poolKey for pool lookup (shared key for mono-build)
255
- function returnToPool(pooledWorkerID) {
256
- const worker = activeWorkers.get(pooledWorkerID);
257
- if (!worker)
343
+ // Helper: place an invocation on a worker, create one, or wait for capacity
344
+ async function dispatch(pending) {
345
+ const { poolKey, build, evt } = pending;
346
+ const { functionID, requestID } = evt.properties;
347
+ const currentMtime = getBundleMtime(build.out);
348
+ const worker = pool.pick(poolKey, currentMtime);
349
+ if (worker) {
350
+ await assign(worker, pending, true);
258
351
  return;
259
- // Check if worker is stale (marked for termination due to rebuild)
260
- if (staleWorkers.has(pooledWorkerID)) {
261
- staleWorkers.delete(pooledWorkerID);
262
- logPool("STALE_TERMINATE", {
263
- pooledWorkerID: pooledWorkerID.slice(0, 8),
264
- functionID: worker.functionID,
265
- reason: "marked-stale-during-rebuild",
266
- });
267
- terminatePooledWorker(pooledWorkerID, "stale");
352
+ }
353
+ if (pool.canCreate(poolKey)) {
354
+ await createAndAssign(pending);
268
355
  return;
269
356
  }
270
- // Clean up current request mappings
271
- const awsWorkerID = reverseMapping.get(pooledWorkerID);
272
- if (awsWorkerID) {
273
- workerIDMapping.delete(awsWorkerID);
274
- reverseMapping.delete(pooledWorkerID);
357
+ let queue = waitQueues.get(poolKey);
358
+ if (!queue) {
359
+ queue = [];
360
+ waitQueues.set(poolKey, queue);
361
+ }
362
+ queue.push(pending);
363
+ logPool("POOL_WAIT", {
364
+ functionID,
365
+ requestID: requestID.slice(0, 8),
366
+ poolKey: poolKey.slice(0, 30),
367
+ queued: queue.length,
368
+ liveWorkers: pool.liveCount(poolKey),
369
+ });
370
+ logInvokeTrace("POOL_WAIT", requestID, `queued=${queue.length}`);
371
+ }
372
+ // Helper: give waiting invocations to whatever capacity exists now
373
+ async function drain(poolKey) {
374
+ const queue = waitQueues.get(poolKey);
375
+ if (!queue || queue.length === 0)
376
+ return;
377
+ while (queue.length > 0) {
378
+ const head = queue[0];
379
+ const currentMtime = getBundleMtime(head.build.out);
380
+ const worker = pool.pick(poolKey, currentMtime);
381
+ if (worker) {
382
+ queue.shift();
383
+ await assign(worker, head, true);
384
+ continue;
385
+ }
386
+ if (pool.canCreate(poolKey)) {
387
+ queue.shift();
388
+ await createAndAssign(head);
389
+ continue;
390
+ }
391
+ break;
275
392
  }
276
- // Use poolKey for pool lookup (shared for mono-build)
277
- let pool = workerPool.get(worker.poolKey);
278
- if (!pool) {
279
- pool = [];
280
- workerPool.set(worker.poolKey, pool);
393
+ if (queue.length === 0)
394
+ waitQueues.delete(poolKey);
395
+ }
396
+ // Helper: an invocation finished on a pooled worker
397
+ function release(pooledWorkerID, requestID) {
398
+ const req = requestID ? requests.get(requestID) : undefined;
399
+ if (req) {
400
+ requests.delete(req.requestID);
401
+ trackRequestEnd(req.functionID);
281
402
  }
282
- if (pool.length >= POOL_SIZE) {
283
- // Pool full, terminate
284
- logPool("POOL_FULL", {
403
+ const worker = pool.get(pooledWorkerID);
404
+ if (!worker)
405
+ return;
406
+ logPool("RESPONSE", {
407
+ pooledWorkerID: pooledWorkerID.slice(0, 8),
408
+ functionID: req?.functionID ?? worker.functionID,
409
+ requestID: requestID?.slice(0, 8),
410
+ inFlight: worker.inFlight - 1,
411
+ durationMs: req ? Date.now() - req.startedAt : undefined,
412
+ });
413
+ const outcome = pool.checkin(worker);
414
+ if (outcome === "terminated")
415
+ return; // stopPooledWorker drains
416
+ if (outcome === "idle") {
417
+ logPool("IDLE", {
285
418
  pooledWorkerID: pooledWorkerID.slice(0, 8),
286
419
  functionID: worker.functionID,
287
420
  poolKey: worker.poolKey.slice(0, 30),
288
- poolSize: pool.length,
289
- maxSize: POOL_SIZE,
421
+ idleTimeoutMs: IDLE_TIMEOUT,
290
422
  });
291
- terminatePooledWorker(pooledWorkerID, "pool_full");
292
- Logger.debug("Pool full, terminated worker", pooledWorkerID);
293
- return;
294
423
  }
295
- // Return to pool with idle timeout
296
- worker.state = "idle";
297
- worker.idleTimer = setTimeout(() => {
298
- const idx = pool.indexOf(worker);
299
- if (idx >= 0)
300
- pool.splice(idx, 1);
301
- terminatePooledWorker(pooledWorkerID, "idle_timeout");
302
- Logger.debug("Idle timeout, terminated worker", pooledWorkerID);
303
- }, IDLE_TIMEOUT);
304
- pool.push(worker);
305
- activeWorkers.delete(pooledWorkerID);
306
- logPool("RETURN_TO_POOL", {
307
- pooledWorkerID: pooledWorkerID.slice(0, 8),
308
- functionID: worker.functionID,
309
- poolKey: worker.poolKey.slice(0, 30),
310
- isSharedPool: worker.isSharedPool,
311
- poolSizeAfter: pool.length,
312
- idleTimeoutMs: IDLE_TIMEOUT,
313
- });
314
- Logger.debug("Returned worker to pool", pooledWorkerID, "pool key:", worker.poolKey, "pool size:", pool.length);
424
+ void drain(worker.poolKey);
315
425
  }
316
- // Build success handler - clear pool for rebuilt function
426
+ // Build success handler - retire workers running the old code
317
427
  handlers.subscribe("function.build.success", async (evt) => {
318
428
  const { functionID } = evt.properties;
319
429
  const props = useFunctions().fromID(functionID);
320
430
  if (!props)
321
431
  return;
322
- // Get build to check if mono-build using global config
323
432
  const build = await builder.artifact(functionID);
324
433
  const isMonoBuild = build ? isMonoBuildPath(build.out) : false;
325
- if (isMonoBuild) {
326
- // For mono-build: clear the entire shared pool since all functions share the same bundle
327
- const sharedPoolKey = `${props.runtime}:mono-build`;
328
- const sharedPool = workerPool.get(sharedPoolKey) || [];
329
- const activeSharedCount = [...activeWorkers.values()].filter((w) => w.isSharedPool && w.poolKey === sharedPoolKey).length;
330
- logPool("MONO_BUILD_CLEAR", {
331
- functionID,
332
- sharedPoolKey,
333
- pooledWorkersCleared: sharedPool.length,
334
- activeWorkersMarkedStale: activeSharedCount,
335
- });
336
- // Terminate all idle workers in the shared pool
337
- for (const worker of sharedPool) {
338
- clearTimeout(worker.idleTimer);
339
- await terminatePooledWorker(worker.pooledWorkerID, "mono-rebuild");
340
- }
341
- workerPool.delete(sharedPoolKey);
342
- // Mark active workers as stale (they'll be terminated after completing their request)
343
- for (const [pooledID, worker] of activeWorkers) {
344
- if (worker.isSharedPool && worker.poolKey === sharedPoolKey) {
345
- staleWorkers.add(pooledID);
346
- logPool("MARK_STALE", {
347
- pooledWorkerID: pooledID.slice(0, 8),
348
- functionID: worker.functionID,
349
- reason: "mono-rebuild",
350
- });
351
- }
352
- }
353
- }
354
- else {
355
- // For non-mono-build: clear pool for this specific function only
356
- const pool = workerPool.get(`${props.runtime}:${functionID}`) || [];
357
- const activeCount = [...activeWorkers.values()].filter((w) => w.functionID === functionID).length;
358
- logPool("BUILD_CLEAR", {
359
- functionID,
360
- pooledWorkersCleared: pool.length,
361
- activeWorkersMarkedStale: activeCount,
362
- });
363
- for (const worker of pool) {
364
- clearTimeout(worker.idleTimer);
365
- await terminatePooledWorker(worker.pooledWorkerID, "rebuild");
366
- }
367
- workerPool.delete(`${props.runtime}:${functionID}`);
368
- // Mark active workers as stale (they'll be terminated after completing their request)
369
- for (const [pooledID, worker] of activeWorkers) {
370
- if (worker.functionID === functionID) {
371
- staleWorkers.add(pooledID);
372
- logPool("MARK_STALE", {
373
- pooledWorkerID: pooledID.slice(0, 8),
374
- functionID: worker.functionID,
375
- reason: "rebuild",
376
- });
377
- }
378
- }
379
- }
434
+ const poolKey = isMonoBuild ? `${props.runtime}:mono-build` : `${props.runtime}:${functionID}`;
435
+ const before = pool.workersFor(poolKey).length;
436
+ const marked = pool.invalidate(poolKey, isMonoBuild ? "mono-rebuild" : "rebuild");
437
+ logPool(isMonoBuild ? "MONO_BUILD_CLEAR" : "BUILD_CLEAR", {
438
+ functionID,
439
+ poolKey,
440
+ pooledWorkersCleared: before - marked.length,
441
+ activeWorkersMarkedStale: marked.length,
442
+ });
380
443
  // Stop non-pooled workers (legacy behavior)
381
444
  for (const [_, worker] of workers) {
382
445
  if (worker.functionID === functionID) {
@@ -389,24 +452,18 @@ export const useRuntimeWorkers = lazy(async () => {
389
452
  }
390
453
  }
391
454
  });
392
- const lastRequestId = new Map();
393
455
  // Main invocation handler
394
456
  bus.subscribe("function.invoked", async (evt) => {
395
457
  const { workerID: awsWorkerID, functionID, requestID, env, event, } = evt.properties;
396
458
  const startTime = Date.now();
397
459
  const requestPath = getRequestPath(event);
398
- // Check if this is a warmup request - force-create new workers for these
399
- // Matches the warmer format: { ding: true } or { warmer: true }
460
+ // Warm pings ({ding}/{warmer}) are ordinary invocations here: they take
461
+ // a pooled worker if one is free and wait for capacity otherwise, so a
462
+ // burst of them can never hold more isolates than the pool allows.
400
463
  const isWarmupRequest = event && typeof event === 'object' &&
401
464
  ('ding' in event || 'warmer' in event || event.__sst_warmup === true);
402
- const warmupId = isWarmupRequest ? (event.warmupId ?? event.index) : undefined;
403
465
  logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} RECEIVED func=${functionID.slice(-30)}`);
404
- if (isWarmupRequest) {
405
- logInvokeTrace("WARMUP_RECEIVED", requestID, `warmupId=${warmupId}`);
406
- }
407
- else {
408
- logInvokeTrace("INVOKE_RECEIVED", requestID, `func=${functionID.slice(-40)}`);
409
- }
466
+ logInvokeTrace(isWarmupRequest ? "WARMUP_RECEIVED" : "INVOKE_RECEIVED", requestID, `func=${functionID.slice(-40)}`);
410
467
  // Send ack immediately
411
468
  bus.publish("function.ack", { functionID, workerID: awsWorkerID, requestID });
412
469
  logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ACK sent elapsed=${Date.now() - startTime}ms`);
@@ -439,12 +496,10 @@ export const useRuntimeWorkers = lazy(async () => {
439
496
  });
440
497
  return;
441
498
  }
442
- logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Getting build artifact...`);
443
499
  logInvokeTrace("BUILD_ARTIFACT_START", requestID);
444
500
  const buildStartTime = Date.now();
445
501
  const build = await builder.artifact(functionID);
446
- const buildElapsed = Date.now() - buildStartTime;
447
- logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Build artifact took ${buildElapsed}ms`);
502
+ logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Build artifact took ${Date.now() - buildStartTime}ms`);
448
503
  logInvokeTrace("BUILD_ARTIFACT_DONE", requestID, build ? `out=${build.out.slice(-30)}` : "NO_BUILD");
449
504
  if (!build) {
450
505
  logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ERROR: Build artifact not ready`);
@@ -459,229 +514,123 @@ export const useRuntimeWorkers = lazy(async () => {
459
514
  });
460
515
  return;
461
516
  }
462
- // Check if this runtime supports pooling
463
- const poolable = isPoolableRuntime(props.runtime, env);
464
- if (poolable) {
517
+ if (isPoolableRuntime(props.runtime, env)) {
465
518
  // === POOLED PATH ===
466
- // Get pool key: shared for mono-build, per-function otherwise
467
519
  const { key: poolKey, isShared } = getPoolKey(functionID, props.runtime, build.out);
468
- logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Looking for pooled worker, poolKey=${poolKey.slice(0, 20)}`);
469
- // For warmup requests, always create new workers (never reuse from pool)
470
- let pooledWorker = isWarmupRequest ? undefined : getIdleWorker(poolKey, functionID, build.out);
471
- let isReuse = false;
472
- if (pooledWorker) {
473
- isReuse = true;
474
- // Update functionID for cross-function reuse (mono-build)
475
- pooledWorker.functionID = functionID;
476
- trackRequestStart(functionID, true);
477
- logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} REUSING pooled worker ${pooledWorker.pooledWorkerID.slice(0, 8)}`);
478
- }
479
- else {
480
- // Create new pooled worker
481
- const pooledWorkerID = crypto.randomBytes(16).toString("hex");
482
- const bundleMtime = getBundleMtime(build.out);
483
- pooledWorker = {
484
- pooledWorkerID,
485
- functionID,
486
- state: "busy",
487
- createdAt: Date.now(),
488
- poolKey,
489
- isSharedPool: isShared,
490
- bundlePath: build.out,
491
- bundleMtime,
492
- };
493
- logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} CREATING new pooled worker ${pooledWorkerID.slice(0, 8)}`);
494
- }
495
- // Set up mappings
496
- workerIDMapping.set(awsWorkerID, pooledWorker.pooledWorkerID);
497
- reverseMapping.set(pooledWorker.pooledWorkerID, awsWorkerID);
498
- lastRequestId.set(pooledWorker.pooledWorkerID, requestID);
499
- activeWorkers.set(pooledWorker.pooledWorkerID, pooledWorker);
500
- if (!isReuse) {
501
- // Start new worker with pooledWorkerID (cold start)
502
- trackRequestStart(functionID, false);
503
- const currentPoolSize = workerPool.get(poolKey)?.length || 0;
504
- logPool(isWarmupRequest ? "WARMUP_CREATE" : "CREATE", {
505
- pooledWorkerID: pooledWorker.pooledWorkerID.slice(0, 8),
506
- functionID,
507
- runtime: props.runtime,
508
- requestID: requestID.slice(0, 8),
509
- poolKey: poolKey.slice(0, 30),
510
- isSharedPool: isShared,
511
- currentPoolSize,
512
- activeWorkers: activeWorkers.size,
513
- ...(isWarmupRequest && { warmupId }),
514
- });
515
- logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Starting worker ${pooledWorker.pooledWorkerID.slice(0, 8)}...`);
516
- logInvokeTrace("WORKER_START", requestID, `pooled=${pooledWorker.pooledWorkerID.slice(0, 8)}`);
517
- const workerStartTime = Date.now();
518
- try {
519
- await handler.startWorker({
520
- ...build,
521
- workerID: pooledWorker.pooledWorkerID,
522
- functionID,
523
- environment: env,
524
- url: `${serverConfig.url}/${pooledWorker.pooledWorkerID}/${serverConfig.API_VERSION}`,
525
- runtime: props.runtime,
526
- isMonoBuild: isShared,
527
- });
528
- startedWorkers.add(pooledWorker.pooledWorkerID);
529
- const workerStartElapsed = Date.now() - workerStartTime;
530
- logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Worker started in ${workerStartElapsed}ms`);
531
- logInvokeTrace("WORKER_STARTED", requestID);
532
- logEventTrace("WORKER_START", {
533
- requestID,
534
- functionID,
535
- workerID: pooledWorker.pooledWorkerID,
536
- path: requestPath,
537
- correlationId: getCorrelationId(event),
538
- apiGwReqId: getApiGatewayRequestId(event),
539
- elapsed: workerStartElapsed,
540
- reused: false,
541
- });
542
- bus.publish("worker.started", {
543
- workerID: awsWorkerID,
544
- functionID,
545
- });
546
- }
547
- catch (ex) {
548
- logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ERROR: Failed to start worker: ${ex.message}`);
549
- Logger.debug("Failed to start pooled worker", ex);
550
- bus.publish("function.error", {
551
- workerID: awsWorkerID,
552
- functionID,
553
- requestID,
554
- errorType: "WorkerStartFailed",
555
- errorMessage: `Failed to start pooled worker: ${ex.message}`,
556
- trace: ex.stack?.split("\n") || [],
557
- });
558
- // Cleanup failed worker state
559
- activeWorkers.delete(pooledWorker.pooledWorkerID);
560
- startedWorkers.delete(pooledWorker.pooledWorkerID);
561
- lastRequestId.delete(pooledWorker.pooledWorkerID);
562
- workerIDMapping.delete(awsWorkerID);
563
- reverseMapping.delete(pooledWorker.pooledWorkerID);
564
- return;
565
- }
566
- }
567
- else {
568
- logInvokeTrace("WORKER_REUSE", requestID, `pooled=${pooledWorker.pooledWorkerID.slice(0, 8)}`);
569
- logEventTrace("WORKER_START", {
570
- requestID,
571
- functionID,
572
- workerID: pooledWorker.pooledWorkerID,
573
- path: requestPath,
574
- correlationId: getCorrelationId(event),
575
- apiGwReqId: getApiGatewayRequestId(event),
576
- reused: true,
577
- });
578
- bus.publish("worker.reused", {
579
- workerID: awsWorkerID,
580
- functionID,
581
- pooledWorkerID: pooledWorker.pooledWorkerID,
582
- });
583
- }
584
- // Route invocation to the pooled worker
520
+ logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Dispatching to pool ${poolKey.slice(0, 20)}`);
521
+ await dispatch({
522
+ evt,
523
+ props,
524
+ handler,
525
+ build,
526
+ poolKey,
527
+ isShared,
528
+ queuedAt: Date.now(),
529
+ });
530
+ return;
531
+ }
532
+ // === NON-POOLED PATH (existing behavior) ===
533
+ lastRequestId.set(awsWorkerID, requestID);
534
+ let worker = workers.get(awsWorkerID);
535
+ if (worker)
536
+ return;
537
+ try {
538
+ await handler.startWorker({
539
+ ...build,
540
+ workerID: awsWorkerID,
541
+ functionID,
542
+ environment: env,
543
+ url: `${serverConfig.url}/${awsWorkerID}/${serverConfig.API_VERSION}`,
544
+ runtime: props.runtime,
545
+ isMonoBuild: isMonoBuildPath(build.out),
546
+ });
547
+ workers.set(awsWorkerID, { workerID: awsWorkerID, functionID });
548
+ bus.publish("worker.started", { workerID: awsWorkerID, functionID });
585
549
  const server = await getServer();
586
- logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Routing invocation to worker ${pooledWorker.pooledWorkerID.slice(0, 8)} elapsed=${Date.now() - startTime}ms`);
587
- logInvokeTrace("ROUTE_INVOCATION", requestID);
588
- server.routeInvocation(pooledWorker.pooledWorkerID, evt.properties);
550
+ server.routeInvocation(awsWorkerID, evt.properties);
589
551
  }
590
- else {
591
- // === NON-POOLED PATH (existing behavior) ===
592
- lastRequestId.set(awsWorkerID, requestID);
593
- let worker = workers.get(awsWorkerID);
594
- if (worker)
595
- return;
596
- try {
597
- await handler.startWorker({
598
- ...build,
599
- workerID: awsWorkerID,
600
- functionID,
601
- environment: env,
602
- url: `${serverConfig.url}/${awsWorkerID}/${serverConfig.API_VERSION}`,
603
- runtime: props.runtime,
604
- isMonoBuild: isMonoBuildPath(build.out),
605
- });
606
- workers.set(awsWorkerID, { workerID: awsWorkerID, functionID });
607
- bus.publish("worker.started", { workerID: awsWorkerID, functionID });
608
- // Route invocation to the non-pooled worker
609
- const server = await getServer();
610
- server.routeInvocation(awsWorkerID, evt.properties);
611
- }
612
- catch (ex) {
613
- Logger.debug("Failed to start worker", ex);
614
- bus.publish("function.error", {
615
- workerID: awsWorkerID,
616
- functionID,
617
- requestID,
618
- errorType: "WorkerStartFailed",
619
- errorMessage: `Failed to start worker: ${ex.message}`,
620
- trace: ex.stack?.split("\n") || [],
621
- });
622
- return;
623
- }
552
+ catch (ex) {
553
+ Logger.debug("Failed to start worker", ex);
554
+ bus.publish("function.error", {
555
+ workerID: awsWorkerID,
556
+ functionID,
557
+ requestID,
558
+ errorType: "WorkerStartFailed",
559
+ errorMessage: `Failed to start worker: ${ex.message}`,
560
+ trace: ex.stack?.split("\n") || [],
561
+ });
562
+ return;
624
563
  }
625
564
  });
565
+ const stats = () => {
566
+ const s = pool.stats();
567
+ return { workers: s.workers, inFlight: s.inFlight, queued: queuedCount() };
568
+ };
569
+ startMemorySampling(stats);
626
570
  // Process exit cleanup
627
571
  process.on("exit", () => {
628
- // Log final metrics summary
629
572
  writeSessionEndSummary();
630
- for (const pool of workerPool.values()) {
631
- for (const worker of pool) {
573
+ for (const worker of pool.all()) {
574
+ if (worker.idleTimer)
632
575
  clearTimeout(worker.idleTimer);
633
- }
634
576
  }
635
577
  });
636
578
  return {
637
579
  fromID(workerID) {
638
- // Check pooled workers first
639
- const pooled = activeWorkers.get(workerID);
580
+ const pooled = pool.get(workerID);
640
581
  if (pooled)
641
582
  return { workerID, functionID: pooled.functionID };
642
- // Check non-pooled workers
643
583
  return workers.get(workerID);
644
584
  },
645
585
  getCurrentRequestID(workerID) {
646
586
  return lastRequestId.get(workerID);
647
587
  },
588
+ /**
589
+ * Who a response belongs to. Pooled workers may hold several requests,
590
+ * so the request id decides; non-pooled workers are their own AWS worker.
591
+ */
592
+ resolveRequest(workerID, requestID) {
593
+ const req = requestID ? requests.get(requestID) : undefined;
594
+ if (req)
595
+ return { awsWorkerID: req.awsWorkerID, functionID: req.functionID };
596
+ const pooled = pool.get(workerID);
597
+ if (pooled) {
598
+ const last = lastRequestId.get(workerID);
599
+ const lastReq = last ? requests.get(last) : undefined;
600
+ return {
601
+ awsWorkerID: lastReq?.awsWorkerID ?? workerID,
602
+ functionID: lastReq?.functionID ?? pooled.functionID,
603
+ };
604
+ }
605
+ const worker = workers.get(workerID);
606
+ if (!worker)
607
+ return undefined;
608
+ return { awsWorkerID: workerID, functionID: worker.functionID };
609
+ },
648
610
  stdout(workerID, message) {
649
- // Check pooled workers first
650
- const pooled = activeWorkers.get(workerID);
611
+ const pooled = pool.get(workerID);
651
612
  if (pooled) {
652
- const requestID = lastRequestId.get(workerID);
653
- if (requestID) {
654
- const trimmedMessage = message.trim();
655
- // Log messages that contain [LOG] prefix
656
- if (trimmedMessage.includes("[LOG]")) {
657
- logEventTrace("WORKER_LOG", {
658
- requestID,
659
- functionID: pooled.functionID,
660
- workerID,
661
- message: trimmedMessage,
662
- });
613
+ for (const chunk of splitAttributed(message)) {
614
+ const requestID = chunk.requestID ?? lastRequestId.get(workerID);
615
+ if (!requestID)
616
+ continue;
617
+ const trimmed = chunk.text.trim();
618
+ if (!trimmed)
619
+ continue;
620
+ const functionID = requests.get(requestID)?.functionID ?? pooled.functionID;
621
+ if (trimmed.includes("[LOG]")) {
622
+ logEventTrace("WORKER_LOG", { requestID, functionID, workerID, message: trimmed });
663
623
  }
664
- bus.publish("worker.stdout", {
665
- workerID,
666
- functionID: pooled.functionID,
667
- message: trimmedMessage,
668
- requestID,
669
- });
624
+ bus.publish("worker.stdout", { workerID, functionID, message: trimmed, requestID });
670
625
  }
671
626
  return;
672
627
  }
673
- // Check if this is a preWarm worker (started but not yet active)
674
- if (startedWorkers.has(workerID)) {
675
- // During preWarm, ignore output since there's no request context
676
- return;
677
- }
678
628
  // Non-pooled worker
679
629
  const worker = workers.get(workerID);
680
630
  if (!worker)
681
631
  return;
682
632
  const trimmedMessage = message.trim();
683
633
  const requestID = lastRequestId.get(workerID);
684
- // Log messages that contain [LOG] prefix
685
634
  if (trimmedMessage.includes("[LOG]") && requestID) {
686
635
  logEventTrace("WORKER_LOG", {
687
636
  requestID,
@@ -697,31 +646,26 @@ export const useRuntimeWorkers = lazy(async () => {
697
646
  });
698
647
  },
699
648
  exited(workerID) {
700
- // Check if pooled worker
701
- if (activeWorkers.has(workerID) || startedWorkers.has(workerID)) {
702
- const worker = activeWorkers.get(workerID);
703
- if (worker) {
704
- const uptime = Date.now() - worker.createdAt;
705
- logPool("EXIT", {
706
- pooledWorkerID: workerID.slice(0, 8),
707
- functionID: worker.functionID,
708
- state: worker.state,
709
- uptimeMs: uptime,
710
- });
711
- // Clean up all mappings
712
- const awsWorkerID = reverseMapping.get(workerID);
713
- if (awsWorkerID) {
714
- workerIDMapping.delete(awsWorkerID);
715
- }
716
- reverseMapping.delete(workerID);
717
- activeWorkers.delete(workerID);
718
- lastRequestId.delete(workerID);
719
- startedWorkers.delete(workerID);
720
- bus.publish("worker.exited", {
721
- workerID: awsWorkerID || workerID,
722
- functionID: worker.functionID,
723
- });
724
- }
649
+ const pooled = pool.get(workerID);
650
+ if (pooled) {
651
+ logPool("EXIT", {
652
+ pooledWorkerID: workerID.slice(0, 8),
653
+ functionID: pooled.functionID,
654
+ inFlight: pooled.inFlight,
655
+ uptimeMs: Date.now() - pooled.createdAt,
656
+ });
657
+ pool.remove(pooled);
658
+ const last = lastRequestId.get(workerID);
659
+ const awsWorkerID = last ? requests.get(last)?.awsWorkerID : undefined;
660
+ // Anything it was holding will never get a response from it
661
+ failRequestsOn(workerID, "WorkerExited", "Local worker exited before responding (out of memory or crashed). Check the dev console for details.");
662
+ lastRequestId.delete(workerID);
663
+ bus.publish("worker.exited", {
664
+ workerID: awsWorkerID ?? workerID,
665
+ functionID: pooled.functionID,
666
+ pooledWorkerID: workerID,
667
+ });
668
+ void drain(pooled.poolKey);
725
669
  return;
726
670
  }
727
671
  // Non-pooled worker
@@ -732,36 +676,32 @@ export const useRuntimeWorkers = lazy(async () => {
732
676
  lastRequestId.delete(workerID);
733
677
  bus.publish("worker.exited", existing);
734
678
  },
735
- // Called by server when response is received - returns worker to pool
736
- onResponse(pooledWorkerID) {
737
- if (activeWorkers.has(pooledWorkerID)) {
738
- const worker = activeWorkers.get(pooledWorkerID);
739
- if (worker) {
740
- trackRequestEnd(worker.functionID);
741
- logPool("RESPONSE", {
742
- pooledWorkerID: pooledWorkerID.slice(0, 8),
743
- functionID: worker.functionID,
744
- requestID: lastRequestId.get(pooledWorkerID)?.slice(0, 8),
745
- });
746
- }
747
- returnToPool(pooledWorkerID);
748
- }
749
- },
750
- // Get AWS workerID from pooled ID (for IoT routing)
751
- getAwsWorkerID(pooledWorkerID) {
752
- return reverseMapping.get(pooledWorkerID);
679
+ // Called by server when a response or error is received
680
+ onResponse(pooledWorkerID, requestID) {
681
+ if (!pool.get(pooledWorkerID))
682
+ return;
683
+ release(pooledWorkerID, requestID ?? lastRequestId.get(pooledWorkerID));
753
684
  },
754
685
  // Check if worker is pooled
755
686
  isPooled(workerID) {
756
- return activeWorkers.has(workerID) || startedWorkers.has(workerID);
687
+ return pool.get(workerID) !== undefined;
757
688
  },
689
+ stats,
758
690
  subscribe: bus.forward("worker.started", "worker.stopped", "worker.exited", "worker.stdout", "worker.reused"),
759
691
  /**
760
- * Trigger warmup by invoking Lambda functions with warmup payloads.
761
- * This sends real requests through the IoT bridge, which naturally creates workers.
762
- * @param count Number of workers to warm up (default: 15)
692
+ * Warm the pool by invoking a Node function with warm pings.
693
+ *
694
+ * Each ping is marked as a fan-out *child* (`__WARMER_INVOCATION__ > 1`)
695
+ * so the app's lambda-warmer preloads its handlers and returns instead of
696
+ * fanning out to `concurrency` more Lambdas, which is what turned the old
697
+ * 30 pings into ~900 worker creations. The count is capped at the pool
698
+ * size, and pings go through the normal pool path, so warmup can never
699
+ * hold more isolates than steady state.
763
700
  */
764
- async triggerWarmup(count = 15) {
701
+ async triggerWarmup(count) {
702
+ count = Math.min(count, POOL_SIZE);
703
+ if (count <= 0)
704
+ return { warmed: 0 };
765
705
  const functions = useFunctions();
766
706
  const allFunctions = functions.all;
767
707
  // Find a nodejs function to use as the warmup target
@@ -777,88 +717,45 @@ export const useRuntimeWorkers = lazy(async () => {
777
717
  break;
778
718
  }
779
719
  if (!targetFunction) {
780
- logPool("WARMUP_SKIP", {
781
- reason: "no nodejs function found",
782
- });
720
+ logPool("WARMUP_SKIP", { reason: "no nodejs function found" });
783
721
  return { warmed: 0 };
784
722
  }
785
723
  const { functionName } = targetFunction;
786
- logPool("WARMUP_START", {
787
- count,
788
- functionName,
789
- });
790
- // Publish warmup start event
724
+ logPool("WARMUP_START", { count, functionName });
791
725
  bus.publish("warmup.start", { count });
792
726
  const startTime = Date.now();
793
727
  let success = 0;
794
728
  let failed = 0;
795
729
  let completed = 0;
796
- // Use the shared AWS client
797
730
  const { useAWSClient } = await import("../credentials.js");
798
731
  const { LambdaClient, InvokeCommand } = await import("@aws-sdk/client-lambda");
799
732
  const lambda = useAWSClient(LambdaClient);
800
- // Helper to publish progress
801
733
  const publishProgress = () => {
802
- bus.publish("warmup.progress", {
803
- completed,
804
- total: count,
805
- success,
806
- failed,
807
- });
734
+ bus.publish("warmup.progress", { completed, total: count, success, failed });
808
735
  };
809
- // Phase 1: Invoke first warmup to populate V8 compile cache
810
- try {
811
- const result = await lambda.send(new InvokeCommand({
812
- FunctionName: functionName,
813
- InvocationType: "RequestResponse",
814
- Payload: JSON.stringify({
815
- ding: true,
816
- concurrency: count,
817
- index: 0,
818
- }),
819
- }));
820
- if (result.StatusCode === 200) {
821
- success++;
736
+ await Promise.all(Array.from({ length: count }, (_, i) => i).map(async (i) => {
737
+ try {
738
+ const result = await lambda.send(new InvokeCommand({
739
+ FunctionName: functionName,
740
+ InvocationType: "RequestResponse",
741
+ Payload: JSON.stringify({
742
+ ding: true,
743
+ __WARMER_INVOCATION__: i + 2,
744
+ __WARMER_CONCURRENCY__: count + 1,
745
+ __WARMER_CORRELATIONID__: `sst-dev-warmup-${startTime}`,
746
+ }),
747
+ }));
748
+ if (result.StatusCode === 200)
749
+ success++;
750
+ else
751
+ failed++;
822
752
  }
823
- else {
753
+ catch {
824
754
  failed++;
825
755
  }
826
- }
827
- catch (ex) {
828
- failed++;
829
- }
830
- completed++;
831
- publishProgress();
832
- // Phase 2: Invoke remaining warmups in parallel
833
- if (count > 1) {
834
- const results = await Promise.all(Array.from({ length: count - 1 }, (_, i) => i + 1).map(async (i) => {
835
- try {
836
- const result = await lambda.send(new InvokeCommand({
837
- FunctionName: functionName,
838
- InvocationType: "RequestResponse",
839
- Payload: JSON.stringify({
840
- ding: true,
841
- concurrency: count,
842
- index: i,
843
- }),
844
- }));
845
- const ok = result.StatusCode === 200;
846
- if (ok)
847
- success++;
848
- else
849
- failed++;
850
- completed++;
851
- publishProgress();
852
- return ok;
853
- }
854
- catch {
855
- failed++;
856
- completed++;
857
- publishProgress();
858
- return false;
859
- }
860
- }));
861
- }
756
+ completed++;
757
+ publishProgress();
758
+ }));
862
759
  const elapsed = Date.now() - startTime;
863
760
  logPool("WARMUP_DONE", {
864
761
  success,
@@ -866,12 +763,7 @@ export const useRuntimeWorkers = lazy(async () => {
866
763
  elapsedMs: elapsed,
867
764
  avgMs: success > 0 ? Math.round(elapsed / success) : 0,
868
765
  });
869
- // Publish warmup complete event
870
- bus.publish("warmup.complete", {
871
- success,
872
- failed,
873
- elapsedMs: elapsed,
874
- });
766
+ bus.publish("warmup.complete", { success, failed, elapsedMs: elapsed });
875
767
  return { warmed: success, elapsed };
876
768
  },
877
769
  };