@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.
- package/iot.js +5 -8
- package/package.json +1 -1
- package/package.json.bak +1 -1
- package/runtime/handlers/node.js +44 -34
- package/runtime/handlers.js +10 -4
- package/runtime/iot.js +5 -1
- package/runtime/server.d.ts +4 -1
- package/runtime/server.js +69 -23
- package/runtime/worker-pool-logging.d.ts +29 -0
- package/runtime/worker-pool-logging.js +190 -0
- package/runtime/workers.d.ts +13 -7
- package/runtime/workers.js +632 -36
- package/support/bridge/live-lambda.mjs +37 -37
- package/support/nodejs-runtime/index.mjs +10 -1
- package/support/python-runtime/runtime.py +131 -60
package/iot.js
CHANGED
|
@@ -149,15 +149,12 @@ export const useIOT = lazy(async () => {
|
|
|
149
149
|
properties,
|
|
150
150
|
sourceID: bus.sourceID,
|
|
151
151
|
};
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}, () => {
|
|
157
|
-
r();
|
|
158
|
-
});
|
|
152
|
+
const fragments = await encode(payload);
|
|
153
|
+
await Promise.all(fragments.map((fragment) => new Promise((r) => {
|
|
154
|
+
device.publish(topic, JSON.stringify(fragment), { qos: 1 }, () => {
|
|
155
|
+
r();
|
|
159
156
|
});
|
|
160
|
-
}
|
|
157
|
+
})));
|
|
161
158
|
Logger.debug("IOT Published", topic, type);
|
|
162
159
|
},
|
|
163
160
|
};
|
package/package.json
CHANGED
package/package.json.bak
CHANGED
package/runtime/handlers/node.js
CHANGED
|
@@ -34,27 +34,25 @@ export const useNodeHandler = () => {
|
|
|
34
34
|
canHandle: (input) => input.startsWith("nodejs"),
|
|
35
35
|
startWorker: async (input) => {
|
|
36
36
|
const workers = await useRuntimeWorkers();
|
|
37
|
-
new
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
stdout: true,
|
|
48
|
-
});
|
|
49
|
-
worker.stdout.on("data", (data) => {
|
|
50
|
-
workers.stdout(input.workerID, data.toString());
|
|
51
|
-
});
|
|
52
|
-
worker.stderr.on("data", (data) => {
|
|
53
|
-
workers.stdout(input.workerID, data.toString());
|
|
54
|
-
});
|
|
55
|
-
worker.on("exit", () => workers.exited(input.workerID));
|
|
56
|
-
threads.set(input.workerID, worker);
|
|
37
|
+
const worker = new Worker(url.fileURLToPath(new URL("../../support/nodejs-runtime/index.mjs", import.meta.url)), {
|
|
38
|
+
env: {
|
|
39
|
+
...input.environment,
|
|
40
|
+
IS_LOCAL: "true",
|
|
41
|
+
},
|
|
42
|
+
execArgv: ["--enable-source-maps"],
|
|
43
|
+
workerData: input,
|
|
44
|
+
stderr: true,
|
|
45
|
+
stdin: true,
|
|
46
|
+
stdout: true,
|
|
57
47
|
});
|
|
48
|
+
worker.stdout.on("data", (data) => {
|
|
49
|
+
workers.stdout(input.workerID, data.toString());
|
|
50
|
+
});
|
|
51
|
+
worker.stderr.on("data", (data) => {
|
|
52
|
+
workers.stdout(input.workerID, data.toString());
|
|
53
|
+
});
|
|
54
|
+
worker.on("exit", () => workers.exited(input.workerID));
|
|
55
|
+
threads.set(input.workerID, worker);
|
|
58
56
|
},
|
|
59
57
|
stopWorker: async (workerID) => {
|
|
60
58
|
const worker = threads.get(workerID);
|
|
@@ -65,26 +63,37 @@ export const useNodeHandler = () => {
|
|
|
65
63
|
const monoBundleDir = path.join(project.paths.root, ".mono-build");
|
|
66
64
|
const monoBundlePath = path.join(monoBundleDir, "index.mjs");
|
|
67
65
|
const monoBundleExists = fsSync.existsSync(monoBundlePath);
|
|
68
|
-
console.log(`[MONO-BUNDLE] mode=${input.mode}, exists=${monoBundleExists}, handler=${input.props.handler}`);
|
|
69
66
|
if (input.mode === "start" && monoBundleExists) {
|
|
70
|
-
|
|
71
|
-
Logger.debug("Using mono-bundle for dev mode:", monoBundlePath);
|
|
67
|
+
Colors.line(Colors.prefix, Colors.dim.bold("MonoBundle"), Colors.dim(`mode=${input.mode}, exists=${monoBundleExists}, handler=${input.props.handler}`));
|
|
72
68
|
// Symlink node_modules to mono-bundle dir for external dependencies
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
const handlerFunctionsDir = path.join(project.paths.root,
|
|
69
|
+
// Only create if symlink doesn't exist or points to wrong location (avoid redundant I/O)
|
|
70
|
+
const parsed = path.parse(input.props.handler);
|
|
71
|
+
const handlerFunctionsDir = path.join(project.paths.root, parsed.dir);
|
|
76
72
|
const root = await findAbove(handlerFunctionsDir, "package.json");
|
|
77
73
|
if (root) {
|
|
78
|
-
const sourceNodeModules = path.
|
|
74
|
+
const sourceNodeModules = path.resolve(root, "node_modules");
|
|
79
75
|
const monoBundleNodeModules = path.join(monoBundleDir, "node_modules");
|
|
80
76
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
const existingTarget = await fs.readlink(monoBundleNodeModules);
|
|
78
|
+
if (existingTarget === sourceNodeModules) {
|
|
79
|
+
// Symlink already correct, skip
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// Symlink points to wrong location, recreate
|
|
83
|
+
await fs.rm(monoBundleNodeModules, { recursive: true, force: true });
|
|
84
|
+
await fs.symlink(sourceNodeModules, monoBundleNodeModules, "dir");
|
|
85
|
+
Logger.debug("Symlinked node_modules for mono-bundle from:", sourceNodeModules);
|
|
86
|
+
}
|
|
85
87
|
}
|
|
86
|
-
catch
|
|
87
|
-
|
|
88
|
+
catch {
|
|
89
|
+
// Symlink doesn't exist, create it
|
|
90
|
+
try {
|
|
91
|
+
await fs.symlink(sourceNodeModules, monoBundleNodeModules, "dir");
|
|
92
|
+
Logger.debug("Symlinked node_modules for mono-bundle from:", sourceNodeModules);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
Logger.debug("Failed to symlink node_modules for mono-bundle:", err);
|
|
96
|
+
}
|
|
88
97
|
}
|
|
89
98
|
}
|
|
90
99
|
return {
|
|
@@ -140,7 +149,8 @@ export const useNodeHandler = () => {
|
|
|
140
149
|
try {
|
|
141
150
|
await fs.symlink(path.resolve(dir), path.resolve(path.join(input.out, "node_modules")), "dir");
|
|
142
151
|
}
|
|
143
|
-
catch {
|
|
152
|
+
catch {
|
|
153
|
+
}
|
|
144
154
|
}
|
|
145
155
|
// Rebuilt using existing esbuild context
|
|
146
156
|
let ctx = rebuildCache[input.functionID]?.ctx;
|
package/runtime/handlers.js
CHANGED
|
@@ -57,9 +57,9 @@ export const useRuntimeHandlers = lazy(() => {
|
|
|
57
57
|
props: func,
|
|
58
58
|
});
|
|
59
59
|
// If mono-bundle detected (handler returned custom out), skip all artifact work
|
|
60
|
+
// Don't fire build events - mono-bundle is built externally by esbuild watch
|
|
61
|
+
// Worker pool invalidation is handled separately when bundle file actually changes
|
|
60
62
|
if (monoBundleCheck.type === "success" && monoBundleCheck.out) {
|
|
61
|
-
bus.publish("function.build.started", { functionID });
|
|
62
|
-
bus.publish("function.build.success", { functionID, monoBundle: true });
|
|
63
63
|
return {
|
|
64
64
|
type: "success",
|
|
65
65
|
handler: monoBundleCheck.handler,
|
|
@@ -138,7 +138,7 @@ export const useRuntimeHandlers = lazy(() => {
|
|
|
138
138
|
export const useFunctionBuilder = lazy(() => {
|
|
139
139
|
const artifacts = new Map();
|
|
140
140
|
const handlers = useRuntimeHandlers();
|
|
141
|
-
const semaphore = new Semaphore(4);
|
|
141
|
+
const semaphore = new Semaphore(parseInt(process.env.SST_BUILD_CONCURRENCY || "4", 10));
|
|
142
142
|
const result = {
|
|
143
143
|
artifact: (functionID) => {
|
|
144
144
|
if (artifacts.has(functionID))
|
|
@@ -166,6 +166,11 @@ export const useFunctionBuilder = lazy(() => {
|
|
|
166
166
|
try {
|
|
167
167
|
const functions = useFunctions();
|
|
168
168
|
for (const [functionID, info] of Object.entries(functions.all)) {
|
|
169
|
+
// Optimization: For mono-build, the artifact path is stable and build is handled externally.
|
|
170
|
+
// We can skip the potentially expensive shouldBuild check and rebuild call.
|
|
171
|
+
const existing = artifacts.get(functionID);
|
|
172
|
+
if (existing?.out.includes(".mono-build"))
|
|
173
|
+
continue;
|
|
169
174
|
const handler = handlers.for(info.runtime);
|
|
170
175
|
if (!handler?.shouldBuild({
|
|
171
176
|
functionID,
|
|
@@ -176,7 +181,8 @@ export const useFunctionBuilder = lazy(() => {
|
|
|
176
181
|
Logger.debug("Rebuilt function", functionID);
|
|
177
182
|
}
|
|
178
183
|
}
|
|
179
|
-
catch {
|
|
184
|
+
catch {
|
|
185
|
+
}
|
|
180
186
|
});
|
|
181
187
|
return result;
|
|
182
188
|
});
|
package/runtime/iot.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useBus } from "../bus.js";
|
|
2
2
|
import { useIOT } from "../iot.js";
|
|
3
3
|
import { lazy } from "../util/lazy.js";
|
|
4
|
+
import { logInvokeTrace } from "./worker-pool-logging.js";
|
|
4
5
|
export const useIOTBridge = lazy(async () => {
|
|
5
6
|
const bus = useBus();
|
|
6
7
|
const iot = await useIOT();
|
|
@@ -12,6 +13,9 @@ export const useIOTBridge = lazy(async () => {
|
|
|
12
13
|
iot.publish(topic + "/" + evt.properties.workerID, "function.error", evt.properties);
|
|
13
14
|
});
|
|
14
15
|
bus.subscribe("function.ack", async (evt) => {
|
|
15
|
-
|
|
16
|
+
const workerID = evt.properties.workerID;
|
|
17
|
+
logInvokeTrace("IOT_ACK_START", workerID, `worker=${workerID.slice(0, 8)}`);
|
|
18
|
+
await iot.publish(topic + "/" + workerID, "function.ack", evt.properties);
|
|
19
|
+
logInvokeTrace("IOT_ACK_DONE", workerID, `worker=${workerID.slice(0, 8)}`);
|
|
16
20
|
});
|
|
17
21
|
});
|
package/runtime/server.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { Events } from "../bus.js";
|
|
1
2
|
export declare const useRuntimeServerConfig: () => Promise<{
|
|
2
3
|
API_VERSION: string;
|
|
3
4
|
port: number;
|
|
4
5
|
url: string;
|
|
5
6
|
}>;
|
|
6
|
-
export declare const useRuntimeServer: () => Promise<
|
|
7
|
+
export declare const useRuntimeServer: () => Promise<{
|
|
8
|
+
routeInvocation: (targetWorkerID: string, invocation: Events["function.invoked"]) => void;
|
|
9
|
+
}>;
|
package/runtime/server.js
CHANGED
|
@@ -31,38 +31,52 @@ export const useRuntimeServer = lazy(async () => {
|
|
|
31
31
|
workersWaiting.set(workerID, resolve);
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
|
-
workers.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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);
|
|
34
|
+
// Route an invocation to a specific workerID (used by workers.ts for pooled workers)
|
|
35
|
+
function routeInvocation(targetWorkerID, invocation) {
|
|
36
|
+
const waiting = workersWaiting.get(targetWorkerID);
|
|
37
|
+
if (waiting) {
|
|
38
|
+
workersWaiting.delete(targetWorkerID);
|
|
39
|
+
waiting(invocation);
|
|
45
40
|
return;
|
|
46
41
|
}
|
|
47
|
-
let arr = invocationsQueued.get(
|
|
42
|
+
let arr = invocationsQueued.get(targetWorkerID);
|
|
48
43
|
if (!arr) {
|
|
49
44
|
arr = [];
|
|
50
|
-
invocationsQueued.set(
|
|
45
|
+
invocationsQueued.set(targetWorkerID, arr);
|
|
51
46
|
}
|
|
52
|
-
arr.push(
|
|
47
|
+
arr.push(invocation);
|
|
48
|
+
}
|
|
49
|
+
workers.subscribe("worker.exited", async (evt) => {
|
|
50
|
+
const waiting = workersWaiting.get(evt.properties.workerID);
|
|
51
|
+
if (!waiting)
|
|
52
|
+
return;
|
|
53
|
+
workersWaiting.delete(evt.properties.workerID);
|
|
53
54
|
});
|
|
55
|
+
// Note: function.invoked routing is handled by workers.ts via routeInvocation()
|
|
56
|
+
// This ensures correct routing for both pooled and non-pooled workers
|
|
54
57
|
app.post(`/:workerID/${cfg.API_VERSION}/runtime/init/error`, express.json({
|
|
55
58
|
strict: false,
|
|
56
59
|
type: ["application/json", "application/*+json"],
|
|
57
60
|
limit: "10mb",
|
|
58
61
|
}), async (req, res) => {
|
|
59
|
-
const
|
|
62
|
+
const pooledWorkerID = req.params.workerID;
|
|
63
|
+
const worker = workers.fromID(pooledWorkerID);
|
|
64
|
+
if (!worker) {
|
|
65
|
+
res.status(404).send();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Get AWS workerID for IoT routing (if pooled)
|
|
69
|
+
const awsWorkerID = workers.isPooled(pooledWorkerID)
|
|
70
|
+
? workers.getAwsWorkerID(pooledWorkerID) || pooledWorkerID
|
|
71
|
+
: pooledWorkerID;
|
|
60
72
|
bus.publish("function.error", {
|
|
61
|
-
requestID: workers.getCurrentRequestID(
|
|
62
|
-
workerID:
|
|
73
|
+
requestID: workers.getCurrentRequestID(pooledWorkerID),
|
|
74
|
+
workerID: awsWorkerID,
|
|
63
75
|
functionID: worker.functionID,
|
|
64
76
|
...req.body,
|
|
65
77
|
});
|
|
78
|
+
// Return pooled worker to pool
|
|
79
|
+
workers.onResponse(pooledWorkerID);
|
|
66
80
|
res.json("ok");
|
|
67
81
|
});
|
|
68
82
|
app.get(`/:workerID/${cfg.API_VERSION}/runtime/invocation/next`, async (req, res) => {
|
|
@@ -77,8 +91,15 @@ export const useRuntimeServer = lazy(async () => {
|
|
|
77
91
|
"Lambda-Runtime-Cognito-Identity": JSON.stringify(payload.context.identity || null),
|
|
78
92
|
"Lambda-Runtime-Log-Group-Name": payload.context.logGroupName,
|
|
79
93
|
"Lambda-Runtime-Log-Stream-Name": payload.context.logStreamName,
|
|
94
|
+
// Pass function ID for mono-build shared pool: allows per-invocation dispatch
|
|
95
|
+
"Lambda-Runtime-Sst-Function-Id": payload.functionID,
|
|
96
|
+
});
|
|
97
|
+
// Wrap event with env for per-invocation environment variable application
|
|
98
|
+
// This prevents env leakage when workers are reused across different functions
|
|
99
|
+
res.json({
|
|
100
|
+
event: payload.event,
|
|
101
|
+
env: payload.env,
|
|
80
102
|
});
|
|
81
|
-
res.json(payload.event);
|
|
82
103
|
});
|
|
83
104
|
app.post(`/:workerID/${cfg.API_VERSION}/runtime/invocation/:awsRequestId/response`, express.json({
|
|
84
105
|
strict: false,
|
|
@@ -87,14 +108,25 @@ export const useRuntimeServer = lazy(async () => {
|
|
|
87
108
|
},
|
|
88
109
|
limit: "10mb",
|
|
89
110
|
}), (req, res) => {
|
|
90
|
-
|
|
91
|
-
|
|
111
|
+
const pooledWorkerID = req.params.workerID;
|
|
112
|
+
Logger.debug("Worker", pooledWorkerID, "got response", req.body);
|
|
113
|
+
const worker = workers.fromID(pooledWorkerID);
|
|
114
|
+
if (!worker) {
|
|
115
|
+
res.status(404).send();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Get AWS workerID for IoT routing (if pooled)
|
|
119
|
+
const awsWorkerID = workers.isPooled(pooledWorkerID)
|
|
120
|
+
? workers.getAwsWorkerID(pooledWorkerID) || pooledWorkerID
|
|
121
|
+
: pooledWorkerID;
|
|
92
122
|
bus.publish("function.success", {
|
|
93
|
-
workerID:
|
|
123
|
+
workerID: awsWorkerID,
|
|
94
124
|
functionID: worker.functionID,
|
|
95
125
|
requestID: req.params.awsRequestId,
|
|
96
126
|
body: req.body,
|
|
97
127
|
});
|
|
128
|
+
// Return pooled worker to pool
|
|
129
|
+
workers.onResponse(pooledWorkerID);
|
|
98
130
|
res.status(202).send();
|
|
99
131
|
});
|
|
100
132
|
app.all(`/proxy*`, express.raw({
|
|
@@ -135,16 +167,30 @@ export const useRuntimeServer = lazy(async () => {
|
|
|
135
167
|
type: ["application/json", "application/*+json"],
|
|
136
168
|
limit: "10mb",
|
|
137
169
|
}), (req, res) => {
|
|
138
|
-
const
|
|
170
|
+
const pooledWorkerID = req.params.workerID;
|
|
171
|
+
const worker = workers.fromID(pooledWorkerID);
|
|
172
|
+
if (!worker) {
|
|
173
|
+
res.status(404).send();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// Get AWS workerID for IoT routing (if pooled)
|
|
177
|
+
const awsWorkerID = workers.isPooled(pooledWorkerID)
|
|
178
|
+
? workers.getAwsWorkerID(pooledWorkerID) || pooledWorkerID
|
|
179
|
+
: pooledWorkerID;
|
|
139
180
|
bus.publish("function.error", {
|
|
140
|
-
workerID:
|
|
181
|
+
workerID: awsWorkerID,
|
|
141
182
|
functionID: worker.functionID,
|
|
142
183
|
errorType: req.body.errorType,
|
|
143
184
|
errorMessage: req.body.errorMessage,
|
|
144
185
|
requestID: req.params.awsRequestId,
|
|
145
186
|
trace: req.body.trace,
|
|
146
187
|
});
|
|
188
|
+
// Return pooled worker to pool
|
|
189
|
+
workers.onResponse(pooledWorkerID);
|
|
147
190
|
res.status(202).send();
|
|
148
191
|
});
|
|
149
192
|
app.listen(cfg.port);
|
|
193
|
+
return {
|
|
194
|
+
routeInvocation,
|
|
195
|
+
};
|
|
150
196
|
});
|
|
@@ -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,190 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
// Configuration
|
|
4
|
+
const SST_BUILD_CONCURRENCY = parseInt(process.env.SST_BUILD_CONCURRENCY || "4", 10);
|
|
5
|
+
export const POOL_SIZE = parseInt(process.env.SST_WORKER_POOL_SIZE || "10", 10);
|
|
6
|
+
export const IDLE_TIMEOUT = parseInt(process.env.SST_WORKER_IDLE_TIMEOUT || "60000", 10);
|
|
7
|
+
const DEBUG_POOL = process.env.SST_DEBUG_POOL === "true";
|
|
8
|
+
const DEBUG_POOL_FILE = process.env.SST_DEBUG_POOL_FILE || ".sst/worker-pool.log";
|
|
9
|
+
const INVOKE_TRACE_FILE = ".sst/invoke-trace.log";
|
|
10
|
+
const metrics = {
|
|
11
|
+
coldStarts: new Map(),
|
|
12
|
+
concurrentRequests: new Map(),
|
|
13
|
+
peakConcurrent: new Map(),
|
|
14
|
+
totalRequests: new Map(),
|
|
15
|
+
poolHits: new Map(),
|
|
16
|
+
avgResponseTime: new Map(),
|
|
17
|
+
responseCount: new Map(),
|
|
18
|
+
};
|
|
19
|
+
let logStream = null;
|
|
20
|
+
// Function name resolver - set by workers.ts
|
|
21
|
+
let functionNameResolver = (id) => id.slice(0, 25);
|
|
22
|
+
/**
|
|
23
|
+
* Set the function name resolver callback
|
|
24
|
+
* Called by workers.ts to provide access to useFunctions()
|
|
25
|
+
*/
|
|
26
|
+
export function setFunctionNameResolver(resolver) {
|
|
27
|
+
functionNameResolver = resolver;
|
|
28
|
+
}
|
|
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
|
+
// Extract readable function name from handler path
|
|
47
|
+
function getFunctionName(functionID) {
|
|
48
|
+
return functionNameResolver(functionID);
|
|
49
|
+
}
|
|
50
|
+
// Calculate bottleneck indicators
|
|
51
|
+
function getBottleneckFlags(functionID) {
|
|
52
|
+
const flags = [];
|
|
53
|
+
const concurrent = metrics.concurrentRequests.get(functionID) || 0;
|
|
54
|
+
const total = metrics.totalRequests.get(functionID) || 0;
|
|
55
|
+
const hits = metrics.poolHits.get(functionID) || 0;
|
|
56
|
+
const hitRate = total > 0 ? (hits / total) * 100 : 0;
|
|
57
|
+
const coldStarts = metrics.coldStarts.get(functionID) || 0;
|
|
58
|
+
// SATURATED: All pool slots in use
|
|
59
|
+
if (concurrent >= POOL_SIZE) {
|
|
60
|
+
flags.push("🚨 SATURATED");
|
|
61
|
+
}
|
|
62
|
+
// HIGH_LOAD: >70% pool utilization
|
|
63
|
+
else if (concurrent >= POOL_SIZE * 0.7) {
|
|
64
|
+
flags.push("⚠️ HIGH_LOAD");
|
|
65
|
+
}
|
|
66
|
+
// COLD_START: Low hit rate indicates frequent cold starts
|
|
67
|
+
if (total >= 5 && hitRate < 30) {
|
|
68
|
+
flags.push("❄️ LOW_REUSE");
|
|
69
|
+
}
|
|
70
|
+
// BOTTLENECK: High cold start ratio
|
|
71
|
+
if (total >= 3 && coldStarts / total > 0.5) {
|
|
72
|
+
flags.push("🔥 COLD_HEAVY");
|
|
73
|
+
}
|
|
74
|
+
return flags.length > 0 ? " " + flags.join(" ") : "";
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Pool debug logging helper - writes to file with bottleneck detection
|
|
78
|
+
*/
|
|
79
|
+
export function logPool(action, details = {}) {
|
|
80
|
+
if (!DEBUG_POOL)
|
|
81
|
+
return;
|
|
82
|
+
initLogFile();
|
|
83
|
+
const timestamp = new Date().toISOString().slice(11, 23); // HH:MM:SS.mmm
|
|
84
|
+
const funcName = details.functionID
|
|
85
|
+
? getFunctionName(details.functionID)
|
|
86
|
+
: "";
|
|
87
|
+
const bottleneckFlags = details.functionID &&
|
|
88
|
+
["CREATE", "REUSE", "POOL_MISS", "RESPONSE"].includes(action)
|
|
89
|
+
? getBottleneckFlags(details.functionID)
|
|
90
|
+
: "";
|
|
91
|
+
// Build metrics string for key actions
|
|
92
|
+
let metricsStr = "";
|
|
93
|
+
if (details.functionID && ["RESPONSE", "RETURN_TO_POOL"].includes(action)) {
|
|
94
|
+
const concurrent = metrics.concurrentRequests.get(details.functionID) || 0;
|
|
95
|
+
const total = metrics.totalRequests.get(details.functionID) || 0;
|
|
96
|
+
const hits = metrics.poolHits.get(details.functionID) || 0;
|
|
97
|
+
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);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Track request lifecycle for metrics - called at request start
|
|
115
|
+
*/
|
|
116
|
+
export function trackRequestStart(functionID, isPoolHit) {
|
|
117
|
+
const current = (metrics.concurrentRequests.get(functionID) || 0) + 1;
|
|
118
|
+
metrics.concurrentRequests.set(functionID, current);
|
|
119
|
+
metrics.totalRequests.set(functionID, (metrics.totalRequests.get(functionID) || 0) + 1);
|
|
120
|
+
const peak = metrics.peakConcurrent.get(functionID) || 0;
|
|
121
|
+
if (current > peak) {
|
|
122
|
+
metrics.peakConcurrent.set(functionID, current);
|
|
123
|
+
}
|
|
124
|
+
if (isPoolHit) {
|
|
125
|
+
metrics.poolHits.set(functionID, (metrics.poolHits.get(functionID) || 0) + 1);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
metrics.coldStarts.set(functionID, (metrics.coldStarts.get(functionID) || 0) + 1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Track request lifecycle for metrics - called at request end
|
|
133
|
+
*/
|
|
134
|
+
export function trackRequestEnd(functionID) {
|
|
135
|
+
const current = metrics.concurrentRequests.get(functionID) || 0;
|
|
136
|
+
if (current > 0) {
|
|
137
|
+
metrics.concurrentRequests.set(functionID, current - 1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Write session end summary and close log stream
|
|
142
|
+
* Called from workers.ts on process exit
|
|
143
|
+
*/
|
|
144
|
+
export function writeSessionEndSummary() {
|
|
145
|
+
if (!DEBUG_POOL || !logStream)
|
|
146
|
+
return;
|
|
147
|
+
let summary = "\n" + "─".repeat(100) + "\n[SESSION END] " + new Date().toISOString() + "\n";
|
|
148
|
+
for (const [funcID, total] of metrics.totalRequests) {
|
|
149
|
+
const hits = metrics.poolHits.get(funcID) || 0;
|
|
150
|
+
const cold = metrics.coldStarts.get(funcID) || 0;
|
|
151
|
+
const peak = metrics.peakConcurrent.get(funcID) || 0;
|
|
152
|
+
const hitRate = total > 0 ? ((hits / total) * 100).toFixed(1) : "0";
|
|
153
|
+
const funcName = getFunctionName(funcID);
|
|
154
|
+
summary += " " + funcName.padEnd(25) + " total=" + total + " poolHits=" + hits + " coldStarts=" + cold + " peakConcurrent=" + peak + " hitRate=" + hitRate + "%\n";
|
|
155
|
+
}
|
|
156
|
+
summary += "─".repeat(100) + "\n";
|
|
157
|
+
logStream.write(summary);
|
|
158
|
+
logStream.end();
|
|
159
|
+
}
|
|
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
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Log invocation trace events to .sst/invoke-trace.log
|
|
181
|
+
* Always enabled for debugging invocation flow
|
|
182
|
+
*/
|
|
183
|
+
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);
|
|
190
|
+
}
|
package/runtime/workers.d.ts
CHANGED
|
@@ -18,20 +18,26 @@ 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
|
+
};
|
|
21
26
|
}
|
|
22
27
|
}
|
|
23
|
-
interface Worker {
|
|
24
|
-
workerID: string;
|
|
25
|
-
functionID: string;
|
|
26
|
-
}
|
|
27
28
|
export declare const useRuntimeWorkers: () => Promise<{
|
|
28
|
-
fromID(workerID: string):
|
|
29
|
+
fromID(workerID: string): {
|
|
30
|
+
workerID: string;
|
|
31
|
+
functionID: string;
|
|
32
|
+
};
|
|
29
33
|
getCurrentRequestID(workerID: string): string | undefined;
|
|
30
34
|
stdout(workerID: string, message: string): void;
|
|
31
35
|
exited(workerID: string): void;
|
|
32
|
-
|
|
36
|
+
onResponse(pooledWorkerID: string): void;
|
|
37
|
+
getAwsWorkerID(pooledWorkerID: string): string | undefined;
|
|
38
|
+
isPooled(workerID: string): boolean;
|
|
39
|
+
subscribe: <Type extends "worker.started" | "worker.stopped" | "worker.exited" | "worker.stdout" | "worker.reused">(type: Type, cb: (payload: import("../bus.js").EventPayload<Type>) => void) => {
|
|
33
40
|
type: keyof import("../bus.js").Events;
|
|
34
41
|
cb: (payload: any) => void;
|
|
35
42
|
};
|
|
36
43
|
}>;
|
|
37
|
-
export {};
|