@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.
Files changed (44) hide show
  1. package/cli/commands/dev.js +10 -6
  2. package/constructs/AstroSite.d.ts +1 -1
  3. package/constructs/EdgeFunction.d.ts +1 -1
  4. package/constructs/EdgeFunction.js +8 -6
  5. package/constructs/Function.d.ts +3 -2
  6. package/constructs/Function.js +2 -1
  7. package/constructs/Job.d.ts +2 -2
  8. package/constructs/Job.js +4 -2
  9. package/constructs/NextjsSite.d.ts +1 -1
  10. package/constructs/NextjsSite.js +2 -2
  11. package/constructs/RemixSite.d.ts +1 -1
  12. package/constructs/SolidStartSite.d.ts +1 -1
  13. package/constructs/SsrFunction.d.ts +2 -2
  14. package/constructs/SsrFunction.js +7 -5
  15. package/constructs/SsrSite.d.ts +2 -2
  16. package/constructs/SsrSite.js +1 -1
  17. package/constructs/Stack.d.ts +1 -1
  18. package/constructs/Stack.js +1 -1
  19. package/constructs/SvelteKitSite.d.ts +1 -1
  20. package/constructs/deprecated/NextjsSite.d.ts +3 -3
  21. package/constructs/deprecated/NextjsSite.js +4 -1
  22. package/constructs/deprecated/cross-region-helper.js +3 -3
  23. package/package.json +3 -3
  24. package/runtime/handlers/node.js +24 -2
  25. package/runtime/handlers.d.ts +4 -0
  26. package/runtime/memory-logging.d.ts +26 -0
  27. package/runtime/memory-logging.js +112 -0
  28. package/runtime/mono-build-config.d.ts +6 -3
  29. package/runtime/mono-build-config.js +34 -10
  30. package/runtime/server.js +35 -44
  31. package/runtime/stdout-attribution.d.ts +18 -0
  32. package/runtime/stdout-attribution.js +43 -0
  33. package/runtime/worker-config.d.ts +19 -0
  34. package/runtime/worker-config.js +26 -0
  35. package/runtime/worker-pool-logging.d.ts +2 -2
  36. package/runtime/worker-pool-logging.js +5 -5
  37. package/runtime/worker-pool.d.ts +77 -0
  38. package/runtime/worker-pool.js +162 -0
  39. package/runtime/workers.d.ts +33 -11
  40. package/runtime/workers.js +405 -513
  41. package/support/nodejs-runtime/index.mjs +214 -100
  42. package/watcher.js +2 -0
  43. package/README.md +0 -43
  44. 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 happens once at startup and the result is cached for the session.
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
- // Check once at startup if mono build exists
20
- const enabled = fsSync.existsSync(monoBundlePath);
21
- if (enabled) {
22
- Logger.debug("Mono build mode enabled:", monoBundlePath);
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 (detected at startup).
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 enabled && buildOut.includes(".mono-build");
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 (enabled && buildOut.includes(".mono-build")) {
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, reject) => {
35
- workersWaiting.set(workerID, resolve);
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
- const waiting = workersWaiting.get(evt.properties.workerID);
59
- if (!waiting)
60
- return;
61
- workersWaiting.delete(evt.properties.workerID);
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 worker = workers.fromID(pooledWorkerID);
72
- if (!worker) {
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: worker.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: worker.functionID,
92
+ workerID: target.awsWorkerID,
93
+ functionID: target.functionID,
92
94
  ...req.body,
93
95
  });
94
- // Return pooled worker to pool
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 worker = workers.fromID(pooledWorkerID);
145
- if (!worker) {
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
- // Get AWS workerID for IoT routing (if pooled)
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: worker.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: worker.functionID,
159
+ workerID: target.awsWorkerID,
160
+ functionID: target.functionID,
164
161
  requestID: requestID,
165
162
  body: req.body,
166
163
  });
167
- // Return pooled worker to pool
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 worker = workers.fromID(pooledWorkerID);
211
- if (!worker) {
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: worker.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: worker.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
- // Return pooled worker to pool
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
- export declare const POOL_SIZE: number;
2
- export declare const IDLE_TIMEOUT: number;
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
- // Configuration
3
- const SST_BUILD_CONCURRENCY = parseInt(process.env.SST_BUILD_CONCURRENCY || "4", 10);
4
- export const POOL_SIZE = parseInt(process.env.SST_WORKER_POOL_SIZE || "10", 10);
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
+ }