@open-mercato/cli 0.6.6-develop.6389.1.135a466c47 → 0.6.6-develop.6391.1.276b99bf26
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/agentic/guides/module-facts.json +9 -9
- package/dist/agentic/guides/modules/auth.md +1 -1
- package/dist/agentic/guides/modules/catalog.md +1 -1
- package/dist/agentic/guides/modules/currencies.md +1 -1
- package/dist/agentic/guides/modules/customer_accounts.md +1 -1
- package/dist/agentic/guides/modules/customers.md +1 -1
- package/dist/agentic/guides/modules/data_sync.md +1 -1
- package/dist/agentic/guides/modules/integrations.md +1 -1
- package/dist/agentic/guides/modules/sales.md +1 -1
- package/dist/agentic/guides/modules/workflows.md +1 -1
- package/dist/lib/auto-spawn-workers.js +8 -1
- package/dist/lib/auto-spawn-workers.js.map +2 -2
- package/dist/lib/queue-worker-supervisor.js +95 -0
- package/dist/lib/queue-worker-supervisor.js.map +2 -2
- package/dist/mercato.js +11 -4
- package/dist/mercato.js.map +2 -2
- package/package.json +5 -5
- package/src/__tests__/mercato.test.ts +13 -0
- package/src/lib/__tests__/auto-spawn-workers.test.ts +19 -0
- package/src/lib/__tests__/queue-worker-supervisor.test.ts +42 -0
- package/src/lib/auto-spawn-workers.ts +8 -0
- package/src/lib/queue-worker-supervisor.ts +105 -0
- package/src/mercato.ts +13 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Copied create-app/agentic/ → dist/agentic/
|
|
3
3
|
Discovered 7 standalone guides → dist/agentic/guides/
|
|
4
4
|
[module-facts] module registry unavailable (registryPath not provided); API route auth omitted for auth
|
|
5
|
+
Generated 9 module fact-sheets → dist/agentic/guides/modules/
|
|
5
6
|
[module-facts] module registry unavailable (registryPath not provided); API route auth omitted for catalog
|
|
6
7
|
[module-facts] module registry unavailable (registryPath not provided); API route auth omitted for currencies
|
|
7
8
|
[module-facts] module registry unavailable (registryPath not provided); API route auth omitted for customer_accounts
|
|
@@ -10,6 +11,5 @@ Discovered 7 standalone guides → dist/agentic/guides/
|
|
|
10
11
|
[module-facts] module registry unavailable (registryPath not provided); API route auth omitted for integrations
|
|
11
12
|
[module-facts] module registry unavailable (registryPath not provided); API route auth omitted for sales
|
|
12
13
|
[module-facts] module registry unavailable (registryPath not provided); API route auth omitted for workflows
|
|
13
|
-
Generated 9 module fact-sheets → dist/agentic/guides/modules/
|
|
14
14
|
Wrote 9 legacy core.<module>.md redirect stubs → dist/agentic/guides/
|
|
15
15
|
[build:cli] built successfully
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"auth": {
|
|
3
3
|
"title": "Authentication & Accounts",
|
|
4
4
|
"description": "User accounts, sessions, roles and password resets.",
|
|
5
|
-
"coreVersion": "0.6.6-develop.
|
|
5
|
+
"coreVersion": "0.6.6-develop.6391.1.276b99bf26",
|
|
6
6
|
"entities": [
|
|
7
7
|
{
|
|
8
8
|
"id": "auth:user",
|
|
@@ -191,7 +191,7 @@
|
|
|
191
191
|
"catalog": {
|
|
192
192
|
"title": "Product Catalog",
|
|
193
193
|
"description": "Configurable catalog for products, variants, and pricing used by the sales module.",
|
|
194
|
-
"coreVersion": "0.6.6-develop.
|
|
194
|
+
"coreVersion": "0.6.6-develop.6391.1.276b99bf26",
|
|
195
195
|
"entities": [
|
|
196
196
|
{
|
|
197
197
|
"id": "catalog:catalog_option_schema_template",
|
|
@@ -405,7 +405,7 @@
|
|
|
405
405
|
"currencies": {
|
|
406
406
|
"title": "Currencies",
|
|
407
407
|
"description": "Currencies and Exchange rate management",
|
|
408
|
-
"coreVersion": "0.6.6-develop.
|
|
408
|
+
"coreVersion": "0.6.6-develop.6391.1.276b99bf26",
|
|
409
409
|
"entities": [
|
|
410
410
|
{
|
|
411
411
|
"id": "currencies:currency",
|
|
@@ -489,7 +489,7 @@
|
|
|
489
489
|
"customer_accounts": {
|
|
490
490
|
"title": "Customer Identity & Portal Authentication",
|
|
491
491
|
"description": "Customer-facing authentication with two-tier identity model and full RBAC.",
|
|
492
|
-
"coreVersion": "0.6.6-develop.
|
|
492
|
+
"coreVersion": "0.6.6-develop.6391.1.276b99bf26",
|
|
493
493
|
"entities": [
|
|
494
494
|
{
|
|
495
495
|
"id": "customer_accounts:customer_user",
|
|
@@ -729,7 +729,7 @@
|
|
|
729
729
|
"customers": {
|
|
730
730
|
"title": "Customer Relationship Management",
|
|
731
731
|
"description": "Core CRM capabilities for people, companies, deals, and activities.",
|
|
732
|
-
"coreVersion": "0.6.6-develop.
|
|
732
|
+
"coreVersion": "0.6.6-develop.6391.1.276b99bf26",
|
|
733
733
|
"entities": [
|
|
734
734
|
{
|
|
735
735
|
"id": "customers:customer_entity",
|
|
@@ -1211,7 +1211,7 @@
|
|
|
1211
1211
|
"data_sync": {
|
|
1212
1212
|
"title": "Data Sync",
|
|
1213
1213
|
"description": "Streaming data sync hub for import/export integrations.",
|
|
1214
|
-
"coreVersion": "0.6.6-develop.
|
|
1214
|
+
"coreVersion": "0.6.6-develop.6391.1.276b99bf26",
|
|
1215
1215
|
"entities": [
|
|
1216
1216
|
{
|
|
1217
1217
|
"id": "data_sync:sync_run",
|
|
@@ -1289,7 +1289,7 @@
|
|
|
1289
1289
|
"integrations": {
|
|
1290
1290
|
"title": "Integrations",
|
|
1291
1291
|
"description": "Core integration framework — external ID mapping, status badges, and integration registry.",
|
|
1292
|
-
"coreVersion": "0.6.6-develop.
|
|
1292
|
+
"coreVersion": "0.6.6-develop.6391.1.276b99bf26",
|
|
1293
1293
|
"entities": [
|
|
1294
1294
|
{
|
|
1295
1295
|
"id": "integrations:sync_external_id_mapping",
|
|
@@ -1365,7 +1365,7 @@
|
|
|
1365
1365
|
"sales": {
|
|
1366
1366
|
"title": "Sales Management",
|
|
1367
1367
|
"description": "Quoting, ordering, fulfillment, and billing capabilities built on modular pricing and tax pipelines.",
|
|
1368
|
-
"coreVersion": "0.6.6-develop.
|
|
1368
|
+
"coreVersion": "0.6.6-develop.6391.1.276b99bf26",
|
|
1369
1369
|
"entities": [
|
|
1370
1370
|
{
|
|
1371
1371
|
"id": "sales:sales_channel",
|
|
@@ -1838,7 +1838,7 @@
|
|
|
1838
1838
|
"workflows": {
|
|
1839
1839
|
"title": "Workflow Engine",
|
|
1840
1840
|
"description": "Orchestrate business processes with state machines, transitions, and activities",
|
|
1841
|
-
"coreVersion": "0.6.6-develop.
|
|
1841
|
+
"coreVersion": "0.6.6-develop.6391.1.276b99bf26",
|
|
1842
1842
|
"entities": [
|
|
1843
1843
|
{
|
|
1844
1844
|
"id": "workflows:workflow_definition",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# customer_accounts — module facts (generated, do not edit)
|
|
2
|
-
<!-- generated from @open-mercato/core 0.6.6-develop.
|
|
2
|
+
<!-- generated from @open-mercato/core 0.6.6-develop.6391.1.276b99bf26 — R1 staleness stamp -->
|
|
3
3
|
|
|
4
4
|
## Entities
|
|
5
5
|
|
|
@@ -23,6 +23,12 @@ function resolveLazyPollMs(env = process.env) {
|
|
|
23
23
|
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_LAZY_POLL_MS;
|
|
24
24
|
return Math.max(MIN_LAZY_POLL_MS, Math.floor(parsed));
|
|
25
25
|
}
|
|
26
|
+
function resolveLazySpawnMode(env = process.env) {
|
|
27
|
+
const raw = env.OM_AUTO_SPAWN_WORKERS_LAZY_MODE?.trim().toLowerCase();
|
|
28
|
+
if (raw === "shared") return "shared";
|
|
29
|
+
if (raw === "per-queue") return "per-queue";
|
|
30
|
+
return "per-queue";
|
|
31
|
+
}
|
|
26
32
|
function resolveLazyRestart(env = process.env) {
|
|
27
33
|
const parsed = parseBooleanToken(env.OM_AUTO_SPAWN_WORKERS_LAZY_RESTART);
|
|
28
34
|
return parsed === null ? true : parsed;
|
|
@@ -32,6 +38,7 @@ export {
|
|
|
32
38
|
resolveAutoSpawnWorkersLazy,
|
|
33
39
|
resolveAutoSpawnWorkersMode,
|
|
34
40
|
resolveLazyPollMs,
|
|
35
|
-
resolveLazyRestart
|
|
41
|
+
resolveLazyRestart,
|
|
42
|
+
resolveLazySpawnMode
|
|
36
43
|
};
|
|
37
44
|
//# sourceMappingURL=auto-spawn-workers.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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;
|
|
4
|
+
"sourcesContent": ["import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\nexport type AutoSpawnWorkersMode = 'off' | 'eager' | 'lazy'\nexport type LazyWorkerSpawnMode = 'per-queue' | 'shared'\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 resolveLazySpawnMode(env: AutoSpawnEnvSource = process.env): LazyWorkerSpawnMode {\n const raw = env.OM_AUTO_SPAWN_WORKERS_LAZY_MODE?.trim().toLowerCase()\n if (raw === 'shared') return 'shared'\n if (raw === 'per-queue') return 'per-queue'\n return 'per-queue'\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;AAOlC,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,qBAAqB,MAA0B,QAAQ,KAA0B;AAC/F,QAAM,MAAM,IAAI,iCAAiC,KAAK,EAAE,YAAY;AACpE,MAAI,QAAQ,SAAU,QAAO;AAC7B,MAAI,QAAQ,YAAa,QAAO;AAChC,SAAO;AACT;AAEO,SAAS,mBAAmB,MAA0B,QAAQ,KAAc;AACjF,QAAM,SAAS,kBAAkB,IAAI,kCAAkC;AACvE,SAAO,WAAW,OAAO,OAAO;AAClC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -28,11 +28,14 @@ const PROBE_ERROR_LOG_THROTTLE_MS = 6e4;
|
|
|
28
28
|
function startLazyWorkerSupervisor(options) {
|
|
29
29
|
const logger = options.logger ?? console;
|
|
30
30
|
const strategy = options.strategy ?? resolveStrategyFromEnv(options.runtimeEnv);
|
|
31
|
+
const spawnMode = options.spawnMode ?? "per-queue";
|
|
31
32
|
const probe = options.probeFn ?? ((queueName) => getQueuePendingProbe(queueName, strategy));
|
|
32
33
|
const groups = groupWorkersByQueue(options.workers);
|
|
33
34
|
const startedQueues = /* @__PURE__ */ new Set();
|
|
34
35
|
const activeChildren = /* @__PURE__ */ new Map();
|
|
35
36
|
const startingQueues = /* @__PURE__ */ new Set();
|
|
37
|
+
let activeSharedChild;
|
|
38
|
+
let startingSharedWorker = false;
|
|
36
39
|
const probeErrorLastLoggedAt = /* @__PURE__ */ new Map();
|
|
37
40
|
let stopping = false;
|
|
38
41
|
let pollTimer = null;
|
|
@@ -50,6 +53,20 @@ function startLazyWorkerSupervisor(options) {
|
|
|
50
53
|
);
|
|
51
54
|
}
|
|
52
55
|
const spawnFn = options.spawnFn ?? ((command, args, opts) => nodeSpawn(command, args, opts));
|
|
56
|
+
const markSharedWorkerStarted = () => {
|
|
57
|
+
for (const group of groups) {
|
|
58
|
+
startedQueues.add(group.queueName);
|
|
59
|
+
activeChildren.set(group.queueName, activeSharedChild);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const clearSharedWorker = () => {
|
|
63
|
+
for (const group of groups) {
|
|
64
|
+
if (activeChildren.get(group.queueName) === activeSharedChild) {
|
|
65
|
+
activeChildren.delete(group.queueName);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
activeSharedChild = void 0;
|
|
69
|
+
};
|
|
53
70
|
function logProbeError(queueName, message) {
|
|
54
71
|
const now = Date.now();
|
|
55
72
|
const lastAt = probeErrorLastLoggedAt.get(queueName) ?? 0;
|
|
@@ -117,6 +134,75 @@ function startLazyWorkerSupervisor(options) {
|
|
|
117
134
|
})();
|
|
118
135
|
});
|
|
119
136
|
}
|
|
137
|
+
function spawnSharedWorker(triggerQueueNames) {
|
|
138
|
+
if (stopping) return;
|
|
139
|
+
if (activeSharedChild) return;
|
|
140
|
+
if (startingSharedWorker) return;
|
|
141
|
+
startingSharedWorker = true;
|
|
142
|
+
const triggerSummary = triggerQueueNames.length > 0 ? ` after pending job(s) on ${triggerQueueNames.join(", ")}` : "";
|
|
143
|
+
logger.log(`[lazy-supervisor] Pending job detected${triggerSummary} \u2014 starting shared worker for all queues`);
|
|
144
|
+
let child;
|
|
145
|
+
try {
|
|
146
|
+
child = spawnFn(
|
|
147
|
+
"node",
|
|
148
|
+
[options.mercatoBin, "queue", "worker", "--all"],
|
|
149
|
+
{
|
|
150
|
+
stdio: "inherit",
|
|
151
|
+
env: options.runtimeEnv,
|
|
152
|
+
cwd: options.appDir
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
startingSharedWorker = false;
|
|
157
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
158
|
+
logger.error(`[lazy-supervisor] Failed to spawn shared worker: ${message}`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
activeSharedChild = child;
|
|
162
|
+
markSharedWorkerStarted();
|
|
163
|
+
startingSharedWorker = false;
|
|
164
|
+
for (const group of groups) {
|
|
165
|
+
if (options.onSpawn) {
|
|
166
|
+
try {
|
|
167
|
+
options.onSpawn(group.queueName, child);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
170
|
+
logger.warn(`[lazy-supervisor] onSpawn callback threw for "${group.queueName}": ${message}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
child.on("exit", (code, signal) => {
|
|
175
|
+
clearSharedWorker();
|
|
176
|
+
for (const group of groups) {
|
|
177
|
+
if (options.onChildExit) {
|
|
178
|
+
try {
|
|
179
|
+
options.onChildExit(group.queueName, code, signal);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
182
|
+
logger.warn(`[lazy-supervisor] onChildExit callback threw for "${group.queueName}": ${message}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (stopping) return;
|
|
187
|
+
const expected = signal === "SIGTERM" || signal === "SIGINT";
|
|
188
|
+
if (expected) return;
|
|
189
|
+
const reason = code !== null ? `code ${code}` : `signal ${signal ?? "unknown"}`;
|
|
190
|
+
logger.warn(`[lazy-supervisor] Shared worker exited (${reason}).`);
|
|
191
|
+
if (!options.restartOnUnexpectedExit) return;
|
|
192
|
+
void (async () => {
|
|
193
|
+
const readyQueues = [];
|
|
194
|
+
for (const group of groups) {
|
|
195
|
+
const result = await safeProbe(group.queueName);
|
|
196
|
+
if (!result || result.error) continue;
|
|
197
|
+
if (result.ready > 0) readyQueues.push(group.queueName);
|
|
198
|
+
}
|
|
199
|
+
if (readyQueues.length > 0) {
|
|
200
|
+
logger.warn("[lazy-supervisor] Restarting shared worker because jobs remain pending.");
|
|
201
|
+
spawnSharedWorker(readyQueues);
|
|
202
|
+
}
|
|
203
|
+
})();
|
|
204
|
+
});
|
|
205
|
+
}
|
|
120
206
|
async function safeProbe(queueName) {
|
|
121
207
|
try {
|
|
122
208
|
return await probe(queueName, strategy);
|
|
@@ -129,6 +215,7 @@ function startLazyWorkerSupervisor(options) {
|
|
|
129
215
|
async function tickOnce() {
|
|
130
216
|
if (stopping) return;
|
|
131
217
|
if (groups.length === 0) return;
|
|
218
|
+
if (spawnMode === "shared" && (activeSharedChild || startingSharedWorker)) return;
|
|
132
219
|
const toCheck = groups.filter((g) => !activeChildren.has(g.queueName) && !startingQueues.has(g.queueName));
|
|
133
220
|
if (toCheck.length === 0) return;
|
|
134
221
|
const probes = await Promise.all(
|
|
@@ -138,6 +225,7 @@ function startLazyWorkerSupervisor(options) {
|
|
|
138
225
|
}))
|
|
139
226
|
);
|
|
140
227
|
if (stopping) return;
|
|
228
|
+
const readySharedQueues = [];
|
|
141
229
|
for (const { group, result } of probes) {
|
|
142
230
|
if (!result) continue;
|
|
143
231
|
if (result.error) {
|
|
@@ -147,9 +235,16 @@ function startLazyWorkerSupervisor(options) {
|
|
|
147
235
|
continue;
|
|
148
236
|
}
|
|
149
237
|
if (result.ready > 0) {
|
|
238
|
+
if (spawnMode === "shared") {
|
|
239
|
+
readySharedQueues.push(group.queueName);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
150
242
|
spawnQueueWorker(group.queueName);
|
|
151
243
|
}
|
|
152
244
|
}
|
|
245
|
+
if (spawnMode === "shared" && readySharedQueues.length > 0) {
|
|
246
|
+
spawnSharedWorker(readySharedQueues);
|
|
247
|
+
}
|
|
153
248
|
}
|
|
154
249
|
function scheduleNext() {
|
|
155
250
|
if (stopping) return;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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": "AAwBA,SAAS,SAAS,iBAAiB;AAEnC;AAAA,EACE;AAAA,OAGK;
|
|
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'\nimport type { LazyWorkerSpawnMode } from './auto-spawn-workers'\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 spawnMode?: LazyWorkerSpawnMode\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 spawnMode: LazyWorkerSpawnMode = options.spawnMode ?? 'per-queue'\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 let activeSharedChild: ChildProcess | undefined\n let startingSharedWorker = false\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 const markSharedWorkerStarted = () => {\n for (const group of groups) {\n startedQueues.add(group.queueName)\n activeChildren.set(group.queueName, activeSharedChild as ChildProcess)\n }\n }\n\n const clearSharedWorker = () => {\n for (const group of groups) {\n if (activeChildren.get(group.queueName) === activeSharedChild) {\n activeChildren.delete(group.queueName)\n }\n }\n activeSharedChild = undefined\n }\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 function spawnSharedWorker(triggerQueueNames: string[]): void {\n if (stopping) return\n if (activeSharedChild) return\n if (startingSharedWorker) return\n startingSharedWorker = true\n\n const triggerSummary = triggerQueueNames.length > 0\n ? ` after pending job(s) on ${triggerQueueNames.join(', ')}`\n : ''\n logger.log(`[lazy-supervisor] Pending job detected${triggerSummary} \u2014 starting shared worker for all queues`)\n let child: ChildProcess\n try {\n child = spawnFn(\n 'node',\n [options.mercatoBin, 'queue', 'worker', '--all'],\n {\n stdio: 'inherit',\n env: options.runtimeEnv,\n cwd: options.appDir,\n },\n )\n } catch (err) {\n startingSharedWorker = false\n const message = err instanceof Error ? err.message : String(err)\n logger.error(`[lazy-supervisor] Failed to spawn shared worker: ${message}`)\n return\n }\n\n activeSharedChild = child\n markSharedWorkerStarted()\n startingSharedWorker = false\n for (const group of groups) {\n if (options.onSpawn) {\n try {\n options.onSpawn(group.queueName, child)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n logger.warn(`[lazy-supervisor] onSpawn callback threw for \"${group.queueName}\": ${message}`)\n }\n }\n }\n\n child.on('exit', (code, signal) => {\n clearSharedWorker()\n for (const group of groups) {\n if (options.onChildExit) {\n try {\n options.onChildExit(group.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 \"${group.queueName}\": ${message}`)\n }\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] Shared worker exited (${reason}).`)\n if (!options.restartOnUnexpectedExit) return\n void (async () => {\n const readyQueues: string[] = []\n for (const group of groups) {\n const result = await safeProbe(group.queueName)\n if (!result || result.error) continue\n if (result.ready > 0) readyQueues.push(group.queueName)\n }\n if (readyQueues.length > 0) {\n logger.warn('[lazy-supervisor] Restarting shared worker because jobs remain pending.')\n spawnSharedWorker(readyQueues)\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 if (spawnMode === 'shared' && (activeSharedChild || startingSharedWorker)) 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 const readySharedQueues: string[] = []\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 if (spawnMode === 'shared') {\n readySharedQueues.push(group.queueName)\n continue\n }\n spawnQueueWorker(group.queueName)\n }\n }\n if (spawnMode === 'shared' && readySharedQueues.length > 0) {\n spawnSharedWorker(readySharedQueues)\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
6
|
"names": []
|
|
7
7
|
}
|
package/dist/mercato.js
CHANGED
|
@@ -6,6 +6,7 @@ import { getRedisUrl, getRedisUrlOrThrow } from "@open-mercato/shared/lib/redis/
|
|
|
6
6
|
import { resolveInitDerivedSecrets } from "./lib/init-secrets.js";
|
|
7
7
|
import {
|
|
8
8
|
resolveAutoSpawnWorkersMode,
|
|
9
|
+
resolveLazySpawnMode,
|
|
9
10
|
resolveLazyPollMs,
|
|
10
11
|
resolveLazyRestart
|
|
11
12
|
} from "./lib/auto-spawn-workers.js";
|
|
@@ -1723,14 +1724,17 @@ async function run(argv = process.argv) {
|
|
|
1723
1724
|
if (discoveredWorkerQueues.length === 0) {
|
|
1724
1725
|
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.");
|
|
1725
1726
|
} else if (autoSpawnWorkersMode === "lazy") {
|
|
1726
|
-
|
|
1727
|
+
const lazySpawnMode = resolveLazySpawnMode(process.env);
|
|
1728
|
+
const lazyModeHint = lazySpawnMode === "shared" ? "shared worker mode \u2014 one `queue worker --all` process; set OM_AUTO_SPAWN_WORKERS_LAZY_MODE=per-queue to change" : "per-queue worker mode \u2014 one process per queue; set OM_AUTO_SPAWN_WORKERS_LAZY_MODE=shared to change";
|
|
1729
|
+
console.log(`[server] Lazy worker auto-spawn enabled \u2014 workers will start on first job (${discoveredWorkerQueues.length} queue(s) watched, ${lazyModeHint}).`);
|
|
1727
1730
|
activeLazySupervisor = startLazyWorkerSupervisor({
|
|
1728
1731
|
mercatoBin,
|
|
1729
1732
|
appDir,
|
|
1730
1733
|
runtimeEnv,
|
|
1731
1734
|
workers: discoveredWorkers,
|
|
1732
1735
|
pollMs: resolveLazyPollMs(process.env),
|
|
1733
|
-
restartOnUnexpectedExit: resolveLazyRestart(process.env)
|
|
1736
|
+
restartOnUnexpectedExit: resolveLazyRestart(process.env),
|
|
1737
|
+
spawnMode: lazySpawnMode
|
|
1734
1738
|
});
|
|
1735
1739
|
} else {
|
|
1736
1740
|
console.log("[server] Eager worker auto-spawn enabled - starting workers for all queues...");
|
|
@@ -1899,14 +1903,17 @@ async function run(argv = process.argv) {
|
|
|
1899
1903
|
if (discoveredWorkerQueues.length === 0) {
|
|
1900
1904
|
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.");
|
|
1901
1905
|
} else if (autoSpawnWorkersMode === "lazy") {
|
|
1902
|
-
|
|
1906
|
+
const lazySpawnMode = resolveLazySpawnMode(process.env);
|
|
1907
|
+
const lazyModeHint = lazySpawnMode === "shared" ? "shared worker mode \u2014 one `queue worker --all` process; set OM_AUTO_SPAWN_WORKERS_LAZY_MODE=per-queue to change" : "per-queue worker mode \u2014 one process per queue; set OM_AUTO_SPAWN_WORKERS_LAZY_MODE=shared to change";
|
|
1908
|
+
console.log(`[server] Lazy worker auto-spawn enabled \u2014 workers will start on first job (${discoveredWorkerQueues.length} queue(s) watched, ${lazyModeHint}).`);
|
|
1903
1909
|
activeLazySupervisor = startLazyWorkerSupervisor({
|
|
1904
1910
|
mercatoBin,
|
|
1905
1911
|
appDir,
|
|
1906
1912
|
runtimeEnv,
|
|
1907
1913
|
workers: discoveredWorkers,
|
|
1908
1914
|
pollMs: resolveLazyPollMs(process.env),
|
|
1909
|
-
restartOnUnexpectedExit: resolveLazyRestart(process.env)
|
|
1915
|
+
restartOnUnexpectedExit: resolveLazyRestart(process.env),
|
|
1916
|
+
spawnMode: lazySpawnMode
|
|
1910
1917
|
});
|
|
1911
1918
|
} else {
|
|
1912
1919
|
console.log("[server] Eager worker auto-spawn enabled - starting workers for all queues...");
|