@intelligems/sst 2.49.6-ig.2 → 2.49.6-ig.4

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,71 +1,616 @@
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
+ // Track workers marked as stale (should not return to pool after completion)
12
+ const staleWorkers = new Set();
13
+ const bundleMtimes = new Map();
14
+ const bundleWatchers = new Map();
15
+ // Clean up watchers on exit
16
+ process.on("exit", () => {
17
+ for (const watcher of bundleWatchers.values()) {
18
+ try {
19
+ watcher.close();
20
+ }
21
+ catch { }
22
+ }
23
+ });
24
+ // Get bundle rebuild timestamp for staleness checking
25
+ function getBundleMtime(buildOut) {
26
+ if (buildOut.includes(".mono-build")) {
27
+ if (bundleMtimes.has(buildOut)) {
28
+ return bundleMtimes.get(buildOut);
29
+ }
30
+ const timestampFile = path.join(buildOut, ".last-rebuild");
31
+ const update = () => {
32
+ try {
33
+ const content = fs.readFileSync(timestampFile, "utf-8");
34
+ const mtime = parseInt(content, 10);
35
+ bundleMtimes.set(buildOut, mtime);
36
+ return mtime;
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ };
42
+ if (!bundleWatchers.has(buildOut)) {
43
+ try {
44
+ const watcher = fs.watch(buildOut, { persistent: false }, (event, filename) => {
45
+ if (!filename || filename === ".last-rebuild") {
46
+ update();
47
+ }
48
+ });
49
+ watcher.on("error", () => {
50
+ bundleWatchers.delete(buildOut);
51
+ bundleMtimes.delete(buildOut);
52
+ try {
53
+ watcher.close();
54
+ }
55
+ catch { }
56
+ });
57
+ bundleWatchers.set(buildOut, watcher);
58
+ }
59
+ catch { }
60
+ }
61
+ return update();
62
+ }
63
+ try {
64
+ // For non-mono-bundle, use bundle directory mtime
65
+ const stat = fs.statSync(buildOut);
66
+ return stat.mtimeMs;
67
+ }
68
+ catch {
69
+ return undefined;
70
+ }
71
+ }
72
+ // Helper: Get pool key for worker lookup
73
+ // For mono-build, uses shared key so any warm worker can serve any handler
74
+ function getPoolKey(functionID, runtime, buildOut) {
75
+ const isMonoBuild = buildOut.includes(".mono-build");
76
+ if (isMonoBuild) {
77
+ return { key: `${runtime}:mono-build`, isShared: true };
78
+ }
79
+ return { key: `${runtime}:${functionID}`, isShared: false };
80
+ }
81
+ // Extract readable function name from handler path
82
+ function getFunctionName(functionID) {
83
+ try {
84
+ const props = useFunctions().fromID(functionID);
85
+ if (!props)
86
+ return functionID.slice(0, 25);
87
+ return (props.functionName || functionID).split("backend-")[1]?.slice(0, 50) || functionID.slice(0, 25);
88
+ }
89
+ catch {
90
+ return functionID.slice(0, 25);
91
+ }
92
+ }
93
+ // Runtimes that support multiple invocations per process (have event loop)
94
+ const POOLABLE_RUNTIMES = new Set([
95
+ // Node.js - has while(true) loop in nodejs-runtime/index.ts
96
+ "nodejs",
97
+ "nodejs14.x",
98
+ "nodejs16.x",
99
+ "nodejs18.x",
100
+ "nodejs20.x",
101
+ "nodejs22.x",
102
+ // Python - has while True loop in python-runtime/runtime.py
103
+ "python",
104
+ "python3.7",
105
+ "python3.8",
106
+ "python3.9",
107
+ "python3.10",
108
+ "python3.11",
109
+ "python3.12",
110
+ "python3.13",
111
+ // Go - AWS Lambda Go SDK has built-in event loop
112
+ "go",
113
+ "go1.x",
114
+ // Java - AWS Lambda Java SDK has built-in event loop
115
+ "java",
116
+ "java8",
117
+ "java8.al2",
118
+ "java11",
119
+ "java17",
120
+ "java21",
121
+ // .NET - AWS Lambda .NET SDK has built-in event loop
122
+ "dotnet",
123
+ "dotnet6",
124
+ "dotnet8",
125
+ "dotnetcore3.1",
126
+ // Rust - AWS Lambda Rust runtime has built-in event loop
127
+ "rust",
128
+ ]);
129
+ function isPoolableRuntime(runtime, env) {
130
+ // Container jobs are NOT poolable
131
+ if (runtime.startsWith("container") && env?.SST_DEBUG_JOB) {
132
+ return false;
133
+ }
134
+ return (POOLABLE_RUNTIMES.has(runtime) ||
135
+ [...POOLABLE_RUNTIMES].some((r) => runtime.startsWith(r)));
136
+ }
6
137
  export const useRuntimeWorkers = lazy(async () => {
138
+ // Set up function name resolver for logging module
139
+ setFunctionNameResolver(getFunctionName);
140
+ // Non-pooled workers (legacy behavior)
7
141
  const workers = new Map();
142
+ // Worker pool data structures
143
+ const workerPool = new Map();
144
+ const activeWorkers = new Map();
145
+ const workerIDMapping = new Map(); // awsWorkerID → pooledWorkerID
146
+ const reverseMapping = new Map(); // pooledWorkerID → awsWorkerID
147
+ const startedWorkers = new Set(); // Track started pooledWorkerIDs
8
148
  const bus = useBus();
9
149
  const handlers = useRuntimeHandlers();
10
150
  const builder = useFunctionBuilder();
11
- const server = await useRuntimeServerConfig();
151
+ const serverConfig = await useRuntimeServerConfig();
152
+ // Log pool configuration on startup
153
+ logPool("INIT", {
154
+ poolSize: POOL_SIZE,
155
+ idleTimeoutMs: IDLE_TIMEOUT,
156
+ poolableRuntimes: [...POOLABLE_RUNTIMES].length,
157
+ });
158
+ // Lazy getter for server to avoid circular initialization
159
+ let _server = null;
160
+ async function getServer() {
161
+ if (!_server) {
162
+ _server = await useRuntimeServer();
163
+ }
164
+ return _server;
165
+ }
166
+ // Helper: Terminate a pooled worker
167
+ async function terminatePooledWorker(pooledWorkerID, reason) {
168
+ const worker = activeWorkers.get(pooledWorkerID) ||
169
+ [...workerPool.values()]
170
+ .flat()
171
+ .find((w) => w.pooledWorkerID === pooledWorkerID);
172
+ if (!worker)
173
+ return;
174
+ const props = useFunctions().fromID(worker.functionID);
175
+ if (!props)
176
+ return;
177
+ const uptime = Date.now() - worker.createdAt;
178
+ logPool("TERMINATE", {
179
+ pooledWorkerID: pooledWorkerID.slice(0, 8),
180
+ functionID: worker.functionID,
181
+ reason: reason || "unknown",
182
+ uptimeMs: uptime,
183
+ });
184
+ const handler = handlers.for(props.runtime);
185
+ await handler?.stopWorker(pooledWorkerID);
186
+ // Clean up mappings
187
+ activeWorkers.delete(pooledWorkerID);
188
+ staleWorkers.delete(pooledWorkerID);
189
+ const awsWorkerID = reverseMapping.get(pooledWorkerID);
190
+ if (awsWorkerID) {
191
+ workerIDMapping.delete(awsWorkerID);
192
+ }
193
+ reverseMapping.delete(pooledWorkerID);
194
+ startedWorkers.delete(pooledWorkerID);
195
+ lastRequestId.delete(pooledWorkerID);
196
+ Logger.debug("Terminated pooled worker", pooledWorkerID);
197
+ }
198
+ // Helper: Get idle worker from pool
199
+ // Uses poolKey for lookup (shared key for mono-build)
200
+ function getIdleWorker(poolKey, functionID, buildOut) {
201
+ const pool = workerPool.get(poolKey);
202
+ if (!pool || pool.length === 0) {
203
+ logPool("POOL_MISS", {
204
+ functionID,
205
+ poolKey: poolKey.slice(0, 30),
206
+ poolSize: 0,
207
+ });
208
+ return undefined;
209
+ }
210
+ // Check current bundle mtime for staleness detection
211
+ const currentMtime = getBundleMtime(buildOut);
212
+ // Try to find a non-stale worker
213
+ while (pool.length > 0) {
214
+ const worker = pool.pop();
215
+ if (!worker)
216
+ break;
217
+ clearTimeout(worker.idleTimer);
218
+ // Check if worker is stale (bundle was modified since worker started)
219
+ if (currentMtime && worker.bundleMtime && currentMtime > worker.bundleMtime) {
220
+ logPool("STALE_MTIME", {
221
+ pooledWorkerID: worker.pooledWorkerID.slice(0, 8),
222
+ functionID,
223
+ workerMtime: worker.bundleMtime,
224
+ currentMtime,
225
+ });
226
+ terminatePooledWorker(worker.pooledWorkerID, "stale_mtime");
227
+ continue; // Try next worker
228
+ }
229
+ worker.state = "busy";
230
+ const age = Date.now() - worker.createdAt;
231
+ const crossFunction = worker.functionID !== functionID;
232
+ logPool("REUSE", {
233
+ pooledWorkerID: worker.pooledWorkerID.slice(0, 8),
234
+ functionID,
235
+ originalFunctionID: crossFunction ? worker.functionID : undefined,
236
+ poolSizeAfter: pool.length,
237
+ workerAgeMs: age,
238
+ crossFunction,
239
+ });
240
+ Logger.debug("Reusing pooled worker", worker.pooledWorkerID, "for", functionID, crossFunction ? "(cross-function reuse)" : "");
241
+ return worker;
242
+ }
243
+ // All workers were stale
244
+ logPool("POOL_MISS", {
245
+ functionID,
246
+ poolKey: poolKey.slice(0, 30),
247
+ poolSize: 0,
248
+ reason: "all_stale",
249
+ });
250
+ return undefined;
251
+ }
252
+ // Helper: Return worker to pool
253
+ // Uses poolKey for pool lookup (shared key for mono-build)
254
+ function returnToPool(pooledWorkerID) {
255
+ const worker = activeWorkers.get(pooledWorkerID);
256
+ if (!worker)
257
+ return;
258
+ // Check if worker is stale (marked for termination due to rebuild)
259
+ if (staleWorkers.has(pooledWorkerID)) {
260
+ staleWorkers.delete(pooledWorkerID);
261
+ logPool("STALE_TERMINATE", {
262
+ pooledWorkerID: pooledWorkerID.slice(0, 8),
263
+ functionID: worker.functionID,
264
+ reason: "marked-stale-during-rebuild",
265
+ });
266
+ terminatePooledWorker(pooledWorkerID, "stale");
267
+ return;
268
+ }
269
+ // Clean up current request mappings
270
+ const awsWorkerID = reverseMapping.get(pooledWorkerID);
271
+ if (awsWorkerID) {
272
+ workerIDMapping.delete(awsWorkerID);
273
+ reverseMapping.delete(pooledWorkerID);
274
+ }
275
+ // Use poolKey for pool lookup (shared for mono-build)
276
+ let pool = workerPool.get(worker.poolKey);
277
+ if (!pool) {
278
+ pool = [];
279
+ workerPool.set(worker.poolKey, pool);
280
+ }
281
+ if (pool.length >= POOL_SIZE) {
282
+ // Pool full, terminate
283
+ logPool("POOL_FULL", {
284
+ pooledWorkerID: pooledWorkerID.slice(0, 8),
285
+ functionID: worker.functionID,
286
+ poolKey: worker.poolKey.slice(0, 30),
287
+ poolSize: pool.length,
288
+ maxSize: POOL_SIZE,
289
+ });
290
+ terminatePooledWorker(pooledWorkerID, "pool_full");
291
+ Logger.debug("Pool full, terminated worker", pooledWorkerID);
292
+ return;
293
+ }
294
+ // Return to pool with idle timeout
295
+ worker.state = "idle";
296
+ worker.idleTimer = setTimeout(() => {
297
+ const idx = pool.indexOf(worker);
298
+ if (idx >= 0)
299
+ pool.splice(idx, 1);
300
+ terminatePooledWorker(pooledWorkerID, "idle_timeout");
301
+ Logger.debug("Idle timeout, terminated worker", pooledWorkerID);
302
+ }, IDLE_TIMEOUT);
303
+ pool.push(worker);
304
+ activeWorkers.delete(pooledWorkerID);
305
+ logPool("RETURN_TO_POOL", {
306
+ pooledWorkerID: pooledWorkerID.slice(0, 8),
307
+ functionID: worker.functionID,
308
+ poolKey: worker.poolKey.slice(0, 30),
309
+ isSharedPool: worker.isSharedPool,
310
+ poolSizeAfter: pool.length,
311
+ idleTimeoutMs: IDLE_TIMEOUT,
312
+ });
313
+ Logger.debug("Returned worker to pool", pooledWorkerID, "pool key:", worker.poolKey, "pool size:", pool.length);
314
+ }
315
+ // Build success handler - clear pool for rebuilt function
12
316
  handlers.subscribe("function.build.success", async (evt) => {
317
+ const { functionID } = evt.properties;
318
+ const props = useFunctions().fromID(functionID);
319
+ if (!props)
320
+ return;
321
+ // Get build to check if mono-build
322
+ const build = await builder.artifact(functionID);
323
+ const isMonoBuild = build?.out.includes(".mono-build") ?? false;
324
+ if (isMonoBuild) {
325
+ // For mono-build: clear the entire shared pool since all functions share the same bundle
326
+ const sharedPoolKey = `${props.runtime}:mono-build`;
327
+ const sharedPool = workerPool.get(sharedPoolKey) || [];
328
+ const activeSharedCount = [...activeWorkers.values()].filter((w) => w.isSharedPool && w.poolKey === sharedPoolKey).length;
329
+ logPool("MONO_BUILD_CLEAR", {
330
+ functionID,
331
+ sharedPoolKey,
332
+ pooledWorkersCleared: sharedPool.length,
333
+ activeWorkersMarkedStale: activeSharedCount,
334
+ });
335
+ // Terminate all idle workers in the shared pool
336
+ for (const worker of sharedPool) {
337
+ clearTimeout(worker.idleTimer);
338
+ await terminatePooledWorker(worker.pooledWorkerID, "mono-rebuild");
339
+ }
340
+ workerPool.delete(sharedPoolKey);
341
+ // Mark active workers as stale (they'll be terminated after completing their request)
342
+ for (const [pooledID, worker] of activeWorkers) {
343
+ if (worker.isSharedPool && worker.poolKey === sharedPoolKey) {
344
+ staleWorkers.add(pooledID);
345
+ logPool("MARK_STALE", {
346
+ pooledWorkerID: pooledID.slice(0, 8),
347
+ functionID: worker.functionID,
348
+ reason: "mono-rebuild",
349
+ });
350
+ }
351
+ }
352
+ }
353
+ else {
354
+ // For non-mono-build: clear pool for this specific function only
355
+ const pool = workerPool.get(`${props.runtime}:${functionID}`) || [];
356
+ const activeCount = [...activeWorkers.values()].filter((w) => w.functionID === functionID).length;
357
+ logPool("BUILD_CLEAR", {
358
+ functionID,
359
+ pooledWorkersCleared: pool.length,
360
+ activeWorkersMarkedStale: activeCount,
361
+ });
362
+ for (const worker of pool) {
363
+ clearTimeout(worker.idleTimer);
364
+ await terminatePooledWorker(worker.pooledWorkerID, "rebuild");
365
+ }
366
+ workerPool.delete(`${props.runtime}:${functionID}`);
367
+ // Mark active workers as stale (they'll be terminated after completing their request)
368
+ for (const [pooledID, worker] of activeWorkers) {
369
+ if (worker.functionID === functionID) {
370
+ staleWorkers.add(pooledID);
371
+ logPool("MARK_STALE", {
372
+ pooledWorkerID: pooledID.slice(0, 8),
373
+ functionID: worker.functionID,
374
+ reason: "rebuild",
375
+ });
376
+ }
377
+ }
378
+ }
379
+ // Stop non-pooled workers (legacy behavior)
13
380
  for (const [_, worker] of workers) {
14
- if (worker.functionID === evt.properties.functionID) {
15
- const props = useFunctions().fromID(worker.functionID);
16
- if (!props)
381
+ if (worker.functionID === functionID) {
382
+ const workerProps = useFunctions().fromID(worker.functionID);
383
+ if (!workerProps)
17
384
  return;
18
- const handler = handlers.for(props.runtime);
385
+ const handler = handlers.for(workerProps.runtime);
19
386
  await handler?.stopWorker(worker.workerID);
20
387
  bus.publish("worker.stopped", worker);
21
388
  }
22
389
  }
23
390
  });
24
391
  const lastRequestId = new Map();
392
+ // Main invocation handler
25
393
  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)
394
+ const { workerID: awsWorkerID, functionID, requestID, env, } = evt.properties;
395
+ const startTime = Date.now();
396
+ logInvokeTrace("INVOKE_RECEIVED", requestID, `func=${functionID.slice(-40)}`);
397
+ // Send ack immediately
398
+ bus.publish("function.ack", { functionID, workerID: awsWorkerID });
399
+ logInvokeTrace("ACK_PUBLISHED", requestID, `elapsed=${Date.now() - startTime}ms`);
400
+ const props = useFunctions().fromID(functionID);
401
+ if (!props) {
402
+ Logger.debug("Function not found:", functionID);
403
+ bus.publish("function.error", {
404
+ workerID: awsWorkerID,
405
+ functionID,
406
+ requestID,
407
+ errorType: "FunctionNotFound",
408
+ errorMessage: `Function ${functionID} not found in project`,
409
+ trace: [],
410
+ });
36
411
  return;
412
+ }
37
413
  const handler = handlers.for(props.runtime);
38
- if (!handler)
414
+ if (!handler) {
415
+ Logger.debug("No handler for runtime:", props.runtime);
416
+ bus.publish("function.error", {
417
+ workerID: awsWorkerID,
418
+ functionID,
419
+ requestID,
420
+ errorType: "RuntimeNotSupported",
421
+ errorMessage: `No handler for runtime ${props.runtime}`,
422
+ trace: [],
423
+ });
39
424
  return;
40
- const build = await builder.artifact(evt.properties.functionID);
41
- if (!build)
425
+ }
426
+ logInvokeTrace("BUILD_ARTIFACT_START", requestID);
427
+ const build = await builder.artifact(functionID);
428
+ logInvokeTrace("BUILD_ARTIFACT_DONE", requestID, build ? `out=${build.out.slice(-30)}` : "NO_BUILD");
429
+ if (!build) {
430
+ Logger.debug("Build artifact not ready for:", functionID);
431
+ bus.publish("function.error", {
432
+ workerID: awsWorkerID,
433
+ functionID,
434
+ requestID,
435
+ errorType: "BuildFailed",
436
+ errorMessage: `Build artifact not available for ${functionID}. Check for build errors.`,
437
+ trace: [],
438
+ });
42
439
  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
- });
440
+ }
441
+ // Check if this runtime supports pooling
442
+ const poolable = isPoolableRuntime(props.runtime, env);
443
+ if (poolable) {
444
+ // === POOLED PATH ===
445
+ // Get pool key: shared for mono-build, per-function otherwise
446
+ const { key: poolKey, isShared } = getPoolKey(functionID, props.runtime, build.out);
447
+ let pooledWorker = getIdleWorker(poolKey, functionID, build.out);
448
+ let isReuse = false;
449
+ if (pooledWorker) {
450
+ isReuse = true;
451
+ // Update functionID for cross-function reuse (mono-build)
452
+ pooledWorker.functionID = functionID;
453
+ trackRequestStart(functionID, true);
454
+ }
455
+ else {
456
+ // Create new pooled worker
457
+ const pooledWorkerID = crypto.randomBytes(16).toString("hex");
458
+ const bundleMtime = getBundleMtime(build.out);
459
+ pooledWorker = {
460
+ pooledWorkerID,
461
+ functionID,
462
+ state: "busy",
463
+ createdAt: Date.now(),
464
+ poolKey,
465
+ isSharedPool: isShared,
466
+ bundlePath: build.out,
467
+ bundleMtime,
468
+ };
469
+ }
470
+ // Set up mappings
471
+ workerIDMapping.set(awsWorkerID, pooledWorker.pooledWorkerID);
472
+ reverseMapping.set(pooledWorker.pooledWorkerID, awsWorkerID);
473
+ lastRequestId.set(pooledWorker.pooledWorkerID, requestID);
474
+ activeWorkers.set(pooledWorker.pooledWorkerID, pooledWorker);
475
+ if (!isReuse) {
476
+ // Start new worker with pooledWorkerID (cold start)
477
+ trackRequestStart(functionID, false);
478
+ const currentPoolSize = workerPool.get(poolKey)?.length || 0;
479
+ logPool("CREATE", {
480
+ pooledWorkerID: pooledWorker.pooledWorkerID.slice(0, 8),
481
+ functionID,
482
+ runtime: props.runtime,
483
+ requestID: requestID.slice(0, 8),
484
+ poolKey: poolKey.slice(0, 30),
485
+ isSharedPool: isShared,
486
+ currentPoolSize,
487
+ activeWorkers: activeWorkers.size,
488
+ });
489
+ logInvokeTrace("WORKER_START", requestID, `pooled=${pooledWorker.pooledWorkerID.slice(0, 8)}`);
490
+ try {
491
+ await handler.startWorker({
492
+ ...build,
493
+ workerID: pooledWorker.pooledWorkerID,
494
+ functionID,
495
+ environment: env,
496
+ url: `${serverConfig.url}/${pooledWorker.pooledWorkerID}/${serverConfig.API_VERSION}`,
497
+ runtime: props.runtime,
498
+ });
499
+ startedWorkers.add(pooledWorker.pooledWorkerID);
500
+ logInvokeTrace("WORKER_STARTED", requestID);
501
+ bus.publish("worker.started", {
502
+ workerID: awsWorkerID,
503
+ functionID,
504
+ });
505
+ }
506
+ catch (ex) {
507
+ Logger.debug("Failed to start pooled worker", ex);
508
+ bus.publish("function.error", {
509
+ workerID: awsWorkerID,
510
+ functionID,
511
+ requestID,
512
+ errorType: "WorkerStartFailed",
513
+ errorMessage: `Failed to start pooled worker: ${ex.message}`,
514
+ trace: ex.stack?.split("\n") || [],
515
+ });
516
+ // Cleanup failed worker state
517
+ activeWorkers.delete(pooledWorker.pooledWorkerID);
518
+ startedWorkers.delete(pooledWorker.pooledWorkerID);
519
+ lastRequestId.delete(pooledWorker.pooledWorkerID);
520
+ workerIDMapping.delete(awsWorkerID);
521
+ reverseMapping.delete(pooledWorker.pooledWorkerID);
522
+ return;
523
+ }
524
+ }
525
+ else {
526
+ logInvokeTrace("WORKER_REUSE", requestID, `pooled=${pooledWorker.pooledWorkerID.slice(0, 8)}`);
527
+ bus.publish("worker.reused", {
528
+ workerID: awsWorkerID,
529
+ functionID,
530
+ pooledWorkerID: pooledWorker.pooledWorkerID,
531
+ });
532
+ }
533
+ // Route invocation to the pooled worker
534
+ const server = await getServer();
535
+ logInvokeTrace("ROUTE_INVOCATION", requestID);
536
+ server.routeInvocation(pooledWorker.pooledWorkerID, evt.properties);
537
+ }
538
+ else {
539
+ // === NON-POOLED PATH (existing behavior) ===
540
+ lastRequestId.set(awsWorkerID, requestID);
541
+ let worker = workers.get(awsWorkerID);
542
+ if (worker)
543
+ return;
544
+ try {
545
+ await handler.startWorker({
546
+ ...build,
547
+ workerID: awsWorkerID,
548
+ functionID,
549
+ environment: env,
550
+ url: `${serverConfig.url}/${awsWorkerID}/${serverConfig.API_VERSION}`,
551
+ runtime: props.runtime,
552
+ });
553
+ workers.set(awsWorkerID, { workerID: awsWorkerID, functionID });
554
+ bus.publish("worker.started", { workerID: awsWorkerID, functionID });
555
+ // Route invocation to the non-pooled worker
556
+ const server = await getServer();
557
+ server.routeInvocation(awsWorkerID, evt.properties);
558
+ }
559
+ catch (ex) {
560
+ Logger.debug("Failed to start worker", ex);
561
+ bus.publish("function.error", {
562
+ workerID: awsWorkerID,
563
+ functionID,
564
+ requestID,
565
+ errorType: "WorkerStartFailed",
566
+ errorMessage: `Failed to start worker: ${ex.message}`,
567
+ trace: ex.stack?.split("\n") || [],
568
+ });
569
+ return;
570
+ }
571
+ }
572
+ });
573
+ // Process exit cleanup
574
+ process.on("exit", () => {
575
+ // Log final metrics summary
576
+ writeSessionEndSummary();
577
+ for (const pool of workerPool.values()) {
578
+ for (const worker of pool) {
579
+ clearTimeout(worker.idleTimer);
580
+ }
581
+ }
59
582
  });
60
583
  return {
61
584
  fromID(workerID) {
585
+ // Check pooled workers first
586
+ const pooled = activeWorkers.get(workerID);
587
+ if (pooled)
588
+ return { workerID, functionID: pooled.functionID };
589
+ // Check non-pooled workers
62
590
  return workers.get(workerID);
63
591
  },
64
592
  getCurrentRequestID(workerID) {
65
593
  return lastRequestId.get(workerID);
66
594
  },
67
595
  stdout(workerID, message) {
596
+ // Check pooled workers first
597
+ const pooled = activeWorkers.get(workerID);
598
+ if (pooled) {
599
+ const requestID = lastRequestId.get(workerID);
600
+ if (requestID) {
601
+ bus.publish("worker.stdout", {
602
+ workerID,
603
+ functionID: pooled.functionID,
604
+ message: message.trim(),
605
+ requestID,
606
+ });
607
+ }
608
+ return;
609
+ }
610
+ // Non-pooled worker
68
611
  const worker = workers.get(workerID);
612
+ if (!worker)
613
+ return;
69
614
  bus.publish("worker.stdout", {
70
615
  ...worker,
71
616
  message: message.trim(),
@@ -73,6 +618,34 @@ export const useRuntimeWorkers = lazy(async () => {
73
618
  });
74
619
  },
75
620
  exited(workerID) {
621
+ // Check if pooled worker
622
+ if (activeWorkers.has(workerID) || startedWorkers.has(workerID)) {
623
+ const worker = activeWorkers.get(workerID);
624
+ if (worker) {
625
+ const uptime = Date.now() - worker.createdAt;
626
+ logPool("EXIT", {
627
+ pooledWorkerID: workerID.slice(0, 8),
628
+ functionID: worker.functionID,
629
+ state: worker.state,
630
+ uptimeMs: uptime,
631
+ });
632
+ // Clean up all mappings
633
+ const awsWorkerID = reverseMapping.get(workerID);
634
+ if (awsWorkerID) {
635
+ workerIDMapping.delete(awsWorkerID);
636
+ }
637
+ reverseMapping.delete(workerID);
638
+ activeWorkers.delete(workerID);
639
+ lastRequestId.delete(workerID);
640
+ startedWorkers.delete(workerID);
641
+ bus.publish("worker.exited", {
642
+ workerID: awsWorkerID || workerID,
643
+ functionID: worker.functionID,
644
+ });
645
+ }
646
+ return;
647
+ }
648
+ // Non-pooled worker
76
649
  const existing = workers.get(workerID);
77
650
  if (!existing)
78
651
  return;
@@ -80,6 +653,29 @@ export const useRuntimeWorkers = lazy(async () => {
80
653
  lastRequestId.delete(workerID);
81
654
  bus.publish("worker.exited", existing);
82
655
  },
83
- subscribe: bus.forward("worker.started", "worker.stopped", "worker.exited", "worker.stdout"),
656
+ // Called by server when response is received - returns worker to pool
657
+ onResponse(pooledWorkerID) {
658
+ if (activeWorkers.has(pooledWorkerID)) {
659
+ const worker = activeWorkers.get(pooledWorkerID);
660
+ if (worker) {
661
+ trackRequestEnd(worker.functionID);
662
+ logPool("RESPONSE", {
663
+ pooledWorkerID: pooledWorkerID.slice(0, 8),
664
+ functionID: worker.functionID,
665
+ requestID: lastRequestId.get(pooledWorkerID)?.slice(0, 8),
666
+ });
667
+ }
668
+ returnToPool(pooledWorkerID);
669
+ }
670
+ },
671
+ // Get AWS workerID from pooled ID (for IoT routing)
672
+ getAwsWorkerID(pooledWorkerID) {
673
+ return reverseMapping.get(pooledWorkerID);
674
+ },
675
+ // Check if worker is pooled
676
+ isPooled(workerID) {
677
+ return activeWorkers.has(workerID) || startedWorkers.has(workerID);
678
+ },
679
+ subscribe: bus.forward("worker.started", "worker.stopped", "worker.exited", "worker.stdout", "worker.reused"),
84
680
  };
85
681
  });