@open-mercato/cli 0.6.1-develop.3167.1.7aedd7b2ba → 0.6.1-develop.3179.1.9ee0441adc
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/.turbo/turbo-build.log +1 -1
- package/dist/lib/auto-spawn-workers.js +37 -0
- package/dist/lib/auto-spawn-workers.js.map +7 -0
- package/dist/lib/queue-worker-supervisor.js +222 -0
- package/dist/lib/queue-worker-supervisor.js.map +7 -0
- package/dist/mercato.js +56 -6
- package/dist/mercato.js.map +2 -2
- package/jest.config.cjs +3 -0
- package/package.json +5 -5
- package/src/__tests__/mercato.test.ts +136 -0
- package/src/lib/__tests__/auto-spawn-workers.test.ts +101 -0
- package/src/lib/__tests__/queue-worker-supervisor.test.ts +285 -0
- package/src/lib/auto-spawn-workers.ts +39 -0
- package/src/lib/queue-worker-supervisor.ts +327 -0
- package/src/mercato.ts +58 -6
package/.turbo/turbo-build.log
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
|
|
2
|
+
const DEFAULT_LAZY_POLL_MS = 1e3;
|
|
3
|
+
const MIN_LAZY_POLL_MS = 250;
|
|
4
|
+
function resolveAutoSpawnWorkersEnabled(env = process.env) {
|
|
5
|
+
const legacy = parseBooleanToken(env.AUTO_SPAWN_WORKERS);
|
|
6
|
+
if (legacy !== null) return legacy;
|
|
7
|
+
const aliased = parseBooleanToken(env.OM_AUTO_SPAWN_WORKERS);
|
|
8
|
+
if (aliased !== null) return aliased;
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
function resolveAutoSpawnWorkersLazy(env = process.env) {
|
|
12
|
+
return parseBooleanToken(env.OM_AUTO_SPAWN_WORKERS_LAZY) === true;
|
|
13
|
+
}
|
|
14
|
+
function resolveAutoSpawnWorkersMode(env = process.env) {
|
|
15
|
+
if (!resolveAutoSpawnWorkersEnabled(env)) return "off";
|
|
16
|
+
if (resolveAutoSpawnWorkersLazy(env)) return "lazy";
|
|
17
|
+
return "eager";
|
|
18
|
+
}
|
|
19
|
+
function resolveLazyPollMs(env = process.env) {
|
|
20
|
+
const raw = env.OM_AUTO_SPAWN_WORKERS_LAZY_POLL_MS;
|
|
21
|
+
if (typeof raw !== "string" || raw.trim() === "") return DEFAULT_LAZY_POLL_MS;
|
|
22
|
+
const parsed = Number(raw);
|
|
23
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_LAZY_POLL_MS;
|
|
24
|
+
return Math.max(MIN_LAZY_POLL_MS, Math.floor(parsed));
|
|
25
|
+
}
|
|
26
|
+
function resolveLazyRestart(env = process.env) {
|
|
27
|
+
const parsed = parseBooleanToken(env.OM_AUTO_SPAWN_WORKERS_LAZY_RESTART);
|
|
28
|
+
return parsed === null ? true : parsed;
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
resolveAutoSpawnWorkersEnabled,
|
|
32
|
+
resolveAutoSpawnWorkersLazy,
|
|
33
|
+
resolveAutoSpawnWorkersMode,
|
|
34
|
+
resolveLazyPollMs,
|
|
35
|
+
resolveLazyRestart
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=auto-spawn-workers.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/lib/auto-spawn-workers.ts"],
|
|
4
|
+
"sourcesContent": ["import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\nexport type AutoSpawnWorkersMode = 'off' | 'eager' | 'lazy'\n\nexport type AutoSpawnEnvSource = Pick<NodeJS.ProcessEnv, string> | Record<string, string | undefined>\n\nconst DEFAULT_LAZY_POLL_MS = 1000\nconst MIN_LAZY_POLL_MS = 250\n\nexport function resolveAutoSpawnWorkersEnabled(env: AutoSpawnEnvSource = process.env): boolean {\n const legacy = parseBooleanToken(env.AUTO_SPAWN_WORKERS)\n if (legacy !== null) return legacy\n const aliased = parseBooleanToken(env.OM_AUTO_SPAWN_WORKERS)\n if (aliased !== null) return aliased\n return true\n}\n\nexport function resolveAutoSpawnWorkersLazy(env: AutoSpawnEnvSource = process.env): boolean {\n return parseBooleanToken(env.OM_AUTO_SPAWN_WORKERS_LAZY) === true\n}\n\nexport function resolveAutoSpawnWorkersMode(env: AutoSpawnEnvSource = process.env): AutoSpawnWorkersMode {\n if (!resolveAutoSpawnWorkersEnabled(env)) return 'off'\n if (resolveAutoSpawnWorkersLazy(env)) return 'lazy'\n return 'eager'\n}\n\nexport function resolveLazyPollMs(env: AutoSpawnEnvSource = process.env): number {\n const raw = env.OM_AUTO_SPAWN_WORKERS_LAZY_POLL_MS\n if (typeof raw !== 'string' || raw.trim() === '') return DEFAULT_LAZY_POLL_MS\n const parsed = Number(raw)\n if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_LAZY_POLL_MS\n return Math.max(MIN_LAZY_POLL_MS, Math.floor(parsed))\n}\n\nexport function resolveLazyRestart(env: AutoSpawnEnvSource = process.env): boolean {\n const parsed = parseBooleanToken(env.OM_AUTO_SPAWN_WORKERS_LAZY_RESTART)\n return parsed === null ? true : parsed\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,yBAAyB;AAMlC,MAAM,uBAAuB;AAC7B,MAAM,mBAAmB;AAElB,SAAS,+BAA+B,MAA0B,QAAQ,KAAc;AAC7F,QAAM,SAAS,kBAAkB,IAAI,kBAAkB;AACvD,MAAI,WAAW,KAAM,QAAO;AAC5B,QAAM,UAAU,kBAAkB,IAAI,qBAAqB;AAC3D,MAAI,YAAY,KAAM,QAAO;AAC7B,SAAO;AACT;AAEO,SAAS,4BAA4B,MAA0B,QAAQ,KAAc;AAC1F,SAAO,kBAAkB,IAAI,0BAA0B,MAAM;AAC/D;AAEO,SAAS,4BAA4B,MAA0B,QAAQ,KAA2B;AACvG,MAAI,CAAC,+BAA+B,GAAG,EAAG,QAAO;AACjD,MAAI,4BAA4B,GAAG,EAAG,QAAO;AAC7C,SAAO;AACT;AAEO,SAAS,kBAAkB,MAA0B,QAAQ,KAAa;AAC/E,QAAM,MAAM,IAAI;AAChB,MAAI,OAAO,QAAQ,YAAY,IAAI,KAAK,MAAM,GAAI,QAAO;AACzD,QAAM,SAAS,OAAO,GAAG;AACzB,MAAI,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,EAAG,QAAO;AACpD,SAAO,KAAK,IAAI,kBAAkB,KAAK,MAAM,MAAM,CAAC;AACtD;AAEO,SAAS,mBAAmB,MAA0B,QAAQ,KAAc;AACjF,QAAM,SAAS,kBAAkB,IAAI,kCAAkC;AACvE,SAAO,WAAW,OAAO,OAAO;AAClC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
getQueuePendingProbe
|
|
4
|
+
} from "@open-mercato/queue";
|
|
5
|
+
function groupWorkersByQueue(workers) {
|
|
6
|
+
const groups = /* @__PURE__ */ new Map();
|
|
7
|
+
for (const worker of workers) {
|
|
8
|
+
const existing = groups.get(worker.queue);
|
|
9
|
+
if (existing) {
|
|
10
|
+
existing.workerCount += 1;
|
|
11
|
+
if (worker.concurrency > existing.concurrency) {
|
|
12
|
+
existing.concurrency = worker.concurrency;
|
|
13
|
+
}
|
|
14
|
+
} else {
|
|
15
|
+
groups.set(worker.queue, {
|
|
16
|
+
queueName: worker.queue,
|
|
17
|
+
concurrency: Math.max(worker.concurrency, 1),
|
|
18
|
+
workerCount: 1
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return [...groups.values()].sort((a, b) => a.queueName.localeCompare(b.queueName));
|
|
23
|
+
}
|
|
24
|
+
function resolveStrategyFromEnv(env) {
|
|
25
|
+
return env.QUEUE_STRATEGY === "async" ? "async" : "local";
|
|
26
|
+
}
|
|
27
|
+
const PROBE_ERROR_LOG_THROTTLE_MS = 6e4;
|
|
28
|
+
function startLazyWorkerSupervisor(options) {
|
|
29
|
+
const logger = options.logger ?? console;
|
|
30
|
+
const strategy = options.strategy ?? resolveStrategyFromEnv(options.runtimeEnv);
|
|
31
|
+
const probe = options.probeFn ?? ((queueName) => getQueuePendingProbe(queueName, strategy));
|
|
32
|
+
const groups = groupWorkersByQueue(options.workers);
|
|
33
|
+
const startedQueues = /* @__PURE__ */ new Set();
|
|
34
|
+
const activeChildren = /* @__PURE__ */ new Map();
|
|
35
|
+
const startingQueues = /* @__PURE__ */ new Set();
|
|
36
|
+
const probeErrorLastLoggedAt = /* @__PURE__ */ new Map();
|
|
37
|
+
let stopping = false;
|
|
38
|
+
let pollTimer = null;
|
|
39
|
+
let watchPromiseResolve = null;
|
|
40
|
+
const done = new Promise((resolve) => {
|
|
41
|
+
watchPromiseResolve = resolve;
|
|
42
|
+
});
|
|
43
|
+
if (groups.length === 0) {
|
|
44
|
+
logger.warn(
|
|
45
|
+
"[lazy-supervisor] No queues discovered. Idle until queues are registered or run `yarn generate`."
|
|
46
|
+
);
|
|
47
|
+
} else {
|
|
48
|
+
logger.log(
|
|
49
|
+
`[lazy-supervisor] Watching ${groups.length} queue(s): ${groups.map((g) => g.queueName).join(", ")}`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const spawnFn = options.spawnFn ?? ((command, args, opts) => nodeSpawn(command, args, opts));
|
|
53
|
+
function logProbeError(queueName, message) {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
const lastAt = probeErrorLastLoggedAt.get(queueName) ?? 0;
|
|
56
|
+
if (now - lastAt < PROBE_ERROR_LOG_THROTTLE_MS) return;
|
|
57
|
+
probeErrorLastLoggedAt.set(queueName, now);
|
|
58
|
+
logger.warn(`[lazy-supervisor] Probe failed for queue "${queueName}": ${message}`);
|
|
59
|
+
}
|
|
60
|
+
function spawnQueueWorker(queueName) {
|
|
61
|
+
if (stopping) return;
|
|
62
|
+
if (activeChildren.has(queueName)) return;
|
|
63
|
+
if (startingQueues.has(queueName)) return;
|
|
64
|
+
startingQueues.add(queueName);
|
|
65
|
+
logger.log(`[lazy-supervisor] Pending job detected \u2014 starting worker for queue "${queueName}"`);
|
|
66
|
+
let child;
|
|
67
|
+
try {
|
|
68
|
+
child = spawnFn(
|
|
69
|
+
"node",
|
|
70
|
+
[options.mercatoBin, "queue", "worker", queueName],
|
|
71
|
+
{
|
|
72
|
+
stdio: "inherit",
|
|
73
|
+
env: options.runtimeEnv,
|
|
74
|
+
cwd: options.appDir
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
startingQueues.delete(queueName);
|
|
79
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
80
|
+
logger.error(`[lazy-supervisor] Failed to spawn worker for "${queueName}": ${message}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
activeChildren.set(queueName, child);
|
|
84
|
+
startedQueues.add(queueName);
|
|
85
|
+
startingQueues.delete(queueName);
|
|
86
|
+
if (options.onSpawn) {
|
|
87
|
+
try {
|
|
88
|
+
options.onSpawn(queueName, child);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
91
|
+
logger.warn(`[lazy-supervisor] onSpawn callback threw for "${queueName}": ${message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
child.on("exit", (code, signal) => {
|
|
95
|
+
activeChildren.delete(queueName);
|
|
96
|
+
if (options.onChildExit) {
|
|
97
|
+
try {
|
|
98
|
+
options.onChildExit(queueName, code, signal);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
101
|
+
logger.warn(`[lazy-supervisor] onChildExit callback threw for "${queueName}": ${message}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (stopping) return;
|
|
105
|
+
const expected = signal === "SIGTERM" || signal === "SIGINT";
|
|
106
|
+
if (expected) return;
|
|
107
|
+
const reason = code !== null ? `code ${code}` : `signal ${signal ?? "unknown"}`;
|
|
108
|
+
logger.warn(`[lazy-supervisor] Worker for "${queueName}" exited (${reason}).`);
|
|
109
|
+
if (!options.restartOnUnexpectedExit) return;
|
|
110
|
+
void (async () => {
|
|
111
|
+
const result = await safeProbe(queueName);
|
|
112
|
+
if (!result || result.error) return;
|
|
113
|
+
if (result.ready > 0) {
|
|
114
|
+
logger.warn(`[lazy-supervisor] Restarting worker for "${queueName}" because jobs remain pending.`);
|
|
115
|
+
spawnQueueWorker(queueName);
|
|
116
|
+
}
|
|
117
|
+
})();
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async function safeProbe(queueName) {
|
|
121
|
+
try {
|
|
122
|
+
return await probe(queueName, strategy);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
125
|
+
logProbeError(queueName, message);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function tickOnce() {
|
|
130
|
+
if (stopping) return;
|
|
131
|
+
if (groups.length === 0) return;
|
|
132
|
+
const toCheck = groups.filter((g) => !activeChildren.has(g.queueName) && !startingQueues.has(g.queueName));
|
|
133
|
+
if (toCheck.length === 0) return;
|
|
134
|
+
const probes = await Promise.all(
|
|
135
|
+
toCheck.map(async (group) => ({
|
|
136
|
+
group,
|
|
137
|
+
result: await safeProbe(group.queueName)
|
|
138
|
+
}))
|
|
139
|
+
);
|
|
140
|
+
if (stopping) return;
|
|
141
|
+
for (const { group, result } of probes) {
|
|
142
|
+
if (!result) continue;
|
|
143
|
+
if (result.error) {
|
|
144
|
+
if (result.errorMessage) {
|
|
145
|
+
logProbeError(group.queueName, result.errorMessage);
|
|
146
|
+
}
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (result.ready > 0) {
|
|
150
|
+
spawnQueueWorker(group.queueName);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function scheduleNext() {
|
|
155
|
+
if (stopping) return;
|
|
156
|
+
pollTimer = setTimeout(() => {
|
|
157
|
+
void (async () => {
|
|
158
|
+
try {
|
|
159
|
+
await tickOnce();
|
|
160
|
+
} catch (err) {
|
|
161
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
162
|
+
logger.warn(`[lazy-supervisor] Poll cycle failed: ${message}`);
|
|
163
|
+
} finally {
|
|
164
|
+
if (!stopping) scheduleNext();
|
|
165
|
+
}
|
|
166
|
+
})();
|
|
167
|
+
}, options.pollMs);
|
|
168
|
+
pollTimer.unref?.();
|
|
169
|
+
}
|
|
170
|
+
void (async () => {
|
|
171
|
+
try {
|
|
172
|
+
await tickOnce();
|
|
173
|
+
} catch (err) {
|
|
174
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
175
|
+
logger.warn(`[lazy-supervisor] Initial poll failed: ${message}`);
|
|
176
|
+
} finally {
|
|
177
|
+
scheduleNext();
|
|
178
|
+
}
|
|
179
|
+
})();
|
|
180
|
+
async function close() {
|
|
181
|
+
if (stopping) {
|
|
182
|
+
await done;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
stopping = true;
|
|
186
|
+
if (pollTimer) {
|
|
187
|
+
clearTimeout(pollTimer);
|
|
188
|
+
pollTimer = null;
|
|
189
|
+
}
|
|
190
|
+
const exitWaiters = [];
|
|
191
|
+
for (const [queueName, child] of activeChildren) {
|
|
192
|
+
exitWaiters.push(
|
|
193
|
+
new Promise((resolve) => {
|
|
194
|
+
if (child.exitCode !== null || child.signalCode !== null) return resolve();
|
|
195
|
+
child.once("exit", () => resolve());
|
|
196
|
+
})
|
|
197
|
+
);
|
|
198
|
+
try {
|
|
199
|
+
if (!child.killed) child.kill("SIGTERM");
|
|
200
|
+
} catch (err) {
|
|
201
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
202
|
+
logger.warn(`[lazy-supervisor] Failed to send SIGTERM to "${queueName}": ${message}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
await Promise.all(exitWaiters);
|
|
206
|
+
activeChildren.clear();
|
|
207
|
+
if (watchPromiseResolve) {
|
|
208
|
+
watchPromiseResolve();
|
|
209
|
+
watchPromiseResolve = null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
startedQueues,
|
|
214
|
+
getActiveChild: (queueName) => activeChildren.get(queueName),
|
|
215
|
+
close,
|
|
216
|
+
done
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
export {
|
|
220
|
+
startLazyWorkerSupervisor
|
|
221
|
+
};
|
|
222
|
+
//# sourceMappingURL=queue-worker-supervisor.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/lib/queue-worker-supervisor.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Lazy queue worker supervisor.\n *\n * Replaces `mercato queue worker --all` with a lightweight watcher that\n * starts a per-queue worker process only after the first ready job appears\n * on that queue. Discovered queues with no jobs stay completely idle.\n *\n * Design:\n * - Probes every queue on a configurable interval using\n * `getQueuePendingProbe()` from `@open-mercato/queue`.\n * - The first non-error probe with `ready > 0` triggers `spawn('node', [\n * mercatoBin, 'queue', 'worker', queueName ])`.\n * - Each spawned child inherits stdio so its logs flow through the\n * surrounding dev runtime.\n * - On unexpected exit while jobs are still pending, the supervisor\n * restarts the worker if `restartOnUnexpectedExit` is enabled.\n * - On `close()`, all active children receive SIGTERM and the supervisor\n * waits for them to exit before resolving.\n *\n * Probes never invoke handlers, never create BullMQ Workers, and never\n * import generated worker handler modules. The contract is preserved by\n * `getQueuePendingProbe()`.\n */\n\nimport { spawn as nodeSpawn } from 'node:child_process'\nimport type { ChildProcess, SpawnOptions } from 'node:child_process'\nimport {\n getQueuePendingProbe,\n type QueuePendingProbeResult,\n type QueueStrategyType,\n} from '@open-mercato/queue'\nimport type { ModuleWorker } from '@open-mercato/shared/modules/registry'\n\nexport type LazySupervisorSpawnFn = (\n command: string,\n args: readonly string[],\n options: SpawnOptions,\n) => ChildProcess\n\nexport type LazySupervisorProbeFn = (\n queueName: string,\n strategy: QueueStrategyType,\n) => Promise<QueuePendingProbeResult>\n\nexport type LazyWorkerSupervisorOptions = {\n mercatoBin: string\n appDir: string\n runtimeEnv: NodeJS.ProcessEnv\n workers: ModuleWorker[]\n pollMs: number\n restartOnUnexpectedExit: boolean\n strategy?: QueueStrategyType\n /** Override for tests. Defaults to `child_process.spawn`. */\n spawnFn?: LazySupervisorSpawnFn\n /** Override for tests. Defaults to `getQueuePendingProbe`. */\n probeFn?: LazySupervisorProbeFn\n /** Logger override. Defaults to `console`. */\n logger?: Pick<Console, 'log' | 'warn' | 'error'>\n onSpawn?: (queueName: string, child: ChildProcess) => void\n onChildExit?: (\n queueName: string,\n code: number | null,\n signal: NodeJS.Signals | null,\n ) => void\n}\n\nexport type LazyWorkerSupervisorHandle = {\n /** Set of queue names whose worker has started at least once. */\n readonly startedQueues: ReadonlySet<string>\n /** Currently running child process per queue (if any). */\n getActiveChild(queueName: string): ChildProcess | undefined\n /** Stop polling, kill children, wait for exit. Idempotent. */\n close(): Promise<void>\n /** Resolves when the watcher loop has stopped (after `close()`). */\n done: Promise<void>\n}\n\ntype QueueGroup = {\n queueName: string\n concurrency: number\n workerCount: number\n}\n\nfunction groupWorkersByQueue(workers: ModuleWorker[]): QueueGroup[] {\n const groups = new Map<string, QueueGroup>()\n for (const worker of workers) {\n const existing = groups.get(worker.queue)\n if (existing) {\n existing.workerCount += 1\n if (worker.concurrency > existing.concurrency) {\n existing.concurrency = worker.concurrency\n }\n } else {\n groups.set(worker.queue, {\n queueName: worker.queue,\n concurrency: Math.max(worker.concurrency, 1),\n workerCount: 1,\n })\n }\n }\n return [...groups.values()].sort((a, b) => a.queueName.localeCompare(b.queueName))\n}\n\nfunction resolveStrategyFromEnv(env: NodeJS.ProcessEnv): QueueStrategyType {\n return env.QUEUE_STRATEGY === 'async' ? 'async' : 'local'\n}\n\nconst PROBE_ERROR_LOG_THROTTLE_MS = 60_000\n\nexport function startLazyWorkerSupervisor(\n options: LazyWorkerSupervisorOptions,\n): LazyWorkerSupervisorHandle {\n const logger = options.logger ?? console\n const strategy: QueueStrategyType = options.strategy ?? resolveStrategyFromEnv(options.runtimeEnv)\n const probe: LazySupervisorProbeFn = options.probeFn\n ?? ((queueName) => getQueuePendingProbe(queueName, strategy))\n const groups = groupWorkersByQueue(options.workers)\n\n const startedQueues = new Set<string>()\n const activeChildren = new Map<string, ChildProcess>()\n const startingQueues = new Set<string>()\n const probeErrorLastLoggedAt = new Map<string, number>()\n\n let stopping = false\n let pollTimer: ReturnType<typeof setTimeout> | null = null\n let watchPromiseResolve: (() => void) | null = null\n const done = new Promise<void>((resolve) => {\n watchPromiseResolve = resolve\n })\n\n if (groups.length === 0) {\n logger.warn(\n '[lazy-supervisor] No queues discovered. Idle until queues are registered or run `yarn generate`.',\n )\n } else {\n logger.log(\n `[lazy-supervisor] Watching ${groups.length} queue(s): ${groups\n .map((g) => g.queueName)\n .join(', ')}`,\n )\n }\n\n const spawnFn: LazySupervisorSpawnFn =\n options.spawnFn ?? ((command, args, opts) => nodeSpawn(command, args as string[], opts))\n\n function logProbeError(queueName: string, message: string): void {\n const now = Date.now()\n const lastAt = probeErrorLastLoggedAt.get(queueName) ?? 0\n if (now - lastAt < PROBE_ERROR_LOG_THROTTLE_MS) return\n probeErrorLastLoggedAt.set(queueName, now)\n logger.warn(`[lazy-supervisor] Probe failed for queue \"${queueName}\": ${message}`)\n }\n\n function spawnQueueWorker(queueName: string): void {\n if (stopping) return\n if (activeChildren.has(queueName)) return\n if (startingQueues.has(queueName)) return\n startingQueues.add(queueName)\n\n logger.log(`[lazy-supervisor] Pending job detected \u2014 starting worker for queue \"${queueName}\"`)\n let child: ChildProcess\n try {\n child = spawnFn(\n 'node',\n [options.mercatoBin, 'queue', 'worker', queueName],\n {\n stdio: 'inherit',\n env: options.runtimeEnv,\n cwd: options.appDir,\n },\n )\n } catch (err) {\n startingQueues.delete(queueName)\n const message = err instanceof Error ? err.message : String(err)\n logger.error(`[lazy-supervisor] Failed to spawn worker for \"${queueName}\": ${message}`)\n return\n }\n\n activeChildren.set(queueName, child)\n startedQueues.add(queueName)\n startingQueues.delete(queueName)\n if (options.onSpawn) {\n try {\n options.onSpawn(queueName, child)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n logger.warn(`[lazy-supervisor] onSpawn callback threw for \"${queueName}\": ${message}`)\n }\n }\n\n child.on('exit', (code, signal) => {\n activeChildren.delete(queueName)\n if (options.onChildExit) {\n try {\n options.onChildExit(queueName, code, signal)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n logger.warn(`[lazy-supervisor] onChildExit callback threw for \"${queueName}\": ${message}`)\n }\n }\n if (stopping) return\n const expected = signal === 'SIGTERM' || signal === 'SIGINT'\n if (expected) return\n const reason = code !== null ? `code ${code}` : `signal ${signal ?? 'unknown'}`\n logger.warn(`[lazy-supervisor] Worker for \"${queueName}\" exited (${reason}).`)\n if (!options.restartOnUnexpectedExit) return\n void (async () => {\n const result = await safeProbe(queueName)\n if (!result || result.error) return\n if (result.ready > 0) {\n logger.warn(`[lazy-supervisor] Restarting worker for \"${queueName}\" because jobs remain pending.`)\n spawnQueueWorker(queueName)\n }\n })()\n })\n }\n\n async function safeProbe(queueName: string): Promise<QueuePendingProbeResult | null> {\n try {\n return await probe(queueName, strategy)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n logProbeError(queueName, message)\n return null\n }\n }\n\n async function tickOnce(): Promise<void> {\n if (stopping) return\n if (groups.length === 0) return\n\n const toCheck = groups.filter((g) => !activeChildren.has(g.queueName) && !startingQueues.has(g.queueName))\n if (toCheck.length === 0) return\n\n const probes = await Promise.all(\n toCheck.map(async (group) => ({\n group,\n result: await safeProbe(group.queueName),\n })),\n )\n\n if (stopping) return\n\n for (const { group, result } of probes) {\n if (!result) continue\n if (result.error) {\n if (result.errorMessage) {\n logProbeError(group.queueName, result.errorMessage)\n }\n continue\n }\n if (result.ready > 0) {\n spawnQueueWorker(group.queueName)\n }\n }\n }\n\n function scheduleNext(): void {\n if (stopping) return\n pollTimer = setTimeout(() => {\n void (async () => {\n try {\n await tickOnce()\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n logger.warn(`[lazy-supervisor] Poll cycle failed: ${message}`)\n } finally {\n if (!stopping) scheduleNext()\n }\n })()\n }, options.pollMs)\n pollTimer.unref?.()\n }\n\n // Kick off immediately so the first probe doesn't wait one full poll interval.\n void (async () => {\n try {\n await tickOnce()\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n logger.warn(`[lazy-supervisor] Initial poll failed: ${message}`)\n } finally {\n scheduleNext()\n }\n })()\n\n async function close(): Promise<void> {\n if (stopping) {\n await done\n return\n }\n stopping = true\n if (pollTimer) {\n clearTimeout(pollTimer)\n pollTimer = null\n }\n\n const exitWaiters: Promise<void>[] = []\n for (const [queueName, child] of activeChildren) {\n exitWaiters.push(\n new Promise<void>((resolve) => {\n if (child.exitCode !== null || child.signalCode !== null) return resolve()\n child.once('exit', () => resolve())\n }),\n )\n try {\n if (!child.killed) child.kill('SIGTERM')\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n logger.warn(`[lazy-supervisor] Failed to send SIGTERM to \"${queueName}\": ${message}`)\n }\n }\n await Promise.all(exitWaiters)\n activeChildren.clear()\n if (watchPromiseResolve) {\n watchPromiseResolve()\n watchPromiseResolve = null\n }\n }\n\n return {\n startedQueues,\n getActiveChild: (queueName) => activeChildren.get(queueName),\n close,\n done,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/mercato.js
CHANGED
|
@@ -4,6 +4,12 @@ import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
|
|
|
4
4
|
import { getSslConfig } from "@open-mercato/shared/lib/db/ssl";
|
|
5
5
|
import { getRedisUrl, getRedisUrlOrThrow } from "@open-mercato/shared/lib/redis/connection";
|
|
6
6
|
import { resolveInitDerivedSecrets } from "./lib/init-secrets.js";
|
|
7
|
+
import {
|
|
8
|
+
resolveAutoSpawnWorkersMode,
|
|
9
|
+
resolveLazyPollMs,
|
|
10
|
+
resolveLazyRestart
|
|
11
|
+
} from "./lib/auto-spawn-workers.js";
|
|
12
|
+
import { startLazyWorkerSupervisor } from "./lib/queue-worker-supervisor.js";
|
|
7
13
|
import { parseModuleInstallArgs } from "./lib/module-install-args.js";
|
|
8
14
|
import { resolveNextBuildIdCandidate } from "./lib/next-build-id.js";
|
|
9
15
|
import { acquireServerStartLock } from "./lib/server-start-lock.js";
|
|
@@ -1382,6 +1388,7 @@ async function run(argv = process.argv) {
|
|
|
1382
1388
|
let didRetryCorruptedTurbopackCache = false;
|
|
1383
1389
|
let stopping = false;
|
|
1384
1390
|
let envChangePromiseResolve = null;
|
|
1391
|
+
let activeLazySupervisor = null;
|
|
1385
1392
|
const envReloader = createDevEnvReloader(appDir, process.env, initialProcessEnvironmentEntries);
|
|
1386
1393
|
function cleanup() {
|
|
1387
1394
|
console.log("[server] Shutting down...");
|
|
@@ -1390,6 +1397,9 @@ async function run(argv = process.argv) {
|
|
|
1390
1397
|
proc.kill("SIGTERM");
|
|
1391
1398
|
}
|
|
1392
1399
|
}
|
|
1400
|
+
if (activeLazySupervisor) {
|
|
1401
|
+
void activeLazySupervisor.close().catch(() => void 0);
|
|
1402
|
+
}
|
|
1393
1403
|
}
|
|
1394
1404
|
async function cleanupAndWait() {
|
|
1395
1405
|
cleanup();
|
|
@@ -1401,6 +1411,13 @@ async function run(argv = process.argv) {
|
|
|
1401
1411
|
})
|
|
1402
1412
|
)
|
|
1403
1413
|
);
|
|
1414
|
+
if (activeLazySupervisor) {
|
|
1415
|
+
try {
|
|
1416
|
+
await activeLazySupervisor.close();
|
|
1417
|
+
} catch {
|
|
1418
|
+
}
|
|
1419
|
+
activeLazySupervisor = null;
|
|
1420
|
+
}
|
|
1404
1421
|
const lockFile = path.join(appDir, ".mercato", "next", "dev", "lock");
|
|
1405
1422
|
try {
|
|
1406
1423
|
fs.unlinkSync(lockFile);
|
|
@@ -1474,7 +1491,7 @@ async function run(argv = process.argv) {
|
|
|
1474
1491
|
while (!stopping) {
|
|
1475
1492
|
envReloader.reload();
|
|
1476
1493
|
const runtimeEnv = buildServerProcessEnvironment(process.env);
|
|
1477
|
-
const
|
|
1494
|
+
const autoSpawnWorkersMode = resolveAutoSpawnWorkersMode(process.env);
|
|
1478
1495
|
const autoSpawnScheduler = process.env.AUTO_SPAWN_SCHEDULER !== "false";
|
|
1479
1496
|
const queueStrategy = process.env.QUEUE_STRATEGY || "local";
|
|
1480
1497
|
const schedulerCommand = lookupModuleCommand(getCliModules(), "scheduler", "start");
|
|
@@ -1482,10 +1499,21 @@ async function run(argv = process.argv) {
|
|
|
1482
1499
|
startNextDev(runtimeEnv),
|
|
1483
1500
|
waitForEnvChange()
|
|
1484
1501
|
];
|
|
1485
|
-
if (
|
|
1486
|
-
const
|
|
1502
|
+
if (autoSpawnWorkersMode !== "off") {
|
|
1503
|
+
const discoveredWorkers = getRegisteredCliWorkers();
|
|
1504
|
+
const discoveredWorkerQueues = [...new Set(discoveredWorkers.map((worker) => worker.queue))];
|
|
1487
1505
|
if (discoveredWorkerQueues.length === 0) {
|
|
1488
1506
|
console.error("[server] AUTO_SPAWN_WORKERS is enabled, but no queues were discovered from CLI modules. Run `yarn generate` and verify `.mercato/generated/modules.cli.generated.ts` contains worker entries. Continuing without auto-spawned workers.");
|
|
1507
|
+
} else if (autoSpawnWorkersMode === "lazy") {
|
|
1508
|
+
console.log(`[server] Lazy worker auto-spawn enabled \u2014 workers will start on first job (${discoveredWorkerQueues.length} queue(s) watched).`);
|
|
1509
|
+
activeLazySupervisor = startLazyWorkerSupervisor({
|
|
1510
|
+
mercatoBin,
|
|
1511
|
+
appDir,
|
|
1512
|
+
runtimeEnv,
|
|
1513
|
+
workers: discoveredWorkers,
|
|
1514
|
+
pollMs: resolveLazyPollMs(process.env),
|
|
1515
|
+
restartOnUnexpectedExit: resolveLazyRestart(process.env)
|
|
1516
|
+
});
|
|
1489
1517
|
} else {
|
|
1490
1518
|
console.log("[server] Starting workers for all queues...");
|
|
1491
1519
|
const workerProcess = spawn("node", [mercatoBin, "queue", "worker", "--all"], {
|
|
@@ -1537,7 +1565,7 @@ async function run(argv = process.argv) {
|
|
|
1537
1565
|
const appDir = env.appDir;
|
|
1538
1566
|
const nodeModulesBases = Array.from(/* @__PURE__ */ new Set([env.rootDir, appDir]));
|
|
1539
1567
|
const processes = [];
|
|
1540
|
-
const
|
|
1568
|
+
const autoSpawnWorkersMode = resolveAutoSpawnWorkersMode(process.env);
|
|
1541
1569
|
const autoSpawnScheduler = process.env.AUTO_SPAWN_SCHEDULER !== "false";
|
|
1542
1570
|
const queueStrategy = process.env.QUEUE_STRATEGY || "local";
|
|
1543
1571
|
const runtimeEnv = buildServerProcessEnvironment(process.env);
|
|
@@ -1545,6 +1573,7 @@ async function run(argv = process.argv) {
|
|
|
1545
1573
|
const serverStartLock = acquireServerStartLock(appDir, {
|
|
1546
1574
|
port: runtimeEnv.PORT ?? process.env.PORT ?? null
|
|
1547
1575
|
});
|
|
1576
|
+
let activeLazySupervisor = null;
|
|
1548
1577
|
function cleanup() {
|
|
1549
1578
|
console.log("[server] Shutting down...");
|
|
1550
1579
|
for (const proc of processes) {
|
|
@@ -1552,6 +1581,9 @@ async function run(argv = process.argv) {
|
|
|
1552
1581
|
proc.kill("SIGTERM");
|
|
1553
1582
|
}
|
|
1554
1583
|
}
|
|
1584
|
+
if (activeLazySupervisor) {
|
|
1585
|
+
void activeLazySupervisor.close().catch(() => void 0);
|
|
1586
|
+
}
|
|
1555
1587
|
}
|
|
1556
1588
|
async function cleanupAndWait() {
|
|
1557
1589
|
cleanup();
|
|
@@ -1563,6 +1595,13 @@ async function run(argv = process.argv) {
|
|
|
1563
1595
|
})
|
|
1564
1596
|
)
|
|
1565
1597
|
);
|
|
1598
|
+
if (activeLazySupervisor) {
|
|
1599
|
+
try {
|
|
1600
|
+
await activeLazySupervisor.close();
|
|
1601
|
+
} catch {
|
|
1602
|
+
}
|
|
1603
|
+
activeLazySupervisor = null;
|
|
1604
|
+
}
|
|
1566
1605
|
}
|
|
1567
1606
|
process.on("SIGTERM", cleanup);
|
|
1568
1607
|
process.on("SIGINT", cleanup);
|
|
@@ -1580,10 +1619,21 @@ async function run(argv = process.argv) {
|
|
|
1580
1619
|
const managedExitPromises = [
|
|
1581
1620
|
waitForManagedProcessExit(nextProcess, "Next.js production server")
|
|
1582
1621
|
];
|
|
1583
|
-
if (
|
|
1584
|
-
const
|
|
1622
|
+
if (autoSpawnWorkersMode !== "off") {
|
|
1623
|
+
const discoveredWorkers = getRegisteredCliWorkers();
|
|
1624
|
+
const discoveredWorkerQueues = [...new Set(discoveredWorkers.map((worker) => worker.queue))];
|
|
1585
1625
|
if (discoveredWorkerQueues.length === 0) {
|
|
1586
1626
|
console.error("[server] AUTO_SPAWN_WORKERS is enabled, but no queues were discovered from CLI modules. Run `yarn generate` and verify `.mercato/generated/modules.cli.generated.ts` contains worker entries. Continuing without auto-spawned workers.");
|
|
1627
|
+
} else if (autoSpawnWorkersMode === "lazy") {
|
|
1628
|
+
console.log(`[server] Lazy worker auto-spawn enabled \u2014 workers will start on first job (${discoveredWorkerQueues.length} queue(s) watched).`);
|
|
1629
|
+
activeLazySupervisor = startLazyWorkerSupervisor({
|
|
1630
|
+
mercatoBin,
|
|
1631
|
+
appDir,
|
|
1632
|
+
runtimeEnv,
|
|
1633
|
+
workers: discoveredWorkers,
|
|
1634
|
+
pollMs: resolveLazyPollMs(process.env),
|
|
1635
|
+
restartOnUnexpectedExit: resolveLazyRestart(process.env)
|
|
1636
|
+
});
|
|
1587
1637
|
} else {
|
|
1588
1638
|
console.log("[server] Starting workers for all queues...");
|
|
1589
1639
|
const workerProcess = spawn("node", [mercatoBin, "queue", "worker", "--all"], {
|