@open-mercato/cli 0.6.6-develop.5586.1.c9ed1d68a8 → 0.6.6-develop.5588.1.a8f6c51d1f

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.
@@ -1,4 +1,4 @@
1
- [build:cli] found 89 entry points
1
+ [build:cli] found 90 entry points
2
2
  Copied create-app/agentic/ → dist/agentic/
3
3
  Discovered 16 standalone guides → dist/agentic/guides/
4
4
  [build:cli] built successfully
@@ -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
@@ -11,6 +11,10 @@ import {
11
11
  } from "./lib/auto-spawn-workers.js";
12
12
  import { startLazyWorkerSupervisor } from "./lib/queue-worker-supervisor.js";
13
13
  import { createPerJobWorkerHandler } from "./lib/worker-job-handler.js";
14
+ import {
15
+ planWorkerConcurrency,
16
+ resolveWorkerConnectionBudget
17
+ } from "./lib/worker-connection-budget.js";
14
18
  import {
15
19
  resolveAutoSpawnSchedulerMode,
16
20
  resolveLazySchedulerPollMs,
@@ -379,6 +383,27 @@ function formatQueueWorkerLabel(queueNames) {
379
383
  const preview = sorted.length > 4 ? `${sorted.slice(0, 4).join(", ")}, +${sorted.length - 4} more` : sorted.join(", ");
380
384
  return `Queue worker (${preview})`;
381
385
  }
386
+ async function resolveWorkerBudgetPlan(requestedByQueue) {
387
+ const { resolvePoolConfig } = await import("@open-mercato/shared/lib/db/mikro");
388
+ const poolMax = resolvePoolConfig(process.env).poolMax;
389
+ const budget = resolveWorkerConnectionBudget(process.env, poolMax);
390
+ const plan = planWorkerConcurrency(requestedByQueue, budget);
391
+ console.log(
392
+ `[worker] DB connection budget: ${plan.budget} (pool max ${poolMax}); requested \u03A3concurrency ${plan.totalRequested}, effective ${plan.totalEffective}`
393
+ );
394
+ if (plan.clamped) {
395
+ const perQueue = plan.entries.map((entry) => `${entry.queue}=${entry.effective}/${entry.requested}`).join(", ");
396
+ console.warn(
397
+ `[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.`
398
+ );
399
+ }
400
+ if (plan.belowQueueFloor) {
401
+ console.warn(
402
+ `[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.`
403
+ );
404
+ }
405
+ return plan;
406
+ }
382
407
  function lookupModuleCommand(allModules, moduleName, commandName) {
383
408
  const mod = allModules.find((entry) => entry.id === moduleName);
384
409
  if (!mod) {
@@ -1226,10 +1251,21 @@ async function run(argv = process.argv) {
1226
1251
  return;
1227
1252
  }
1228
1253
  const { createRequestContainer } = await import("@open-mercato/shared/lib/di/container");
1254
+ const requestedByQueue = discoveredQueues.map((queue) => {
1255
+ const queueWorkers = allWorkers.filter((w) => w.queue === queue);
1256
+ return {
1257
+ queue,
1258
+ concurrency: concurrencyOverride ?? Math.max(...queueWorkers.map((w) => w.concurrency), 1)
1259
+ };
1260
+ });
1261
+ const budgetPlan = await resolveWorkerBudgetPlan(requestedByQueue);
1262
+ const effectiveByQueue = new Map(
1263
+ budgetPlan.entries.map((entry) => [entry.queue, entry.effective])
1264
+ );
1229
1265
  console.log(`[worker] Starting workers for all queues: ${discoveredQueues.join(", ")}`);
1230
1266
  const workerPromises = discoveredQueues.map(async (queue) => {
1231
1267
  const queueWorkers = allWorkers.filter((w) => w.queue === queue);
1232
- const concurrency = concurrencyOverride ?? Math.max(...queueWorkers.map((w) => w.concurrency), 1);
1268
+ const concurrency = effectiveByQueue.get(queue) ?? concurrencyOverride ?? Math.max(...queueWorkers.map((w) => w.concurrency), 1);
1233
1269
  console.log(`[worker] Starting "${queue}" with ${queueWorkers.length} handler(s), concurrency: ${concurrency}`);
1234
1270
  const queueRedisUrl = getRedisUrl("QUEUE");
1235
1271
  await runWorker({
@@ -1248,7 +1284,9 @@ async function run(argv = process.argv) {
1248
1284
  const queueWorkers = allWorkers.filter((w) => w.queue === queueName);
1249
1285
  if (queueWorkers.length > 0) {
1250
1286
  const { createRequestContainer } = await import("@open-mercato/shared/lib/di/container");
1251
- const concurrency = concurrencyOverride ?? Math.max(...queueWorkers.map((w) => w.concurrency), 1);
1287
+ const requested = concurrencyOverride ?? Math.max(...queueWorkers.map((w) => w.concurrency), 1);
1288
+ const budgetPlan = await resolveWorkerBudgetPlan([{ queue: queueName, concurrency: requested }]);
1289
+ const concurrency = budgetPlan.entries[0]?.effective ?? requested;
1252
1290
  console.log(`[worker] Found ${queueWorkers.length} worker(s) for queue "${queueName}"`);
1253
1291
  const queueRedisUrl = getRedisUrl("QUEUE");
1254
1292
  await runWorker({