@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
|
@@ -10,23 +10,47 @@ import { Logger } from "../logger.js";
|
|
|
10
10
|
* instead of building each handler individually. This significantly speeds up dev mode
|
|
11
11
|
* by sharing compilation work across all handlers.
|
|
12
12
|
*
|
|
13
|
-
* Detection
|
|
13
|
+
* Detection latches on: the first check that finds the bundle enables mono
|
|
14
|
+
* build for the rest of the session. It deliberately does NOT latch off, so a
|
|
15
|
+
* caller arriving before the bundle has been written cannot disable it.
|
|
14
16
|
*/
|
|
15
17
|
export const useMonoBuildConfig = lazy(() => {
|
|
16
18
|
const project = useProject();
|
|
17
19
|
const monoBundleDir = path.join(project.paths.root, ".mono-build");
|
|
18
20
|
const monoBundlePath = path.join(monoBundleDir, "index.mjs");
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Latches on true, never on false.
|
|
23
|
+
*
|
|
24
|
+
* This was a single `existsSync` at first call, cached for the session by
|
|
25
|
+
* `lazy`. Dev start deletes `.mono-build/index.mjs` and then rebuilds it, so
|
|
26
|
+
* any caller landing in that window — typically an invocation that beat the
|
|
27
|
+
* bundle to disk — pinned this to false for the whole session. Every handler
|
|
28
|
+
* then took the per-function esbuild path, which does not carry the mono
|
|
29
|
+
* build's `external` list, and failed to resolve packages the mono bundle
|
|
30
|
+
* never opens. The bundle finishing changed nothing, because the flag had
|
|
31
|
+
* already been decided, so the session stayed broken until restarted.
|
|
32
|
+
*
|
|
33
|
+
* Re-checking until it is found costs one `existsSync` per call for the few
|
|
34
|
+
* seconds before the bundle lands, and nothing afterwards.
|
|
35
|
+
*/
|
|
36
|
+
let enabled = false;
|
|
37
|
+
const isEnabled = () => {
|
|
38
|
+
if (!enabled && fsSync.existsSync(monoBundlePath)) {
|
|
39
|
+
enabled = true;
|
|
40
|
+
Logger.debug("Mono build mode enabled:", monoBundlePath);
|
|
41
|
+
}
|
|
42
|
+
return enabled;
|
|
43
|
+
};
|
|
44
|
+
isEnabled();
|
|
24
45
|
return {
|
|
25
46
|
/**
|
|
26
|
-
* Whether mono build mode is enabled
|
|
47
|
+
* Whether mono build mode is enabled. Re-checked until the bundle is
|
|
48
|
+
* found, so a check made before it was written does not stick.
|
|
27
49
|
* When true, all Node.js handlers use the shared .mono-build bundle.
|
|
28
50
|
*/
|
|
29
|
-
enabled
|
|
51
|
+
get enabled() {
|
|
52
|
+
return isEnabled();
|
|
53
|
+
},
|
|
30
54
|
/**
|
|
31
55
|
* The mono bundle directory (.mono-build)
|
|
32
56
|
*/
|
|
@@ -44,7 +68,7 @@ export const useMonoBuildConfig = lazy(() => {
|
|
|
44
68
|
* This is useful when you have a build result and need to determine its type.
|
|
45
69
|
*/
|
|
46
70
|
isMonoBuildPath(buildOut) {
|
|
47
|
-
return
|
|
71
|
+
return isEnabled() && buildOut.includes(".mono-build");
|
|
48
72
|
},
|
|
49
73
|
/**
|
|
50
74
|
* Get the pool key for a function based on mono build status.
|
|
@@ -52,7 +76,7 @@ export const useMonoBuildConfig = lazy(() => {
|
|
|
52
76
|
* For non-mono build: per-function key
|
|
53
77
|
*/
|
|
54
78
|
getPoolKey(functionID, runtime, buildOut) {
|
|
55
|
-
if (
|
|
79
|
+
if (isEnabled() && buildOut.includes(".mono-build")) {
|
|
56
80
|
return { key: `${runtime}:mono-build`, isShared: true };
|
|
57
81
|
}
|
|
58
82
|
return { key: `${runtime}:${functionID}`, isShared: false };
|
package/runtime/server.js
CHANGED
|
@@ -24,6 +24,8 @@ export const useRuntimeServer = lazy(async () => {
|
|
|
24
24
|
const app = express();
|
|
25
25
|
const workers = await useRuntimeWorkers();
|
|
26
26
|
const cfg = await useRuntimeServerConfig();
|
|
27
|
+
// A worker running several invocations at once has several /next calls
|
|
28
|
+
// outstanding, so each worker keeps a list of waiting resolvers.
|
|
27
29
|
const workersWaiting = new Map();
|
|
28
30
|
const invocationsQueued = new Map();
|
|
29
31
|
function next(workerID) {
|
|
@@ -31,17 +33,21 @@ export const useRuntimeServer = lazy(async () => {
|
|
|
31
33
|
const value = queue?.shift();
|
|
32
34
|
if (value)
|
|
33
35
|
return value;
|
|
34
|
-
return new Promise((resolve
|
|
35
|
-
workersWaiting.
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
let waiting = workersWaiting.get(workerID);
|
|
38
|
+
if (!waiting) {
|
|
39
|
+
waiting = [];
|
|
40
|
+
workersWaiting.set(workerID, waiting);
|
|
41
|
+
}
|
|
42
|
+
waiting.push(resolve);
|
|
36
43
|
});
|
|
37
44
|
}
|
|
38
45
|
// Route an invocation to a specific workerID (used by workers.ts for pooled workers)
|
|
39
46
|
function routeInvocation(targetWorkerID, invocation) {
|
|
40
47
|
const requestPath = getRequestPath(invocation.event);
|
|
41
48
|
const requestID = invocation.requestID;
|
|
42
|
-
const waiting = workersWaiting.get(targetWorkerID);
|
|
49
|
+
const waiting = workersWaiting.get(targetWorkerID)?.shift();
|
|
43
50
|
if (waiting) {
|
|
44
|
-
workersWaiting.delete(targetWorkerID);
|
|
45
51
|
logServer(`path=${requestPath} reqId=${requestID.slice(0, 8)} Worker ${targetWorkerID.slice(0, 8)} was waiting, delivering immediately`);
|
|
46
52
|
waiting(invocation);
|
|
47
53
|
return;
|
|
@@ -55,10 +61,10 @@ export const useRuntimeServer = lazy(async () => {
|
|
|
55
61
|
logServer(`path=${requestPath} reqId=${requestID.slice(0, 8)} Worker ${targetWorkerID.slice(0, 8)} not waiting, QUEUED (queueSize=${arr.length})`);
|
|
56
62
|
}
|
|
57
63
|
workers.subscribe("worker.exited", async (evt) => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
// Pooled workers are keyed by their pooled id here, not the AWS worker id
|
|
65
|
+
const id = evt.properties.pooledWorkerID ?? evt.properties.workerID;
|
|
66
|
+
workersWaiting.delete(id);
|
|
67
|
+
invocationsQueued.delete(id);
|
|
62
68
|
});
|
|
63
69
|
// Note: function.invoked routing is handled by workers.ts via routeInvocation()
|
|
64
70
|
// This ensures correct routing for both pooled and non-pooled workers
|
|
@@ -68,31 +74,26 @@ export const useRuntimeServer = lazy(async () => {
|
|
|
68
74
|
limit: "10mb",
|
|
69
75
|
}), async (req, res) => {
|
|
70
76
|
const pooledWorkerID = req.params.workerID;
|
|
71
|
-
const
|
|
72
|
-
|
|
77
|
+
const requestID = workers.getCurrentRequestID(pooledWorkerID);
|
|
78
|
+
const target = workers.resolveRequest(pooledWorkerID, requestID);
|
|
79
|
+
if (!target) {
|
|
73
80
|
res.status(404).send();
|
|
74
81
|
return;
|
|
75
82
|
}
|
|
76
|
-
// Get AWS workerID for IoT routing (if pooled)
|
|
77
|
-
const awsWorkerID = workers.isPooled(pooledWorkerID)
|
|
78
|
-
? workers.getAwsWorkerID(pooledWorkerID) || pooledWorkerID
|
|
79
|
-
: pooledWorkerID;
|
|
80
|
-
const requestID = workers.getCurrentRequestID(pooledWorkerID);
|
|
81
83
|
logEventTrace("WORKER_END", {
|
|
82
84
|
requestID: requestID || "unknown",
|
|
83
|
-
functionID:
|
|
85
|
+
functionID: target.functionID,
|
|
84
86
|
workerID: pooledWorkerID,
|
|
85
87
|
status: "error",
|
|
86
88
|
errorType: req.body?.errorType || "init_error",
|
|
87
89
|
});
|
|
88
90
|
bus.publish("function.error", {
|
|
89
91
|
requestID,
|
|
90
|
-
workerID: awsWorkerID,
|
|
91
|
-
functionID:
|
|
92
|
+
workerID: target.awsWorkerID,
|
|
93
|
+
functionID: target.functionID,
|
|
92
94
|
...req.body,
|
|
93
95
|
});
|
|
94
|
-
|
|
95
|
-
workers.onResponse(pooledWorkerID);
|
|
96
|
+
workers.onResponse(pooledWorkerID, requestID);
|
|
96
97
|
res.json("ok");
|
|
97
98
|
});
|
|
98
99
|
app.get(`/:workerID/${cfg.API_VERSION}/runtime/invocation/next`, async (req, res) => {
|
|
@@ -141,31 +142,26 @@ export const useRuntimeServer = lazy(async () => {
|
|
|
141
142
|
const requestID = req.params.awsRequestId;
|
|
142
143
|
logServer(`reqId=${requestID.slice(0, 8)} Worker ${pooledWorkerID.slice(0, 8)} posting /response`);
|
|
143
144
|
Logger.debug("Worker", pooledWorkerID, "got response", req.body);
|
|
144
|
-
const
|
|
145
|
-
if (!
|
|
145
|
+
const target = workers.resolveRequest(pooledWorkerID, requestID);
|
|
146
|
+
if (!target) {
|
|
146
147
|
logServer(`reqId=${requestID.slice(0, 8)} ERROR: Worker ${pooledWorkerID.slice(0, 8)} not found`);
|
|
147
148
|
res.status(404).send();
|
|
148
149
|
return;
|
|
149
150
|
}
|
|
150
|
-
|
|
151
|
-
const awsWorkerID = workers.isPooled(pooledWorkerID)
|
|
152
|
-
? workers.getAwsWorkerID(pooledWorkerID) || pooledWorkerID
|
|
153
|
-
: pooledWorkerID;
|
|
154
|
-
logServer(`reqId=${requestID.slice(0, 8)} Publishing function.success awsWorkerID=${awsWorkerID.slice(0, 8)}`);
|
|
151
|
+
logServer(`reqId=${requestID.slice(0, 8)} Publishing function.success awsWorkerID=${target.awsWorkerID.slice(0, 8)}`);
|
|
155
152
|
logEventTrace("WORKER_END", {
|
|
156
153
|
requestID,
|
|
157
|
-
functionID:
|
|
154
|
+
functionID: target.functionID,
|
|
158
155
|
workerID: pooledWorkerID,
|
|
159
156
|
status: "success",
|
|
160
157
|
});
|
|
161
158
|
bus.publish("function.success", {
|
|
162
|
-
workerID: awsWorkerID,
|
|
163
|
-
functionID:
|
|
159
|
+
workerID: target.awsWorkerID,
|
|
160
|
+
functionID: target.functionID,
|
|
164
161
|
requestID: requestID,
|
|
165
162
|
body: req.body,
|
|
166
163
|
});
|
|
167
|
-
|
|
168
|
-
workers.onResponse(pooledWorkerID);
|
|
164
|
+
workers.onResponse(pooledWorkerID, requestID);
|
|
169
165
|
res.status(202).send();
|
|
170
166
|
});
|
|
171
167
|
app.all(`/proxy*`, express.raw({
|
|
@@ -207,33 +203,28 @@ export const useRuntimeServer = lazy(async () => {
|
|
|
207
203
|
limit: "10mb",
|
|
208
204
|
}), (req, res) => {
|
|
209
205
|
const pooledWorkerID = req.params.workerID;
|
|
210
|
-
const
|
|
211
|
-
|
|
206
|
+
const requestID = req.params.awsRequestId;
|
|
207
|
+
const target = workers.resolveRequest(pooledWorkerID, requestID);
|
|
208
|
+
if (!target) {
|
|
212
209
|
res.status(404).send();
|
|
213
210
|
return;
|
|
214
211
|
}
|
|
215
|
-
// Get AWS workerID for IoT routing (if pooled)
|
|
216
|
-
const awsWorkerID = workers.isPooled(pooledWorkerID)
|
|
217
|
-
? workers.getAwsWorkerID(pooledWorkerID) || pooledWorkerID
|
|
218
|
-
: pooledWorkerID;
|
|
219
|
-
const requestID = req.params.awsRequestId;
|
|
220
212
|
logEventTrace("WORKER_END", {
|
|
221
213
|
requestID,
|
|
222
|
-
functionID:
|
|
214
|
+
functionID: target.functionID,
|
|
223
215
|
workerID: pooledWorkerID,
|
|
224
216
|
status: "error",
|
|
225
217
|
errorType: req.body.errorType,
|
|
226
218
|
});
|
|
227
219
|
bus.publish("function.error", {
|
|
228
|
-
workerID: awsWorkerID,
|
|
229
|
-
functionID:
|
|
220
|
+
workerID: target.awsWorkerID,
|
|
221
|
+
functionID: target.functionID,
|
|
230
222
|
errorType: req.body.errorType,
|
|
231
223
|
errorMessage: req.body.errorMessage,
|
|
232
224
|
requestID,
|
|
233
225
|
trace: req.body.trace,
|
|
234
226
|
});
|
|
235
|
-
|
|
236
|
-
workers.onResponse(pooledWorkerID);
|
|
227
|
+
workers.onResponse(pooledWorkerID, requestID);
|
|
237
228
|
res.status(202).send();
|
|
238
229
|
});
|
|
239
230
|
app.listen(cfg.port);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When one worker runs several invocations at once, its stdout is a single
|
|
3
|
+
* stream. The worker runtime prefixes each console line with the request it
|
|
4
|
+
* belongs to, and the parent splits the stream back out here.
|
|
5
|
+
*
|
|
6
|
+
* Shared by `support/nodejs-runtime` (writer) and `runtime/workers.ts` (reader).
|
|
7
|
+
*/
|
|
8
|
+
export declare function tagLine(requestID: string, line: string): string;
|
|
9
|
+
export interface AttributedChunk {
|
|
10
|
+
requestID?: string;
|
|
11
|
+
text: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Split a stdout chunk into runs of consecutive lines that belong to the same
|
|
15
|
+
* request. Lines without a tag get `requestID: undefined` so the caller can
|
|
16
|
+
* fall back to whatever it knows about the worker.
|
|
17
|
+
*/
|
|
18
|
+
export declare function splitAttributed(chunk: string): AttributedChunk[];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When one worker runs several invocations at once, its stdout is a single
|
|
3
|
+
* stream. The worker runtime prefixes each console line with the request it
|
|
4
|
+
* belongs to, and the parent splits the stream back out here.
|
|
5
|
+
*
|
|
6
|
+
* Shared by `support/nodejs-runtime` (writer) and `runtime/workers.ts` (reader).
|
|
7
|
+
*/
|
|
8
|
+
const MARK = "\u001e"; // ASCII record separator, never appears in normal logs
|
|
9
|
+
export function tagLine(requestID, line) {
|
|
10
|
+
return `${MARK}${requestID}${MARK}${line}`;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Split a stdout chunk into runs of consecutive lines that belong to the same
|
|
14
|
+
* request. Lines without a tag get `requestID: undefined` so the caller can
|
|
15
|
+
* fall back to whatever it knows about the worker.
|
|
16
|
+
*/
|
|
17
|
+
export function splitAttributed(chunk) {
|
|
18
|
+
const out = [];
|
|
19
|
+
const lines = chunk.split("\n");
|
|
20
|
+
// A trailing newline yields an empty last element; drop it so we do not
|
|
21
|
+
// emit an empty untagged chunk.
|
|
22
|
+
if (lines.length > 1 && lines[lines.length - 1] === "")
|
|
23
|
+
lines.pop();
|
|
24
|
+
for (const raw of lines) {
|
|
25
|
+
let requestID;
|
|
26
|
+
let text = raw;
|
|
27
|
+
if (raw.startsWith(MARK)) {
|
|
28
|
+
const end = raw.indexOf(MARK, 1);
|
|
29
|
+
if (end > 1) {
|
|
30
|
+
requestID = raw.slice(1, end);
|
|
31
|
+
text = raw.slice(end + 1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const last = out[out.length - 1];
|
|
35
|
+
if (last && last.requestID === requestID) {
|
|
36
|
+
last.text += "\n" + text;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
out.push({ requestID, text });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-mode worker knobs. Every value is overridable through the environment;
|
|
3
|
+
* the defaults are sized so a laptop running `sst dev` stays responsive.
|
|
4
|
+
*/
|
|
5
|
+
/** Max live workers per pool key (one key for all mono-build Node functions). */
|
|
6
|
+
export declare const POOL_SIZE: number;
|
|
7
|
+
/** How long an idle worker is kept before it is terminated. */
|
|
8
|
+
export declare const IDLE_TIMEOUT: number;
|
|
9
|
+
/** Invocations one Node worker may run at the same time. */
|
|
10
|
+
export declare const WORKER_CONCURRENCY: number;
|
|
11
|
+
/** Warm pings sent at dev start. 0 disables warmup. Capped at POOL_SIZE. */
|
|
12
|
+
export declare const WARMUP_COUNT: number;
|
|
13
|
+
/** V8 old-space cap for each Node worker thread, in MB. 0 leaves it unbounded. */
|
|
14
|
+
export declare const WORKER_MAX_HEAP_MB: number;
|
|
15
|
+
/** Run Node workers with --enable-source-maps (costs memory per worker). */
|
|
16
|
+
export declare const SOURCE_MAPS: boolean;
|
|
17
|
+
/** Sample process and worker memory to .sst/memory.log. */
|
|
18
|
+
export declare const DEBUG_MEMORY: boolean;
|
|
19
|
+
export declare const BUILD_CONCURRENCY: number;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-mode worker knobs. Every value is overridable through the environment;
|
|
3
|
+
* the defaults are sized so a laptop running `sst dev` stays responsive.
|
|
4
|
+
*/
|
|
5
|
+
function int(name, fallback) {
|
|
6
|
+
const raw = process.env[name];
|
|
7
|
+
if (raw === undefined || raw === "")
|
|
8
|
+
return fallback;
|
|
9
|
+
const value = parseInt(raw, 10);
|
|
10
|
+
return Number.isFinite(value) ? value : fallback;
|
|
11
|
+
}
|
|
12
|
+
/** Max live workers per pool key (one key for all mono-build Node functions). */
|
|
13
|
+
export const POOL_SIZE = int("SST_WORKER_POOL_SIZE", 4);
|
|
14
|
+
/** How long an idle worker is kept before it is terminated. */
|
|
15
|
+
export const IDLE_TIMEOUT = int("SST_WORKER_IDLE_TIMEOUT", 5 * 60 * 1000);
|
|
16
|
+
/** Invocations one Node worker may run at the same time. */
|
|
17
|
+
export const WORKER_CONCURRENCY = Math.max(1, int("SST_WORKER_CONCURRENCY", 5));
|
|
18
|
+
/** Warm pings sent at dev start. 0 disables warmup. Capped at POOL_SIZE. */
|
|
19
|
+
export const WARMUP_COUNT = Math.min(POOL_SIZE, Math.max(0, int("SST_WARMUP_COUNT", POOL_SIZE)));
|
|
20
|
+
/** V8 old-space cap for each Node worker thread, in MB. 0 leaves it unbounded. */
|
|
21
|
+
export const WORKER_MAX_HEAP_MB = int("SST_WORKER_MAX_HEAP_MB", 1024);
|
|
22
|
+
/** Run Node workers with --enable-source-maps (costs memory per worker). */
|
|
23
|
+
export const SOURCE_MAPS = process.env.SST_SOURCE_MAPS === "true";
|
|
24
|
+
/** Sample process and worker memory to .sst/memory.log. */
|
|
25
|
+
export const DEBUG_MEMORY = process.env.SST_DEBUG_MEMORY === "true";
|
|
26
|
+
export const BUILD_CONCURRENCY = int("SST_BUILD_CONCURRENCY", 4);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
1
|
+
import { IDLE_TIMEOUT, POOL_SIZE } from "./worker-config.js";
|
|
2
|
+
export { POOL_SIZE, IDLE_TIMEOUT };
|
|
3
3
|
/**
|
|
4
4
|
* Set the function name resolver callback
|
|
5
5
|
* Called by workers.ts to provide access to useFunctions()
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { createDebugFileLogger } from "./debug-file-logger.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
export const IDLE_TIMEOUT = parseInt(process.env.SST_WORKER_IDLE_TIMEOUT || "60000", 10);
|
|
2
|
+
import { BUILD_CONCURRENCY as SST_BUILD_CONCURRENCY, IDLE_TIMEOUT, POOL_SIZE, WORKER_CONCURRENCY, } from "./worker-config.js";
|
|
3
|
+
import { memorySummary } from "./memory-logging.js";
|
|
4
|
+
export { POOL_SIZE, IDLE_TIMEOUT };
|
|
6
5
|
const DEBUG_POOL = process.env.SST_DEBUG_POOL === "true";
|
|
7
6
|
const DEBUG_POOL_FILE = process.env.SST_DEBUG_POOL_FILE || ".sst/worker-pool.log";
|
|
8
7
|
const INVOKE_TRACE_FILE = ".sst/invoke-trace.log";
|
|
@@ -24,7 +23,7 @@ function getPoolLogger() {
|
|
|
24
23
|
poolLogger = createDebugFileLogger({
|
|
25
24
|
filePath: DEBUG_POOL_FILE,
|
|
26
25
|
sessionName: "POOL",
|
|
27
|
-
sessionHeader: `POOL_SIZE=${POOL_SIZE} IDLE_TIMEOUT=${IDLE_TIMEOUT}ms BUILD_CONCURRENCY=${SST_BUILD_CONCURRENCY}`,
|
|
26
|
+
sessionHeader: `POOL_SIZE=${POOL_SIZE} IDLE_TIMEOUT=${IDLE_TIMEOUT}ms CONCURRENCY=${WORKER_CONCURRENCY} BUILD_CONCURRENCY=${SST_BUILD_CONCURRENCY}`,
|
|
28
27
|
width: 100,
|
|
29
28
|
});
|
|
30
29
|
}
|
|
@@ -158,6 +157,7 @@ export function writeSessionEndSummary() {
|
|
|
158
157
|
const funcName = getFunctionName(funcID);
|
|
159
158
|
summary += ` ${funcName.padEnd(25)} total=${total} poolHits=${hits} coldStarts=${cold} peakConcurrent=${peak} hitRate=${hitRate}%\n`;
|
|
160
159
|
}
|
|
160
|
+
summary += memorySummary();
|
|
161
161
|
logger.close(summary);
|
|
162
162
|
}
|
|
163
163
|
// Invocation trace logging - uses the abstract logger
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bookkeeping for pooled dev workers.
|
|
3
|
+
*
|
|
4
|
+
* A pool key groups workers that can serve the same invocations: one key per
|
|
5
|
+
* function normally, one shared key for every Node function in mono-build
|
|
6
|
+
* mode. Each worker may hold up to `maxConcurrency` invocations at once, and a
|
|
7
|
+
* key never holds more than `maxWorkers` live (non-stale) workers, so the
|
|
8
|
+
* memory a key can consume is bounded by `maxWorkers` isolates.
|
|
9
|
+
*
|
|
10
|
+
* This module decides *which* worker serves a request and when a worker is
|
|
11
|
+
* retired. It never starts or stops threads itself: `onTerminate` is the hook
|
|
12
|
+
* the owner uses to actually stop the worker.
|
|
13
|
+
*/
|
|
14
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
15
|
+
export interface PoolWorker {
|
|
16
|
+
id: string;
|
|
17
|
+
poolKey: string;
|
|
18
|
+
/** Function of the most recent invocation (shared pools serve many). */
|
|
19
|
+
functionID: string;
|
|
20
|
+
runtime: string;
|
|
21
|
+
inFlight: number;
|
|
22
|
+
maxConcurrency: number;
|
|
23
|
+
/** A stale worker finishes what it holds, then terminates. */
|
|
24
|
+
stale: boolean;
|
|
25
|
+
createdAt: number;
|
|
26
|
+
bundlePath: string;
|
|
27
|
+
bundleMtime?: number;
|
|
28
|
+
isSharedPool: boolean;
|
|
29
|
+
idleTimer?: NodeJS.Timeout;
|
|
30
|
+
}
|
|
31
|
+
export type TerminateReason = "idle_timeout" | "stale_mtime" | "rebuild" | "mono-rebuild" | "stale";
|
|
32
|
+
export interface WorkerPoolOptions {
|
|
33
|
+
maxWorkers: number;
|
|
34
|
+
idleTimeoutMs: number;
|
|
35
|
+
onTerminate: (worker: PoolWorker, reason: TerminateReason) => void;
|
|
36
|
+
}
|
|
37
|
+
export interface PoolStats {
|
|
38
|
+
workers: number;
|
|
39
|
+
idle: number;
|
|
40
|
+
inFlight: number;
|
|
41
|
+
}
|
|
42
|
+
export declare class WorkerPool {
|
|
43
|
+
private readonly opts;
|
|
44
|
+
private readonly pools;
|
|
45
|
+
private readonly byID;
|
|
46
|
+
constructor(opts: WorkerPoolOptions);
|
|
47
|
+
add(worker: PoolWorker): void;
|
|
48
|
+
get(id: string): PoolWorker | undefined;
|
|
49
|
+
workersFor(poolKey: string): readonly PoolWorker[];
|
|
50
|
+
all(): PoolWorker[];
|
|
51
|
+
/** Live workers count toward the cap; stale ones are on their way out. */
|
|
52
|
+
liveCount(poolKey: string): number;
|
|
53
|
+
canCreate(poolKey: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Least-loaded live worker with spare capacity, or undefined. Workers whose
|
|
56
|
+
* bundle is older than `currentMtime` are retired on the way: idle ones now,
|
|
57
|
+
* busy ones once they drain.
|
|
58
|
+
*/
|
|
59
|
+
pick(poolKey: string, currentMtime?: number): PoolWorker | undefined;
|
|
60
|
+
/** Hand an invocation to a worker. */
|
|
61
|
+
checkout(worker: PoolWorker, functionID: string): void;
|
|
62
|
+
/**
|
|
63
|
+
* An invocation finished. Returns what became of the worker: it is gone
|
|
64
|
+
* (stale and drained), idle (timer armed), or still busy.
|
|
65
|
+
*/
|
|
66
|
+
checkin(worker: PoolWorker): "terminated" | "idle" | "busy";
|
|
67
|
+
/**
|
|
68
|
+
* The code behind `poolKey` changed. Idle workers go now; busy ones are
|
|
69
|
+
* marked stale and go when they drain. Returns the ids of workers that
|
|
70
|
+
* were only marked.
|
|
71
|
+
*/
|
|
72
|
+
invalidate(poolKey: string, reason: "rebuild" | "mono-rebuild"): string[];
|
|
73
|
+
/** Forget a worker that exited on its own (crash, OOM, self-timeout). */
|
|
74
|
+
remove(worker: PoolWorker): void;
|
|
75
|
+
stats(): PoolStats;
|
|
76
|
+
private retire;
|
|
77
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bookkeeping for pooled dev workers.
|
|
3
|
+
*
|
|
4
|
+
* A pool key groups workers that can serve the same invocations: one key per
|
|
5
|
+
* function normally, one shared key for every Node function in mono-build
|
|
6
|
+
* mode. Each worker may hold up to `maxConcurrency` invocations at once, and a
|
|
7
|
+
* key never holds more than `maxWorkers` live (non-stale) workers, so the
|
|
8
|
+
* memory a key can consume is bounded by `maxWorkers` isolates.
|
|
9
|
+
*
|
|
10
|
+
* This module decides *which* worker serves a request and when a worker is
|
|
11
|
+
* retired. It never starts or stops threads itself: `onTerminate` is the hook
|
|
12
|
+
* the owner uses to actually stop the worker.
|
|
13
|
+
*/
|
|
14
|
+
export class WorkerPool {
|
|
15
|
+
opts;
|
|
16
|
+
pools = new Map();
|
|
17
|
+
byID = new Map();
|
|
18
|
+
constructor(opts) {
|
|
19
|
+
this.opts = opts;
|
|
20
|
+
}
|
|
21
|
+
add(worker) {
|
|
22
|
+
let pool = this.pools.get(worker.poolKey);
|
|
23
|
+
if (!pool) {
|
|
24
|
+
pool = [];
|
|
25
|
+
this.pools.set(worker.poolKey, pool);
|
|
26
|
+
}
|
|
27
|
+
pool.push(worker);
|
|
28
|
+
this.byID.set(worker.id, worker);
|
|
29
|
+
}
|
|
30
|
+
get(id) {
|
|
31
|
+
return this.byID.get(id);
|
|
32
|
+
}
|
|
33
|
+
workersFor(poolKey) {
|
|
34
|
+
return this.pools.get(poolKey) ?? [];
|
|
35
|
+
}
|
|
36
|
+
all() {
|
|
37
|
+
return [...this.byID.values()];
|
|
38
|
+
}
|
|
39
|
+
/** Live workers count toward the cap; stale ones are on their way out. */
|
|
40
|
+
liveCount(poolKey) {
|
|
41
|
+
return this.workersFor(poolKey).filter((w) => !w.stale).length;
|
|
42
|
+
}
|
|
43
|
+
canCreate(poolKey) {
|
|
44
|
+
return this.liveCount(poolKey) < this.opts.maxWorkers;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Least-loaded live worker with spare capacity, or undefined. Workers whose
|
|
48
|
+
* bundle is older than `currentMtime` are retired on the way: idle ones now,
|
|
49
|
+
* busy ones once they drain.
|
|
50
|
+
*/
|
|
51
|
+
pick(poolKey, currentMtime) {
|
|
52
|
+
const pool = this.pools.get(poolKey);
|
|
53
|
+
if (!pool || pool.length === 0)
|
|
54
|
+
return undefined;
|
|
55
|
+
for (const worker of [...pool]) {
|
|
56
|
+
if (currentMtime &&
|
|
57
|
+
worker.bundleMtime &&
|
|
58
|
+
currentMtime > worker.bundleMtime &&
|
|
59
|
+
!worker.stale) {
|
|
60
|
+
this.retire(worker, "stale_mtime");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
let best;
|
|
64
|
+
for (const worker of pool) {
|
|
65
|
+
if (worker.stale)
|
|
66
|
+
continue;
|
|
67
|
+
if (worker.inFlight >= worker.maxConcurrency)
|
|
68
|
+
continue;
|
|
69
|
+
if (!best || worker.inFlight < best.inFlight)
|
|
70
|
+
best = worker;
|
|
71
|
+
}
|
|
72
|
+
return best;
|
|
73
|
+
}
|
|
74
|
+
/** Hand an invocation to a worker. */
|
|
75
|
+
checkout(worker, functionID) {
|
|
76
|
+
if (worker.idleTimer) {
|
|
77
|
+
clearTimeout(worker.idleTimer);
|
|
78
|
+
worker.idleTimer = undefined;
|
|
79
|
+
}
|
|
80
|
+
worker.inFlight += 1;
|
|
81
|
+
worker.functionID = functionID;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* An invocation finished. Returns what became of the worker: it is gone
|
|
85
|
+
* (stale and drained), idle (timer armed), or still busy.
|
|
86
|
+
*/
|
|
87
|
+
checkin(worker) {
|
|
88
|
+
if (worker.inFlight > 0)
|
|
89
|
+
worker.inFlight -= 1;
|
|
90
|
+
if (worker.inFlight > 0)
|
|
91
|
+
return "busy";
|
|
92
|
+
if (worker.stale) {
|
|
93
|
+
this.remove(worker);
|
|
94
|
+
this.opts.onTerminate(worker, "stale");
|
|
95
|
+
return "terminated";
|
|
96
|
+
}
|
|
97
|
+
worker.idleTimer = setTimeout(() => {
|
|
98
|
+
worker.idleTimer = undefined;
|
|
99
|
+
if (worker.inFlight > 0)
|
|
100
|
+
return;
|
|
101
|
+
if (!this.byID.has(worker.id))
|
|
102
|
+
return;
|
|
103
|
+
this.remove(worker);
|
|
104
|
+
this.opts.onTerminate(worker, "idle_timeout");
|
|
105
|
+
}, this.opts.idleTimeoutMs);
|
|
106
|
+
return "idle";
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The code behind `poolKey` changed. Idle workers go now; busy ones are
|
|
110
|
+
* marked stale and go when they drain. Returns the ids of workers that
|
|
111
|
+
* were only marked.
|
|
112
|
+
*/
|
|
113
|
+
invalidate(poolKey, reason) {
|
|
114
|
+
const marked = [];
|
|
115
|
+
for (const worker of [...this.workersFor(poolKey)]) {
|
|
116
|
+
if (worker.inFlight === 0) {
|
|
117
|
+
this.remove(worker);
|
|
118
|
+
this.opts.onTerminate(worker, reason);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
worker.stale = true;
|
|
122
|
+
marked.push(worker.id);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return marked;
|
|
126
|
+
}
|
|
127
|
+
/** Forget a worker that exited on its own (crash, OOM, self-timeout). */
|
|
128
|
+
remove(worker) {
|
|
129
|
+
if (worker.idleTimer) {
|
|
130
|
+
clearTimeout(worker.idleTimer);
|
|
131
|
+
worker.idleTimer = undefined;
|
|
132
|
+
}
|
|
133
|
+
this.byID.delete(worker.id);
|
|
134
|
+
const pool = this.pools.get(worker.poolKey);
|
|
135
|
+
if (!pool)
|
|
136
|
+
return;
|
|
137
|
+
const idx = pool.indexOf(worker);
|
|
138
|
+
if (idx >= 0)
|
|
139
|
+
pool.splice(idx, 1);
|
|
140
|
+
if (pool.length === 0)
|
|
141
|
+
this.pools.delete(worker.poolKey);
|
|
142
|
+
}
|
|
143
|
+
stats() {
|
|
144
|
+
let idle = 0;
|
|
145
|
+
let inFlight = 0;
|
|
146
|
+
for (const worker of this.byID.values()) {
|
|
147
|
+
if (worker.inFlight === 0)
|
|
148
|
+
idle += 1;
|
|
149
|
+
inFlight += worker.inFlight;
|
|
150
|
+
}
|
|
151
|
+
return { workers: this.byID.size, idle, inFlight };
|
|
152
|
+
}
|
|
153
|
+
retire(worker, reason) {
|
|
154
|
+
if (worker.inFlight === 0) {
|
|
155
|
+
this.remove(worker);
|
|
156
|
+
this.opts.onTerminate(worker, reason);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
worker.stale = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|