@open-mercato/cli 0.6.6-develop.5586.1.c9ed1d68a8 → 0.6.6-develop.5594.1.30cd738303
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/events-single-delivery.js +27 -0
- package/dist/lib/events-single-delivery.js.map +7 -0
- package/dist/lib/worker-connection-budget.js +63 -0
- package/dist/lib/worker-connection-budget.js.map +7 -0
- package/dist/mercato.js +43 -2
- package/dist/mercato.js.map +2 -2
- package/package.json +5 -5
- package/src/__tests__/mercato.test.ts +23 -0
- package/src/lib/__tests__/events-single-delivery.test.ts +68 -0
- package/src/lib/__tests__/worker-connection-budget.test.ts +115 -0
- package/src/lib/events-single-delivery.ts +69 -0
- package/src/lib/worker-connection-budget.ts +131 -0
- package/src/mercato.ts +81 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { parseBooleanWithDefault } from "@open-mercato/shared/lib/boolean";
|
|
2
|
+
const EVENTS_SINGLE_DELIVERY_ENV = "OM_EVENTS_SINGLE_DELIVERY";
|
|
3
|
+
const EVENTS_EXTERNAL_WORKER_ENV = "OM_EVENTS_EXTERNAL_WORKER";
|
|
4
|
+
function reconcileEventsSingleDelivery(env, autoSpawnWorkersMode) {
|
|
5
|
+
const requested = parseBooleanWithDefault(env[EVENTS_SINGLE_DELIVERY_ENV], true);
|
|
6
|
+
if (!requested) return { effective: false };
|
|
7
|
+
const externalWorker = parseBooleanWithDefault(env[EVENTS_EXTERNAL_WORKER_ENV], false);
|
|
8
|
+
const workersAvailable = autoSpawnWorkersMode !== "off" || externalWorker;
|
|
9
|
+
if (workersAvailable) return { effective: true };
|
|
10
|
+
return {
|
|
11
|
+
effective: false,
|
|
12
|
+
warning: `[events] ${EVENTS_SINGLE_DELIVERY_ENV} is on (default) but this process auto-spawns no events worker (AUTO_SPAWN_WORKERS=off) and ${EVENTS_EXTERNAL_WORKER_ENV} is not set. Persistent subscribers would be skipped inline with nothing to drain the queue, silently dropping notifications, queued emails, and indexing. Falling back to legacy inline dual-dispatch for safety. To keep single-delivery, run an events worker (\`mercato queue worker events\`) and set ${EVENTS_EXTERNAL_WORKER_ENV}=true, or enable AUTO_SPAWN_WORKERS.`
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function applyEventsSingleDeliveryGuard(args) {
|
|
16
|
+
const result = reconcileEventsSingleDelivery(args.processEnv, args.autoSpawnWorkersMode);
|
|
17
|
+
if (result.warning) console.error(result.warning);
|
|
18
|
+
const value = result.effective ? "true" : "false";
|
|
19
|
+
args.processEnv[EVENTS_SINGLE_DELIVERY_ENV] = value;
|
|
20
|
+
args.runtimeEnv[EVENTS_SINGLE_DELIVERY_ENV] = value;
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
applyEventsSingleDeliveryGuard,
|
|
25
|
+
reconcileEventsSingleDelivery
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=events-single-delivery.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/lib/events-single-delivery.ts"],
|
|
4
|
+
"sourcesContent": ["import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport type { AutoSpawnWorkersMode } from './auto-spawn-workers'\n\n// Keep these env names in sync with the runtime source of truth,\n// `@open-mercato/events/single-delivery`. The CLI cannot import the events\n// package (no dependency edge), so the reconcile logic is mirrored here for the\n// server-bootstrap guard only; the bus and worker own the runtime behavior.\nconst EVENTS_SINGLE_DELIVERY_ENV = 'OM_EVENTS_SINGLE_DELIVERY'\nconst EVENTS_EXTERNAL_WORKER_ENV = 'OM_EVENTS_EXTERNAL_WORKER'\n\ntype EnvSource = NodeJS.ProcessEnv | Record<string, string | undefined>\n\nexport type SingleDeliveryReconciliation = {\n effective: boolean\n warning?: string\n}\n\n/**\n * Server-bootstrap guard for the default-on single-delivery dispatch.\n *\n * With single-delivery on, persistent subscribers are skipped inline and ONLY\n * the events worker dispatches them. A process that runs NO events worker\n * (auto-spawn off and no acknowledged external worker) would skip those\n * subscribers with nothing to drain the queue \u2014 silently dropping notifications,\n * queued emails, and indexing. This fails safe by disabling single-delivery for\n * such a process (back to inline dual-dispatch) and returning a loud warning.\n *\n * Transient worker downtime is NOT this guard's concern: the durable queue holds\n * the job until a worker returns. Only the \"no worker at all\" config is guarded.\n */\nexport function reconcileEventsSingleDelivery(\n env: EnvSource,\n autoSpawnWorkersMode: AutoSpawnWorkersMode,\n): SingleDeliveryReconciliation {\n const requested = parseBooleanWithDefault(env[EVENTS_SINGLE_DELIVERY_ENV], true)\n if (!requested) return { effective: false }\n const externalWorker = parseBooleanWithDefault(env[EVENTS_EXTERNAL_WORKER_ENV], false)\n const workersAvailable = autoSpawnWorkersMode !== 'off' || externalWorker\n if (workersAvailable) return { effective: true }\n return {\n effective: false,\n warning:\n `[events] ${EVENTS_SINGLE_DELIVERY_ENV} is on (default) but this process auto-spawns no events worker ` +\n `(AUTO_SPAWN_WORKERS=off) and ${EVENTS_EXTERNAL_WORKER_ENV} is not set. Persistent subscribers would be ` +\n `skipped inline with nothing to drain the queue, silently dropping notifications, queued emails, and ` +\n `indexing. Falling back to legacy inline dual-dispatch for safety. To keep single-delivery, run an events ` +\n `worker (\\`mercato queue worker events\\`) and set ${EVENTS_EXTERNAL_WORKER_ENV}=true, or enable ` +\n `AUTO_SPAWN_WORKERS.`,\n }\n}\n\n/**\n * Applies {@link reconcileEventsSingleDelivery} and writes the effective value\n * into both the current process env (for the in-process app/bus) and the spawned\n * worker/child env (so a child worker reads the same value the bus does). Logs\n * the warning once when single-delivery is disabled for safety.\n */\nexport function applyEventsSingleDeliveryGuard(args: {\n processEnv: NodeJS.ProcessEnv\n runtimeEnv: NodeJS.ProcessEnv\n autoSpawnWorkersMode: AutoSpawnWorkersMode\n}): SingleDeliveryReconciliation {\n const result = reconcileEventsSingleDelivery(args.processEnv, args.autoSpawnWorkersMode)\n if (result.warning) console.error(result.warning)\n const value = result.effective ? 'true' : 'false'\n args.processEnv[EVENTS_SINGLE_DELIVERY_ENV] = value\n args.runtimeEnv[EVENTS_SINGLE_DELIVERY_ENV] = value\n return result\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,+BAA+B;AAOxC,MAAM,6BAA6B;AACnC,MAAM,6BAA6B;AAsB5B,SAAS,8BACd,KACA,sBAC8B;AAC9B,QAAM,YAAY,wBAAwB,IAAI,0BAA0B,GAAG,IAAI;AAC/E,MAAI,CAAC,UAAW,QAAO,EAAE,WAAW,MAAM;AAC1C,QAAM,iBAAiB,wBAAwB,IAAI,0BAA0B,GAAG,KAAK;AACrF,QAAM,mBAAmB,yBAAyB,SAAS;AAC3D,MAAI,iBAAkB,QAAO,EAAE,WAAW,KAAK;AAC/C,SAAO;AAAA,IACL,WAAW;AAAA,IACX,SACE,YAAY,0BAA0B,+FACN,0BAA0B,8SAGN,0BAA0B;AAAA,EAElF;AACF;AAQO,SAAS,+BAA+B,MAId;AAC/B,QAAM,SAAS,8BAA8B,KAAK,YAAY,KAAK,oBAAoB;AACvF,MAAI,OAAO,QAAS,SAAQ,MAAM,OAAO,OAAO;AAChD,QAAM,QAAQ,OAAO,YAAY,SAAS;AAC1C,OAAK,WAAW,0BAA0B,IAAI;AAC9C,OAAK,WAAW,0BAA0B,IAAI;AAC9C,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
function toPositiveInt(value, fallback) {
|
|
2
|
+
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
3
|
+
const floored = Math.floor(value);
|
|
4
|
+
return floored > 0 ? floored : fallback;
|
|
5
|
+
}
|
|
6
|
+
function resolveWorkerConnectionBudget(env, poolMax) {
|
|
7
|
+
const safePoolMax = toPositiveInt(poolMax, 1);
|
|
8
|
+
const raw = env.OM_WORKERS_DB_CONNECTION_BUDGET;
|
|
9
|
+
if (!raw) return safePoolMax;
|
|
10
|
+
const parsed = Number.parseInt(raw, 10);
|
|
11
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : safePoolMax;
|
|
12
|
+
}
|
|
13
|
+
function planWorkerConcurrency(queues, budget) {
|
|
14
|
+
const safeBudget = toPositiveInt(budget, 1);
|
|
15
|
+
const requested = queues.map((entry) => ({
|
|
16
|
+
queue: entry.queue,
|
|
17
|
+
requested: toPositiveInt(entry.concurrency, 1)
|
|
18
|
+
}));
|
|
19
|
+
const totalRequested = requested.reduce((sum, entry) => sum + entry.requested, 0);
|
|
20
|
+
if (totalRequested <= safeBudget) {
|
|
21
|
+
return {
|
|
22
|
+
budget: safeBudget,
|
|
23
|
+
totalRequested,
|
|
24
|
+
totalEffective: totalRequested,
|
|
25
|
+
clamped: false,
|
|
26
|
+
belowQueueFloor: false,
|
|
27
|
+
entries: requested.map((entry) => ({ ...entry, effective: entry.requested }))
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const effective = requested.map(() => 1);
|
|
31
|
+
let used = effective.length;
|
|
32
|
+
while (used < safeBudget) {
|
|
33
|
+
let bestIndex = -1;
|
|
34
|
+
let bestDeficit = 0;
|
|
35
|
+
for (let index = 0; index < requested.length; index += 1) {
|
|
36
|
+
const deficit = requested[index].requested - effective[index];
|
|
37
|
+
if (deficit > bestDeficit) {
|
|
38
|
+
bestDeficit = deficit;
|
|
39
|
+
bestIndex = index;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (bestIndex === -1) break;
|
|
43
|
+
effective[bestIndex] += 1;
|
|
44
|
+
used += 1;
|
|
45
|
+
}
|
|
46
|
+
const totalEffective = effective.reduce((sum, value) => sum + value, 0);
|
|
47
|
+
return {
|
|
48
|
+
budget: safeBudget,
|
|
49
|
+
totalRequested,
|
|
50
|
+
totalEffective,
|
|
51
|
+
clamped: true,
|
|
52
|
+
belowQueueFloor: requested.length > safeBudget,
|
|
53
|
+
entries: requested.map((entry, index) => ({
|
|
54
|
+
...entry,
|
|
55
|
+
effective: effective[index]
|
|
56
|
+
}))
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
planWorkerConcurrency,
|
|
61
|
+
resolveWorkerConnectionBudget
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=worker-connection-budget.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/lib/worker-connection-budget.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Bound the DB-connection demand of background queue workers.\n *\n * Since #2970/#3011 each worker job builds its own request container, so its own\n * `EntityManager` checks out a dedicated pooled DB connection for the job's\n * duration. The peak connection demand of `worker --all` is therefore the sum of\n * every queue's concurrency. Nothing previously bounded that sum against the DB\n * pool (`DB_POOL_MAX`) or the database's global `max_connections`, so a storm of\n * slow/failing jobs could over-subscribe connections \u2014 thrashing on the worker's\n * own acquire timeout and, across the worker + web processes, starving the\n * request/onboarding path that shares the same database.\n *\n * These helpers derive a per-queue effective-concurrency plan whose total never\n * exceeds a connection budget (defaulting to the resolved pool max), while\n * guaranteeing every queue keeps at least one worker.\n */\n\nexport type WorkerQueueConcurrency = {\n queue: string\n concurrency: number\n}\n\nexport type WorkerConcurrencyPlanEntry = {\n queue: string\n requested: number\n effective: number\n}\n\nexport type WorkerConcurrencyPlan = {\n /** Connection budget the plan was fitted to. */\n budget: number\n /** Sum of requested concurrency across all queues. */\n totalRequested: number\n /** Sum of effective concurrency the plan grants. */\n totalEffective: number\n /** True when any queue's concurrency was reduced to fit the budget. */\n clamped: boolean\n /**\n * True when the budget is smaller than the number of queues, so the per-queue\n * floor of 1 forces the total above the budget. The caller should surface this\n * as a misconfiguration (raise `DB_POOL_MAX` or the budget).\n */\n belowQueueFloor: boolean\n entries: WorkerConcurrencyPlanEntry[]\n}\n\nfunction toPositiveInt(value: number | undefined, fallback: number): number {\n if (value === undefined || !Number.isFinite(value)) return fallback\n const floored = Math.floor(value)\n return floored > 0 ? floored : fallback\n}\n\n/**\n * Resolve the connection budget for background workers. Defaults to the resolved\n * DB pool max so the worker never tries to use more connections than its own pool\n * can hand out; override with `OM_WORKERS_DB_CONNECTION_BUDGET` (positive int).\n */\nexport function resolveWorkerConnectionBudget(\n env: NodeJS.ProcessEnv,\n poolMax: number,\n): number {\n const safePoolMax = toPositiveInt(poolMax, 1)\n const raw = env.OM_WORKERS_DB_CONNECTION_BUDGET\n if (!raw) return safePoolMax\n const parsed = Number.parseInt(raw, 10)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : safePoolMax\n}\n\n/**\n * Fit per-queue concurrency to a connection budget.\n *\n * - Every queue keeps a floor of 1 worker (no queue is starved).\n * - No queue exceeds its requested concurrency.\n * - When the sum exceeds the budget, leftover capacity is distributed greedily to\n * the queues furthest below their request, so the total matches the budget\n * exactly (when `budget >= queueCount`) and the allocation is deterministic.\n */\nexport function planWorkerConcurrency(\n queues: WorkerQueueConcurrency[],\n budget: number,\n): WorkerConcurrencyPlan {\n const safeBudget = toPositiveInt(budget, 1)\n const requested = queues.map((entry) => ({\n queue: entry.queue,\n requested: toPositiveInt(entry.concurrency, 1),\n }))\n const totalRequested = requested.reduce((sum, entry) => sum + entry.requested, 0)\n\n if (totalRequested <= safeBudget) {\n return {\n budget: safeBudget,\n totalRequested,\n totalEffective: totalRequested,\n clamped: false,\n belowQueueFloor: false,\n entries: requested.map((entry) => ({ ...entry, effective: entry.requested })),\n }\n }\n\n // Floor every queue at 1, then water-fill the remaining budget to the queues\n // with the largest unmet request first.\n const effective = requested.map(() => 1)\n let used = effective.length\n while (used < safeBudget) {\n let bestIndex = -1\n let bestDeficit = 0\n for (let index = 0; index < requested.length; index += 1) {\n const deficit = requested[index].requested - effective[index]\n if (deficit > bestDeficit) {\n bestDeficit = deficit\n bestIndex = index\n }\n }\n if (bestIndex === -1) break\n effective[bestIndex] += 1\n used += 1\n }\n\n const totalEffective = effective.reduce((sum, value) => sum + value, 0)\n return {\n budget: safeBudget,\n totalRequested,\n totalEffective,\n clamped: true,\n belowQueueFloor: requested.length > safeBudget,\n entries: requested.map((entry, index) => ({\n ...entry,\n effective: effective[index],\n })),\n }\n}\n"],
|
|
5
|
+
"mappings": "AA8CA,SAAS,cAAc,OAA2B,UAA0B;AAC1E,MAAI,UAAU,UAAa,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AAC3D,QAAM,UAAU,KAAK,MAAM,KAAK;AAChC,SAAO,UAAU,IAAI,UAAU;AACjC;AAOO,SAAS,8BACd,KACA,SACQ;AACR,QAAM,cAAc,cAAc,SAAS,CAAC;AAC5C,QAAM,MAAM,IAAI;AAChB,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,SAAS,OAAO,SAAS,KAAK,EAAE;AACtC,SAAO,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,SAAS;AAC1D;AAWO,SAAS,sBACd,QACA,QACuB;AACvB,QAAM,aAAa,cAAc,QAAQ,CAAC;AAC1C,QAAM,YAAY,OAAO,IAAI,CAAC,WAAW;AAAA,IACvC,OAAO,MAAM;AAAA,IACb,WAAW,cAAc,MAAM,aAAa,CAAC;AAAA,EAC/C,EAAE;AACF,QAAM,iBAAiB,UAAU,OAAO,CAAC,KAAK,UAAU,MAAM,MAAM,WAAW,CAAC;AAEhF,MAAI,kBAAkB,YAAY;AAChC,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA,gBAAgB;AAAA,MAChB,SAAS;AAAA,MACT,iBAAiB;AAAA,MACjB,SAAS,UAAU,IAAI,CAAC,WAAW,EAAE,GAAG,OAAO,WAAW,MAAM,UAAU,EAAE;AAAA,IAC9E;AAAA,EACF;AAIA,QAAM,YAAY,UAAU,IAAI,MAAM,CAAC;AACvC,MAAI,OAAO,UAAU;AACrB,SAAO,OAAO,YAAY;AACxB,QAAI,YAAY;AAChB,QAAI,cAAc;AAClB,aAAS,QAAQ,GAAG,QAAQ,UAAU,QAAQ,SAAS,GAAG;AACxD,YAAM,UAAU,UAAU,KAAK,EAAE,YAAY,UAAU,KAAK;AAC5D,UAAI,UAAU,aAAa;AACzB,sBAAc;AACd,oBAAY;AAAA,MACd;AAAA,IACF;AACA,QAAI,cAAc,GAAI;AACtB,cAAU,SAAS,KAAK;AACxB,YAAQ;AAAA,EACV;AAEA,QAAM,iBAAiB,UAAU,OAAO,CAAC,KAAK,UAAU,MAAM,OAAO,CAAC;AACtE,SAAO;AAAA,IACL,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA,SAAS;AAAA,IACT,iBAAiB,UAAU,SAAS;AAAA,IACpC,SAAS,UAAU,IAAI,CAAC,OAAO,WAAW;AAAA,MACxC,GAAG;AAAA,MACH,WAAW,UAAU,KAAK;AAAA,IAC5B,EAAE;AAAA,EACJ;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/mercato.js
CHANGED
|
@@ -10,7 +10,12 @@ import {
|
|
|
10
10
|
resolveLazyRestart
|
|
11
11
|
} from "./lib/auto-spawn-workers.js";
|
|
12
12
|
import { startLazyWorkerSupervisor } from "./lib/queue-worker-supervisor.js";
|
|
13
|
+
import { applyEventsSingleDeliveryGuard } from "./lib/events-single-delivery.js";
|
|
13
14
|
import { createPerJobWorkerHandler } from "./lib/worker-job-handler.js";
|
|
15
|
+
import {
|
|
16
|
+
planWorkerConcurrency,
|
|
17
|
+
resolveWorkerConnectionBudget
|
|
18
|
+
} from "./lib/worker-connection-budget.js";
|
|
14
19
|
import {
|
|
15
20
|
resolveAutoSpawnSchedulerMode,
|
|
16
21
|
resolveLazySchedulerPollMs,
|
|
@@ -379,6 +384,27 @@ function formatQueueWorkerLabel(queueNames) {
|
|
|
379
384
|
const preview = sorted.length > 4 ? `${sorted.slice(0, 4).join(", ")}, +${sorted.length - 4} more` : sorted.join(", ");
|
|
380
385
|
return `Queue worker (${preview})`;
|
|
381
386
|
}
|
|
387
|
+
async function resolveWorkerBudgetPlan(requestedByQueue) {
|
|
388
|
+
const { resolvePoolConfig } = await import("@open-mercato/shared/lib/db/mikro");
|
|
389
|
+
const poolMax = resolvePoolConfig(process.env).poolMax;
|
|
390
|
+
const budget = resolveWorkerConnectionBudget(process.env, poolMax);
|
|
391
|
+
const plan = planWorkerConcurrency(requestedByQueue, budget);
|
|
392
|
+
console.log(
|
|
393
|
+
`[worker] DB connection budget: ${plan.budget} (pool max ${poolMax}); requested \u03A3concurrency ${plan.totalRequested}, effective ${plan.totalEffective}`
|
|
394
|
+
);
|
|
395
|
+
if (plan.clamped) {
|
|
396
|
+
const perQueue = plan.entries.map((entry) => `${entry.queue}=${entry.effective}/${entry.requested}`).join(", ");
|
|
397
|
+
console.warn(
|
|
398
|
+
`[worker] Worker concurrency clamped to fit the DB connection budget (${plan.budget}): ${perQueue}. Raise DB_POOL_MAX or set OM_WORKERS_DB_CONNECTION_BUDGET to change this. Keep web_pool_max + worker_pool_max + overhead <= Postgres max_connections.`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
if (plan.belowQueueFloor) {
|
|
402
|
+
console.warn(
|
|
403
|
+
`[worker] DB connection budget (${plan.budget}) is smaller than the number of queues (${requestedByQueue.length}); every queue runs at concurrency 1 and total demand (${plan.totalEffective}) still exceeds the budget. Raise DB_POOL_MAX.`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
return plan;
|
|
407
|
+
}
|
|
382
408
|
function lookupModuleCommand(allModules, moduleName, commandName) {
|
|
383
409
|
const mod = allModules.find((entry) => entry.id === moduleName);
|
|
384
410
|
if (!mod) {
|
|
@@ -1226,10 +1252,21 @@ async function run(argv = process.argv) {
|
|
|
1226
1252
|
return;
|
|
1227
1253
|
}
|
|
1228
1254
|
const { createRequestContainer } = await import("@open-mercato/shared/lib/di/container");
|
|
1255
|
+
const requestedByQueue = discoveredQueues.map((queue) => {
|
|
1256
|
+
const queueWorkers = allWorkers.filter((w) => w.queue === queue);
|
|
1257
|
+
return {
|
|
1258
|
+
queue,
|
|
1259
|
+
concurrency: concurrencyOverride ?? Math.max(...queueWorkers.map((w) => w.concurrency), 1)
|
|
1260
|
+
};
|
|
1261
|
+
});
|
|
1262
|
+
const budgetPlan = await resolveWorkerBudgetPlan(requestedByQueue);
|
|
1263
|
+
const effectiveByQueue = new Map(
|
|
1264
|
+
budgetPlan.entries.map((entry) => [entry.queue, entry.effective])
|
|
1265
|
+
);
|
|
1229
1266
|
console.log(`[worker] Starting workers for all queues: ${discoveredQueues.join(", ")}`);
|
|
1230
1267
|
const workerPromises = discoveredQueues.map(async (queue) => {
|
|
1231
1268
|
const queueWorkers = allWorkers.filter((w) => w.queue === queue);
|
|
1232
|
-
const concurrency = concurrencyOverride ?? Math.max(...queueWorkers.map((w) => w.concurrency), 1);
|
|
1269
|
+
const concurrency = effectiveByQueue.get(queue) ?? concurrencyOverride ?? Math.max(...queueWorkers.map((w) => w.concurrency), 1);
|
|
1233
1270
|
console.log(`[worker] Starting "${queue}" with ${queueWorkers.length} handler(s), concurrency: ${concurrency}`);
|
|
1234
1271
|
const queueRedisUrl = getRedisUrl("QUEUE");
|
|
1235
1272
|
await runWorker({
|
|
@@ -1248,7 +1285,9 @@ async function run(argv = process.argv) {
|
|
|
1248
1285
|
const queueWorkers = allWorkers.filter((w) => w.queue === queueName);
|
|
1249
1286
|
if (queueWorkers.length > 0) {
|
|
1250
1287
|
const { createRequestContainer } = await import("@open-mercato/shared/lib/di/container");
|
|
1251
|
-
const
|
|
1288
|
+
const requested = concurrencyOverride ?? Math.max(...queueWorkers.map((w) => w.concurrency), 1);
|
|
1289
|
+
const budgetPlan = await resolveWorkerBudgetPlan([{ queue: queueName, concurrency: requested }]);
|
|
1290
|
+
const concurrency = budgetPlan.entries[0]?.effective ?? requested;
|
|
1252
1291
|
console.log(`[worker] Found ${queueWorkers.length} worker(s) for queue "${queueName}"`);
|
|
1253
1292
|
const queueRedisUrl = getRedisUrl("QUEUE");
|
|
1254
1293
|
await runWorker({
|
|
@@ -1639,6 +1678,7 @@ async function run(argv = process.argv) {
|
|
|
1639
1678
|
envReloader.reload();
|
|
1640
1679
|
const runtimeEnv = buildServerProcessEnvironment(process.env);
|
|
1641
1680
|
const autoSpawnWorkersMode = resolveAutoSpawnWorkersMode(process.env);
|
|
1681
|
+
applyEventsSingleDeliveryGuard({ processEnv: process.env, runtimeEnv, autoSpawnWorkersMode });
|
|
1642
1682
|
const autoSpawnSchedulerMode = resolveAutoSpawnSchedulerMode(process.env);
|
|
1643
1683
|
const queueStrategy = process.env.QUEUE_STRATEGY || "local";
|
|
1644
1684
|
const schedulerCommand = lookupModuleCommand(getCliModules(), "scheduler", "start");
|
|
@@ -1773,6 +1813,7 @@ async function run(argv = process.argv) {
|
|
|
1773
1813
|
const autoSpawnSchedulerMode = resolveAutoSpawnSchedulerMode(process.env);
|
|
1774
1814
|
const queueStrategy = process.env.QUEUE_STRATEGY || "local";
|
|
1775
1815
|
const runtimeEnv = buildServerProcessEnvironment(process.env);
|
|
1816
|
+
applyEventsSingleDeliveryGuard({ processEnv: process.env, runtimeEnv, autoSpawnWorkersMode });
|
|
1776
1817
|
assertSingleInstanceStrategies(runtimeEnv);
|
|
1777
1818
|
const schedulerCommand = lookupModuleCommand(getCliModules(), "scheduler", "start");
|
|
1778
1819
|
const serverStartLock = acquireServerStartLock(appDir, {
|