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

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.
@@ -8,6 +8,10 @@ import { useFunctions } from "../constructs/Function.js";
8
8
  import { lazy } from "../util/lazy.js";
9
9
  import { Logger } from "../logger.js";
10
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";
11
15
  // Track workers marked as stale (should not return to pool after completion)
12
16
  const staleWorkers = new Set();
13
17
  const bundleMtimes = new Map();
@@ -23,7 +27,7 @@ process.on("exit", () => {
23
27
  });
24
28
  // Get bundle rebuild timestamp for staleness checking
25
29
  function getBundleMtime(buildOut) {
26
- if (buildOut.includes(".mono-build")) {
30
+ if (isMonoBuildPath(buildOut)) {
27
31
  if (bundleMtimes.has(buildOut)) {
28
32
  return bundleMtimes.get(buildOut);
29
33
  }
@@ -72,11 +76,8 @@ function getBundleMtime(buildOut) {
72
76
  // Helper: Get pool key for worker lookup
73
77
  // For mono-build, uses shared key so any warm worker can serve any handler
74
78
  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 };
79
+ // Use the global mono build config for pool key calculation
80
+ return useMonoBuildConfig().getPoolKey(functionID, runtime, buildOut);
80
81
  }
81
82
  // Extract readable function name from handler path
82
83
  function getFunctionName(functionID) {
@@ -318,9 +319,9 @@ export const useRuntimeWorkers = lazy(async () => {
318
319
  const props = useFunctions().fromID(functionID);
319
320
  if (!props)
320
321
  return;
321
- // Get build to check if mono-build
322
+ // Get build to check if mono-build using global config
322
323
  const build = await builder.artifact(functionID);
323
- const isMonoBuild = build?.out.includes(".mono-build") ?? false;
324
+ const isMonoBuild = build ? isMonoBuildPath(build.out) : false;
324
325
  if (isMonoBuild) {
325
326
  // For mono-build: clear the entire shared pool since all functions share the same bundle
326
327
  const sharedPoolKey = `${props.runtime}:mono-build`;
@@ -391,14 +392,28 @@ export const useRuntimeWorkers = lazy(async () => {
391
392
  const lastRequestId = new Map();
392
393
  // Main invocation handler
393
394
  bus.subscribe("function.invoked", async (evt) => {
394
- const { workerID: awsWorkerID, functionID, requestID, env, } = evt.properties;
395
+ const { workerID: awsWorkerID, functionID, requestID, env, event, } = evt.properties;
395
396
  const startTime = Date.now();
396
- logInvokeTrace("INVOKE_RECEIVED", requestID, `func=${functionID.slice(-40)}`);
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
+ }
397
410
  // Send ack immediately
398
- bus.publish("function.ack", { functionID, workerID: awsWorkerID });
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`);
399
413
  logInvokeTrace("ACK_PUBLISHED", requestID, `elapsed=${Date.now() - startTime}ms`);
400
414
  const props = useFunctions().fromID(functionID);
401
415
  if (!props) {
416
+ logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ERROR: Function not found`);
402
417
  Logger.debug("Function not found:", functionID);
403
418
  bus.publish("function.error", {
404
419
  workerID: awsWorkerID,
@@ -412,6 +427,7 @@ export const useRuntimeWorkers = lazy(async () => {
412
427
  }
413
428
  const handler = handlers.for(props.runtime);
414
429
  if (!handler) {
430
+ logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ERROR: No handler for runtime ${props.runtime}`);
415
431
  Logger.debug("No handler for runtime:", props.runtime);
416
432
  bus.publish("function.error", {
417
433
  workerID: awsWorkerID,
@@ -423,10 +439,15 @@ export const useRuntimeWorkers = lazy(async () => {
423
439
  });
424
440
  return;
425
441
  }
442
+ logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Getting build artifact...`);
426
443
  logInvokeTrace("BUILD_ARTIFACT_START", requestID);
444
+ const buildStartTime = Date.now();
427
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`);
428
448
  logInvokeTrace("BUILD_ARTIFACT_DONE", requestID, build ? `out=${build.out.slice(-30)}` : "NO_BUILD");
429
449
  if (!build) {
450
+ logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ERROR: Build artifact not ready`);
430
451
  Logger.debug("Build artifact not ready for:", functionID);
431
452
  bus.publish("function.error", {
432
453
  workerID: awsWorkerID,
@@ -444,13 +465,16 @@ export const useRuntimeWorkers = lazy(async () => {
444
465
  // === POOLED PATH ===
445
466
  // Get pool key: shared for mono-build, per-function otherwise
446
467
  const { key: poolKey, isShared } = getPoolKey(functionID, props.runtime, build.out);
447
- let pooledWorker = getIdleWorker(poolKey, functionID, 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);
448
471
  let isReuse = false;
449
472
  if (pooledWorker) {
450
473
  isReuse = true;
451
474
  // Update functionID for cross-function reuse (mono-build)
452
475
  pooledWorker.functionID = functionID;
453
476
  trackRequestStart(functionID, true);
477
+ logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} REUSING pooled worker ${pooledWorker.pooledWorkerID.slice(0, 8)}`);
454
478
  }
455
479
  else {
456
480
  // Create new pooled worker
@@ -466,6 +490,7 @@ export const useRuntimeWorkers = lazy(async () => {
466
490
  bundlePath: build.out,
467
491
  bundleMtime,
468
492
  };
493
+ logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} CREATING new pooled worker ${pooledWorkerID.slice(0, 8)}`);
469
494
  }
470
495
  // Set up mappings
471
496
  workerIDMapping.set(awsWorkerID, pooledWorker.pooledWorkerID);
@@ -476,7 +501,7 @@ export const useRuntimeWorkers = lazy(async () => {
476
501
  // Start new worker with pooledWorkerID (cold start)
477
502
  trackRequestStart(functionID, false);
478
503
  const currentPoolSize = workerPool.get(poolKey)?.length || 0;
479
- logPool("CREATE", {
504
+ logPool(isWarmupRequest ? "WARMUP_CREATE" : "CREATE", {
480
505
  pooledWorkerID: pooledWorker.pooledWorkerID.slice(0, 8),
481
506
  functionID,
482
507
  runtime: props.runtime,
@@ -485,8 +510,11 @@ export const useRuntimeWorkers = lazy(async () => {
485
510
  isSharedPool: isShared,
486
511
  currentPoolSize,
487
512
  activeWorkers: activeWorkers.size,
513
+ ...(isWarmupRequest && { warmupId }),
488
514
  });
515
+ logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Starting worker ${pooledWorker.pooledWorkerID.slice(0, 8)}...`);
489
516
  logInvokeTrace("WORKER_START", requestID, `pooled=${pooledWorker.pooledWorkerID.slice(0, 8)}`);
517
+ const workerStartTime = Date.now();
490
518
  try {
491
519
  await handler.startWorker({
492
520
  ...build,
@@ -495,15 +523,29 @@ export const useRuntimeWorkers = lazy(async () => {
495
523
  environment: env,
496
524
  url: `${serverConfig.url}/${pooledWorker.pooledWorkerID}/${serverConfig.API_VERSION}`,
497
525
  runtime: props.runtime,
526
+ isMonoBuild: isShared,
498
527
  });
499
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`);
500
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
+ });
501
542
  bus.publish("worker.started", {
502
543
  workerID: awsWorkerID,
503
544
  functionID,
504
545
  });
505
546
  }
506
547
  catch (ex) {
548
+ logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ERROR: Failed to start worker: ${ex.message}`);
507
549
  Logger.debug("Failed to start pooled worker", ex);
508
550
  bus.publish("function.error", {
509
551
  workerID: awsWorkerID,
@@ -524,6 +566,15 @@ export const useRuntimeWorkers = lazy(async () => {
524
566
  }
525
567
  else {
526
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
+ });
527
578
  bus.publish("worker.reused", {
528
579
  workerID: awsWorkerID,
529
580
  functionID,
@@ -532,6 +583,7 @@ export const useRuntimeWorkers = lazy(async () => {
532
583
  }
533
584
  // Route invocation to the pooled worker
534
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`);
535
587
  logInvokeTrace("ROUTE_INVOCATION", requestID);
536
588
  server.routeInvocation(pooledWorker.pooledWorkerID, evt.properties);
537
589
  }
@@ -549,6 +601,7 @@ export const useRuntimeWorkers = lazy(async () => {
549
601
  environment: env,
550
602
  url: `${serverConfig.url}/${awsWorkerID}/${serverConfig.API_VERSION}`,
551
603
  runtime: props.runtime,
604
+ isMonoBuild: isMonoBuildPath(build.out),
552
605
  });
553
606
  workers.set(awsWorkerID, { workerID: awsWorkerID, functionID });
554
607
  bus.publish("worker.started", { workerID: awsWorkerID, functionID });
@@ -598,23 +651,49 @@ export const useRuntimeWorkers = lazy(async () => {
598
651
  if (pooled) {
599
652
  const requestID = lastRequestId.get(workerID);
600
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
+ }
601
664
  bus.publish("worker.stdout", {
602
665
  workerID,
603
666
  functionID: pooled.functionID,
604
- message: message.trim(),
667
+ message: trimmedMessage,
605
668
  requestID,
606
669
  });
607
670
  }
608
671
  return;
609
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
+ }
610
678
  // Non-pooled worker
611
679
  const worker = workers.get(workerID);
612
680
  if (!worker)
613
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
+ }
614
693
  bus.publish("worker.stdout", {
615
694
  ...worker,
616
- message: message.trim(),
617
- requestID: lastRequestId.get(workerID),
695
+ message: trimmedMessage,
696
+ requestID: requestID,
618
697
  });
619
698
  },
620
699
  exited(workerID) {
@@ -677,5 +756,123 @@ export const useRuntimeWorkers = lazy(async () => {
677
756
  return activeWorkers.has(workerID) || startedWorkers.has(workerID);
678
757
  },
679
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
+ },
680
877
  };
681
878
  });