@intelligems/sst 2.49.6-ig.9 → 2.49.8-ig.2
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 +10 -6
- package/constructs/AstroSite.d.ts +1 -1
- package/constructs/EdgeFunction.d.ts +1 -1
- package/constructs/EdgeFunction.js +8 -6
- package/constructs/Function.d.ts +3 -2
- package/constructs/Function.js +2 -1
- package/constructs/Job.d.ts +2 -2
- package/constructs/Job.js +4 -2
- package/constructs/NextjsSite.d.ts +1 -1
- package/constructs/NextjsSite.js +2 -2
- package/constructs/RemixSite.d.ts +1 -1
- package/constructs/SolidStartSite.d.ts +1 -1
- package/constructs/SsrFunction.d.ts +2 -2
- package/constructs/SsrFunction.js +7 -5
- package/constructs/SsrSite.d.ts +2 -2
- package/constructs/SsrSite.js +1 -1
- package/constructs/Stack.d.ts +1 -1
- package/constructs/Stack.js +1 -1
- package/constructs/SvelteKitSite.d.ts +1 -1
- package/constructs/deprecated/NextjsSite.d.ts +3 -3
- package/constructs/deprecated/NextjsSite.js +4 -1
- package/constructs/deprecated/cross-region-helper.js +3 -3
- package/package.json +3 -3
- package/runtime/handlers/node.js +24 -2
- package/runtime/handlers.d.ts +4 -0
- package/runtime/memory-logging.d.ts +26 -0
- package/runtime/memory-logging.js +112 -0
- package/runtime/mono-build-config.d.ts +6 -3
- package/runtime/mono-build-config.js +34 -10
- package/runtime/server.js +35 -44
- package/runtime/stdout-attribution.d.ts +18 -0
- package/runtime/stdout-attribution.js +43 -0
- package/runtime/worker-config.d.ts +19 -0
- package/runtime/worker-config.js +26 -0
- package/runtime/worker-pool-logging.d.ts +2 -2
- package/runtime/worker-pool-logging.js +5 -5
- package/runtime/worker-pool.d.ts +77 -0
- package/runtime/worker-pool.js +162 -0
- package/runtime/workers.d.ts +33 -11
- package/runtime/workers.js +405 -513
- package/support/nodejs-runtime/index.mjs +214 -100
- package/watcher.js +2 -0
- package/README.md +0 -43
- package/package.json.bak +0 -156
package/runtime/workers.js
CHANGED
|
@@ -7,13 +7,15 @@ import { useRuntimeServerConfig, useRuntimeServer } from "./server.js";
|
|
|
7
7
|
import { useFunctions } from "../constructs/Function.js";
|
|
8
8
|
import { lazy } from "../util/lazy.js";
|
|
9
9
|
import { Logger } from "../logger.js";
|
|
10
|
-
import {
|
|
10
|
+
import { logPool, logInvokeTrace, trackRequestStart, trackRequestEnd, setFunctionNameResolver, writeSessionEndSummary, } from "./worker-pool-logging.js";
|
|
11
|
+
import { POOL_SIZE, IDLE_TIMEOUT, WORKER_CONCURRENCY, DEBUG_MEMORY, } from "./worker-config.js";
|
|
12
|
+
import { WorkerPool } from "./worker-pool.js";
|
|
13
|
+
import { splitAttributed } from "./stdout-attribution.js";
|
|
14
|
+
import { startMemorySampling } from "./memory-logging.js";
|
|
11
15
|
import { useMonoBuildConfig, isMonoBuildPath } from "./mono-build-config.js";
|
|
12
16
|
import { getRequestPath, getCorrelationId, getApiGatewayRequestId } from "./request-utils.js";
|
|
13
17
|
import { logWorkers } from "./debug-bridge-logging.js";
|
|
14
18
|
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
19
|
const bundleMtimes = new Map();
|
|
18
20
|
const bundleWatchers = new Map();
|
|
19
21
|
// Clean up watchers on exit
|
|
@@ -76,7 +78,6 @@ function getBundleMtime(buildOut) {
|
|
|
76
78
|
// Helper: Get pool key for worker lookup
|
|
77
79
|
// For mono-build, uses shared key so any warm worker can serve any handler
|
|
78
80
|
function getPoolKey(functionID, runtime, buildOut) {
|
|
79
|
-
// Use the global mono build config for pool key calculation
|
|
80
81
|
return useMonoBuildConfig().getPoolKey(functionID, runtime, buildOut);
|
|
81
82
|
}
|
|
82
83
|
// Extract readable function name from handler path
|
|
@@ -100,6 +101,7 @@ const POOLABLE_RUNTIMES = new Set([
|
|
|
100
101
|
"nodejs18.x",
|
|
101
102
|
"nodejs20.x",
|
|
102
103
|
"nodejs22.x",
|
|
104
|
+
"nodejs24.x",
|
|
103
105
|
// Python - has while True loop in python-runtime/runtime.py
|
|
104
106
|
"python",
|
|
105
107
|
"python3.7",
|
|
@@ -135,25 +137,35 @@ function isPoolableRuntime(runtime, env) {
|
|
|
135
137
|
return (POOLABLE_RUNTIMES.has(runtime) ||
|
|
136
138
|
[...POOLABLE_RUNTIMES].some((r) => runtime.startsWith(r)));
|
|
137
139
|
}
|
|
140
|
+
/** Only the Node runtime shim knows how to run invocations side by side. */
|
|
141
|
+
function concurrencyFor(runtime) {
|
|
142
|
+
return runtime.startsWith("nodejs") ? WORKER_CONCURRENCY : 1;
|
|
143
|
+
}
|
|
138
144
|
export const useRuntimeWorkers = lazy(async () => {
|
|
139
145
|
// Set up function name resolver for logging module
|
|
140
146
|
setFunctionNameResolver(getFunctionName);
|
|
141
147
|
// Non-pooled workers (legacy behavior)
|
|
142
148
|
const workers = new Map();
|
|
143
|
-
//
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
const reverseMapping = new Map(); // pooledWorkerID → awsWorkerID
|
|
148
|
-
const startedWorkers = new Set(); // Track started pooledWorkerIDs
|
|
149
|
+
// Pooled workers and the invocations they hold
|
|
150
|
+
const requests = new Map(); // requestID → request
|
|
151
|
+
const lastRequestId = new Map(); // workerID → most recent requestID
|
|
152
|
+
const waitQueues = new Map(); // poolKey → waiting
|
|
149
153
|
const bus = useBus();
|
|
150
154
|
const handlers = useRuntimeHandlers();
|
|
151
155
|
const builder = useFunctionBuilder();
|
|
152
156
|
const serverConfig = await useRuntimeServerConfig();
|
|
157
|
+
const pool = new WorkerPool({
|
|
158
|
+
maxWorkers: POOL_SIZE,
|
|
159
|
+
idleTimeoutMs: IDLE_TIMEOUT,
|
|
160
|
+
onTerminate: (worker, reason) => {
|
|
161
|
+
void stopPooledWorker(worker, reason);
|
|
162
|
+
},
|
|
163
|
+
});
|
|
153
164
|
// Log pool configuration on startup
|
|
154
165
|
logPool("INIT", {
|
|
155
166
|
poolSize: POOL_SIZE,
|
|
156
167
|
idleTimeoutMs: IDLE_TIMEOUT,
|
|
168
|
+
concurrency: WORKER_CONCURRENCY,
|
|
157
169
|
poolableRuntimes: [...POOLABLE_RUNTIMES].length,
|
|
158
170
|
});
|
|
159
171
|
// Lazy getter for server to avoid circular initialization
|
|
@@ -164,219 +176,270 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
164
176
|
}
|
|
165
177
|
return _server;
|
|
166
178
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return;
|
|
178
|
-
const uptime = Date.now() - worker.createdAt;
|
|
179
|
+
function queuedCount(poolKey) {
|
|
180
|
+
if (poolKey)
|
|
181
|
+
return waitQueues.get(poolKey)?.length ?? 0;
|
|
182
|
+
let total = 0;
|
|
183
|
+
for (const q of waitQueues.values())
|
|
184
|
+
total += q.length;
|
|
185
|
+
return total;
|
|
186
|
+
}
|
|
187
|
+
// Helper: stop a pooled worker that the pool has already forgotten
|
|
188
|
+
async function stopPooledWorker(worker, reason) {
|
|
179
189
|
logPool("TERMINATE", {
|
|
180
|
-
pooledWorkerID:
|
|
190
|
+
pooledWorkerID: worker.id.slice(0, 8),
|
|
181
191
|
functionID: worker.functionID,
|
|
182
|
-
reason
|
|
183
|
-
uptimeMs:
|
|
192
|
+
reason,
|
|
193
|
+
uptimeMs: Date.now() - worker.createdAt,
|
|
194
|
+
inFlight: worker.inFlight,
|
|
184
195
|
});
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (awsWorkerID) {
|
|
192
|
-
workerIDMapping.delete(awsWorkerID);
|
|
196
|
+
try {
|
|
197
|
+
const handler = handlers.for(worker.runtime);
|
|
198
|
+
await handler?.stopWorker(worker.id);
|
|
199
|
+
}
|
|
200
|
+
catch (ex) {
|
|
201
|
+
Logger.debug("Failed to stop pooled worker", worker.id, ex);
|
|
193
202
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
203
|
+
lastRequestId.delete(worker.id);
|
|
204
|
+
Logger.debug("Terminated pooled worker", worker.id);
|
|
205
|
+
// A slot opened up
|
|
206
|
+
void drain(worker.poolKey);
|
|
198
207
|
}
|
|
199
|
-
// Helper:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
+
// Helper: publish an error for every invocation a worker was holding
|
|
209
|
+
function failRequestsOn(pooledWorkerID, errorType, errorMessage) {
|
|
210
|
+
for (const req of [...requests.values()]) {
|
|
211
|
+
if (req.pooledWorkerID !== pooledWorkerID)
|
|
212
|
+
continue;
|
|
213
|
+
requests.delete(req.requestID);
|
|
214
|
+
trackRequestEnd(req.functionID);
|
|
215
|
+
bus.publish("function.error", {
|
|
216
|
+
workerID: req.awsWorkerID,
|
|
217
|
+
functionID: req.functionID,
|
|
218
|
+
requestID: req.requestID,
|
|
219
|
+
errorType,
|
|
220
|
+
errorMessage,
|
|
221
|
+
trace: [],
|
|
208
222
|
});
|
|
209
|
-
return undefined;
|
|
210
223
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
continue; // Try next worker
|
|
229
|
-
}
|
|
230
|
-
worker.state = "busy";
|
|
231
|
-
const age = Date.now() - worker.createdAt;
|
|
232
|
-
const crossFunction = worker.functionID !== functionID;
|
|
224
|
+
}
|
|
225
|
+
// Helper: hand an invocation to a worker (new or reused)
|
|
226
|
+
async function assign(worker, pending, reused) {
|
|
227
|
+
const { evt } = pending;
|
|
228
|
+
const { workerID: awsWorkerID, functionID, requestID, event } = evt.properties;
|
|
229
|
+
const requestPath = getRequestPath(event);
|
|
230
|
+
pool.checkout(worker, functionID);
|
|
231
|
+
requests.set(requestID, {
|
|
232
|
+
requestID,
|
|
233
|
+
awsWorkerID,
|
|
234
|
+
functionID,
|
|
235
|
+
pooledWorkerID: worker.id,
|
|
236
|
+
startedAt: Date.now(),
|
|
237
|
+
});
|
|
238
|
+
lastRequestId.set(worker.id, requestID);
|
|
239
|
+
trackRequestStart(functionID, reused);
|
|
240
|
+
if (reused) {
|
|
233
241
|
logPool("REUSE", {
|
|
234
|
-
pooledWorkerID: worker.
|
|
242
|
+
pooledWorkerID: worker.id.slice(0, 8),
|
|
243
|
+
functionID,
|
|
244
|
+
inFlight: worker.inFlight,
|
|
245
|
+
workerAgeMs: Date.now() - worker.createdAt,
|
|
246
|
+
waitedMs: Date.now() - pending.queuedAt,
|
|
247
|
+
});
|
|
248
|
+
logInvokeTrace("WORKER_REUSE", requestID, `pooled=${worker.id.slice(0, 8)}`);
|
|
249
|
+
logEventTrace("WORKER_START", {
|
|
250
|
+
requestID,
|
|
251
|
+
functionID,
|
|
252
|
+
workerID: worker.id,
|
|
253
|
+
path: requestPath,
|
|
254
|
+
correlationId: getCorrelationId(event),
|
|
255
|
+
apiGwReqId: getApiGatewayRequestId(event),
|
|
256
|
+
reused: true,
|
|
257
|
+
});
|
|
258
|
+
bus.publish("worker.reused", {
|
|
259
|
+
workerID: awsWorkerID,
|
|
235
260
|
functionID,
|
|
236
|
-
|
|
237
|
-
poolSizeAfter: pool.length,
|
|
238
|
-
workerAgeMs: age,
|
|
239
|
-
crossFunction,
|
|
261
|
+
pooledWorkerID: worker.id,
|
|
240
262
|
});
|
|
241
|
-
Logger.debug("Reusing pooled worker", worker.pooledWorkerID, "for", functionID, crossFunction ? "(cross-function reuse)" : "");
|
|
242
|
-
return worker;
|
|
243
263
|
}
|
|
244
|
-
|
|
245
|
-
|
|
264
|
+
const server = await getServer();
|
|
265
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Routing invocation to worker ${worker.id.slice(0, 8)} inFlight=${worker.inFlight}`);
|
|
266
|
+
logInvokeTrace("ROUTE_INVOCATION", requestID);
|
|
267
|
+
server.routeInvocation(worker.id, evt.properties);
|
|
268
|
+
}
|
|
269
|
+
// Helper: create a worker for an invocation. The worker is registered (and
|
|
270
|
+
// counted against the cap) before the thread starts, so concurrent
|
|
271
|
+
// dispatches cannot overshoot the pool size.
|
|
272
|
+
async function createAndAssign(pending) {
|
|
273
|
+
const { evt, props, handler, build, poolKey, isShared } = pending;
|
|
274
|
+
const { workerID: awsWorkerID, functionID, requestID, env, event } = evt.properties;
|
|
275
|
+
const requestPath = getRequestPath(event);
|
|
276
|
+
const worker = {
|
|
277
|
+
id: crypto.randomBytes(16).toString("hex"),
|
|
278
|
+
poolKey,
|
|
246
279
|
functionID,
|
|
280
|
+
runtime: props.runtime,
|
|
281
|
+
inFlight: 0,
|
|
282
|
+
maxConcurrency: concurrencyFor(props.runtime),
|
|
283
|
+
stale: false,
|
|
284
|
+
createdAt: Date.now(),
|
|
285
|
+
bundlePath: build.out,
|
|
286
|
+
bundleMtime: getBundleMtime(build.out),
|
|
287
|
+
isSharedPool: isShared,
|
|
288
|
+
};
|
|
289
|
+
pool.add(worker);
|
|
290
|
+
logPool("CREATE", {
|
|
291
|
+
pooledWorkerID: worker.id.slice(0, 8),
|
|
292
|
+
functionID,
|
|
293
|
+
runtime: props.runtime,
|
|
294
|
+
requestID: requestID.slice(0, 8),
|
|
247
295
|
poolKey: poolKey.slice(0, 30),
|
|
248
|
-
|
|
249
|
-
|
|
296
|
+
isSharedPool: isShared,
|
|
297
|
+
liveWorkers: pool.liveCount(poolKey),
|
|
298
|
+
waitedMs: Date.now() - pending.queuedAt,
|
|
250
299
|
});
|
|
251
|
-
|
|
300
|
+
// Route first so the invocation is queued at the server by the time the
|
|
301
|
+
// worker asks for it.
|
|
302
|
+
await assign(worker, pending, false);
|
|
303
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Starting worker ${worker.id.slice(0, 8)}...`);
|
|
304
|
+
logInvokeTrace("WORKER_START", requestID, `pooled=${worker.id.slice(0, 8)}`);
|
|
305
|
+
const workerStartTime = Date.now();
|
|
306
|
+
try {
|
|
307
|
+
await handler.startWorker({
|
|
308
|
+
...build,
|
|
309
|
+
workerID: worker.id,
|
|
310
|
+
functionID,
|
|
311
|
+
environment: env,
|
|
312
|
+
url: `${serverConfig.url}/${worker.id}/${serverConfig.API_VERSION}`,
|
|
313
|
+
runtime: props.runtime,
|
|
314
|
+
isMonoBuild: isShared,
|
|
315
|
+
concurrency: worker.maxConcurrency,
|
|
316
|
+
debugMemory: DEBUG_MEMORY,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
catch (ex) {
|
|
320
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ERROR: Failed to start worker: ${ex.message}`);
|
|
321
|
+
Logger.debug("Failed to start pooled worker", ex);
|
|
322
|
+
pool.remove(worker);
|
|
323
|
+
failRequestsOn(worker.id, "WorkerStartFailed", `Failed to start pooled worker: ${ex.message}`);
|
|
324
|
+
lastRequestId.delete(worker.id);
|
|
325
|
+
void drain(poolKey);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const workerStartElapsed = Date.now() - workerStartTime;
|
|
329
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Worker started in ${workerStartElapsed}ms`);
|
|
330
|
+
logInvokeTrace("WORKER_STARTED", requestID);
|
|
331
|
+
logEventTrace("WORKER_START", {
|
|
332
|
+
requestID,
|
|
333
|
+
functionID,
|
|
334
|
+
workerID: worker.id,
|
|
335
|
+
path: requestPath,
|
|
336
|
+
correlationId: getCorrelationId(event),
|
|
337
|
+
apiGwReqId: getApiGatewayRequestId(event),
|
|
338
|
+
elapsed: workerStartElapsed,
|
|
339
|
+
reused: false,
|
|
340
|
+
});
|
|
341
|
+
bus.publish("worker.started", { workerID: awsWorkerID, functionID });
|
|
252
342
|
}
|
|
253
|
-
// Helper:
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
343
|
+
// Helper: place an invocation on a worker, create one, or wait for capacity
|
|
344
|
+
async function dispatch(pending) {
|
|
345
|
+
const { poolKey, build, evt } = pending;
|
|
346
|
+
const { functionID, requestID } = evt.properties;
|
|
347
|
+
const currentMtime = getBundleMtime(build.out);
|
|
348
|
+
const worker = pool.pick(poolKey, currentMtime);
|
|
349
|
+
if (worker) {
|
|
350
|
+
await assign(worker, pending, true);
|
|
258
351
|
return;
|
|
259
|
-
|
|
260
|
-
if (
|
|
261
|
-
|
|
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");
|
|
352
|
+
}
|
|
353
|
+
if (pool.canCreate(poolKey)) {
|
|
354
|
+
await createAndAssign(pending);
|
|
268
355
|
return;
|
|
269
356
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
357
|
+
let queue = waitQueues.get(poolKey);
|
|
358
|
+
if (!queue) {
|
|
359
|
+
queue = [];
|
|
360
|
+
waitQueues.set(poolKey, queue);
|
|
361
|
+
}
|
|
362
|
+
queue.push(pending);
|
|
363
|
+
logPool("POOL_WAIT", {
|
|
364
|
+
functionID,
|
|
365
|
+
requestID: requestID.slice(0, 8),
|
|
366
|
+
poolKey: poolKey.slice(0, 30),
|
|
367
|
+
queued: queue.length,
|
|
368
|
+
liveWorkers: pool.liveCount(poolKey),
|
|
369
|
+
});
|
|
370
|
+
logInvokeTrace("POOL_WAIT", requestID, `queued=${queue.length}`);
|
|
371
|
+
}
|
|
372
|
+
// Helper: give waiting invocations to whatever capacity exists now
|
|
373
|
+
async function drain(poolKey) {
|
|
374
|
+
const queue = waitQueues.get(poolKey);
|
|
375
|
+
if (!queue || queue.length === 0)
|
|
376
|
+
return;
|
|
377
|
+
while (queue.length > 0) {
|
|
378
|
+
const head = queue[0];
|
|
379
|
+
const currentMtime = getBundleMtime(head.build.out);
|
|
380
|
+
const worker = pool.pick(poolKey, currentMtime);
|
|
381
|
+
if (worker) {
|
|
382
|
+
queue.shift();
|
|
383
|
+
await assign(worker, head, true);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (pool.canCreate(poolKey)) {
|
|
387
|
+
queue.shift();
|
|
388
|
+
await createAndAssign(head);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
break;
|
|
275
392
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
393
|
+
if (queue.length === 0)
|
|
394
|
+
waitQueues.delete(poolKey);
|
|
395
|
+
}
|
|
396
|
+
// Helper: an invocation finished on a pooled worker
|
|
397
|
+
function release(pooledWorkerID, requestID) {
|
|
398
|
+
const req = requestID ? requests.get(requestID) : undefined;
|
|
399
|
+
if (req) {
|
|
400
|
+
requests.delete(req.requestID);
|
|
401
|
+
trackRequestEnd(req.functionID);
|
|
281
402
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
403
|
+
const worker = pool.get(pooledWorkerID);
|
|
404
|
+
if (!worker)
|
|
405
|
+
return;
|
|
406
|
+
logPool("RESPONSE", {
|
|
407
|
+
pooledWorkerID: pooledWorkerID.slice(0, 8),
|
|
408
|
+
functionID: req?.functionID ?? worker.functionID,
|
|
409
|
+
requestID: requestID?.slice(0, 8),
|
|
410
|
+
inFlight: worker.inFlight - 1,
|
|
411
|
+
durationMs: req ? Date.now() - req.startedAt : undefined,
|
|
412
|
+
});
|
|
413
|
+
const outcome = pool.checkin(worker);
|
|
414
|
+
if (outcome === "terminated")
|
|
415
|
+
return; // stopPooledWorker drains
|
|
416
|
+
if (outcome === "idle") {
|
|
417
|
+
logPool("IDLE", {
|
|
285
418
|
pooledWorkerID: pooledWorkerID.slice(0, 8),
|
|
286
419
|
functionID: worker.functionID,
|
|
287
420
|
poolKey: worker.poolKey.slice(0, 30),
|
|
288
|
-
|
|
289
|
-
maxSize: POOL_SIZE,
|
|
421
|
+
idleTimeoutMs: IDLE_TIMEOUT,
|
|
290
422
|
});
|
|
291
|
-
terminatePooledWorker(pooledWorkerID, "pool_full");
|
|
292
|
-
Logger.debug("Pool full, terminated worker", pooledWorkerID);
|
|
293
|
-
return;
|
|
294
423
|
}
|
|
295
|
-
|
|
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);
|
|
424
|
+
void drain(worker.poolKey);
|
|
315
425
|
}
|
|
316
|
-
// Build success handler -
|
|
426
|
+
// Build success handler - retire workers running the old code
|
|
317
427
|
handlers.subscribe("function.build.success", async (evt) => {
|
|
318
428
|
const { functionID } = evt.properties;
|
|
319
429
|
const props = useFunctions().fromID(functionID);
|
|
320
430
|
if (!props)
|
|
321
431
|
return;
|
|
322
|
-
// Get build to check if mono-build using global config
|
|
323
432
|
const build = await builder.artifact(functionID);
|
|
324
433
|
const isMonoBuild = build ? isMonoBuildPath(build.out) : false;
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
}
|
|
434
|
+
const poolKey = isMonoBuild ? `${props.runtime}:mono-build` : `${props.runtime}:${functionID}`;
|
|
435
|
+
const before = pool.workersFor(poolKey).length;
|
|
436
|
+
const marked = pool.invalidate(poolKey, isMonoBuild ? "mono-rebuild" : "rebuild");
|
|
437
|
+
logPool(isMonoBuild ? "MONO_BUILD_CLEAR" : "BUILD_CLEAR", {
|
|
438
|
+
functionID,
|
|
439
|
+
poolKey,
|
|
440
|
+
pooledWorkersCleared: before - marked.length,
|
|
441
|
+
activeWorkersMarkedStale: marked.length,
|
|
442
|
+
});
|
|
380
443
|
// Stop non-pooled workers (legacy behavior)
|
|
381
444
|
for (const [_, worker] of workers) {
|
|
382
445
|
if (worker.functionID === functionID) {
|
|
@@ -389,24 +452,18 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
389
452
|
}
|
|
390
453
|
}
|
|
391
454
|
});
|
|
392
|
-
const lastRequestId = new Map();
|
|
393
455
|
// Main invocation handler
|
|
394
456
|
bus.subscribe("function.invoked", async (evt) => {
|
|
395
457
|
const { workerID: awsWorkerID, functionID, requestID, env, event, } = evt.properties;
|
|
396
458
|
const startTime = Date.now();
|
|
397
459
|
const requestPath = getRequestPath(event);
|
|
398
|
-
//
|
|
399
|
-
//
|
|
460
|
+
// Warm pings ({ding}/{warmer}) are ordinary invocations here: they take
|
|
461
|
+
// a pooled worker if one is free and wait for capacity otherwise, so a
|
|
462
|
+
// burst of them can never hold more isolates than the pool allows.
|
|
400
463
|
const isWarmupRequest = event && typeof event === 'object' &&
|
|
401
464
|
('ding' in event || 'warmer' in event || event.__sst_warmup === true);
|
|
402
|
-
const warmupId = isWarmupRequest ? (event.warmupId ?? event.index) : undefined;
|
|
403
465
|
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} RECEIVED func=${functionID.slice(-30)}`);
|
|
404
|
-
|
|
405
|
-
logInvokeTrace("WARMUP_RECEIVED", requestID, `warmupId=${warmupId}`);
|
|
406
|
-
}
|
|
407
|
-
else {
|
|
408
|
-
logInvokeTrace("INVOKE_RECEIVED", requestID, `func=${functionID.slice(-40)}`);
|
|
409
|
-
}
|
|
466
|
+
logInvokeTrace(isWarmupRequest ? "WARMUP_RECEIVED" : "INVOKE_RECEIVED", requestID, `func=${functionID.slice(-40)}`);
|
|
410
467
|
// Send ack immediately
|
|
411
468
|
bus.publish("function.ack", { functionID, workerID: awsWorkerID, requestID });
|
|
412
469
|
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ACK sent elapsed=${Date.now() - startTime}ms`);
|
|
@@ -439,12 +496,10 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
439
496
|
});
|
|
440
497
|
return;
|
|
441
498
|
}
|
|
442
|
-
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Getting build artifact...`);
|
|
443
499
|
logInvokeTrace("BUILD_ARTIFACT_START", requestID);
|
|
444
500
|
const buildStartTime = Date.now();
|
|
445
501
|
const build = await builder.artifact(functionID);
|
|
446
|
-
|
|
447
|
-
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Build artifact took ${buildElapsed}ms`);
|
|
502
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Build artifact took ${Date.now() - buildStartTime}ms`);
|
|
448
503
|
logInvokeTrace("BUILD_ARTIFACT_DONE", requestID, build ? `out=${build.out.slice(-30)}` : "NO_BUILD");
|
|
449
504
|
if (!build) {
|
|
450
505
|
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} ERROR: Build artifact not ready`);
|
|
@@ -459,229 +514,123 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
459
514
|
});
|
|
460
515
|
return;
|
|
461
516
|
}
|
|
462
|
-
|
|
463
|
-
const poolable = isPoolableRuntime(props.runtime, env);
|
|
464
|
-
if (poolable) {
|
|
517
|
+
if (isPoolableRuntime(props.runtime, env)) {
|
|
465
518
|
// === POOLED PATH ===
|
|
466
|
-
// Get pool key: shared for mono-build, per-function otherwise
|
|
467
519
|
const { key: poolKey, isShared } = getPoolKey(functionID, props.runtime, build.out);
|
|
468
|
-
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
|
|
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
|
|
520
|
+
logWorkers(`path=${requestPath} reqId=${requestID.slice(0, 8)} Dispatching to pool ${poolKey.slice(0, 20)}`);
|
|
521
|
+
await dispatch({
|
|
522
|
+
evt,
|
|
523
|
+
props,
|
|
524
|
+
handler,
|
|
525
|
+
build,
|
|
526
|
+
poolKey,
|
|
527
|
+
isShared,
|
|
528
|
+
queuedAt: Date.now(),
|
|
529
|
+
});
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
// === NON-POOLED PATH (existing behavior) ===
|
|
533
|
+
lastRequestId.set(awsWorkerID, requestID);
|
|
534
|
+
let worker = workers.get(awsWorkerID);
|
|
535
|
+
if (worker)
|
|
536
|
+
return;
|
|
537
|
+
try {
|
|
538
|
+
await handler.startWorker({
|
|
539
|
+
...build,
|
|
540
|
+
workerID: awsWorkerID,
|
|
541
|
+
functionID,
|
|
542
|
+
environment: env,
|
|
543
|
+
url: `${serverConfig.url}/${awsWorkerID}/${serverConfig.API_VERSION}`,
|
|
544
|
+
runtime: props.runtime,
|
|
545
|
+
isMonoBuild: isMonoBuildPath(build.out),
|
|
546
|
+
});
|
|
547
|
+
workers.set(awsWorkerID, { workerID: awsWorkerID, functionID });
|
|
548
|
+
bus.publish("worker.started", { workerID: awsWorkerID, functionID });
|
|
585
549
|
const server = await getServer();
|
|
586
|
-
|
|
587
|
-
logInvokeTrace("ROUTE_INVOCATION", requestID);
|
|
588
|
-
server.routeInvocation(pooledWorker.pooledWorkerID, evt.properties);
|
|
550
|
+
server.routeInvocation(awsWorkerID, evt.properties);
|
|
589
551
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
}
|
|
552
|
+
catch (ex) {
|
|
553
|
+
Logger.debug("Failed to start worker", ex);
|
|
554
|
+
bus.publish("function.error", {
|
|
555
|
+
workerID: awsWorkerID,
|
|
556
|
+
functionID,
|
|
557
|
+
requestID,
|
|
558
|
+
errorType: "WorkerStartFailed",
|
|
559
|
+
errorMessage: `Failed to start worker: ${ex.message}`,
|
|
560
|
+
trace: ex.stack?.split("\n") || [],
|
|
561
|
+
});
|
|
562
|
+
return;
|
|
624
563
|
}
|
|
625
564
|
});
|
|
565
|
+
const stats = () => {
|
|
566
|
+
const s = pool.stats();
|
|
567
|
+
return { workers: s.workers, inFlight: s.inFlight, queued: queuedCount() };
|
|
568
|
+
};
|
|
569
|
+
startMemorySampling(stats);
|
|
626
570
|
// Process exit cleanup
|
|
627
571
|
process.on("exit", () => {
|
|
628
|
-
// Log final metrics summary
|
|
629
572
|
writeSessionEndSummary();
|
|
630
|
-
for (const
|
|
631
|
-
|
|
573
|
+
for (const worker of pool.all()) {
|
|
574
|
+
if (worker.idleTimer)
|
|
632
575
|
clearTimeout(worker.idleTimer);
|
|
633
|
-
}
|
|
634
576
|
}
|
|
635
577
|
});
|
|
636
578
|
return {
|
|
637
579
|
fromID(workerID) {
|
|
638
|
-
|
|
639
|
-
const pooled = activeWorkers.get(workerID);
|
|
580
|
+
const pooled = pool.get(workerID);
|
|
640
581
|
if (pooled)
|
|
641
582
|
return { workerID, functionID: pooled.functionID };
|
|
642
|
-
// Check non-pooled workers
|
|
643
583
|
return workers.get(workerID);
|
|
644
584
|
},
|
|
645
585
|
getCurrentRequestID(workerID) {
|
|
646
586
|
return lastRequestId.get(workerID);
|
|
647
587
|
},
|
|
588
|
+
/**
|
|
589
|
+
* Who a response belongs to. Pooled workers may hold several requests,
|
|
590
|
+
* so the request id decides; non-pooled workers are their own AWS worker.
|
|
591
|
+
*/
|
|
592
|
+
resolveRequest(workerID, requestID) {
|
|
593
|
+
const req = requestID ? requests.get(requestID) : undefined;
|
|
594
|
+
if (req)
|
|
595
|
+
return { awsWorkerID: req.awsWorkerID, functionID: req.functionID };
|
|
596
|
+
const pooled = pool.get(workerID);
|
|
597
|
+
if (pooled) {
|
|
598
|
+
const last = lastRequestId.get(workerID);
|
|
599
|
+
const lastReq = last ? requests.get(last) : undefined;
|
|
600
|
+
return {
|
|
601
|
+
awsWorkerID: lastReq?.awsWorkerID ?? workerID,
|
|
602
|
+
functionID: lastReq?.functionID ?? pooled.functionID,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
const worker = workers.get(workerID);
|
|
606
|
+
if (!worker)
|
|
607
|
+
return undefined;
|
|
608
|
+
return { awsWorkerID: workerID, functionID: worker.functionID };
|
|
609
|
+
},
|
|
648
610
|
stdout(workerID, message) {
|
|
649
|
-
|
|
650
|
-
const pooled = activeWorkers.get(workerID);
|
|
611
|
+
const pooled = pool.get(workerID);
|
|
651
612
|
if (pooled) {
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
});
|
|
613
|
+
for (const chunk of splitAttributed(message)) {
|
|
614
|
+
const requestID = chunk.requestID ?? lastRequestId.get(workerID);
|
|
615
|
+
if (!requestID)
|
|
616
|
+
continue;
|
|
617
|
+
const trimmed = chunk.text.trim();
|
|
618
|
+
if (!trimmed)
|
|
619
|
+
continue;
|
|
620
|
+
const functionID = requests.get(requestID)?.functionID ?? pooled.functionID;
|
|
621
|
+
if (trimmed.includes("[LOG]")) {
|
|
622
|
+
logEventTrace("WORKER_LOG", { requestID, functionID, workerID, message: trimmed });
|
|
663
623
|
}
|
|
664
|
-
bus.publish("worker.stdout", {
|
|
665
|
-
workerID,
|
|
666
|
-
functionID: pooled.functionID,
|
|
667
|
-
message: trimmedMessage,
|
|
668
|
-
requestID,
|
|
669
|
-
});
|
|
624
|
+
bus.publish("worker.stdout", { workerID, functionID, message: trimmed, requestID });
|
|
670
625
|
}
|
|
671
626
|
return;
|
|
672
627
|
}
|
|
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
628
|
// Non-pooled worker
|
|
679
629
|
const worker = workers.get(workerID);
|
|
680
630
|
if (!worker)
|
|
681
631
|
return;
|
|
682
632
|
const trimmedMessage = message.trim();
|
|
683
633
|
const requestID = lastRequestId.get(workerID);
|
|
684
|
-
// Log messages that contain [LOG] prefix
|
|
685
634
|
if (trimmedMessage.includes("[LOG]") && requestID) {
|
|
686
635
|
logEventTrace("WORKER_LOG", {
|
|
687
636
|
requestID,
|
|
@@ -697,31 +646,26 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
697
646
|
});
|
|
698
647
|
},
|
|
699
648
|
exited(workerID) {
|
|
700
|
-
|
|
701
|
-
if (
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
bus.publish("worker.exited", {
|
|
721
|
-
workerID: awsWorkerID || workerID,
|
|
722
|
-
functionID: worker.functionID,
|
|
723
|
-
});
|
|
724
|
-
}
|
|
649
|
+
const pooled = pool.get(workerID);
|
|
650
|
+
if (pooled) {
|
|
651
|
+
logPool("EXIT", {
|
|
652
|
+
pooledWorkerID: workerID.slice(0, 8),
|
|
653
|
+
functionID: pooled.functionID,
|
|
654
|
+
inFlight: pooled.inFlight,
|
|
655
|
+
uptimeMs: Date.now() - pooled.createdAt,
|
|
656
|
+
});
|
|
657
|
+
pool.remove(pooled);
|
|
658
|
+
const last = lastRequestId.get(workerID);
|
|
659
|
+
const awsWorkerID = last ? requests.get(last)?.awsWorkerID : undefined;
|
|
660
|
+
// Anything it was holding will never get a response from it
|
|
661
|
+
failRequestsOn(workerID, "WorkerExited", "Local worker exited before responding (out of memory or crashed). Check the dev console for details.");
|
|
662
|
+
lastRequestId.delete(workerID);
|
|
663
|
+
bus.publish("worker.exited", {
|
|
664
|
+
workerID: awsWorkerID ?? workerID,
|
|
665
|
+
functionID: pooled.functionID,
|
|
666
|
+
pooledWorkerID: workerID,
|
|
667
|
+
});
|
|
668
|
+
void drain(pooled.poolKey);
|
|
725
669
|
return;
|
|
726
670
|
}
|
|
727
671
|
// Non-pooled worker
|
|
@@ -732,36 +676,32 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
732
676
|
lastRequestId.delete(workerID);
|
|
733
677
|
bus.publish("worker.exited", existing);
|
|
734
678
|
},
|
|
735
|
-
// Called by server when response is received
|
|
736
|
-
onResponse(pooledWorkerID) {
|
|
737
|
-
if (
|
|
738
|
-
|
|
739
|
-
|
|
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);
|
|
679
|
+
// Called by server when a response or error is received
|
|
680
|
+
onResponse(pooledWorkerID, requestID) {
|
|
681
|
+
if (!pool.get(pooledWorkerID))
|
|
682
|
+
return;
|
|
683
|
+
release(pooledWorkerID, requestID ?? lastRequestId.get(pooledWorkerID));
|
|
753
684
|
},
|
|
754
685
|
// Check if worker is pooled
|
|
755
686
|
isPooled(workerID) {
|
|
756
|
-
return
|
|
687
|
+
return pool.get(workerID) !== undefined;
|
|
757
688
|
},
|
|
689
|
+
stats,
|
|
758
690
|
subscribe: bus.forward("worker.started", "worker.stopped", "worker.exited", "worker.stdout", "worker.reused"),
|
|
759
691
|
/**
|
|
760
|
-
*
|
|
761
|
-
*
|
|
762
|
-
*
|
|
692
|
+
* Warm the pool by invoking a Node function with warm pings.
|
|
693
|
+
*
|
|
694
|
+
* Each ping is marked as a fan-out *child* (`__WARMER_INVOCATION__ > 1`)
|
|
695
|
+
* so the app's lambda-warmer preloads its handlers and returns instead of
|
|
696
|
+
* fanning out to `concurrency` more Lambdas, which is what turned the old
|
|
697
|
+
* 30 pings into ~900 worker creations. The count is capped at the pool
|
|
698
|
+
* size, and pings go through the normal pool path, so warmup can never
|
|
699
|
+
* hold more isolates than steady state.
|
|
763
700
|
*/
|
|
764
|
-
async triggerWarmup(count
|
|
701
|
+
async triggerWarmup(count) {
|
|
702
|
+
count = Math.min(count, POOL_SIZE);
|
|
703
|
+
if (count <= 0)
|
|
704
|
+
return { warmed: 0 };
|
|
765
705
|
const functions = useFunctions();
|
|
766
706
|
const allFunctions = functions.all;
|
|
767
707
|
// Find a nodejs function to use as the warmup target
|
|
@@ -777,88 +717,45 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
777
717
|
break;
|
|
778
718
|
}
|
|
779
719
|
if (!targetFunction) {
|
|
780
|
-
logPool("WARMUP_SKIP", {
|
|
781
|
-
reason: "no nodejs function found",
|
|
782
|
-
});
|
|
720
|
+
logPool("WARMUP_SKIP", { reason: "no nodejs function found" });
|
|
783
721
|
return { warmed: 0 };
|
|
784
722
|
}
|
|
785
723
|
const { functionName } = targetFunction;
|
|
786
|
-
logPool("WARMUP_START", {
|
|
787
|
-
count,
|
|
788
|
-
functionName,
|
|
789
|
-
});
|
|
790
|
-
// Publish warmup start event
|
|
724
|
+
logPool("WARMUP_START", { count, functionName });
|
|
791
725
|
bus.publish("warmup.start", { count });
|
|
792
726
|
const startTime = Date.now();
|
|
793
727
|
let success = 0;
|
|
794
728
|
let failed = 0;
|
|
795
729
|
let completed = 0;
|
|
796
|
-
// Use the shared AWS client
|
|
797
730
|
const { useAWSClient } = await import("../credentials.js");
|
|
798
731
|
const { LambdaClient, InvokeCommand } = await import("@aws-sdk/client-lambda");
|
|
799
732
|
const lambda = useAWSClient(LambdaClient);
|
|
800
|
-
// Helper to publish progress
|
|
801
733
|
const publishProgress = () => {
|
|
802
|
-
bus.publish("warmup.progress", {
|
|
803
|
-
completed,
|
|
804
|
-
total: count,
|
|
805
|
-
success,
|
|
806
|
-
failed,
|
|
807
|
-
});
|
|
734
|
+
bus.publish("warmup.progress", { completed, total: count, success, failed });
|
|
808
735
|
};
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
736
|
+
await Promise.all(Array.from({ length: count }, (_, i) => i).map(async (i) => {
|
|
737
|
+
try {
|
|
738
|
+
const result = await lambda.send(new InvokeCommand({
|
|
739
|
+
FunctionName: functionName,
|
|
740
|
+
InvocationType: "RequestResponse",
|
|
741
|
+
Payload: JSON.stringify({
|
|
742
|
+
ding: true,
|
|
743
|
+
__WARMER_INVOCATION__: i + 2,
|
|
744
|
+
__WARMER_CONCURRENCY__: count + 1,
|
|
745
|
+
__WARMER_CORRELATIONID__: `sst-dev-warmup-${startTime}`,
|
|
746
|
+
}),
|
|
747
|
+
}));
|
|
748
|
+
if (result.StatusCode === 200)
|
|
749
|
+
success++;
|
|
750
|
+
else
|
|
751
|
+
failed++;
|
|
822
752
|
}
|
|
823
|
-
|
|
753
|
+
catch {
|
|
824
754
|
failed++;
|
|
825
755
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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
|
-
}
|
|
756
|
+
completed++;
|
|
757
|
+
publishProgress();
|
|
758
|
+
}));
|
|
862
759
|
const elapsed = Date.now() - startTime;
|
|
863
760
|
logPool("WARMUP_DONE", {
|
|
864
761
|
success,
|
|
@@ -866,12 +763,7 @@ export const useRuntimeWorkers = lazy(async () => {
|
|
|
866
763
|
elapsedMs: elapsed,
|
|
867
764
|
avgMs: success > 0 ? Math.round(elapsed / success) : 0,
|
|
868
765
|
});
|
|
869
|
-
|
|
870
|
-
bus.publish("warmup.complete", {
|
|
871
|
-
success,
|
|
872
|
-
failed,
|
|
873
|
-
elapsedMs: elapsed,
|
|
874
|
-
});
|
|
766
|
+
bus.publish("warmup.complete", { success, failed, elapsedMs: elapsed });
|
|
875
767
|
return { warmed: success, elapsed };
|
|
876
768
|
},
|
|
877
769
|
};
|