@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/cli/commands/dev.js +69 -3
- package/cli/sst.js +0 -1
- package/constructs/Function.d.ts +2 -1
- package/constructs/Function.js +4 -3
- package/credentials.js +4 -3
- package/iot.d.ts +31 -0
- package/iot.js +171 -10
- package/package.json +2 -2
- package/runtime/debug-bridge-logging.d.ts +24 -0
- package/runtime/debug-bridge-logging.js +98 -0
- package/runtime/debug-file-logger.d.ts +80 -0
- package/runtime/debug-file-logger.js +148 -0
- package/runtime/event-trace-logging.d.ts +27 -0
- package/runtime/event-trace-logging.js +69 -0
- package/runtime/handlers/node.js +62 -21
- package/runtime/handlers.d.ts +6 -2
- package/runtime/handlers.js +130 -59
- package/runtime/iot.js +31 -4
- package/runtime/mono-build-config.d.ts +58 -0
- package/runtime/mono-build-config.js +104 -0
- package/runtime/request-utils.d.ts +21 -0
- package/runtime/request-utils.js +102 -0
- package/runtime/runtime.d.ts +1 -0
- package/runtime/server.d.ts +4 -1
- package/runtime/server.js +122 -29
- package/runtime/worker-pool-logging.d.ts +29 -0
- package/runtime/worker-pool-logging.js +185 -0
- package/runtime/workers.d.ts +39 -7
- package/runtime/workers.js +831 -38
- package/stacks/deploy.js +14 -10
- package/stacks/synth.js +5 -4
- package/support/bridge/live-lambda.mjs +38 -38
- package/support/nodejs-runtime/index.mjs +44 -11
- package/support/python-runtime/runtime.py +116 -45
- package/util/user-configuration.js +19 -7
- package/README.md +0 -43
- package/package.json.bak +0 -156
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.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
workersWaiting.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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(
|
|
49
|
+
let arr = invocationsQueued.get(targetWorkerID);
|
|
48
50
|
if (!arr) {
|
|
49
51
|
arr = [];
|
|
50
|
-
invocationsQueued.set(
|
|
52
|
+
invocationsQueued.set(targetWorkerID, arr);
|
|
51
53
|
}
|
|
52
|
-
arr.push(
|
|
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
|
|
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
|
|
62
|
-
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
|
-
|
|
70
|
-
|
|
71
|
-
Logger.debug("Worker",
|
|
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":
|
|
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
|
-
|
|
91
|
-
const
|
|
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:
|
|
162
|
+
workerID: awsWorkerID,
|
|
94
163
|
functionID: worker.functionID,
|
|
95
|
-
requestID:
|
|
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
|
|
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:
|
|
228
|
+
workerID: awsWorkerID,
|
|
141
229
|
functionID: worker.functionID,
|
|
142
230
|
errorType: req.body.errorType,
|
|
143
231
|
errorMessage: req.body.errorMessage,
|
|
144
|
-
requestID
|
|
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
|
+
}
|
package/runtime/workers.d.ts
CHANGED
|
@@ -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):
|
|
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
|
-
|
|
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 {};
|