@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/workers.js
CHANGED
|
@@ -1,78 +1,730 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
1
4
|
import { useBus } from "../bus.js";
|
|
2
5
|
import { useFunctionBuilder, useRuntimeHandlers } from "./handlers.js";
|
|
3
|
-
import { useRuntimeServerConfig } from "./server.js";
|
|
6
|
+
import { useRuntimeServerConfig, useRuntimeServer } from "./server.js";
|
|
4
7
|
import { useFunctions } from "../constructs/Function.js";
|
|
5
8
|
import { lazy } from "../util/lazy.js";
|
|
9
|
+
import { Logger } from "../logger.js";
|
|
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";
|
|
15
|
+
// Track workers marked as stale (should not return to pool after completion)
|
|
16
|
+
const staleWorkers = new Set();
|
|
17
|
+
const bundleMtimes = new Map();
|
|
18
|
+
const bundleWatchers = new Map();
|
|
19
|
+
// Clean up watchers on exit
|
|
20
|
+
process.on("exit", () => {
|
|
21
|
+
for (const watcher of bundleWatchers.values()) {
|
|
22
|
+
try {
|
|
23
|
+
watcher.close();
|
|
24
|
+
}
|
|
25
|
+
catch { }
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
// Get bundle rebuild timestamp for staleness checking
|
|
29
|
+
function getBundleMtime(buildOut) {
|
|
30
|
+
if (isMonoBuildPath(buildOut)) {
|
|
31
|
+
if (bundleMtimes.has(buildOut)) {
|
|
32
|
+
return bundleMtimes.get(buildOut);
|
|
33
|
+
}
|
|
34
|
+
const timestampFile = path.join(buildOut, ".last-rebuild");
|
|
35
|
+
const update = () => {
|
|
36
|
+
try {
|
|
37
|
+
const content = fs.readFileSync(timestampFile, "utf-8");
|
|
38
|
+
const mtime = parseInt(content, 10);
|
|
39
|
+
bundleMtimes.set(buildOut, mtime);
|
|
40
|
+
return mtime;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
if (!bundleWatchers.has(buildOut)) {
|
|
47
|
+
try {
|
|
48
|
+
const watcher = fs.watch(buildOut, { persistent: false }, (event, filename) => {
|
|
49
|
+
if (!filename || filename === ".last-rebuild") {
|
|
50
|
+
update();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
watcher.on("error", () => {
|
|
54
|
+
bundleWatchers.delete(buildOut);
|
|
55
|
+
bundleMtimes.delete(buildOut);
|
|
56
|
+
try {
|
|
57
|
+
watcher.close();
|
|
58
|
+
}
|
|
59
|
+
catch { }
|
|
60
|
+
});
|
|
61
|
+
bundleWatchers.set(buildOut, watcher);
|
|
62
|
+
}
|
|
63
|
+
catch { }
|
|
64
|
+
}
|
|
65
|
+
return update();
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
// For non-mono-bundle, use bundle directory mtime
|
|
69
|
+
const stat = fs.statSync(buildOut);
|
|
70
|
+
return stat.mtimeMs;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Helper: Get pool key for worker lookup
|
|
77
|
+
// For mono-build, uses shared key so any warm worker can serve any handler
|
|
78
|
+
function getPoolKey(functionID, runtime, buildOut) {
|
|
79
|
+
// Use the global mono build config for pool key calculation
|
|
80
|
+
return useMonoBuildConfig().getPoolKey(functionID, runtime, buildOut);
|
|
81
|
+
}
|
|
82
|
+
// Extract readable function name from handler path
|
|
83
|
+
function getFunctionName(functionID) {
|
|
84
|
+
try {
|
|
85
|
+
const props = useFunctions().fromID(functionID);
|
|
86
|
+
if (!props)
|
|
87
|
+
return functionID.slice(0, 25);
|
|
88
|
+
return (props.functionName || functionID).split("backend-")[1]?.slice(0, 50) || functionID.slice(0, 25);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return functionID.slice(0, 25);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Runtimes that support multiple invocations per process (have event loop)
|
|
95
|
+
const POOLABLE_RUNTIMES = new Set([
|
|
96
|
+
// Node.js - has while(true) loop in nodejs-runtime/index.ts
|
|
97
|
+
"nodejs",
|
|
98
|
+
"nodejs14.x",
|
|
99
|
+
"nodejs16.x",
|
|
100
|
+
"nodejs18.x",
|
|
101
|
+
"nodejs20.x",
|
|
102
|
+
"nodejs22.x",
|
|
103
|
+
// Python - has while True loop in python-runtime/runtime.py
|
|
104
|
+
"python",
|
|
105
|
+
"python3.7",
|
|
106
|
+
"python3.8",
|
|
107
|
+
"python3.9",
|
|
108
|
+
"python3.10",
|
|
109
|
+
"python3.11",
|
|
110
|
+
"python3.12",
|
|
111
|
+
"python3.13",
|
|
112
|
+
// Go - AWS Lambda Go SDK has built-in event loop
|
|
113
|
+
"go",
|
|
114
|
+
"go1.x",
|
|
115
|
+
// Java - AWS Lambda Java SDK has built-in event loop
|
|
116
|
+
"java",
|
|
117
|
+
"java8",
|
|
118
|
+
"java8.al2",
|
|
119
|
+
"java11",
|
|
120
|
+
"java17",
|
|
121
|
+
"java21",
|
|
122
|
+
// .NET - AWS Lambda .NET SDK has built-in event loop
|
|
123
|
+
"dotnet",
|
|
124
|
+
"dotnet6",
|
|
125
|
+
"dotnet8",
|
|
126
|
+
"dotnetcore3.1",
|
|
127
|
+
// Rust - AWS Lambda Rust runtime has built-in event loop
|
|
128
|
+
"rust",
|
|
129
|
+
]);
|
|
130
|
+
function isPoolableRuntime(runtime, env) {
|
|
131
|
+
// Container jobs are NOT poolable
|
|
132
|
+
if (runtime.startsWith("container") && env?.SST_DEBUG_JOB) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
return (POOLABLE_RUNTIMES.has(runtime) ||
|
|
136
|
+
[...POOLABLE_RUNTIMES].some((r) => runtime.startsWith(r)));
|
|
137
|
+
}
|
|
6
138
|
export const useRuntimeWorkers = lazy(async () => {
|
|
139
|
+
// Set up function name resolver for logging module
|
|
140
|
+
setFunctionNameResolver(getFunctionName);
|
|
141
|
+
// Non-pooled workers (legacy behavior)
|
|
7
142
|
const workers = new Map();
|
|
143
|
+
// Worker pool data structures
|
|
144
|
+
const workerPool = new Map();
|
|
145
|
+
const activeWorkers = new Map();
|
|
146
|
+
const workerIDMapping = new Map(); // awsWorkerID → pooledWorkerID
|
|
147
|
+
const reverseMapping = new Map(); // pooledWorkerID → awsWorkerID
|
|
148
|
+
const startedWorkers = new Set(); // Track started pooledWorkerIDs
|
|
8
149
|
const bus = useBus();
|
|
9
150
|
const handlers = useRuntimeHandlers();
|
|
10
151
|
const builder = useFunctionBuilder();
|
|
11
|
-
const
|
|
152
|
+
const serverConfig = await useRuntimeServerConfig();
|
|
153
|
+
// Log pool configuration on startup
|
|
154
|
+
logPool("INIT", {
|
|
155
|
+
poolSize: POOL_SIZE,
|
|
156
|
+
idleTimeoutMs: IDLE_TIMEOUT,
|
|
157
|
+
poolableRuntimes: [...POOLABLE_RUNTIMES].length,
|
|
158
|
+
});
|
|
159
|
+
// Lazy getter for server to avoid circular initialization
|
|
160
|
+
let _server = null;
|
|
161
|
+
async function getServer() {
|
|
162
|
+
if (!_server) {
|
|
163
|
+
_server = await useRuntimeServer();
|
|
164
|
+
}
|
|
165
|
+
return _server;
|
|
166
|
+
}
|
|
167
|
+
// Helper: Terminate a pooled worker
|
|
168
|
+
async function terminatePooledWorker(pooledWorkerID, reason) {
|
|
169
|
+
const worker = activeWorkers.get(pooledWorkerID) ||
|
|
170
|
+
[...workerPool.values()]
|
|
171
|
+
.flat()
|
|
172
|
+
.find((w) => w.pooledWorkerID === pooledWorkerID);
|
|
173
|
+
if (!worker)
|
|
174
|
+
return;
|
|
175
|
+
const props = useFunctions().fromID(worker.functionID);
|
|
176
|
+
if (!props)
|
|
177
|
+
return;
|
|
178
|
+
const uptime = Date.now() - worker.createdAt;
|
|
179
|
+
logPool("TERMINATE", {
|
|
180
|
+
pooledWorkerID: pooledWorkerID.slice(0, 8),
|
|
181
|
+
functionID: worker.functionID,
|
|
182
|
+
reason: reason || "unknown",
|
|
183
|
+
uptimeMs: uptime,
|
|
184
|
+
});
|
|
185
|
+
const handler = handlers.for(props.runtime);
|
|
186
|
+
await handler?.stopWorker(pooledWorkerID);
|
|
187
|
+
// Clean up mappings
|
|
188
|
+
activeWorkers.delete(pooledWorkerID);
|
|
189
|
+
staleWorkers.delete(pooledWorkerID);
|
|
190
|
+
const awsWorkerID = reverseMapping.get(pooledWorkerID);
|
|
191
|
+
if (awsWorkerID) {
|
|
192
|
+
workerIDMapping.delete(awsWorkerID);
|
|
193
|
+
}
|
|
194
|
+
reverseMapping.delete(pooledWorkerID);
|
|
195
|
+
startedWorkers.delete(pooledWorkerID);
|
|
196
|
+
lastRequestId.delete(pooledWorkerID);
|
|
197
|
+
Logger.debug("Terminated pooled worker", pooledWorkerID);
|
|
198
|
+
}
|
|
199
|
+
// Helper: Get idle worker from pool
|
|
200
|
+
// Uses poolKey for lookup (shared key for mono-build)
|
|
201
|
+
function getIdleWorker(poolKey, functionID, buildOut) {
|
|
202
|
+
const pool = workerPool.get(poolKey);
|
|
203
|
+
if (!pool || pool.length === 0) {
|
|
204
|
+
logPool("POOL_MISS", {
|
|
205
|
+
functionID,
|
|
206
|
+
poolKey: poolKey.slice(0, 30),
|
|
207
|
+
poolSize: 0,
|
|
208
|
+
});
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
// Check current bundle mtime for staleness detection
|
|
212
|
+
const currentMtime = getBundleMtime(buildOut);
|
|
213
|
+
// Try to find a non-stale worker
|
|
214
|
+
while (pool.length > 0) {
|
|
215
|
+
const worker = pool.pop();
|
|
216
|
+
if (!worker)
|
|
217
|
+
break;
|
|
218
|
+
clearTimeout(worker.idleTimer);
|
|
219
|
+
// Check if worker is stale (bundle was modified since worker started)
|
|
220
|
+
if (currentMtime && worker.bundleMtime && currentMtime > worker.bundleMtime) {
|
|
221
|
+
logPool("STALE_MTIME", {
|
|
222
|
+
pooledWorkerID: worker.pooledWorkerID.slice(0, 8),
|
|
223
|
+
functionID,
|
|
224
|
+
workerMtime: worker.bundleMtime,
|
|
225
|
+
currentMtime,
|
|
226
|
+
});
|
|
227
|
+
terminatePooledWorker(worker.pooledWorkerID, "stale_mtime");
|
|
228
|
+
continue; // Try next worker
|
|
229
|
+
}
|
|
230
|
+
worker.state = "busy";
|
|
231
|
+
const age = Date.now() - worker.createdAt;
|
|
232
|
+
const crossFunction = worker.functionID !== functionID;
|
|
233
|
+
logPool("REUSE", {
|
|
234
|
+
pooledWorkerID: worker.pooledWorkerID.slice(0, 8),
|
|
235
|
+
functionID,
|
|
236
|
+
originalFunctionID: crossFunction ? worker.functionID : undefined,
|
|
237
|
+
poolSizeAfter: pool.length,
|
|
238
|
+
workerAgeMs: age,
|
|
239
|
+
crossFunction,
|
|
240
|
+
});
|
|
241
|
+
Logger.debug("Reusing pooled worker", worker.pooledWorkerID, "for", functionID, crossFunction ? "(cross-function reuse)" : "");
|
|
242
|
+
return worker;
|
|
243
|
+
}
|
|
244
|
+
// All workers were stale
|
|
245
|
+
logPool("POOL_MISS", {
|
|
246
|
+
functionID,
|
|
247
|
+
poolKey: poolKey.slice(0, 30),
|
|
248
|
+
poolSize: 0,
|
|
249
|
+
reason: "all_stale",
|
|
250
|
+
});
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
// Helper: Return worker to pool
|
|
254
|
+
// Uses poolKey for pool lookup (shared key for mono-build)
|
|
255
|
+
function returnToPool(pooledWorkerID) {
|
|
256
|
+
const worker = activeWorkers.get(pooledWorkerID);
|
|
257
|
+
if (!worker)
|
|
258
|
+
return;
|
|
259
|
+
// Check if worker is stale (marked for termination due to rebuild)
|
|
260
|
+
if (staleWorkers.has(pooledWorkerID)) {
|
|
261
|
+
staleWorkers.delete(pooledWorkerID);
|
|
262
|
+
logPool("STALE_TERMINATE", {
|
|
263
|
+
pooledWorkerID: pooledWorkerID.slice(0, 8),
|
|
264
|
+
functionID: worker.functionID,
|
|
265
|
+
reason: "marked-stale-during-rebuild",
|
|
266
|
+
});
|
|
267
|
+
terminatePooledWorker(pooledWorkerID, "stale");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
// Clean up current request mappings
|
|
271
|
+
const awsWorkerID = reverseMapping.get(pooledWorkerID);
|
|
272
|
+
if (awsWorkerID) {
|
|
273
|
+
workerIDMapping.delete(awsWorkerID);
|
|
274
|
+
reverseMapping.delete(pooledWorkerID);
|
|
275
|
+
}
|
|
276
|
+
// Use poolKey for pool lookup (shared for mono-build)
|
|
277
|
+
let pool = workerPool.get(worker.poolKey);
|
|
278
|
+
if (!pool) {
|
|
279
|
+
pool = [];
|
|
280
|
+
workerPool.set(worker.poolKey, pool);
|
|
281
|
+
}
|
|
282
|
+
if (pool.length >= POOL_SIZE) {
|
|
283
|
+
// Pool full, terminate
|
|
284
|
+
logPool("POOL_FULL", {
|
|
285
|
+
pooledWorkerID: pooledWorkerID.slice(0, 8),
|
|
286
|
+
functionID: worker.functionID,
|
|
287
|
+
poolKey: worker.poolKey.slice(0, 30),
|
|
288
|
+
poolSize: pool.length,
|
|
289
|
+
maxSize: POOL_SIZE,
|
|
290
|
+
});
|
|
291
|
+
terminatePooledWorker(pooledWorkerID, "pool_full");
|
|
292
|
+
Logger.debug("Pool full, terminated worker", pooledWorkerID);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
// Return to pool with idle timeout
|
|
296
|
+
worker.state = "idle";
|
|
297
|
+
worker.idleTimer = setTimeout(() => {
|
|
298
|
+
const idx = pool.indexOf(worker);
|
|
299
|
+
if (idx >= 0)
|
|
300
|
+
pool.splice(idx, 1);
|
|
301
|
+
terminatePooledWorker(pooledWorkerID, "idle_timeout");
|
|
302
|
+
Logger.debug("Idle timeout, terminated worker", pooledWorkerID);
|
|
303
|
+
}, IDLE_TIMEOUT);
|
|
304
|
+
pool.push(worker);
|
|
305
|
+
activeWorkers.delete(pooledWorkerID);
|
|
306
|
+
logPool("RETURN_TO_POOL", {
|
|
307
|
+
pooledWorkerID: pooledWorkerID.slice(0, 8),
|
|
308
|
+
functionID: worker.functionID,
|
|
309
|
+
poolKey: worker.poolKey.slice(0, 30),
|
|
310
|
+
isSharedPool: worker.isSharedPool,
|
|
311
|
+
poolSizeAfter: pool.length,
|
|
312
|
+
idleTimeoutMs: IDLE_TIMEOUT,
|
|
313
|
+
});
|
|
314
|
+
Logger.debug("Returned worker to pool", pooledWorkerID, "pool key:", worker.poolKey, "pool size:", pool.length);
|
|
315
|
+
}
|
|
316
|
+
// Build success handler - clear pool for rebuilt function
|
|
12
317
|
handlers.subscribe("function.build.success", async (evt) => {
|
|
318
|
+
const { functionID } = evt.properties;
|
|
319
|
+
const props = useFunctions().fromID(functionID);
|
|
320
|
+
if (!props)
|
|
321
|
+
return;
|
|
322
|
+
// Get build to check if mono-build using global config
|
|
323
|
+
const build = await builder.artifact(functionID);
|
|
324
|
+
const isMonoBuild = build ? isMonoBuildPath(build.out) : false;
|
|
325
|
+
if (isMonoBuild) {
|
|
326
|
+
// For mono-build: clear the entire shared pool since all functions share the same bundle
|
|
327
|
+
const sharedPoolKey = `${props.runtime}:mono-build`;
|
|
328
|
+
const sharedPool = workerPool.get(sharedPoolKey) || [];
|
|
329
|
+
const activeSharedCount = [...activeWorkers.values()].filter((w) => w.isSharedPool && w.poolKey === sharedPoolKey).length;
|
|
330
|
+
logPool("MONO_BUILD_CLEAR", {
|
|
331
|
+
functionID,
|
|
332
|
+
sharedPoolKey,
|
|
333
|
+
pooledWorkersCleared: sharedPool.length,
|
|
334
|
+
activeWorkersMarkedStale: activeSharedCount,
|
|
335
|
+
});
|
|
336
|
+
// Terminate all idle workers in the shared pool
|
|
337
|
+
for (const worker of sharedPool) {
|
|
338
|
+
clearTimeout(worker.idleTimer);
|
|
339
|
+
await terminatePooledWorker(worker.pooledWorkerID, "mono-rebuild");
|
|
340
|
+
}
|
|
341
|
+
workerPool.delete(sharedPoolKey);
|
|
342
|
+
// Mark active workers as stale (they'll be terminated after completing their request)
|
|
343
|
+
for (const [pooledID, worker] of activeWorkers) {
|
|
344
|
+
if (worker.isSharedPool && worker.poolKey === sharedPoolKey) {
|
|
345
|
+
staleWorkers.add(pooledID);
|
|
346
|
+
logPool("MARK_STALE", {
|
|
347
|
+
pooledWorkerID: pooledID.slice(0, 8),
|
|
348
|
+
functionID: worker.functionID,
|
|
349
|
+
reason: "mono-rebuild",
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
// For non-mono-build: clear pool for this specific function only
|
|
356
|
+
const pool = workerPool.get(`${props.runtime}:${functionID}`) || [];
|
|
357
|
+
const activeCount = [...activeWorkers.values()].filter((w) => w.functionID === functionID).length;
|
|
358
|
+
logPool("BUILD_CLEAR", {
|
|
359
|
+
functionID,
|
|
360
|
+
pooledWorkersCleared: pool.length,
|
|
361
|
+
activeWorkersMarkedStale: activeCount,
|
|
362
|
+
});
|
|
363
|
+
for (const worker of pool) {
|
|
364
|
+
clearTimeout(worker.idleTimer);
|
|
365
|
+
await terminatePooledWorker(worker.pooledWorkerID, "rebuild");
|
|
366
|
+
}
|
|
367
|
+
workerPool.delete(`${props.runtime}:${functionID}`);
|
|
368
|
+
// Mark active workers as stale (they'll be terminated after completing their request)
|
|
369
|
+
for (const [pooledID, worker] of activeWorkers) {
|
|
370
|
+
if (worker.functionID === functionID) {
|
|
371
|
+
staleWorkers.add(pooledID);
|
|
372
|
+
logPool("MARK_STALE", {
|
|
373
|
+
pooledWorkerID: pooledID.slice(0, 8),
|
|
374
|
+
functionID: worker.functionID,
|
|
375
|
+
reason: "rebuild",
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// Stop non-pooled workers (legacy behavior)
|
|
13
381
|
for (const [_, worker] of workers) {
|
|
14
|
-
if (worker.functionID ===
|
|
15
|
-
const
|
|
16
|
-
if (!
|
|
382
|
+
if (worker.functionID === functionID) {
|
|
383
|
+
const workerProps = useFunctions().fromID(worker.functionID);
|
|
384
|
+
if (!workerProps)
|
|
17
385
|
return;
|
|
18
|
-
const handler = handlers.for(
|
|
386
|
+
const handler = handlers.for(workerProps.runtime);
|
|
19
387
|
await handler?.stopWorker(worker.workerID);
|
|
20
388
|
bus.publish("worker.stopped", worker);
|
|
21
389
|
}
|
|
22
390
|
}
|
|
23
391
|
});
|
|
24
392
|
const lastRequestId = new Map();
|
|
393
|
+
// Main invocation handler
|
|
25
394
|
bus.subscribe("function.invoked", async (evt) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (
|
|
395
|
+
const { workerID: awsWorkerID, functionID, requestID, env, event, } = evt.properties;
|
|
396
|
+
const startTime = Date.now();
|
|
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
|
+
}
|
|
410
|
+
// Send ack immediately
|
|
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`);
|
|
413
|
+
logInvokeTrace("ACK_PUBLISHED", requestID, `elapsed=${Date.now() - startTime}ms`);
|
|
414
|
+
const props = useFunctions().fromID(functionID);
|
|
415
|
+
if (!props) {
|
|
416
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ERROR: Function not found`);
|
|
417
|
+
Logger.debug("Function not found:", functionID);
|
|
418
|
+
bus.publish("function.error", {
|
|
419
|
+
workerID: awsWorkerID,
|
|
420
|
+
functionID,
|
|
421
|
+
requestID,
|
|
422
|
+
errorType: "FunctionNotFound",
|
|
423
|
+
errorMessage: `Function ${functionID} not found in project`,
|
|
424
|
+
trace: [],
|
|
425
|
+
});
|
|
36
426
|
return;
|
|
427
|
+
}
|
|
37
428
|
const handler = handlers.for(props.runtime);
|
|
38
|
-
if (!handler)
|
|
429
|
+
if (!handler) {
|
|
430
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ERROR: No handler for runtime ${props.runtime}`);
|
|
431
|
+
Logger.debug("No handler for runtime:", props.runtime);
|
|
432
|
+
bus.publish("function.error", {
|
|
433
|
+
workerID: awsWorkerID,
|
|
434
|
+
functionID,
|
|
435
|
+
requestID,
|
|
436
|
+
errorType: "RuntimeNotSupported",
|
|
437
|
+
errorMessage: `No handler for runtime ${props.runtime}`,
|
|
438
|
+
trace: [],
|
|
439
|
+
});
|
|
39
440
|
return;
|
|
40
|
-
|
|
41
|
-
|
|
441
|
+
}
|
|
442
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Getting build artifact...`);
|
|
443
|
+
logInvokeTrace("BUILD_ARTIFACT_START", requestID);
|
|
444
|
+
const buildStartTime = Date.now();
|
|
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`);
|
|
448
|
+
logInvokeTrace("BUILD_ARTIFACT_DONE", requestID, build ? `out=${build.out.slice(-30)}` : "NO_BUILD");
|
|
449
|
+
if (!build) {
|
|
450
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ERROR: Build artifact not ready`);
|
|
451
|
+
Logger.debug("Build artifact not ready for:", functionID);
|
|
452
|
+
bus.publish("function.error", {
|
|
453
|
+
workerID: awsWorkerID,
|
|
454
|
+
functionID,
|
|
455
|
+
requestID,
|
|
456
|
+
errorType: "BuildFailed",
|
|
457
|
+
errorMessage: `Build artifact not available for ${functionID}. Check for build errors.`,
|
|
458
|
+
trace: [],
|
|
459
|
+
});
|
|
42
460
|
return;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
461
|
+
}
|
|
462
|
+
// Check if this runtime supports pooling
|
|
463
|
+
const poolable = isPoolableRuntime(props.runtime, env);
|
|
464
|
+
if (poolable) {
|
|
465
|
+
// === POOLED PATH ===
|
|
466
|
+
// Get pool key: shared for mono-build, per-function otherwise
|
|
467
|
+
const { key: poolKey, isShared } = getPoolKey(functionID, props.runtime, 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);
|
|
471
|
+
let isReuse = false;
|
|
472
|
+
if (pooledWorker) {
|
|
473
|
+
isReuse = true;
|
|
474
|
+
// Update functionID for cross-function reuse (mono-build)
|
|
475
|
+
pooledWorker.functionID = functionID;
|
|
476
|
+
trackRequestStart(functionID, true);
|
|
477
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} REUSING pooled worker ${pooledWorker.pooledWorkerID.slice(0, 8)}`);
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
// Create new pooled worker
|
|
481
|
+
const pooledWorkerID = crypto.randomBytes(16).toString("hex");
|
|
482
|
+
const bundleMtime = getBundleMtime(build.out);
|
|
483
|
+
pooledWorker = {
|
|
484
|
+
pooledWorkerID,
|
|
485
|
+
functionID,
|
|
486
|
+
state: "busy",
|
|
487
|
+
createdAt: Date.now(),
|
|
488
|
+
poolKey,
|
|
489
|
+
isSharedPool: isShared,
|
|
490
|
+
bundlePath: build.out,
|
|
491
|
+
bundleMtime,
|
|
492
|
+
};
|
|
493
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} CREATING new pooled worker ${pooledWorkerID.slice(0, 8)}`);
|
|
494
|
+
}
|
|
495
|
+
// Set up mappings
|
|
496
|
+
workerIDMapping.set(awsWorkerID, pooledWorker.pooledWorkerID);
|
|
497
|
+
reverseMapping.set(pooledWorker.pooledWorkerID, awsWorkerID);
|
|
498
|
+
lastRequestId.set(pooledWorker.pooledWorkerID, requestID);
|
|
499
|
+
activeWorkers.set(pooledWorker.pooledWorkerID, pooledWorker);
|
|
500
|
+
if (!isReuse) {
|
|
501
|
+
// Start new worker with pooledWorkerID (cold start)
|
|
502
|
+
trackRequestStart(functionID, false);
|
|
503
|
+
const currentPoolSize = workerPool.get(poolKey)?.length || 0;
|
|
504
|
+
logPool(isWarmupRequest ? "WARMUP_CREATE" : "CREATE", {
|
|
505
|
+
pooledWorkerID: pooledWorker.pooledWorkerID.slice(0, 8),
|
|
506
|
+
functionID,
|
|
507
|
+
runtime: props.runtime,
|
|
508
|
+
requestID: requestID.slice(0, 8),
|
|
509
|
+
poolKey: poolKey.slice(0, 30),
|
|
510
|
+
isSharedPool: isShared,
|
|
511
|
+
currentPoolSize,
|
|
512
|
+
activeWorkers: activeWorkers.size,
|
|
513
|
+
...(isWarmupRequest && { warmupId }),
|
|
514
|
+
});
|
|
515
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Starting worker ${pooledWorker.pooledWorkerID.slice(0, 8)}...`);
|
|
516
|
+
logInvokeTrace("WORKER_START", requestID, `pooled=${pooledWorker.pooledWorkerID.slice(0, 8)}`);
|
|
517
|
+
const workerStartTime = Date.now();
|
|
518
|
+
try {
|
|
519
|
+
await handler.startWorker({
|
|
520
|
+
...build,
|
|
521
|
+
workerID: pooledWorker.pooledWorkerID,
|
|
522
|
+
functionID,
|
|
523
|
+
environment: env,
|
|
524
|
+
url: `${serverConfig.url}/${pooledWorker.pooledWorkerID}/${serverConfig.API_VERSION}`,
|
|
525
|
+
runtime: props.runtime,
|
|
526
|
+
isMonoBuild: isShared,
|
|
527
|
+
});
|
|
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`);
|
|
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
|
+
});
|
|
542
|
+
bus.publish("worker.started", {
|
|
543
|
+
workerID: awsWorkerID,
|
|
544
|
+
functionID,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
catch (ex) {
|
|
548
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ERROR: Failed to start worker: ${ex.message}`);
|
|
549
|
+
Logger.debug("Failed to start pooled worker", ex);
|
|
550
|
+
bus.publish("function.error", {
|
|
551
|
+
workerID: awsWorkerID,
|
|
552
|
+
functionID,
|
|
553
|
+
requestID,
|
|
554
|
+
errorType: "WorkerStartFailed",
|
|
555
|
+
errorMessage: `Failed to start pooled worker: ${ex.message}`,
|
|
556
|
+
trace: ex.stack?.split("\n") || [],
|
|
557
|
+
});
|
|
558
|
+
// Cleanup failed worker state
|
|
559
|
+
activeWorkers.delete(pooledWorker.pooledWorkerID);
|
|
560
|
+
startedWorkers.delete(pooledWorker.pooledWorkerID);
|
|
561
|
+
lastRequestId.delete(pooledWorker.pooledWorkerID);
|
|
562
|
+
workerIDMapping.delete(awsWorkerID);
|
|
563
|
+
reverseMapping.delete(pooledWorker.pooledWorkerID);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
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
|
+
});
|
|
578
|
+
bus.publish("worker.reused", {
|
|
579
|
+
workerID: awsWorkerID,
|
|
580
|
+
functionID,
|
|
581
|
+
pooledWorkerID: pooledWorker.pooledWorkerID,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
// Route invocation to the pooled worker
|
|
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`);
|
|
587
|
+
logInvokeTrace("ROUTE_INVOCATION", requestID);
|
|
588
|
+
server.routeInvocation(pooledWorker.pooledWorkerID, evt.properties);
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
// === NON-POOLED PATH (existing behavior) ===
|
|
592
|
+
lastRequestId.set(awsWorkerID, requestID);
|
|
593
|
+
let worker = workers.get(awsWorkerID);
|
|
594
|
+
if (worker)
|
|
595
|
+
return;
|
|
596
|
+
try {
|
|
597
|
+
await handler.startWorker({
|
|
598
|
+
...build,
|
|
599
|
+
workerID: awsWorkerID,
|
|
600
|
+
functionID,
|
|
601
|
+
environment: env,
|
|
602
|
+
url: `${serverConfig.url}/${awsWorkerID}/${serverConfig.API_VERSION}`,
|
|
603
|
+
runtime: props.runtime,
|
|
604
|
+
isMonoBuild: isMonoBuildPath(build.out),
|
|
605
|
+
});
|
|
606
|
+
workers.set(awsWorkerID, { workerID: awsWorkerID, functionID });
|
|
607
|
+
bus.publish("worker.started", { workerID: awsWorkerID, functionID });
|
|
608
|
+
// Route invocation to the non-pooled worker
|
|
609
|
+
const server = await getServer();
|
|
610
|
+
server.routeInvocation(awsWorkerID, evt.properties);
|
|
611
|
+
}
|
|
612
|
+
catch (ex) {
|
|
613
|
+
Logger.debug("Failed to start worker", ex);
|
|
614
|
+
bus.publish("function.error", {
|
|
615
|
+
workerID: awsWorkerID,
|
|
616
|
+
functionID,
|
|
617
|
+
requestID,
|
|
618
|
+
errorType: "WorkerStartFailed",
|
|
619
|
+
errorMessage: `Failed to start worker: ${ex.message}`,
|
|
620
|
+
trace: ex.stack?.split("\n") || [],
|
|
621
|
+
});
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
// Process exit cleanup
|
|
627
|
+
process.on("exit", () => {
|
|
628
|
+
// Log final metrics summary
|
|
629
|
+
writeSessionEndSummary();
|
|
630
|
+
for (const pool of workerPool.values()) {
|
|
631
|
+
for (const worker of pool) {
|
|
632
|
+
clearTimeout(worker.idleTimer);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
59
635
|
});
|
|
60
636
|
return {
|
|
61
637
|
fromID(workerID) {
|
|
638
|
+
// Check pooled workers first
|
|
639
|
+
const pooled = activeWorkers.get(workerID);
|
|
640
|
+
if (pooled)
|
|
641
|
+
return { workerID, functionID: pooled.functionID };
|
|
642
|
+
// Check non-pooled workers
|
|
62
643
|
return workers.get(workerID);
|
|
63
644
|
},
|
|
64
645
|
getCurrentRequestID(workerID) {
|
|
65
646
|
return lastRequestId.get(workerID);
|
|
66
647
|
},
|
|
67
648
|
stdout(workerID, message) {
|
|
649
|
+
// Check pooled workers first
|
|
650
|
+
const pooled = activeWorkers.get(workerID);
|
|
651
|
+
if (pooled) {
|
|
652
|
+
const requestID = lastRequestId.get(workerID);
|
|
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
|
+
}
|
|
664
|
+
bus.publish("worker.stdout", {
|
|
665
|
+
workerID,
|
|
666
|
+
functionID: pooled.functionID,
|
|
667
|
+
message: trimmedMessage,
|
|
668
|
+
requestID,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
return;
|
|
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
|
+
}
|
|
678
|
+
// Non-pooled worker
|
|
68
679
|
const worker = workers.get(workerID);
|
|
680
|
+
if (!worker)
|
|
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
|
+
}
|
|
69
693
|
bus.publish("worker.stdout", {
|
|
70
694
|
...worker,
|
|
71
|
-
message:
|
|
72
|
-
requestID:
|
|
695
|
+
message: trimmedMessage,
|
|
696
|
+
requestID: requestID,
|
|
73
697
|
});
|
|
74
698
|
},
|
|
75
699
|
exited(workerID) {
|
|
700
|
+
// Check if pooled worker
|
|
701
|
+
if (activeWorkers.has(workerID) || startedWorkers.has(workerID)) {
|
|
702
|
+
const worker = activeWorkers.get(workerID);
|
|
703
|
+
if (worker) {
|
|
704
|
+
const uptime = Date.now() - worker.createdAt;
|
|
705
|
+
logPool("EXIT", {
|
|
706
|
+
pooledWorkerID: workerID.slice(0, 8),
|
|
707
|
+
functionID: worker.functionID,
|
|
708
|
+
state: worker.state,
|
|
709
|
+
uptimeMs: uptime,
|
|
710
|
+
});
|
|
711
|
+
// Clean up all mappings
|
|
712
|
+
const awsWorkerID = reverseMapping.get(workerID);
|
|
713
|
+
if (awsWorkerID) {
|
|
714
|
+
workerIDMapping.delete(awsWorkerID);
|
|
715
|
+
}
|
|
716
|
+
reverseMapping.delete(workerID);
|
|
717
|
+
activeWorkers.delete(workerID);
|
|
718
|
+
lastRequestId.delete(workerID);
|
|
719
|
+
startedWorkers.delete(workerID);
|
|
720
|
+
bus.publish("worker.exited", {
|
|
721
|
+
workerID: awsWorkerID || workerID,
|
|
722
|
+
functionID: worker.functionID,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
// Non-pooled worker
|
|
76
728
|
const existing = workers.get(workerID);
|
|
77
729
|
if (!existing)
|
|
78
730
|
return;
|
|
@@ -80,6 +732,147 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
80
732
|
lastRequestId.delete(workerID);
|
|
81
733
|
bus.publish("worker.exited", existing);
|
|
82
734
|
},
|
|
83
|
-
|
|
735
|
+
// Called by server when response is received - returns worker to pool
|
|
736
|
+
onResponse(pooledWorkerID) {
|
|
737
|
+
if (activeWorkers.has(pooledWorkerID)) {
|
|
738
|
+
const worker = activeWorkers.get(pooledWorkerID);
|
|
739
|
+
if (worker) {
|
|
740
|
+
trackRequestEnd(worker.functionID);
|
|
741
|
+
logPool("RESPONSE", {
|
|
742
|
+
pooledWorkerID: pooledWorkerID.slice(0, 8),
|
|
743
|
+
functionID: worker.functionID,
|
|
744
|
+
requestID: lastRequestId.get(pooledWorkerID)?.slice(0, 8),
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
returnToPool(pooledWorkerID);
|
|
748
|
+
}
|
|
749
|
+
},
|
|
750
|
+
// Get AWS workerID from pooled ID (for IoT routing)
|
|
751
|
+
getAwsWorkerID(pooledWorkerID) {
|
|
752
|
+
return reverseMapping.get(pooledWorkerID);
|
|
753
|
+
},
|
|
754
|
+
// Check if worker is pooled
|
|
755
|
+
isPooled(workerID) {
|
|
756
|
+
return activeWorkers.has(workerID) || startedWorkers.has(workerID);
|
|
757
|
+
},
|
|
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
|
+
},
|
|
84
877
|
};
|
|
85
878
|
});
|