@intelligems/sst 2.49.3 → 2.49.6-ig.10

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.
@@ -1,78 +1,730 @@
1
+ import crypto from "crypto";
2
+ import fs from "fs";
3
+ import path from "path";
1
4
  import { useBus } from "../bus.js";
2
5
  import { useFunctionBuilder, useRuntimeHandlers } from "./handlers.js";
3
- import { useRuntimeServerConfig } from "./server.js";
6
+ import { useRuntimeServerConfig, useRuntimeServer } from "./server.js";
4
7
  import { useFunctions } from "../constructs/Function.js";
5
8
  import { lazy } from "../util/lazy.js";
9
+ import { Logger } from "../logger.js";
10
+ import { POOL_SIZE, IDLE_TIMEOUT, logPool, logInvokeTrace, trackRequestStart, trackRequestEnd, setFunctionNameResolver, writeSessionEndSummary, } from "./worker-pool-logging.js";
11
+ import { useMonoBuildConfig, isMonoBuildPath } from "./mono-build-config.js";
12
+ import { getRequestPath, getCorrelationId, getApiGatewayRequestId } from "./request-utils.js";
13
+ import { logWorkers } from "./debug-bridge-logging.js";
14
+ 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
+ const bundleMtimes = new Map();
18
+ const bundleWatchers = new Map();
19
+ // Clean up watchers on exit
20
+ process.on("exit", () => {
21
+ for (const watcher of bundleWatchers.values()) {
22
+ try {
23
+ watcher.close();
24
+ }
25
+ catch { }
26
+ }
27
+ });
28
+ // Get bundle rebuild timestamp for staleness checking
29
+ function getBundleMtime(buildOut) {
30
+ if (isMonoBuildPath(buildOut)) {
31
+ if (bundleMtimes.has(buildOut)) {
32
+ return bundleMtimes.get(buildOut);
33
+ }
34
+ const timestampFile = path.join(buildOut, ".last-rebuild");
35
+ const update = () => {
36
+ try {
37
+ const content = fs.readFileSync(timestampFile, "utf-8");
38
+ const mtime = parseInt(content, 10);
39
+ bundleMtimes.set(buildOut, mtime);
40
+ return mtime;
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ };
46
+ if (!bundleWatchers.has(buildOut)) {
47
+ try {
48
+ const watcher = fs.watch(buildOut, { persistent: false }, (event, filename) => {
49
+ if (!filename || filename === ".last-rebuild") {
50
+ update();
51
+ }
52
+ });
53
+ watcher.on("error", () => {
54
+ bundleWatchers.delete(buildOut);
55
+ bundleMtimes.delete(buildOut);
56
+ try {
57
+ watcher.close();
58
+ }
59
+ catch { }
60
+ });
61
+ bundleWatchers.set(buildOut, watcher);
62
+ }
63
+ catch { }
64
+ }
65
+ return update();
66
+ }
67
+ try {
68
+ // For non-mono-bundle, use bundle directory mtime
69
+ const stat = fs.statSync(buildOut);
70
+ return stat.mtimeMs;
71
+ }
72
+ catch {
73
+ return undefined;
74
+ }
75
+ }
76
+ // Helper: Get pool key for worker lookup
77
+ // For mono-build, uses shared key so any warm worker can serve any handler
78
+ function getPoolKey(functionID, runtime, buildOut) {
79
+ // Use the global mono build config for pool key calculation
80
+ return useMonoBuildConfig().getPoolKey(functionID, runtime, buildOut);
81
+ }
82
+ // Extract readable function name from handler path
83
+ function getFunctionName(functionID) {
84
+ try {
85
+ const props = useFunctions().fromID(functionID);
86
+ if (!props)
87
+ return functionID.slice(0, 25);
88
+ return (props.functionName || functionID).split("backend-")[1]?.slice(0, 50) || functionID.slice(0, 25);
89
+ }
90
+ catch {
91
+ return functionID.slice(0, 25);
92
+ }
93
+ }
94
+ // Runtimes that support multiple invocations per process (have event loop)
95
+ const POOLABLE_RUNTIMES = new Set([
96
+ // Node.js - has while(true) loop in nodejs-runtime/index.ts
97
+ "nodejs",
98
+ "nodejs14.x",
99
+ "nodejs16.x",
100
+ "nodejs18.x",
101
+ "nodejs20.x",
102
+ "nodejs22.x",
103
+ // Python - has while True loop in python-runtime/runtime.py
104
+ "python",
105
+ "python3.7",
106
+ "python3.8",
107
+ "python3.9",
108
+ "python3.10",
109
+ "python3.11",
110
+ "python3.12",
111
+ "python3.13",
112
+ // Go - AWS Lambda Go SDK has built-in event loop
113
+ "go",
114
+ "go1.x",
115
+ // Java - AWS Lambda Java SDK has built-in event loop
116
+ "java",
117
+ "java8",
118
+ "java8.al2",
119
+ "java11",
120
+ "java17",
121
+ "java21",
122
+ // .NET - AWS Lambda .NET SDK has built-in event loop
123
+ "dotnet",
124
+ "dotnet6",
125
+ "dotnet8",
126
+ "dotnetcore3.1",
127
+ // Rust - AWS Lambda Rust runtime has built-in event loop
128
+ "rust",
129
+ ]);
130
+ function isPoolableRuntime(runtime, env) {
131
+ // Container jobs are NOT poolable
132
+ if (runtime.startsWith("container") && env?.SST_DEBUG_JOB) {
133
+ return false;
134
+ }
135
+ return (POOLABLE_RUNTIMES.has(runtime) ||
136
+ [...POOLABLE_RUNTIMES].some((r) => runtime.startsWith(r)));
137
+ }
6
138
  export const useRuntimeWorkers = lazy(async () => {
139
+ // Set up function name resolver for logging module
140
+ setFunctionNameResolver(getFunctionName);
141
+ // Non-pooled workers (legacy behavior)
7
142
  const workers = new Map();
143
+ // Worker pool data structures
144
+ const workerPool = new Map();
145
+ const activeWorkers = new Map();
146
+ const workerIDMapping = new Map(); // awsWorkerID → pooledWorkerID
147
+ const reverseMapping = new Map(); // pooledWorkerID → awsWorkerID
148
+ const startedWorkers = new Set(); // Track started pooledWorkerIDs
8
149
  const bus = useBus();
9
150
  const handlers = useRuntimeHandlers();
10
151
  const builder = useFunctionBuilder();
11
- const server = await useRuntimeServerConfig();
152
+ const serverConfig = await useRuntimeServerConfig();
153
+ // Log pool configuration on startup
154
+ logPool("INIT", {
155
+ poolSize: POOL_SIZE,
156
+ idleTimeoutMs: IDLE_TIMEOUT,
157
+ poolableRuntimes: [...POOLABLE_RUNTIMES].length,
158
+ });
159
+ // Lazy getter for server to avoid circular initialization
160
+ let _server = null;
161
+ async function getServer() {
162
+ if (!_server) {
163
+ _server = await useRuntimeServer();
164
+ }
165
+ return _server;
166
+ }
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
+ logPool("TERMINATE", {
180
+ pooledWorkerID: pooledWorkerID.slice(0, 8),
181
+ functionID: worker.functionID,
182
+ reason: reason || "unknown",
183
+ uptimeMs: uptime,
184
+ });
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);
193
+ }
194
+ reverseMapping.delete(pooledWorkerID);
195
+ startedWorkers.delete(pooledWorkerID);
196
+ lastRequestId.delete(pooledWorkerID);
197
+ Logger.debug("Terminated pooled worker", pooledWorkerID);
198
+ }
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
+ });
209
+ return undefined;
210
+ }
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;
233
+ logPool("REUSE", {
234
+ pooledWorkerID: worker.pooledWorkerID.slice(0, 8),
235
+ functionID,
236
+ originalFunctionID: crossFunction ? worker.functionID : undefined,
237
+ poolSizeAfter: pool.length,
238
+ workerAgeMs: age,
239
+ crossFunction,
240
+ });
241
+ Logger.debug("Reusing pooled worker", worker.pooledWorkerID, "for", functionID, crossFunction ? "(cross-function reuse)" : "");
242
+ return worker;
243
+ }
244
+ // All workers were stale
245
+ logPool("POOL_MISS", {
246
+ functionID,
247
+ poolKey: poolKey.slice(0, 30),
248
+ poolSize: 0,
249
+ reason: "all_stale",
250
+ });
251
+ return undefined;
252
+ }
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)
258
+ 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");
268
+ return;
269
+ }
270
+ // Clean up current request mappings
271
+ const awsWorkerID = reverseMapping.get(pooledWorkerID);
272
+ if (awsWorkerID) {
273
+ workerIDMapping.delete(awsWorkerID);
274
+ reverseMapping.delete(pooledWorkerID);
275
+ }
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);
281
+ }
282
+ if (pool.length >= POOL_SIZE) {
283
+ // Pool full, terminate
284
+ logPool("POOL_FULL", {
285
+ pooledWorkerID: pooledWorkerID.slice(0, 8),
286
+ functionID: worker.functionID,
287
+ poolKey: worker.poolKey.slice(0, 30),
288
+ poolSize: pool.length,
289
+ maxSize: POOL_SIZE,
290
+ });
291
+ terminatePooledWorker(pooledWorkerID, "pool_full");
292
+ Logger.debug("Pool full, terminated worker", pooledWorkerID);
293
+ return;
294
+ }
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);
315
+ }
316
+ // Build success handler - clear pool for rebuilt function
12
317
  handlers.subscribe("function.build.success", async (evt) => {
318
+ const { functionID } = evt.properties;
319
+ const props = useFunctions().fromID(functionID);
320
+ if (!props)
321
+ return;
322
+ // Get build to check if mono-build using global config
323
+ const build = await builder.artifact(functionID);
324
+ 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
+ }
380
+ // Stop non-pooled workers (legacy behavior)
13
381
  for (const [_, worker] of workers) {
14
- if (worker.functionID === evt.properties.functionID) {
15
- const props = useFunctions().fromID(worker.functionID);
16
- if (!props)
382
+ if (worker.functionID === functionID) {
383
+ const workerProps = useFunctions().fromID(worker.functionID);
384
+ if (!workerProps)
17
385
  return;
18
- const handler = handlers.for(props.runtime);
386
+ const handler = handlers.for(workerProps.runtime);
19
387
  await handler?.stopWorker(worker.workerID);
20
388
  bus.publish("worker.stopped", worker);
21
389
  }
22
390
  }
23
391
  });
24
392
  const lastRequestId = new Map();
393
+ // Main invocation handler
25
394
  bus.subscribe("function.invoked", async (evt) => {
26
- bus.publish("function.ack", {
27
- functionID: evt.properties.functionID,
28
- workerID: evt.properties.workerID,
29
- });
30
- lastRequestId.set(evt.properties.workerID, evt.properties.requestID);
31
- let worker = workers.get(evt.properties.workerID);
32
- if (worker)
33
- return;
34
- const props = useFunctions().fromID(evt.properties.functionID);
35
- if (!props)
395
+ const { workerID: awsWorkerID, functionID, requestID, env, event, } = evt.properties;
396
+ const startTime = Date.now();
397
+ 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 }
400
+ const isWarmupRequest = event && typeof event === 'object' &&
401
+ ('ding' in event || 'warmer' in event || event.__sst_warmup === true);
402
+ const warmupId = isWarmupRequest ? (event.warmupId ?? event.index) : undefined;
403
+ 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
+ }
410
+ // Send ack immediately
411
+ bus.publish("function.ack", { functionID, workerID: awsWorkerID, requestID });
412
+ logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ACK sent elapsed=${Date.now() - startTime}ms`);
413
+ logInvokeTrace("ACK_PUBLISHED", requestID, `elapsed=${Date.now() - startTime}ms`);
414
+ const props = useFunctions().fromID(functionID);
415
+ if (!props) {
416
+ logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ERROR: Function not found`);
417
+ Logger.debug("Function not found:", functionID);
418
+ bus.publish("function.error", {
419
+ workerID: awsWorkerID,
420
+ functionID,
421
+ requestID,
422
+ errorType: "FunctionNotFound",
423
+ errorMessage: `Function ${functionID} not found in project`,
424
+ trace: [],
425
+ });
36
426
  return;
427
+ }
37
428
  const handler = handlers.for(props.runtime);
38
- if (!handler)
429
+ if (!handler) {
430
+ logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ERROR: No handler for runtime ${props.runtime}`);
431
+ Logger.debug("No handler for runtime:", props.runtime);
432
+ bus.publish("function.error", {
433
+ workerID: awsWorkerID,
434
+ functionID,
435
+ requestID,
436
+ errorType: "RuntimeNotSupported",
437
+ errorMessage: `No handler for runtime ${props.runtime}`,
438
+ trace: [],
439
+ });
39
440
  return;
40
- const build = await builder.artifact(evt.properties.functionID);
41
- if (!build)
441
+ }
442
+ logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Getting build artifact...`);
443
+ logInvokeTrace("BUILD_ARTIFACT_START", requestID);
444
+ const buildStartTime = Date.now();
445
+ 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`);
448
+ logInvokeTrace("BUILD_ARTIFACT_DONE", requestID, build ? `out=${build.out.slice(-30)}` : "NO_BUILD");
449
+ if (!build) {
450
+ logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ERROR: Build artifact not ready`);
451
+ Logger.debug("Build artifact not ready for:", functionID);
452
+ bus.publish("function.error", {
453
+ workerID: awsWorkerID,
454
+ functionID,
455
+ requestID,
456
+ errorType: "BuildFailed",
457
+ errorMessage: `Build artifact not available for ${functionID}. Check for build errors.`,
458
+ trace: [],
459
+ });
42
460
  return;
43
- await handler.startWorker({
44
- ...build,
45
- workerID: evt.properties.workerID,
46
- functionID: evt.properties.functionID,
47
- environment: evt.properties.env,
48
- url: `${server.url}/${evt.properties.workerID}/${server.API_VERSION}`,
49
- runtime: props.runtime,
50
- });
51
- workers.set(evt.properties.workerID, {
52
- workerID: evt.properties.workerID,
53
- functionID: evt.properties.functionID,
54
- });
55
- bus.publish("worker.started", {
56
- workerID: evt.properties.workerID,
57
- functionID: evt.properties.functionID,
58
- });
461
+ }
462
+ // Check if this runtime supports pooling
463
+ const poolable = isPoolableRuntime(props.runtime, env);
464
+ if (poolable) {
465
+ // === POOLED PATH ===
466
+ // Get pool key: shared for mono-build, per-function otherwise
467
+ 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
585
+ 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);
589
+ }
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
+ }
624
+ }
625
+ });
626
+ // Process exit cleanup
627
+ process.on("exit", () => {
628
+ // Log final metrics summary
629
+ writeSessionEndSummary();
630
+ for (const pool of workerPool.values()) {
631
+ for (const worker of pool) {
632
+ clearTimeout(worker.idleTimer);
633
+ }
634
+ }
59
635
  });
60
636
  return {
61
637
  fromID(workerID) {
638
+ // Check pooled workers first
639
+ const pooled = activeWorkers.get(workerID);
640
+ if (pooled)
641
+ return { workerID, functionID: pooled.functionID };
642
+ // Check non-pooled workers
62
643
  return workers.get(workerID);
63
644
  },
64
645
  getCurrentRequestID(workerID) {
65
646
  return lastRequestId.get(workerID);
66
647
  },
67
648
  stdout(workerID, message) {
649
+ // Check pooled workers first
650
+ const pooled = activeWorkers.get(workerID);
651
+ 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
+ });
663
+ }
664
+ bus.publish("worker.stdout", {
665
+ workerID,
666
+ functionID: pooled.functionID,
667
+ message: trimmedMessage,
668
+ requestID,
669
+ });
670
+ }
671
+ return;
672
+ }
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
+ // Non-pooled worker
68
679
  const worker = workers.get(workerID);
680
+ if (!worker)
681
+ return;
682
+ const trimmedMessage = message.trim();
683
+ const requestID = lastRequestId.get(workerID);
684
+ // Log messages that contain [LOG] prefix
685
+ if (trimmedMessage.includes("[LOG]") && requestID) {
686
+ logEventTrace("WORKER_LOG", {
687
+ requestID,
688
+ functionID: worker.functionID,
689
+ workerID,
690
+ message: trimmedMessage,
691
+ });
692
+ }
69
693
  bus.publish("worker.stdout", {
70
694
  ...worker,
71
- message: message.trim(),
72
- requestID: lastRequestId.get(workerID),
695
+ message: trimmedMessage,
696
+ requestID: requestID,
73
697
  });
74
698
  },
75
699
  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
+ }
725
+ return;
726
+ }
727
+ // Non-pooled worker
76
728
  const existing = workers.get(workerID);
77
729
  if (!existing)
78
730
  return;
@@ -80,6 +732,147 @@ export const useRuntimeWorkers = lazy(async () => {
80
732
  lastRequestId.delete(workerID);
81
733
  bus.publish("worker.exited", existing);
82
734
  },
83
- subscribe: bus.forward("worker.started", "worker.stopped", "worker.exited", "worker.stdout"),
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);
753
+ },
754
+ // Check if worker is pooled
755
+ isPooled(workerID) {
756
+ return activeWorkers.has(workerID) || startedWorkers.has(workerID);
757
+ },
758
+ subscribe: bus.forward("worker.started", "worker.stopped", "worker.exited", "worker.stdout", "worker.reused"),
759
+ /**
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)
763
+ */
764
+ async triggerWarmup(count = 15) {
765
+ const functions = useFunctions();
766
+ const allFunctions = functions.all;
767
+ // Find a nodejs function to use as the warmup target
768
+ let targetFunction = null;
769
+ for (const [id, props] of Object.entries(allFunctions)) {
770
+ if (!props.runtime?.startsWith("nodejs"))
771
+ continue;
772
+ if (!isPoolableRuntime(props.runtime))
773
+ continue;
774
+ if (!props.functionName)
775
+ continue;
776
+ targetFunction = { id, props, functionName: props.functionName };
777
+ break;
778
+ }
779
+ if (!targetFunction) {
780
+ logPool("WARMUP_SKIP", {
781
+ reason: "no nodejs function found",
782
+ });
783
+ return { warmed: 0 };
784
+ }
785
+ const { functionName } = targetFunction;
786
+ logPool("WARMUP_START", {
787
+ count,
788
+ functionName,
789
+ });
790
+ // Publish warmup start event
791
+ bus.publish("warmup.start", { count });
792
+ const startTime = Date.now();
793
+ let success = 0;
794
+ let failed = 0;
795
+ let completed = 0;
796
+ // Use the shared AWS client
797
+ const { useAWSClient } = await import("../credentials.js");
798
+ const { LambdaClient, InvokeCommand } = await import("@aws-sdk/client-lambda");
799
+ const lambda = useAWSClient(LambdaClient);
800
+ // Helper to publish progress
801
+ const publishProgress = () => {
802
+ bus.publish("warmup.progress", {
803
+ completed,
804
+ total: count,
805
+ success,
806
+ failed,
807
+ });
808
+ };
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++;
822
+ }
823
+ else {
824
+ failed++;
825
+ }
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
+ }
862
+ const elapsed = Date.now() - startTime;
863
+ logPool("WARMUP_DONE", {
864
+ success,
865
+ failed,
866
+ elapsedMs: elapsed,
867
+ avgMs: success > 0 ? Math.round(elapsed / success) : 0,
868
+ });
869
+ // Publish warmup complete event
870
+ bus.publish("warmup.complete", {
871
+ success,
872
+ failed,
873
+ elapsedMs: elapsed,
874
+ });
875
+ return { warmed: success, elapsed };
876
+ },
84
877
  };
85
878
  });