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