@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.
package/runtime/server.js CHANGED
@@ -2,9 +2,13 @@ import express from "express";
2
2
  import { useBus } from "../bus.js";
3
3
  import { Logger } from "../logger.js";
4
4
  import { useRuntimeWorkers } from "./workers.js";
5
+ import { useFunctions } from "../constructs/Function.js";
5
6
  import https from "https";
6
7
  import getPort from "get-port";
7
8
  import { lazy } from "../util/lazy.js";
9
+ import { getRequestPath } from "./request-utils.js";
10
+ import { logServer } from "./debug-bridge-logging.js";
11
+ import { logEventTrace } from "./event-trace-logging.js";
8
12
  export const useRuntimeServerConfig = lazy(async () => {
9
13
  const port = await getPort({
10
14
  port: 12557,
@@ -31,54 +35,100 @@ export const useRuntimeServer = lazy(async () => {
31
35
  workersWaiting.set(workerID, resolve);
32
36
  });
33
37
  }
34
- workers.subscribe("worker.exited", async (evt) => {
35
- const waiting = workersWaiting.get(evt.properties.workerID);
36
- if (!waiting)
37
- return;
38
- workersWaiting.delete(evt.properties.workerID);
39
- });
40
- bus.subscribe("function.invoked", async (evt) => {
41
- const worker = workersWaiting.get(evt.properties.workerID);
42
- if (worker) {
43
- workersWaiting.delete(evt.properties.workerID);
44
- worker(evt.properties);
38
+ // Route an invocation to a specific workerID (used by workers.ts for pooled workers)
39
+ function routeInvocation(targetWorkerID, invocation) {
40
+ const requestPath = getRequestPath(invocation.event);
41
+ const requestID = invocation.requestID;
42
+ const waiting = workersWaiting.get(targetWorkerID);
43
+ if (waiting) {
44
+ workersWaiting.delete(targetWorkerID);
45
+ logServer(`path=${requestPath} reqId=${requestID.slice(0, 8)} Worker ${targetWorkerID.slice(0, 8)} was waiting, delivering immediately`);
46
+ waiting(invocation);
45
47
  return;
46
48
  }
47
- let arr = invocationsQueued.get(evt.properties.workerID);
49
+ let arr = invocationsQueued.get(targetWorkerID);
48
50
  if (!arr) {
49
51
  arr = [];
50
- invocationsQueued.set(evt.properties.workerID, arr);
52
+ invocationsQueued.set(targetWorkerID, arr);
51
53
  }
52
- arr.push(evt.properties);
54
+ arr.push(invocation);
55
+ logServer(`path=${requestPath} reqId=${requestID.slice(0, 8)} Worker ${targetWorkerID.slice(0, 8)} not waiting, QUEUED (queueSize=${arr.length})`);
56
+ }
57
+ workers.subscribe("worker.exited", async (evt) => {
58
+ const waiting = workersWaiting.get(evt.properties.workerID);
59
+ if (!waiting)
60
+ return;
61
+ workersWaiting.delete(evt.properties.workerID);
53
62
  });
63
+ // Note: function.invoked routing is handled by workers.ts via routeInvocation()
64
+ // This ensures correct routing for both pooled and non-pooled workers
54
65
  app.post(`/:workerID/${cfg.API_VERSION}/runtime/init/error`, express.json({
55
66
  strict: false,
56
67
  type: ["application/json", "application/*+json"],
57
68
  limit: "10mb",
58
69
  }), async (req, res) => {
59
- const worker = workers.fromID(req.params.workerID);
70
+ const pooledWorkerID = req.params.workerID;
71
+ const worker = workers.fromID(pooledWorkerID);
72
+ if (!worker) {
73
+ res.status(404).send();
74
+ return;
75
+ }
76
+ // Get AWS workerID for IoT routing (if pooled)
77
+ const awsWorkerID = workers.isPooled(pooledWorkerID)
78
+ ? workers.getAwsWorkerID(pooledWorkerID) || pooledWorkerID
79
+ : pooledWorkerID;
80
+ const requestID = workers.getCurrentRequestID(pooledWorkerID);
81
+ logEventTrace("WORKER_END", {
82
+ requestID: requestID || "unknown",
83
+ functionID: worker.functionID,
84
+ workerID: pooledWorkerID,
85
+ status: "error",
86
+ errorType: req.body?.errorType || "init_error",
87
+ });
60
88
  bus.publish("function.error", {
61
- requestID: workers.getCurrentRequestID(worker.workerID),
62
- workerID: worker.workerID,
89
+ requestID,
90
+ workerID: awsWorkerID,
63
91
  functionID: worker.functionID,
64
92
  ...req.body,
65
93
  });
94
+ // Return pooled worker to pool
95
+ workers.onResponse(pooledWorkerID);
66
96
  res.json("ok");
67
97
  });
68
98
  app.get(`/:workerID/${cfg.API_VERSION}/runtime/invocation/next`, async (req, res) => {
69
- Logger.debug("Worker", req.params.workerID, "is waiting for next invocation");
70
- const payload = await next(req.params.workerID);
71
- Logger.debug("Worker", req.params.workerID, "sending next payload");
99
+ const workerID = req.params.workerID;
100
+ logServer(`Worker ${workerID.slice(0, 8)} requesting /next`);
101
+ Logger.debug("Worker", workerID, "is waiting for next invocation");
102
+ const waitStart = Date.now();
103
+ const payload = await next(workerID);
104
+ const requestPath = getRequestPath(payload.event);
105
+ const requestID = payload.context.awsRequestId;
106
+ logServer(`path=${requestPath} reqId=${requestID.slice(0, 8)} Worker ${workerID.slice(0, 8)} received payload after waiting ${Date.now() - waitStart}ms`);
107
+ Logger.debug("Worker", workerID, "sending next payload");
108
+ // Get function properties for per-invocation context
109
+ // This is essential for mono-build shared workers that serve multiple functions
110
+ const funcProps = useFunctions().fromID(payload.functionID);
72
111
  res.set({
73
- "Lambda-Runtime-Aws-Request-Id": payload.context.awsRequestId,
112
+ "Lambda-Runtime-Aws-Request-Id": requestID,
74
113
  "Lambda-Runtime-Deadline-Ms": Date.now() + payload.deadline,
75
114
  "Lambda-Runtime-Invoked-Function-Arn": payload.context.invokedFunctionArn,
76
115
  "Lambda-Runtime-Client-Context": JSON.stringify(payload.context.clientContext || null),
77
116
  "Lambda-Runtime-Cognito-Identity": JSON.stringify(payload.context.identity || null),
78
117
  "Lambda-Runtime-Log-Group-Name": payload.context.logGroupName,
79
118
  "Lambda-Runtime-Log-Stream-Name": payload.context.logStreamName,
119
+ // Pass function ID for mono-build shared pool: allows per-invocation dispatch
120
+ "Lambda-Runtime-Sst-Function-Id": payload.functionID,
121
+ // Per-invocation function context for mono-build shared workers
122
+ "Lambda-Runtime-Function-Name": funcProps?.functionName || "",
123
+ "Lambda-Runtime-Function-Version": "$LATEST",
124
+ "Lambda-Runtime-Function-Memory-Size": String(funcProps?.memorySize || 1024),
125
+ });
126
+ // Wrap event with env for per-invocation environment variable application
127
+ // This prevents env leakage when workers are reused across different functions
128
+ res.json({
129
+ event: payload.event,
130
+ env: payload.env,
80
131
  });
81
- res.json(payload.event);
82
132
  });
83
133
  app.post(`/:workerID/${cfg.API_VERSION}/runtime/invocation/:awsRequestId/response`, express.json({
84
134
  strict: false,
@@ -87,14 +137,35 @@ export const useRuntimeServer = lazy(async () => {
87
137
  },
88
138
  limit: "10mb",
89
139
  }), (req, res) => {
90
- Logger.debug("Worker", req.params.workerID, "got response", req.body);
91
- const worker = workers.fromID(req.params.workerID);
140
+ const pooledWorkerID = req.params.workerID;
141
+ const requestID = req.params.awsRequestId;
142
+ logServer(`reqId=${requestID.slice(0, 8)} Worker ${pooledWorkerID.slice(0, 8)} posting /response`);
143
+ Logger.debug("Worker", pooledWorkerID, "got response", req.body);
144
+ const worker = workers.fromID(pooledWorkerID);
145
+ if (!worker) {
146
+ logServer(`reqId=${requestID.slice(0, 8)} ERROR: Worker ${pooledWorkerID.slice(0, 8)} not found`);
147
+ res.status(404).send();
148
+ return;
149
+ }
150
+ // Get AWS workerID for IoT routing (if pooled)
151
+ const awsWorkerID = workers.isPooled(pooledWorkerID)
152
+ ? workers.getAwsWorkerID(pooledWorkerID) || pooledWorkerID
153
+ : pooledWorkerID;
154
+ logServer(`reqId=${requestID.slice(0, 8)} Publishing function.success awsWorkerID=${awsWorkerID.slice(0, 8)}`);
155
+ logEventTrace("WORKER_END", {
156
+ requestID,
157
+ functionID: worker.functionID,
158
+ workerID: pooledWorkerID,
159
+ status: "success",
160
+ });
92
161
  bus.publish("function.success", {
93
- workerID: worker.workerID,
162
+ workerID: awsWorkerID,
94
163
  functionID: worker.functionID,
95
- requestID: req.params.awsRequestId,
164
+ requestID: requestID,
96
165
  body: req.body,
97
166
  });
167
+ // Return pooled worker to pool
168
+ workers.onResponse(pooledWorkerID);
98
169
  res.status(202).send();
99
170
  });
100
171
  app.all(`/proxy*`, express.raw({
@@ -135,16 +206,38 @@ export const useRuntimeServer = lazy(async () => {
135
206
  type: ["application/json", "application/*+json"],
136
207
  limit: "10mb",
137
208
  }), (req, res) => {
138
- const worker = workers.fromID(req.params.workerID);
209
+ const pooledWorkerID = req.params.workerID;
210
+ const worker = workers.fromID(pooledWorkerID);
211
+ if (!worker) {
212
+ res.status(404).send();
213
+ return;
214
+ }
215
+ // Get AWS workerID for IoT routing (if pooled)
216
+ const awsWorkerID = workers.isPooled(pooledWorkerID)
217
+ ? workers.getAwsWorkerID(pooledWorkerID) || pooledWorkerID
218
+ : pooledWorkerID;
219
+ const requestID = req.params.awsRequestId;
220
+ logEventTrace("WORKER_END", {
221
+ requestID,
222
+ functionID: worker.functionID,
223
+ workerID: pooledWorkerID,
224
+ status: "error",
225
+ errorType: req.body.errorType,
226
+ });
139
227
  bus.publish("function.error", {
140
- workerID: worker.workerID,
228
+ workerID: awsWorkerID,
141
229
  functionID: worker.functionID,
142
230
  errorType: req.body.errorType,
143
231
  errorMessage: req.body.errorMessage,
144
- requestID: req.params.awsRequestId,
232
+ requestID,
145
233
  trace: req.body.trace,
146
234
  });
235
+ // Return pooled worker to pool
236
+ workers.onResponse(pooledWorkerID);
147
237
  res.status(202).send();
148
238
  });
149
239
  app.listen(cfg.port);
240
+ return {
241
+ routeInvocation,
242
+ };
150
243
  });
@@ -0,0 +1,29 @@
1
+ export declare const POOL_SIZE: number;
2
+ export declare const IDLE_TIMEOUT: number;
3
+ /**
4
+ * Set the function name resolver callback
5
+ * Called by workers.ts to provide access to useFunctions()
6
+ */
7
+ export declare function setFunctionNameResolver(resolver: (functionID: string) => string): void;
8
+ /**
9
+ * Pool debug logging helper - writes to file with bottleneck detection
10
+ */
11
+ export declare function logPool(action: string, details?: Record<string, any>): void;
12
+ /**
13
+ * Track request lifecycle for metrics - called at request start
14
+ */
15
+ export declare function trackRequestStart(functionID: string, isPoolHit: boolean): void;
16
+ /**
17
+ * Track request lifecycle for metrics - called at request end
18
+ */
19
+ export declare function trackRequestEnd(functionID: string): void;
20
+ /**
21
+ * Write session end summary and close log stream
22
+ * Called from workers.ts on process exit
23
+ */
24
+ export declare function writeSessionEndSummary(): void;
25
+ /**
26
+ * Log invocation trace events to .sst/invoke-trace.log
27
+ * Always enabled for debugging invocation flow
28
+ */
29
+ export declare function logInvokeTrace(stage: string, requestID: string, details?: string): void;
@@ -0,0 +1,185 @@
1
+ import { createDebugFileLogger } from "./debug-file-logger.js";
2
+ // Configuration
3
+ const SST_BUILD_CONCURRENCY = parseInt(process.env.SST_BUILD_CONCURRENCY || "4", 10);
4
+ export const POOL_SIZE = parseInt(process.env.SST_WORKER_POOL_SIZE || "10", 10);
5
+ export const IDLE_TIMEOUT = parseInt(process.env.SST_WORKER_IDLE_TIMEOUT || "60000", 10);
6
+ const DEBUG_POOL = process.env.SST_DEBUG_POOL === "true";
7
+ const DEBUG_POOL_FILE = process.env.SST_DEBUG_POOL_FILE || ".sst/worker-pool.log";
8
+ const INVOKE_TRACE_FILE = ".sst/invoke-trace.log";
9
+ const metrics = {
10
+ coldStarts: new Map(),
11
+ concurrentRequests: new Map(),
12
+ peakConcurrent: new Map(),
13
+ totalRequests: new Map(),
14
+ poolHits: new Map(),
15
+ avgResponseTime: new Map(),
16
+ responseCount: new Map(),
17
+ };
18
+ // Create pool logger using the abstract logger
19
+ let poolLogger = null;
20
+ function getPoolLogger() {
21
+ if (!DEBUG_POOL)
22
+ return null;
23
+ if (!poolLogger) {
24
+ poolLogger = createDebugFileLogger({
25
+ filePath: DEBUG_POOL_FILE,
26
+ sessionName: "POOL",
27
+ sessionHeader: `POOL_SIZE=${POOL_SIZE} IDLE_TIMEOUT=${IDLE_TIMEOUT}ms BUILD_CONCURRENCY=${SST_BUILD_CONCURRENCY}`,
28
+ width: 100,
29
+ });
30
+ }
31
+ return poolLogger;
32
+ }
33
+ // Function name resolver - set by workers.ts
34
+ let functionNameResolver = (id) => id.slice(0, 25);
35
+ /**
36
+ * Set the function name resolver callback
37
+ * Called by workers.ts to provide access to useFunctions()
38
+ */
39
+ export function setFunctionNameResolver(resolver) {
40
+ functionNameResolver = resolver;
41
+ }
42
+ // Extract readable function name from handler path
43
+ function getFunctionName(functionID) {
44
+ return functionNameResolver(functionID);
45
+ }
46
+ // Calculate bottleneck indicators
47
+ function getBottleneckFlags(functionID) {
48
+ const flags = [];
49
+ const concurrent = metrics.concurrentRequests.get(functionID) || 0;
50
+ const total = metrics.totalRequests.get(functionID) || 0;
51
+ const hits = metrics.poolHits.get(functionID) || 0;
52
+ const hitRate = total > 0 ? (hits / total) * 100 : 0;
53
+ const coldStarts = metrics.coldStarts.get(functionID) || 0;
54
+ // SATURATED: All pool slots in use
55
+ if (concurrent >= POOL_SIZE) {
56
+ flags.push("SATURATED");
57
+ }
58
+ // HIGH_LOAD: >70% pool utilization
59
+ else if (concurrent >= POOL_SIZE * 0.7) {
60
+ flags.push("HIGH_LOAD");
61
+ }
62
+ // COLD_START: Low hit rate indicates frequent cold starts
63
+ if (total >= 5 && hitRate < 30) {
64
+ flags.push("LOW_REUSE");
65
+ }
66
+ // BOTTLENECK: High cold start ratio
67
+ if (total >= 3 && coldStarts / total > 0.5) {
68
+ flags.push("COLD_HEAVY");
69
+ }
70
+ return flags.length > 0 ? " [" + flags.join(" ") + "]" : "";
71
+ }
72
+ /**
73
+ * Pool debug logging helper - writes to file with bottleneck detection
74
+ */
75
+ export function logPool(action, details = {}) {
76
+ const logger = getPoolLogger();
77
+ if (!logger)
78
+ return;
79
+ const funcName = details.functionID
80
+ ? getFunctionName(details.functionID)
81
+ : "";
82
+ const bottleneckFlags = details.functionID &&
83
+ ["CREATE", "REUSE", "POOL_MISS", "RESPONSE"].includes(action)
84
+ ? getBottleneckFlags(details.functionID)
85
+ : "";
86
+ // Build metrics string for key actions
87
+ let metricsStr = "";
88
+ if (details.functionID && ["RESPONSE", "RETURN_TO_POOL"].includes(action)) {
89
+ const concurrent = metrics.concurrentRequests.get(details.functionID) || 0;
90
+ const total = metrics.totalRequests.get(details.functionID) || 0;
91
+ const hits = metrics.poolHits.get(details.functionID) || 0;
92
+ const hitRate = total > 0 ? ((hits / total) * 100).toFixed(0) : "0";
93
+ metricsStr = `concurrent=${concurrent} hitRate=${hitRate}%`;
94
+ }
95
+ // Build the log details
96
+ const logDetails = {};
97
+ // Add function name first if present
98
+ if (funcName) {
99
+ logDetails.func = funcName;
100
+ }
101
+ // Add all other details except functionID
102
+ for (const [k, v] of Object.entries(details)) {
103
+ if (k !== "functionID") {
104
+ logDetails[k] = v;
105
+ }
106
+ }
107
+ // Add metrics if present
108
+ if (metricsStr) {
109
+ logDetails.metrics = metricsStr;
110
+ }
111
+ // Add bottleneck flags
112
+ if (bottleneckFlags) {
113
+ logDetails.status = bottleneckFlags.trim();
114
+ }
115
+ logger.log(action, logDetails);
116
+ }
117
+ /**
118
+ * Track request lifecycle for metrics - called at request start
119
+ */
120
+ export function trackRequestStart(functionID, isPoolHit) {
121
+ const current = (metrics.concurrentRequests.get(functionID) || 0) + 1;
122
+ metrics.concurrentRequests.set(functionID, current);
123
+ metrics.totalRequests.set(functionID, (metrics.totalRequests.get(functionID) || 0) + 1);
124
+ const peak = metrics.peakConcurrent.get(functionID) || 0;
125
+ if (current > peak) {
126
+ metrics.peakConcurrent.set(functionID, current);
127
+ }
128
+ if (isPoolHit) {
129
+ metrics.poolHits.set(functionID, (metrics.poolHits.get(functionID) || 0) + 1);
130
+ }
131
+ else {
132
+ metrics.coldStarts.set(functionID, (metrics.coldStarts.get(functionID) || 0) + 1);
133
+ }
134
+ }
135
+ /**
136
+ * Track request lifecycle for metrics - called at request end
137
+ */
138
+ export function trackRequestEnd(functionID) {
139
+ const current = metrics.concurrentRequests.get(functionID) || 0;
140
+ if (current > 0) {
141
+ metrics.concurrentRequests.set(functionID, current - 1);
142
+ }
143
+ }
144
+ /**
145
+ * Write session end summary and close log stream
146
+ * Called from workers.ts on process exit
147
+ */
148
+ export function writeSessionEndSummary() {
149
+ const logger = getPoolLogger();
150
+ if (!logger)
151
+ return;
152
+ let summary = "";
153
+ for (const [funcID, total] of metrics.totalRequests) {
154
+ const hits = metrics.poolHits.get(funcID) || 0;
155
+ const cold = metrics.coldStarts.get(funcID) || 0;
156
+ const peak = metrics.peakConcurrent.get(funcID) || 0;
157
+ const hitRate = total > 0 ? ((hits / total) * 100).toFixed(1) : "0";
158
+ const funcName = getFunctionName(funcID);
159
+ summary += ` ${funcName.padEnd(25)} total=${total} poolHits=${hits} coldStarts=${cold} peakConcurrent=${peak} hitRate=${hitRate}%\n`;
160
+ }
161
+ logger.close(summary);
162
+ }
163
+ // Invocation trace logging - uses the abstract logger
164
+ let traceLogger = null;
165
+ function getTraceLogger() {
166
+ if (!traceLogger) {
167
+ traceLogger = createDebugFileLogger({
168
+ filePath: INVOKE_TRACE_FILE,
169
+ sessionName: "TRACE",
170
+ width: 80,
171
+ });
172
+ }
173
+ return traceLogger;
174
+ }
175
+ /**
176
+ * Log invocation trace events to .sst/invoke-trace.log
177
+ * Always enabled for debugging invocation flow
178
+ */
179
+ export function logInvokeTrace(stage, requestID, details) {
180
+ const logger = getTraceLogger();
181
+ logger.log(stage, {
182
+ req: requestID.slice(0, 8),
183
+ ...(details ? { info: details } : {}),
184
+ });
185
+ }
@@ -18,20 +18,52 @@ declare module "../bus.js" {
18
18
  requestID: string;
19
19
  message: string;
20
20
  };
21
+ "worker.reused": {
22
+ workerID: string;
23
+ functionID: string;
24
+ pooledWorkerID: string;
25
+ };
26
+ "warmup.start": {
27
+ count: number;
28
+ };
29
+ "warmup.progress": {
30
+ completed: number;
31
+ total: number;
32
+ success: number;
33
+ failed: number;
34
+ };
35
+ "warmup.complete": {
36
+ success: number;
37
+ failed: number;
38
+ elapsedMs: number;
39
+ };
21
40
  }
22
41
  }
23
- interface Worker {
24
- workerID: string;
25
- functionID: string;
26
- }
27
42
  export declare const useRuntimeWorkers: () => Promise<{
28
- fromID(workerID: string): Worker;
43
+ fromID(workerID: string): {
44
+ workerID: string;
45
+ functionID: string;
46
+ };
29
47
  getCurrentRequestID(workerID: string): string | undefined;
30
48
  stdout(workerID: string, message: string): void;
31
49
  exited(workerID: string): void;
32
- subscribe: <Type extends "worker.started" | "worker.stopped" | "worker.exited" | "worker.stdout">(type: Type, cb: (payload: import("../bus.js").EventPayload<Type>) => void) => {
50
+ onResponse(pooledWorkerID: string): void;
51
+ getAwsWorkerID(pooledWorkerID: string): string | undefined;
52
+ isPooled(workerID: string): boolean;
53
+ subscribe: <Type extends "worker.started" | "worker.stopped" | "worker.exited" | "worker.stdout" | "worker.reused">(type: Type, cb: (payload: import("../bus.js").EventPayload<Type>) => void) => {
33
54
  type: keyof import("../bus.js").Events;
34
55
  cb: (payload: any) => void;
35
56
  };
57
+ /**
58
+ * Trigger warmup by invoking Lambda functions with warmup payloads.
59
+ * This sends real requests through the IoT bridge, which naturally creates workers.
60
+ * @param count Number of workers to warm up (default: 15)
61
+ */
62
+ triggerWarmup(count?: number): Promise<{
63
+ warmed: number;
64
+ elapsed?: undefined;
65
+ } | {
66
+ warmed: number;
67
+ elapsed: number;
68
+ }>;
36
69
  }>;
37
- export {};