@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.
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,
@@ -33,9 +37,12 @@ export const useRuntimeServer = lazy(async () => {
33
37
  }
34
38
  // Route an invocation to a specific workerID (used by workers.ts for pooled workers)
35
39
  function routeInvocation(targetWorkerID, invocation) {
40
+ const requestPath = getRequestPath(invocation.event);
41
+ const requestID = invocation.requestID;
36
42
  const waiting = workersWaiting.get(targetWorkerID);
37
43
  if (waiting) {
38
44
  workersWaiting.delete(targetWorkerID);
45
+ logServer(`path=${requestPath} reqId=${requestID.slice(0, 8)} Worker ${targetWorkerID.slice(0, 8)} was waiting, delivering immediately`);
39
46
  waiting(invocation);
40
47
  return;
41
48
  }
@@ -45,6 +52,7 @@ export const useRuntimeServer = lazy(async () => {
45
52
  invocationsQueued.set(targetWorkerID, arr);
46
53
  }
47
54
  arr.push(invocation);
55
+ logServer(`path=${requestPath} reqId=${requestID.slice(0, 8)} Worker ${targetWorkerID.slice(0, 8)} not waiting, QUEUED (queueSize=${arr.length})`);
48
56
  }
49
57
  workers.subscribe("worker.exited", async (evt) => {
50
58
  const waiting = workersWaiting.get(evt.properties.workerID);
@@ -69,8 +77,16 @@ export const useRuntimeServer = lazy(async () => {
69
77
  const awsWorkerID = workers.isPooled(pooledWorkerID)
70
78
  ? workers.getAwsWorkerID(pooledWorkerID) || pooledWorkerID
71
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
+ });
72
88
  bus.publish("function.error", {
73
- requestID: workers.getCurrentRequestID(pooledWorkerID),
89
+ requestID,
74
90
  workerID: awsWorkerID,
75
91
  functionID: worker.functionID,
76
92
  ...req.body,
@@ -80,11 +96,20 @@ export const useRuntimeServer = lazy(async () => {
80
96
  res.json("ok");
81
97
  });
82
98
  app.get(`/:workerID/${cfg.API_VERSION}/runtime/invocation/next`, async (req, res) => {
83
- Logger.debug("Worker", req.params.workerID, "is waiting for next invocation");
84
- const payload = await next(req.params.workerID);
85
- 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);
86
111
  res.set({
87
- "Lambda-Runtime-Aws-Request-Id": payload.context.awsRequestId,
112
+ "Lambda-Runtime-Aws-Request-Id": requestID,
88
113
  "Lambda-Runtime-Deadline-Ms": Date.now() + payload.deadline,
89
114
  "Lambda-Runtime-Invoked-Function-Arn": payload.context.invokedFunctionArn,
90
115
  "Lambda-Runtime-Client-Context": JSON.stringify(payload.context.clientContext || null),
@@ -93,6 +118,10 @@ export const useRuntimeServer = lazy(async () => {
93
118
  "Lambda-Runtime-Log-Stream-Name": payload.context.logStreamName,
94
119
  // Pass function ID for mono-build shared pool: allows per-invocation dispatch
95
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),
96
125
  });
97
126
  // Wrap event with env for per-invocation environment variable application
98
127
  // This prevents env leakage when workers are reused across different functions
@@ -109,9 +138,12 @@ export const useRuntimeServer = lazy(async () => {
109
138
  limit: "10mb",
110
139
  }), (req, res) => {
111
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`);
112
143
  Logger.debug("Worker", pooledWorkerID, "got response", req.body);
113
144
  const worker = workers.fromID(pooledWorkerID);
114
145
  if (!worker) {
146
+ logServer(`reqId=${requestID.slice(0, 8)} ERROR: Worker ${pooledWorkerID.slice(0, 8)} not found`);
115
147
  res.status(404).send();
116
148
  return;
117
149
  }
@@ -119,10 +151,17 @@ export const useRuntimeServer = lazy(async () => {
119
151
  const awsWorkerID = workers.isPooled(pooledWorkerID)
120
152
  ? workers.getAwsWorkerID(pooledWorkerID) || pooledWorkerID
121
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
+ });
122
161
  bus.publish("function.success", {
123
162
  workerID: awsWorkerID,
124
163
  functionID: worker.functionID,
125
- requestID: req.params.awsRequestId,
164
+ requestID: requestID,
126
165
  body: req.body,
127
166
  });
128
167
  // Return pooled worker to pool
@@ -177,12 +216,20 @@ export const useRuntimeServer = lazy(async () => {
177
216
  const awsWorkerID = workers.isPooled(pooledWorkerID)
178
217
  ? workers.getAwsWorkerID(pooledWorkerID) || pooledWorkerID
179
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
+ });
180
227
  bus.publish("function.error", {
181
228
  workerID: awsWorkerID,
182
229
  functionID: worker.functionID,
183
230
  errorType: req.body.errorType,
184
231
  errorMessage: req.body.errorMessage,
185
- requestID: req.params.awsRequestId,
232
+ requestID,
186
233
  trace: req.body.trace,
187
234
  });
188
235
  // Return pooled worker to pool
@@ -1,5 +1,4 @@
1
- import fs from "fs";
2
- import path from "path";
1
+ import { createDebugFileLogger } from "./debug-file-logger.js";
3
2
  // Configuration
4
3
  const SST_BUILD_CONCURRENCY = parseInt(process.env.SST_BUILD_CONCURRENCY || "4", 10);
5
4
  export const POOL_SIZE = parseInt(process.env.SST_WORKER_POOL_SIZE || "10", 10);
@@ -16,7 +15,21 @@ const metrics = {
16
15
  avgResponseTime: new Map(),
17
16
  responseCount: new Map(),
18
17
  };
19
- let logStream = null;
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
+ }
20
33
  // Function name resolver - set by workers.ts
21
34
  let functionNameResolver = (id) => id.slice(0, 25);
22
35
  /**
@@ -26,23 +39,6 @@ let functionNameResolver = (id) => id.slice(0, 25);
26
39
  export function setFunctionNameResolver(resolver) {
27
40
  functionNameResolver = resolver;
28
41
  }
29
- function initLogFile() {
30
- if (!DEBUG_POOL || logStream)
31
- return;
32
- try {
33
- const logDir = path.dirname(DEBUG_POOL_FILE);
34
- if (!fs.existsSync(logDir)) {
35
- fs.mkdirSync(logDir, { recursive: true });
36
- }
37
- logStream = fs.createWriteStream(DEBUG_POOL_FILE, { flags: "w" });
38
- logStream.write("\n" + "=".repeat(100) + "\n" +
39
- "[SESSION START] " + new Date().toISOString() + " | POOL_SIZE=" + POOL_SIZE + " IDLE_TIMEOUT=" + IDLE_TIMEOUT + "ms BUILD_CONCURRENCY=" + SST_BUILD_CONCURRENCY + "\n" +
40
- "=".repeat(100) + "\n");
41
- }
42
- catch {
43
- // Fall back to no logging if file creation fails
44
- }
45
- }
46
42
  // Extract readable function name from handler path
47
43
  function getFunctionName(functionID) {
48
44
  return functionNameResolver(functionID);
@@ -57,30 +53,29 @@ function getBottleneckFlags(functionID) {
57
53
  const coldStarts = metrics.coldStarts.get(functionID) || 0;
58
54
  // SATURATED: All pool slots in use
59
55
  if (concurrent >= POOL_SIZE) {
60
- flags.push("🚨 SATURATED");
56
+ flags.push("SATURATED");
61
57
  }
62
58
  // HIGH_LOAD: >70% pool utilization
63
59
  else if (concurrent >= POOL_SIZE * 0.7) {
64
- flags.push("⚠️ HIGH_LOAD");
60
+ flags.push("HIGH_LOAD");
65
61
  }
66
62
  // COLD_START: Low hit rate indicates frequent cold starts
67
63
  if (total >= 5 && hitRate < 30) {
68
- flags.push("❄️ LOW_REUSE");
64
+ flags.push("LOW_REUSE");
69
65
  }
70
66
  // BOTTLENECK: High cold start ratio
71
67
  if (total >= 3 && coldStarts / total > 0.5) {
72
- flags.push("🔥 COLD_HEAVY");
68
+ flags.push("COLD_HEAVY");
73
69
  }
74
- return flags.length > 0 ? " " + flags.join(" ") : "";
70
+ return flags.length > 0 ? " [" + flags.join(" ") + "]" : "";
75
71
  }
76
72
  /**
77
73
  * Pool debug logging helper - writes to file with bottleneck detection
78
74
  */
79
75
  export function logPool(action, details = {}) {
80
- if (!DEBUG_POOL)
76
+ const logger = getPoolLogger();
77
+ if (!logger)
81
78
  return;
82
- initLogFile();
83
- const timestamp = new Date().toISOString().slice(11, 23); // HH:MM:SS.mmm
84
79
  const funcName = details.functionID
85
80
  ? getFunctionName(details.functionID)
86
81
  : "";
@@ -95,20 +90,29 @@ export function logPool(action, details = {}) {
95
90
  const total = metrics.totalRequests.get(details.functionID) || 0;
96
91
  const hits = metrics.poolHits.get(details.functionID) || 0;
97
92
  const hitRate = total > 0 ? ((hits / total) * 100).toFixed(0) : "0";
98
- metricsStr = " | concurrent=" + concurrent + " hitRate=" + hitRate + "%";
99
- }
100
- // Format details, replacing functionID with funcName
101
- const filteredDetails = { ...details };
102
- delete filteredDetails.functionID;
103
- const detailStr = Object.entries(filteredDetails)
104
- .map(([k, v]) => k + "=" + (typeof v === "object" ? JSON.stringify(v) : v))
105
- .join(" ");
106
- const actionPadded = action.padEnd(14);
107
- const funcPadded = funcName.padEnd(25);
108
- const line = "[" + timestamp + "] " + actionPadded + " " + funcPadded + " " + detailStr + metricsStr + bottleneckFlags + "\n";
109
- if (logStream) {
110
- logStream.write(line);
93
+ metricsStr = `concurrent=${concurrent} hitRate=${hitRate}%`;
111
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);
112
116
  }
113
117
  /**
114
118
  * Track request lifecycle for metrics - called at request start
@@ -142,49 +146,40 @@ export function trackRequestEnd(functionID) {
142
146
  * Called from workers.ts on process exit
143
147
  */
144
148
  export function writeSessionEndSummary() {
145
- if (!DEBUG_POOL || !logStream)
149
+ const logger = getPoolLogger();
150
+ if (!logger)
146
151
  return;
147
- let summary = "\n" + "─".repeat(100) + "\n[SESSION END] " + new Date().toISOString() + "\n";
152
+ let summary = "";
148
153
  for (const [funcID, total] of metrics.totalRequests) {
149
154
  const hits = metrics.poolHits.get(funcID) || 0;
150
155
  const cold = metrics.coldStarts.get(funcID) || 0;
151
156
  const peak = metrics.peakConcurrent.get(funcID) || 0;
152
157
  const hitRate = total > 0 ? ((hits / total) * 100).toFixed(1) : "0";
153
158
  const funcName = getFunctionName(funcID);
154
- summary += " " + funcName.padEnd(25) + " total=" + total + " poolHits=" + hits + " coldStarts=" + cold + " peakConcurrent=" + peak + " hitRate=" + hitRate + "%\n";
159
+ summary += ` ${funcName.padEnd(25)} total=${total} poolHits=${hits} coldStarts=${cold} peakConcurrent=${peak} hitRate=${hitRate}%\n`;
155
160
  }
156
- summary += "─".repeat(100) + "\n";
157
- logStream.write(summary);
158
- logStream.end();
161
+ logger.close(summary);
159
162
  }
160
- // Invocation trace logging - always enabled, writes to separate file
161
- let traceStream = null;
162
- function initTraceFile() {
163
- if (traceStream)
164
- return;
165
- try {
166
- const logDir = path.dirname(INVOKE_TRACE_FILE);
167
- if (!fs.existsSync(logDir)) {
168
- fs.mkdirSync(logDir, { recursive: true });
169
- }
170
- traceStream = fs.createWriteStream(INVOKE_TRACE_FILE, { flags: "w" });
171
- traceStream.write("\n" + "=".repeat(80) + "\n" +
172
- "[TRACE START] " + new Date().toISOString() + "\n" +
173
- "=".repeat(80) + "\n");
174
- }
175
- catch {
176
- // Fall back to no logging if file creation fails
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
+ });
177
172
  }
173
+ return traceLogger;
178
174
  }
179
175
  /**
180
176
  * Log invocation trace events to .sst/invoke-trace.log
181
177
  * Always enabled for debugging invocation flow
182
178
  */
183
179
  export function logInvokeTrace(stage, requestID, details) {
184
- initTraceFile();
185
- if (!traceStream)
186
- return;
187
- const timestamp = new Date().toISOString().slice(11, 23); // HH:MM:SS.mmm
188
- const line = `[${timestamp}] ${stage.padEnd(20)} req=${requestID.slice(0, 8)} ${details || ""}\n`;
189
- traceStream.write(line);
180
+ const logger = getTraceLogger();
181
+ logger.log(stage, {
182
+ req: requestID.slice(0, 8),
183
+ ...(details ? { info: details } : {}),
184
+ });
190
185
  }
@@ -23,6 +23,20 @@ declare module "../bus.js" {
23
23
  functionID: string;
24
24
  pooledWorkerID: string;
25
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
+ };
26
40
  }
27
41
  }
28
42
  export declare const useRuntimeWorkers: () => Promise<{
@@ -40,4 +54,16 @@ export declare const useRuntimeWorkers: () => Promise<{
40
54
  type: keyof import("../bus.js").Events;
41
55
  cb: (payload: any) => void;
42
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
+ }>;
43
69
  }>;