@pattern-stack/codegen 0.8.1 → 0.9.1
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/CHANGELOG.md +29 -0
- package/dist/{job-orchestrator.protocol-BwsBd37o.d.ts → job-orchestrator.protocol-CHOEqBDk.d.ts} +36 -1
- package/dist/runtime/subsystems/bridge/bridge-delivery-handler.d.ts +2 -2
- package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js.map +1 -1
- package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js.map +1 -1
- package/dist/runtime/subsystems/bridge/bridge.module.d.ts +5 -1
- package/dist/runtime/subsystems/bridge/bridge.module.js +930 -275
- package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
- package/dist/runtime/subsystems/bridge/event-flow.service.d.ts +1 -1
- package/dist/runtime/subsystems/bridge/event-flow.service.js.map +1 -1
- package/dist/runtime/subsystems/bridge/index.d.ts +4 -1
- package/dist/runtime/subsystems/bridge/index.js +837 -182
- package/dist/runtime/subsystems/bridge/index.js.map +1 -1
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.d.ts +3 -1
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +92 -1
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/events/event-bus.memory-backend.d.ts +3 -1
- package/dist/runtime/subsystems/events/event-bus.memory-backend.js +99 -0
- package/dist/runtime/subsystems/events/event-bus.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/events/event-bus.redis-backend.js.map +1 -1
- package/dist/runtime/subsystems/events/event-keyset-cursor.d.ts +32 -0
- package/dist/runtime/subsystems/events/event-keyset-cursor.js +38 -0
- package/dist/runtime/subsystems/events/event-keyset-cursor.js.map +1 -0
- package/dist/runtime/subsystems/events/event-read.protocol.d.ts +94 -0
- package/dist/runtime/subsystems/events/event-read.protocol.js +9 -0
- package/dist/runtime/subsystems/events/event-read.protocol.js.map +1 -0
- package/dist/runtime/subsystems/events/events.module.js +177 -3
- package/dist/runtime/subsystems/events/events.module.js.map +1 -1
- package/dist/runtime/subsystems/events/events.tokens.d.ts +16 -1
- package/dist/runtime/subsystems/events/events.tokens.js +2 -0
- package/dist/runtime/subsystems/events/events.tokens.js.map +1 -1
- package/dist/runtime/subsystems/events/generated/bus.js.map +1 -1
- package/dist/runtime/subsystems/events/generated/index.js.map +1 -1
- package/dist/runtime/subsystems/events/index.d.ts +2 -1
- package/dist/runtime/subsystems/events/index.js +178 -3
- package/dist/runtime/subsystems/events/index.js.map +1 -1
- package/dist/runtime/subsystems/index.d.ts +3 -2
- package/dist/runtime/subsystems/index.js +1194 -264
- package/dist/runtime/subsystems/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/bullmq.config.d.ts +98 -0
- package/dist/runtime/subsystems/jobs/bullmq.config.js +143 -0
- package/dist/runtime/subsystems/jobs/bullmq.config.js.map +1 -0
- package/dist/runtime/subsystems/jobs/index.d.ts +8 -3
- package/dist/runtime/subsystems/jobs/index.js +861 -201
- package/dist/runtime/subsystems/jobs/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-handler.base.d.ts +2 -1
- package/dist/runtime/subsystems/jobs/job-handler.base.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.d.ts +108 -0
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +922 -0
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.d.ts +2 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.d.ts +2 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.protocol.d.ts +2 -1
- package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.d.ts +53 -0
- package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.js +57 -0
- package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.d.ts +4 -2
- package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +81 -1
- package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.d.ts +4 -2
- package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +81 -0
- package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-run-service.protocol.d.ts +76 -2
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.d.ts +49 -0
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +374 -0
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js.map +1 -0
- package/dist/runtime/subsystems/jobs/job-worker.d.ts +2 -1
- package/dist/runtime/subsystems/jobs/job-worker.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +44 -5
- package/dist/runtime/subsystems/jobs/job-worker.module.js +832 -178
- package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.module.d.ts +10 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js +519 -20
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
- package/dist/runtime/subsystems/jobs/jobs-errors.d.ts +2 -1
- package/dist/runtime/subsystems/jobs/pool-config.loader.d.ts +9 -1
- package/dist/runtime/subsystems/jobs/pool-config.loader.js +4 -0
- package/dist/runtime/subsystems/jobs/pool-config.loader.js.map +1 -1
- package/dist/runtime/subsystems/observability/index.d.ts +4 -3
- package/dist/runtime/subsystems/observability/index.js +109 -2
- package/dist/runtime/subsystems/observability/index.js.map +1 -1
- package/dist/runtime/subsystems/observability/observability.module.js +109 -2
- package/dist/runtime/subsystems/observability/observability.module.js.map +1 -1
- package/dist/runtime/subsystems/observability/observability.protocol.d.ts +64 -3
- package/dist/runtime/subsystems/observability/observability.service.d.ts +22 -4
- package/dist/runtime/subsystems/observability/observability.service.js +109 -2
- package/dist/runtime/subsystems/observability/observability.service.js.map +1 -1
- package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.d.ts +3 -2
- package/dist/runtime/subsystems/observability/reporters/index.d.ts +3 -2
- package/dist/src/cli/index.js +30 -6
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +5 -1
- package/runtime/subsystems/bridge/bridge.module.ts +5 -0
- package/runtime/subsystems/events/event-bus.drizzle-backend.ts +109 -3
- package/runtime/subsystems/events/event-bus.memory-backend.ts +103 -1
- package/runtime/subsystems/events/event-keyset-cursor.ts +59 -0
- package/runtime/subsystems/events/event-read.protocol.ts +97 -0
- package/runtime/subsystems/events/events.module.ts +18 -2
- package/runtime/subsystems/events/events.tokens.ts +16 -0
- package/runtime/subsystems/events/index.ts +7 -0
- package/runtime/subsystems/jobs/bullmq.config.ts +125 -0
- package/runtime/subsystems/jobs/index.ts +22 -0
- package/runtime/subsystems/jobs/job-handler.base.ts +36 -0
- package/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts +381 -0
- package/runtime/subsystems/jobs/job-run-keyset-cursor.ts +88 -0
- package/runtime/subsystems/jobs/job-run-service.drizzle-backend.ts +59 -1
- package/runtime/subsystems/jobs/job-run-service.memory-backend.ts +53 -0
- package/runtime/subsystems/jobs/job-run-service.protocol.ts +77 -0
- package/runtime/subsystems/jobs/job-worker.bullmq-backend.ts +311 -0
- package/runtime/subsystems/jobs/job-worker.module.ts +124 -10
- package/runtime/subsystems/jobs/jobs-domain.module.ts +40 -21
- package/runtime/subsystems/jobs/pool-config.loader.ts +11 -0
- package/runtime/subsystems/observability/index.ts +8 -0
- package/runtime/subsystems/observability/observability.protocol.ts +76 -0
- package/runtime/subsystems/observability/observability.service.ts +148 -1
- package/templates/entity/new/clean-lite-ps/prompt-extension.js +14 -12
- package/templates/relationship/new/prompt.js +8 -5
- package/templates/subsystem/jobs/worker.ejs.t +30 -7
- package/templates/subsystem/sync/sync-audit.schema.ejs.t +12 -16
|
@@ -933,18 +933,18 @@ EventFlowService = __decorateClass([
|
|
|
933
933
|
|
|
934
934
|
// runtime/subsystems/bridge/bridge.module.ts
|
|
935
935
|
import {
|
|
936
|
-
Inject as
|
|
936
|
+
Inject as Inject13,
|
|
937
937
|
Module as Module3,
|
|
938
|
-
Optional as
|
|
938
|
+
Optional as Optional8
|
|
939
939
|
} from "@nestjs/common";
|
|
940
940
|
|
|
941
941
|
// runtime/subsystems/jobs/job-worker.module.ts
|
|
942
942
|
import {
|
|
943
|
-
Inject as
|
|
944
|
-
Injectable as
|
|
945
|
-
Logger as
|
|
943
|
+
Inject as Inject12,
|
|
944
|
+
Injectable as Injectable13,
|
|
945
|
+
Logger as Logger8,
|
|
946
946
|
Module as Module2,
|
|
947
|
-
Optional as
|
|
947
|
+
Optional as Optional7
|
|
948
948
|
} from "@nestjs/common";
|
|
949
949
|
|
|
950
950
|
// runtime/subsystems/jobs/jobs-domain.module.ts
|
|
@@ -1360,7 +1360,58 @@ function notInStatus(statuses) {
|
|
|
1360
1360
|
|
|
1361
1361
|
// runtime/subsystems/jobs/job-run-service.drizzle-backend.ts
|
|
1362
1362
|
import { Inject as Inject6, Injectable as Injectable6 } from "@nestjs/common";
|
|
1363
|
-
import { and as and3, asc, desc as desc2, eq as eq3, inArray as inArray2, isNull as isNull2, sql as sql6 } from "drizzle-orm";
|
|
1363
|
+
import { and as and3, asc, desc as desc2, eq as eq3, gte as gte2, inArray as inArray2, isNull as isNull2, lt, or, sql as sql6 } from "drizzle-orm";
|
|
1364
|
+
|
|
1365
|
+
// runtime/subsystems/jobs/job-run-keyset-cursor.ts
|
|
1366
|
+
var DEFAULT_LIST_LIMIT = 50;
|
|
1367
|
+
var MAX_LIST_LIMIT = 200;
|
|
1368
|
+
function clampLimit(limit) {
|
|
1369
|
+
if (typeof limit !== "number" || !Number.isFinite(limit)) {
|
|
1370
|
+
return DEFAULT_LIST_LIMIT;
|
|
1371
|
+
}
|
|
1372
|
+
const floored = Math.floor(limit);
|
|
1373
|
+
if (floored < 1) return 1;
|
|
1374
|
+
if (floored > MAX_LIST_LIMIT) return MAX_LIST_LIMIT;
|
|
1375
|
+
return floored;
|
|
1376
|
+
}
|
|
1377
|
+
function encodeKeysetCursor(keyset) {
|
|
1378
|
+
const tuple = [keyset.createdAt.toISOString(), keyset.id];
|
|
1379
|
+
return Buffer.from(JSON.stringify(tuple), "utf8").toString("base64url");
|
|
1380
|
+
}
|
|
1381
|
+
function decodeKeysetCursor(cursor) {
|
|
1382
|
+
try {
|
|
1383
|
+
const json = Buffer.from(cursor, "base64url").toString("utf8");
|
|
1384
|
+
const parsed = JSON.parse(json);
|
|
1385
|
+
if (!Array.isArray(parsed) || parsed.length !== 2) return null;
|
|
1386
|
+
const [iso, id] = parsed;
|
|
1387
|
+
if (typeof iso !== "string" || typeof id !== "string") return null;
|
|
1388
|
+
const createdAt = new Date(iso);
|
|
1389
|
+
if (Number.isNaN(createdAt.getTime())) return null;
|
|
1390
|
+
return { createdAt, id };
|
|
1391
|
+
} catch {
|
|
1392
|
+
return null;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
function toJobRunSummary(r) {
|
|
1396
|
+
return {
|
|
1397
|
+
runId: r.id,
|
|
1398
|
+
rootRunId: r.rootRunId,
|
|
1399
|
+
jobType: r.jobType,
|
|
1400
|
+
pool: r.pool,
|
|
1401
|
+
status: r.status,
|
|
1402
|
+
scopeEntityType: r.scopeEntityType,
|
|
1403
|
+
scopeEntityId: r.scopeEntityId,
|
|
1404
|
+
tenantId: r.tenantId,
|
|
1405
|
+
attempts: r.attempts,
|
|
1406
|
+
errorMessage: r.error?.message ?? null,
|
|
1407
|
+
runAt: r.runAt,
|
|
1408
|
+
startedAt: r.startedAt,
|
|
1409
|
+
finishedAt: r.finishedAt,
|
|
1410
|
+
createdAt: r.createdAt
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// runtime/subsystems/jobs/job-run-service.drizzle-backend.ts
|
|
1364
1415
|
var NON_TERMINAL_STATUSES = [
|
|
1365
1416
|
"pending",
|
|
1366
1417
|
"running",
|
|
@@ -1483,6 +1534,37 @@ var DrizzleJobRunService = class {
|
|
|
1483
1534
|
createdAt: r.createdAt
|
|
1484
1535
|
}));
|
|
1485
1536
|
}
|
|
1537
|
+
async listJobRuns(query = {}) {
|
|
1538
|
+
const limit = clampLimit(query.limit);
|
|
1539
|
+
const conditions = [];
|
|
1540
|
+
const tenantCond = this.tenantCondition("listJobRuns", query.tenantId);
|
|
1541
|
+
if (tenantCond) conditions.push(tenantCond);
|
|
1542
|
+
if (query.poolId) conditions.push(eq3(jobRuns.pool, query.poolId));
|
|
1543
|
+
if (query.rootRunId) conditions.push(eq3(jobRuns.rootRunId, query.rootRunId));
|
|
1544
|
+
if (query.status) conditions.push(eq3(jobRuns.status, query.status));
|
|
1545
|
+
if (query.since) conditions.push(gte2(jobRuns.createdAt, query.since));
|
|
1546
|
+
if (query.cursor) {
|
|
1547
|
+
const keyset = decodeKeysetCursor(query.cursor);
|
|
1548
|
+
if (keyset) {
|
|
1549
|
+
conditions.push(
|
|
1550
|
+
or(
|
|
1551
|
+
lt(jobRuns.createdAt, keyset.createdAt),
|
|
1552
|
+
and3(
|
|
1553
|
+
eq3(jobRuns.createdAt, keyset.createdAt),
|
|
1554
|
+
lt(jobRuns.id, keyset.id)
|
|
1555
|
+
)
|
|
1556
|
+
)
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
const rows = await this.db.select().from(jobRuns).where(conditions.length > 0 ? and3(...conditions) : void 0).orderBy(desc2(jobRuns.createdAt), desc2(jobRuns.id)).limit(limit + 1);
|
|
1561
|
+
const hasMore = rows.length > limit;
|
|
1562
|
+
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
1563
|
+
const items = page.map(toJobRunSummary);
|
|
1564
|
+
const last = page[page.length - 1];
|
|
1565
|
+
const nextCursor = hasMore && last ? encodeKeysetCursor({ createdAt: last.createdAt, id: last.id }) : null;
|
|
1566
|
+
return { items, nextCursor };
|
|
1567
|
+
}
|
|
1486
1568
|
/**
|
|
1487
1569
|
* Internal helper used by cascade paths (not on the public protocol).
|
|
1488
1570
|
* Exposed as a public method on the concrete class so infrastructure
|
|
@@ -1544,9 +1626,394 @@ DrizzleJobStepService = __decorateClass([
|
|
|
1544
1626
|
__decorateParam(0, Inject7(DRIZZLE))
|
|
1545
1627
|
], DrizzleJobStepService);
|
|
1546
1628
|
|
|
1629
|
+
// runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts
|
|
1630
|
+
import { createHash } from "crypto";
|
|
1631
|
+
import { Inject as Inject8, Injectable as Injectable8, Logger as Logger4, Optional as Optional5 } from "@nestjs/common";
|
|
1632
|
+
import { eq as eq5 } from "drizzle-orm";
|
|
1633
|
+
|
|
1634
|
+
// runtime/subsystems/jobs/pool-config.loader.ts
|
|
1635
|
+
import { existsSync, readFileSync } from "fs";
|
|
1636
|
+
import { resolve } from "path";
|
|
1637
|
+
import { parse as parseYaml } from "yaml";
|
|
1638
|
+
var FRAMEWORK_POOLS = Object.freeze({
|
|
1639
|
+
events_inbound: Object.freeze({
|
|
1640
|
+
queue: "jobs-events-inbound",
|
|
1641
|
+
concurrency: 20,
|
|
1642
|
+
reserved: true,
|
|
1643
|
+
description: "Inbound events drain (events subsystem outbox)."
|
|
1644
|
+
}),
|
|
1645
|
+
events_change: Object.freeze({
|
|
1646
|
+
queue: "jobs-events-change",
|
|
1647
|
+
concurrency: 30,
|
|
1648
|
+
reserved: true,
|
|
1649
|
+
description: "Change events drain (events subsystem outbox)."
|
|
1650
|
+
}),
|
|
1651
|
+
events_outbound: Object.freeze({
|
|
1652
|
+
queue: "jobs-events-outbound",
|
|
1653
|
+
concurrency: 10,
|
|
1654
|
+
reserved: true,
|
|
1655
|
+
description: "Outbound events drain (events subsystem outbox)."
|
|
1656
|
+
}),
|
|
1657
|
+
interactive: Object.freeze({
|
|
1658
|
+
queue: "jobs-interactive",
|
|
1659
|
+
concurrency: 20,
|
|
1660
|
+
reserved: false,
|
|
1661
|
+
description: "User-facing latency-sensitive jobs."
|
|
1662
|
+
}),
|
|
1663
|
+
batch: Object.freeze({
|
|
1664
|
+
queue: "jobs-batch",
|
|
1665
|
+
concurrency: 5,
|
|
1666
|
+
reserved: false,
|
|
1667
|
+
description: "Default pool for background jobs."
|
|
1668
|
+
})
|
|
1669
|
+
});
|
|
1670
|
+
var RESERVED_POOL_NAMES = new Set(
|
|
1671
|
+
Object.entries(FRAMEWORK_POOLS).filter(([, def]) => def.reserved).map(([name]) => name)
|
|
1672
|
+
);
|
|
1673
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1674
|
+
function loadPoolConfig(configPath) {
|
|
1675
|
+
const resolved = resolve(configPath ?? `${process.cwd()}/codegen.config.yaml`);
|
|
1676
|
+
const cached = cache.get(resolved);
|
|
1677
|
+
if (cached) return cached;
|
|
1678
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1679
|
+
for (const [name, def] of Object.entries(FRAMEWORK_POOLS)) {
|
|
1680
|
+
merged.set(name, { ...def });
|
|
1681
|
+
}
|
|
1682
|
+
if (!existsSync(resolved)) {
|
|
1683
|
+
cache.set(resolved, merged);
|
|
1684
|
+
return merged;
|
|
1685
|
+
}
|
|
1686
|
+
let raw;
|
|
1687
|
+
try {
|
|
1688
|
+
raw = parseYaml(readFileSync(resolved, "utf8"));
|
|
1689
|
+
} catch (err) {
|
|
1690
|
+
throw new Error(
|
|
1691
|
+
`pool-config.loader: failed to parse YAML at ${resolved}: ${err.message}`
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
const userPools = extractUserPools(raw);
|
|
1695
|
+
for (const [name, userDef] of Object.entries(userPools)) {
|
|
1696
|
+
const existing = merged.get(name);
|
|
1697
|
+
if (existing) {
|
|
1698
|
+
const next = {
|
|
1699
|
+
queue: existing.queue,
|
|
1700
|
+
concurrency: typeof userDef.concurrency === "number" ? userDef.concurrency : existing.concurrency,
|
|
1701
|
+
reserved: existing.reserved,
|
|
1702
|
+
description: userDef.description ?? existing.description
|
|
1703
|
+
};
|
|
1704
|
+
merged.set(name, next);
|
|
1705
|
+
continue;
|
|
1706
|
+
}
|
|
1707
|
+
if (typeof userDef.queue !== "string" || userDef.queue.length === 0) {
|
|
1708
|
+
throw new Error(
|
|
1709
|
+
`pool-config.loader: pool '${name}' must declare a non-empty 'queue'.`
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
if (typeof userDef.concurrency !== "number" || userDef.concurrency <= 0) {
|
|
1713
|
+
throw new Error(
|
|
1714
|
+
`pool-config.loader: pool '${name}' must declare a positive 'concurrency'.`
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
if (userDef.reserved === true) {
|
|
1718
|
+
throw new Error(
|
|
1719
|
+
`pool-config.loader: user-defined pool '${name}' cannot set 'reserved: true' \u2014 reserved is framework-only.`
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
merged.set(name, {
|
|
1723
|
+
queue: userDef.queue,
|
|
1724
|
+
concurrency: userDef.concurrency,
|
|
1725
|
+
reserved: false,
|
|
1726
|
+
description: userDef.description
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
cache.set(resolved, merged);
|
|
1730
|
+
return merged;
|
|
1731
|
+
}
|
|
1732
|
+
function allNonReservedPoolNames(config) {
|
|
1733
|
+
const out = [];
|
|
1734
|
+
for (const [name, def] of config) {
|
|
1735
|
+
if (!def.reserved) out.push(name);
|
|
1736
|
+
}
|
|
1737
|
+
return out;
|
|
1738
|
+
}
|
|
1739
|
+
function allPoolNames(config) {
|
|
1740
|
+
return [...config.keys()];
|
|
1741
|
+
}
|
|
1742
|
+
function extractUserPools(raw) {
|
|
1743
|
+
if (!raw || typeof raw !== "object") return {};
|
|
1744
|
+
const jobs2 = raw.jobs;
|
|
1745
|
+
if (!jobs2 || typeof jobs2 !== "object") return {};
|
|
1746
|
+
const pools = jobs2.pools;
|
|
1747
|
+
if (!pools || typeof pools !== "object") return {};
|
|
1748
|
+
const out = {};
|
|
1749
|
+
for (const [name, def] of Object.entries(pools)) {
|
|
1750
|
+
if (!def || typeof def !== "object") continue;
|
|
1751
|
+
out[name] = def;
|
|
1752
|
+
}
|
|
1753
|
+
return out;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// runtime/subsystems/jobs/bullmq.config.ts
|
|
1757
|
+
var BULLMQ_CONNECTION = /* @__PURE__ */ Symbol("BULLMQ_CONNECTION");
|
|
1758
|
+
var BULLMQ_RESOLVED_CONFIG = /* @__PURE__ */ Symbol("BULLMQ_RESOLVED_CONFIG");
|
|
1759
|
+
var DEFAULT_REDIS_URL = "redis://localhost:6379";
|
|
1760
|
+
var DEFAULT_BULL_BOARD_MOUNT = "/admin/queues";
|
|
1761
|
+
function resolveBullMqConfig(ext) {
|
|
1762
|
+
const url = ext?.redis_url ?? process.env.REDIS_URL ?? DEFAULT_REDIS_URL;
|
|
1763
|
+
const resolved = {
|
|
1764
|
+
connection: { url },
|
|
1765
|
+
queuePrefix: ext?.queue_prefix
|
|
1766
|
+
};
|
|
1767
|
+
if (ext?.bull_board?.enabled) {
|
|
1768
|
+
resolved.bullBoard = {
|
|
1769
|
+
enabled: true,
|
|
1770
|
+
mountPath: ext.bull_board.mount_path ?? DEFAULT_BULL_BOARD_MOUNT
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
return resolved;
|
|
1774
|
+
}
|
|
1775
|
+
function resolvePoolQueueName(pool, config, poolConfig = loadPoolConfig()) {
|
|
1776
|
+
const alias = poolConfig.get(pool)?.queue ?? pool;
|
|
1777
|
+
const prefix = config?.queuePrefix;
|
|
1778
|
+
return prefix ? `${prefix}:${alias}` : alias;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
// runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts
|
|
1782
|
+
function sha1JobId(idempotencyKey) {
|
|
1783
|
+
return createHash("sha1").update(idempotencyKey).digest("hex");
|
|
1784
|
+
}
|
|
1785
|
+
var BullMQJobOrchestrator = class extends DrizzleJobOrchestrator {
|
|
1786
|
+
constructor(db, multiTenant, connection, bullConfig = null) {
|
|
1787
|
+
super(db, multiTenant);
|
|
1788
|
+
this.connection = connection;
|
|
1789
|
+
this.bullConfig = bullConfig;
|
|
1790
|
+
this.bullDb = db;
|
|
1791
|
+
}
|
|
1792
|
+
connection;
|
|
1793
|
+
bullConfig;
|
|
1794
|
+
// TODO(logging-subsystem): swap to ILogger once ADR-028 lands
|
|
1795
|
+
bullLogger = new Logger4(BullMQJobOrchestrator.name);
|
|
1796
|
+
/** Lazily-opened `Queue` handles, one per pool. */
|
|
1797
|
+
queues = /* @__PURE__ */ new Map();
|
|
1798
|
+
/** Single FlowProducer for parent/child hierarchies. Lazily opened. */
|
|
1799
|
+
_flow = null;
|
|
1800
|
+
/**
|
|
1801
|
+
* Cached `bullmq` value constructors, populated by `loadBullMq()` on first
|
|
1802
|
+
* use (the `start`/`cancel`/`replay` entrypoints `await` it before touching
|
|
1803
|
+
* a queue). Kept off the import graph so a `drizzle`-only consumer never
|
|
1804
|
+
* resolves the optional `'bullmq'` package.
|
|
1805
|
+
*/
|
|
1806
|
+
QueueCtor = null;
|
|
1807
|
+
FlowProducerCtor = null;
|
|
1808
|
+
bullMqLoad = null;
|
|
1809
|
+
/**
|
|
1810
|
+
* Own reference to the Drizzle client. `DrizzleJobOrchestrator.db` is
|
|
1811
|
+
* `private` (can't be redeclared even privately in a subclass), and the
|
|
1812
|
+
* spec forbids touching that file — so the subclass keeps its own handle
|
|
1813
|
+
* under a distinct name (same instance, passed through to `super`) for the
|
|
1814
|
+
* cancel-cascade snapshot + definition/run loads below.
|
|
1815
|
+
*/
|
|
1816
|
+
bullDb;
|
|
1817
|
+
/**
|
|
1818
|
+
* Lazily load the optional `bullmq` package and cache its value
|
|
1819
|
+
* constructors. Idempotent (single in-flight promise). Throws a friendly,
|
|
1820
|
+
* actionable error when the consumer selected `backend: 'bullmq'` but did
|
|
1821
|
+
* not install the package — mirrors `createRedisClient` in the redis event
|
|
1822
|
+
* backend. Must be `await`ed before any `queueFor`/`flow` access.
|
|
1823
|
+
*/
|
|
1824
|
+
async loadBullMq() {
|
|
1825
|
+
if (this.QueueCtor && this.FlowProducerCtor) return;
|
|
1826
|
+
if (!this.bullMqLoad) {
|
|
1827
|
+
this.bullMqLoad = (async () => {
|
|
1828
|
+
try {
|
|
1829
|
+
const mod = await import("bullmq");
|
|
1830
|
+
this.QueueCtor = mod.Queue;
|
|
1831
|
+
this.FlowProducerCtor = mod.FlowProducer;
|
|
1832
|
+
} catch {
|
|
1833
|
+
throw new Error(
|
|
1834
|
+
'BullMQ backend requires the "bullmq" package. Install it with: npm install bullmq'
|
|
1835
|
+
);
|
|
1836
|
+
}
|
|
1837
|
+
})();
|
|
1838
|
+
}
|
|
1839
|
+
await this.bullMqLoad;
|
|
1840
|
+
}
|
|
1841
|
+
/**
|
|
1842
|
+
* Open (or reuse) the `Queue` for a pool. Synchronous — callers `await
|
|
1843
|
+
* loadBullMq()` first so `QueueCtor` is populated.
|
|
1844
|
+
*/
|
|
1845
|
+
queueFor(pool) {
|
|
1846
|
+
if (!this.QueueCtor) {
|
|
1847
|
+
throw new Error("BullMQJobOrchestrator: queueFor called before loadBullMq()");
|
|
1848
|
+
}
|
|
1849
|
+
const name = resolvePoolQueueName(pool, this.bullConfig);
|
|
1850
|
+
let q = this.queues.get(name);
|
|
1851
|
+
if (!q) {
|
|
1852
|
+
q = new this.QueueCtor(name, { connection: this.connection });
|
|
1853
|
+
this.queues.set(name, q);
|
|
1854
|
+
}
|
|
1855
|
+
return q;
|
|
1856
|
+
}
|
|
1857
|
+
flow() {
|
|
1858
|
+
if (!this.FlowProducerCtor) {
|
|
1859
|
+
throw new Error("BullMQJobOrchestrator: flow called before loadBullMq()");
|
|
1860
|
+
}
|
|
1861
|
+
if (!this._flow) {
|
|
1862
|
+
this._flow = new this.FlowProducerCtor({ connection: this.connection });
|
|
1863
|
+
}
|
|
1864
|
+
return this._flow;
|
|
1865
|
+
}
|
|
1866
|
+
// ==========================================================================
|
|
1867
|
+
// start — Postgres insert (super) + BullMQ dispatch
|
|
1868
|
+
// ==========================================================================
|
|
1869
|
+
async start(type, input, opts = {}, tx) {
|
|
1870
|
+
const run = await super.start(type, input, opts, tx);
|
|
1871
|
+
await this.dispatch(run, type);
|
|
1872
|
+
return run;
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Map a `job_run` row onto a BullMQ job via `queue.add`. When the run has a
|
|
1876
|
+
* `parentRunId` we attach it to the parent's existing BullMQ job through the
|
|
1877
|
+
* `parent: { id, queue }` opt — BullMQ then tracks the parent/child link in
|
|
1878
|
+
* its own graph. (The FlowProducer is reserved for whole-tree atomic
|
|
1879
|
+
* submits, exposed as an opt-in extension via `flowProducer()`; runtime
|
|
1880
|
+
* `ctx.spawnChild` is incremental, so `queue.add` with a parent ref is the
|
|
1881
|
+
* correct primitive here.)
|
|
1882
|
+
*
|
|
1883
|
+
* The `jobId` is colon-safe + stable: `sha1(dedupeKey)` when a dedupe key is
|
|
1884
|
+
* present (so the same logical key dedups), else the `job_run.id` UUID
|
|
1885
|
+
* (already colon-free).
|
|
1886
|
+
*
|
|
1887
|
+
* The domain `parentClosePolicy` cascade is still enforced in Postgres by
|
|
1888
|
+
* the shared `cancel` path — BullMQ's parent link is dispatch bookkeeping,
|
|
1889
|
+
* not the authority.
|
|
1890
|
+
*/
|
|
1891
|
+
async dispatch(run, type) {
|
|
1892
|
+
await this.loadBullMq();
|
|
1893
|
+
const def = await this.loadDefinition(type);
|
|
1894
|
+
const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;
|
|
1895
|
+
const jobOpts = {
|
|
1896
|
+
jobId,
|
|
1897
|
+
...this.retryOpts(def),
|
|
1898
|
+
...this.dedupeOpts(run, def)
|
|
1899
|
+
};
|
|
1900
|
+
if (run.parentRunId) {
|
|
1901
|
+
const parentRow = await this.loadRun(run.parentRunId);
|
|
1902
|
+
if (parentRow) {
|
|
1903
|
+
const parentJobId = parentRow.dedupeKey ? sha1JobId(parentRow.dedupeKey) : parentRow.id;
|
|
1904
|
+
jobOpts.parent = {
|
|
1905
|
+
id: parentJobId,
|
|
1906
|
+
queue: resolvePoolQueueName(parentRow.pool, this.bullConfig)
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
const payload = { runId: run.id, type, input: run.input };
|
|
1911
|
+
await this.queueFor(run.pool).add(type, payload, jobOpts);
|
|
1912
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* Opt-in extension (spec §Extensions): expose the FlowProducer for
|
|
1915
|
+
* consumers that want to submit a whole parent/child DAG atomically up
|
|
1916
|
+
* front, rather than incrementally via `ctx.spawnChild`. Backend-specific —
|
|
1917
|
+
* code using it is not portable to the Drizzle backend. Async because it
|
|
1918
|
+
* lazily loads the optional `bullmq` package on first use.
|
|
1919
|
+
*/
|
|
1920
|
+
async flowProducer() {
|
|
1921
|
+
await this.loadBullMq();
|
|
1922
|
+
return this.flow();
|
|
1923
|
+
}
|
|
1924
|
+
retryOpts(def) {
|
|
1925
|
+
const policy = def.retryPolicy;
|
|
1926
|
+
if (!policy) return {};
|
|
1927
|
+
return {
|
|
1928
|
+
attempts: policy.attempts,
|
|
1929
|
+
backoff: {
|
|
1930
|
+
type: policy.backoff === "exponential" ? "exponential" : "fixed",
|
|
1931
|
+
delay: policy.baseMs
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
dedupeOpts(run, def) {
|
|
1936
|
+
if (!run.dedupeKey || !def.dedupeWindowMs) return {};
|
|
1937
|
+
return {
|
|
1938
|
+
deduplication: {
|
|
1939
|
+
id: sha1JobId(run.dedupeKey),
|
|
1940
|
+
ttl: def.dedupeWindowMs
|
|
1941
|
+
}
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
// ==========================================================================
|
|
1945
|
+
// cancel — Postgres cascade (super) + remove from queue
|
|
1946
|
+
// ==========================================================================
|
|
1947
|
+
async cancel(runId, opts = {}) {
|
|
1948
|
+
const target = await this.loadRun(runId);
|
|
1949
|
+
await super.cancel(runId, opts);
|
|
1950
|
+
if (!target) return;
|
|
1951
|
+
await this.loadBullMq();
|
|
1952
|
+
await this.removeFromQueue(target);
|
|
1953
|
+
if (opts.cascade === false) return;
|
|
1954
|
+
const descendants = await this.bullDb.select().from(jobRuns).where(eq5(jobRuns.rootRunId, target.rootRunId));
|
|
1955
|
+
for (const child of descendants) {
|
|
1956
|
+
if (child.id === runId) continue;
|
|
1957
|
+
await this.removeFromQueue(child);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
async removeFromQueue(run) {
|
|
1961
|
+
const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;
|
|
1962
|
+
try {
|
|
1963
|
+
const job = await this.queueFor(run.pool).getJob(jobId);
|
|
1964
|
+
if (job) await job.remove();
|
|
1965
|
+
} catch (err) {
|
|
1966
|
+
this.bullLogger.warn(
|
|
1967
|
+
`cancel: could not remove BullMQ job ${jobId} (pool=${run.pool}): ${err.message}`
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
// ==========================================================================
|
|
1972
|
+
// replay — Postgres reset (super) + re-enqueue
|
|
1973
|
+
// ==========================================================================
|
|
1974
|
+
async replay(runId) {
|
|
1975
|
+
const run = await super.replay(runId);
|
|
1976
|
+
await this.dispatch(run, run.jobType);
|
|
1977
|
+
return run;
|
|
1978
|
+
}
|
|
1979
|
+
// ==========================================================================
|
|
1980
|
+
// Internals
|
|
1981
|
+
// ==========================================================================
|
|
1982
|
+
async loadDefinition(type) {
|
|
1983
|
+
const [def] = await this.bullDb.select().from(jobs).where(eq5(jobs.type, type)).limit(1);
|
|
1984
|
+
if (!def) {
|
|
1985
|
+
throw new Error(`BullMQJobOrchestrator: no job definition for '${type}'`);
|
|
1986
|
+
}
|
|
1987
|
+
return def;
|
|
1988
|
+
}
|
|
1989
|
+
async loadRun(id) {
|
|
1990
|
+
const [row] = await this.bullDb.select().from(jobRuns).where(eq5(jobRuns.id, id)).limit(1);
|
|
1991
|
+
return row ?? null;
|
|
1992
|
+
}
|
|
1993
|
+
/** Close all open queue + flow connections. Called on module destroy. */
|
|
1994
|
+
async closeConnections() {
|
|
1995
|
+
for (const q of this.queues.values()) {
|
|
1996
|
+
await q.close().catch(() => void 0);
|
|
1997
|
+
}
|
|
1998
|
+
this.queues.clear();
|
|
1999
|
+
if (this._flow) {
|
|
2000
|
+
await this._flow.close().catch(() => void 0);
|
|
2001
|
+
this._flow = null;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
};
|
|
2005
|
+
BullMQJobOrchestrator = __decorateClass([
|
|
2006
|
+
Injectable8(),
|
|
2007
|
+
__decorateParam(0, Inject8(DRIZZLE)),
|
|
2008
|
+
__decorateParam(1, Inject8(JOBS_MULTI_TENANT)),
|
|
2009
|
+
__decorateParam(2, Inject8(BULLMQ_CONNECTION)),
|
|
2010
|
+
__decorateParam(3, Optional5()),
|
|
2011
|
+
__decorateParam(3, Inject8(BULLMQ_RESOLVED_CONFIG))
|
|
2012
|
+
], BullMQJobOrchestrator);
|
|
2013
|
+
|
|
1547
2014
|
// runtime/subsystems/jobs/job-orchestrator.memory-backend.ts
|
|
1548
2015
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
1549
|
-
import { Inject as
|
|
2016
|
+
import { Inject as Inject9, Injectable as Injectable9, Logger as Logger5, Optional as Optional6 } from "@nestjs/common";
|
|
1550
2017
|
var QUEUED_RUN_AT = /* @__PURE__ */ new Date(864e13);
|
|
1551
2018
|
var TERMINAL_STATUSES2 = [
|
|
1552
2019
|
"completed",
|
|
@@ -1593,7 +2060,7 @@ var MemoryJobOrchestrator = class {
|
|
|
1593
2060
|
stepService;
|
|
1594
2061
|
multiTenant;
|
|
1595
2062
|
moduleRef;
|
|
1596
|
-
logger = new
|
|
2063
|
+
logger = new Logger5(MemoryJobOrchestrator.name);
|
|
1597
2064
|
mutex = new PromiseMutex();
|
|
1598
2065
|
handlerRegistry = /* @__PURE__ */ new Map();
|
|
1599
2066
|
/**
|
|
@@ -1942,7 +2409,7 @@ var MemoryJobOrchestrator = class {
|
|
|
1942
2409
|
run,
|
|
1943
2410
|
step: this.makeStepFn(run),
|
|
1944
2411
|
spawnChild: this.makeSpawnFn(run),
|
|
1945
|
-
logger: new
|
|
2412
|
+
logger: new Logger5(`JobRun:${run.id}`)
|
|
1946
2413
|
};
|
|
1947
2414
|
const attemptsBefore = run.attempts ?? 0;
|
|
1948
2415
|
try {
|
|
@@ -2115,9 +2582,9 @@ var MemoryJobOrchestrator = class {
|
|
|
2115
2582
|
}
|
|
2116
2583
|
};
|
|
2117
2584
|
MemoryJobOrchestrator = __decorateClass([
|
|
2118
|
-
|
|
2119
|
-
__decorateParam(2,
|
|
2120
|
-
__decorateParam(3,
|
|
2585
|
+
Injectable9(),
|
|
2586
|
+
__decorateParam(2, Inject9(JOBS_MULTI_TENANT)),
|
|
2587
|
+
__decorateParam(3, Optional6())
|
|
2121
2588
|
], MemoryJobOrchestrator);
|
|
2122
2589
|
function classifyError(err, policy, currentAttempts) {
|
|
2123
2590
|
if (!policy) return "fail";
|
|
@@ -2151,7 +2618,7 @@ function serialiseError(err, attempt, retryable) {
|
|
|
2151
2618
|
}
|
|
2152
2619
|
|
|
2153
2620
|
// runtime/subsystems/jobs/job-run-service.memory-backend.ts
|
|
2154
|
-
import { Inject as
|
|
2621
|
+
import { Inject as Inject10, Injectable as Injectable10 } from "@nestjs/common";
|
|
2155
2622
|
var NON_TERMINAL_STATUSES2 = [
|
|
2156
2623
|
"pending",
|
|
2157
2624
|
"running",
|
|
@@ -2268,6 +2735,38 @@ var MemoryJobRunService = class {
|
|
|
2268
2735
|
createdAt: r.createdAt
|
|
2269
2736
|
}));
|
|
2270
2737
|
}
|
|
2738
|
+
async listJobRuns(query = {}) {
|
|
2739
|
+
const limit = clampLimit(query.limit);
|
|
2740
|
+
const tenantCheck = this.tenantPredicate("listJobRuns", query.tenantId);
|
|
2741
|
+
const keyset = query.cursor ? decodeKeysetCursor(query.cursor) : null;
|
|
2742
|
+
const matched = [];
|
|
2743
|
+
for (const r of this.store.runs.values()) {
|
|
2744
|
+
if (tenantCheck && !tenantCheck(r)) continue;
|
|
2745
|
+
if (query.poolId && r.pool !== query.poolId) continue;
|
|
2746
|
+
if (query.rootRunId && r.rootRunId !== query.rootRunId) continue;
|
|
2747
|
+
if (query.status && r.status !== query.status) continue;
|
|
2748
|
+
if (query.since && r.createdAt.getTime() < query.since.getTime()) continue;
|
|
2749
|
+
matched.push(r);
|
|
2750
|
+
}
|
|
2751
|
+
matched.sort((a, b) => {
|
|
2752
|
+
const dt = b.createdAt.getTime() - a.createdAt.getTime();
|
|
2753
|
+
if (dt !== 0) return dt;
|
|
2754
|
+
return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
|
|
2755
|
+
});
|
|
2756
|
+
const seeked = keyset ? matched.filter((r) => {
|
|
2757
|
+
const ct = r.createdAt.getTime();
|
|
2758
|
+
const kt = keyset.createdAt.getTime();
|
|
2759
|
+
if (ct < kt) return true;
|
|
2760
|
+
if (ct > kt) return false;
|
|
2761
|
+
return r.id < keyset.id;
|
|
2762
|
+
}) : matched;
|
|
2763
|
+
const hasMore = seeked.length > limit;
|
|
2764
|
+
const page = hasMore ? seeked.slice(0, limit) : seeked;
|
|
2765
|
+
const items = page.map(toJobRunSummary);
|
|
2766
|
+
const last = page[page.length - 1];
|
|
2767
|
+
const nextCursor = hasMore && last ? encodeKeysetCursor({ createdAt: last.createdAt, id: last.id }) : null;
|
|
2768
|
+
return { items, nextCursor };
|
|
2769
|
+
}
|
|
2271
2770
|
/**
|
|
2272
2771
|
* Direct lookup. Not on the protocol — concrete-class convenience for
|
|
2273
2772
|
* tests. Matches `DrizzleJobRunService.findByRootRunId` in spirit; both
|
|
@@ -2286,9 +2785,9 @@ var MemoryJobRunService = class {
|
|
|
2286
2785
|
}
|
|
2287
2786
|
};
|
|
2288
2787
|
MemoryJobRunService = __decorateClass([
|
|
2289
|
-
|
|
2290
|
-
__decorateParam(1,
|
|
2291
|
-
__decorateParam(2,
|
|
2788
|
+
Injectable10(),
|
|
2789
|
+
__decorateParam(1, Inject10(JOB_ORCHESTRATOR)),
|
|
2790
|
+
__decorateParam(2, Inject10(JOBS_MULTI_TENANT))
|
|
2292
2791
|
], MemoryJobRunService);
|
|
2293
2792
|
function compareBy(a, b, order) {
|
|
2294
2793
|
switch (order) {
|
|
@@ -2306,7 +2805,7 @@ function compareBy(a, b, order) {
|
|
|
2306
2805
|
|
|
2307
2806
|
// runtime/subsystems/jobs/job-step-service.memory-backend.ts
|
|
2308
2807
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
2309
|
-
import { Injectable as
|
|
2808
|
+
import { Injectable as Injectable11 } from "@nestjs/common";
|
|
2310
2809
|
var MemoryJobStepService = class {
|
|
2311
2810
|
constructor(store) {
|
|
2312
2811
|
this.store = store;
|
|
@@ -2399,7 +2898,7 @@ var MemoryJobStepService = class {
|
|
|
2399
2898
|
}
|
|
2400
2899
|
};
|
|
2401
2900
|
MemoryJobStepService = __decorateClass([
|
|
2402
|
-
|
|
2901
|
+
Injectable11()
|
|
2403
2902
|
], MemoryJobStepService);
|
|
2404
2903
|
|
|
2405
2904
|
// runtime/subsystems/jobs/memory-job-store.ts
|
|
@@ -2421,7 +2920,6 @@ var MemoryJobStore = class {
|
|
|
2421
2920
|
// runtime/subsystems/jobs/jobs-domain.module.ts
|
|
2422
2921
|
var JobsDomainModule = class {
|
|
2423
2922
|
static forRoot(opts) {
|
|
2424
|
-
void opts.extensions;
|
|
2425
2923
|
const multiTenant = opts.multiTenant ?? false;
|
|
2426
2924
|
const providers = [
|
|
2427
2925
|
// JOB-8 — boolean provider consumed by the four service-layer backends.
|
|
@@ -2440,21 +2938,32 @@ var JobsDomainModule = class {
|
|
|
2440
2938
|
providers.push({ provide: JOB_ORCHESTRATOR, useExisting: MemoryJobOrchestrator });
|
|
2441
2939
|
providers.push(MemoryJobRunService);
|
|
2442
2940
|
providers.push({ provide: JOB_RUN_SERVICE, useExisting: MemoryJobRunService });
|
|
2941
|
+
} else if (opts.backend === "bullmq") {
|
|
2942
|
+
const resolved = resolveBullMqConfig(opts.extensions?.bullmq);
|
|
2943
|
+
providers.push({ provide: BULLMQ_CONNECTION, useValue: resolved.connection });
|
|
2944
|
+
providers.push({ provide: BULLMQ_RESOLVED_CONFIG, useValue: resolved });
|
|
2945
|
+
providers.push({ provide: JOB_ORCHESTRATOR, useClass: BullMQJobOrchestrator });
|
|
2946
|
+
providers.push({ provide: JOB_RUN_SERVICE, useClass: DrizzleJobRunService });
|
|
2947
|
+
providers.push({ provide: JOB_STEP_SERVICE, useClass: DrizzleJobStepService });
|
|
2443
2948
|
} else {
|
|
2444
2949
|
providers.push({ provide: JOB_ORCHESTRATOR, useClass: DrizzleJobOrchestrator });
|
|
2445
2950
|
providers.push({ provide: JOB_RUN_SERVICE, useClass: DrizzleJobRunService });
|
|
2446
2951
|
providers.push({ provide: JOB_STEP_SERVICE, useClass: DrizzleJobStepService });
|
|
2447
2952
|
}
|
|
2953
|
+
const exports = [
|
|
2954
|
+
JOB_ORCHESTRATOR,
|
|
2955
|
+
JOB_RUN_SERVICE,
|
|
2956
|
+
JOB_STEP_SERVICE,
|
|
2957
|
+
JOBS_MULTI_TENANT
|
|
2958
|
+
];
|
|
2959
|
+
if (opts.backend === "bullmq") {
|
|
2960
|
+
exports.push(BULLMQ_CONNECTION, BULLMQ_RESOLVED_CONFIG);
|
|
2961
|
+
}
|
|
2448
2962
|
return {
|
|
2449
2963
|
module: JobsDomainModule,
|
|
2450
2964
|
global: true,
|
|
2451
2965
|
providers,
|
|
2452
|
-
exports
|
|
2453
|
-
JOB_ORCHESTRATOR,
|
|
2454
|
-
JOB_RUN_SERVICE,
|
|
2455
|
-
JOB_STEP_SERVICE,
|
|
2456
|
-
JOBS_MULTI_TENANT
|
|
2457
|
-
]
|
|
2966
|
+
exports
|
|
2458
2967
|
};
|
|
2459
2968
|
}
|
|
2460
2969
|
};
|
|
@@ -2462,128 +2971,9 @@ JobsDomainModule = __decorateClass([
|
|
|
2462
2971
|
Module({})
|
|
2463
2972
|
], JobsDomainModule);
|
|
2464
2973
|
|
|
2465
|
-
// runtime/subsystems/jobs/pool-config.loader.ts
|
|
2466
|
-
import { existsSync, readFileSync } from "fs";
|
|
2467
|
-
import { resolve } from "path";
|
|
2468
|
-
import { parse as parseYaml } from "yaml";
|
|
2469
|
-
var FRAMEWORK_POOLS = Object.freeze({
|
|
2470
|
-
events_inbound: Object.freeze({
|
|
2471
|
-
queue: "jobs-events-inbound",
|
|
2472
|
-
concurrency: 20,
|
|
2473
|
-
reserved: true,
|
|
2474
|
-
description: "Inbound events drain (events subsystem outbox)."
|
|
2475
|
-
}),
|
|
2476
|
-
events_change: Object.freeze({
|
|
2477
|
-
queue: "jobs-events-change",
|
|
2478
|
-
concurrency: 30,
|
|
2479
|
-
reserved: true,
|
|
2480
|
-
description: "Change events drain (events subsystem outbox)."
|
|
2481
|
-
}),
|
|
2482
|
-
events_outbound: Object.freeze({
|
|
2483
|
-
queue: "jobs-events-outbound",
|
|
2484
|
-
concurrency: 10,
|
|
2485
|
-
reserved: true,
|
|
2486
|
-
description: "Outbound events drain (events subsystem outbox)."
|
|
2487
|
-
}),
|
|
2488
|
-
interactive: Object.freeze({
|
|
2489
|
-
queue: "jobs-interactive",
|
|
2490
|
-
concurrency: 20,
|
|
2491
|
-
reserved: false,
|
|
2492
|
-
description: "User-facing latency-sensitive jobs."
|
|
2493
|
-
}),
|
|
2494
|
-
batch: Object.freeze({
|
|
2495
|
-
queue: "jobs-batch",
|
|
2496
|
-
concurrency: 5,
|
|
2497
|
-
reserved: false,
|
|
2498
|
-
description: "Default pool for background jobs."
|
|
2499
|
-
})
|
|
2500
|
-
});
|
|
2501
|
-
var RESERVED_POOL_NAMES = new Set(
|
|
2502
|
-
Object.entries(FRAMEWORK_POOLS).filter(([, def]) => def.reserved).map(([name]) => name)
|
|
2503
|
-
);
|
|
2504
|
-
var cache = /* @__PURE__ */ new Map();
|
|
2505
|
-
function loadPoolConfig(configPath) {
|
|
2506
|
-
const resolved = resolve(configPath ?? `${process.cwd()}/codegen.config.yaml`);
|
|
2507
|
-
const cached = cache.get(resolved);
|
|
2508
|
-
if (cached) return cached;
|
|
2509
|
-
const merged = /* @__PURE__ */ new Map();
|
|
2510
|
-
for (const [name, def] of Object.entries(FRAMEWORK_POOLS)) {
|
|
2511
|
-
merged.set(name, { ...def });
|
|
2512
|
-
}
|
|
2513
|
-
if (!existsSync(resolved)) {
|
|
2514
|
-
cache.set(resolved, merged);
|
|
2515
|
-
return merged;
|
|
2516
|
-
}
|
|
2517
|
-
let raw;
|
|
2518
|
-
try {
|
|
2519
|
-
raw = parseYaml(readFileSync(resolved, "utf8"));
|
|
2520
|
-
} catch (err) {
|
|
2521
|
-
throw new Error(
|
|
2522
|
-
`pool-config.loader: failed to parse YAML at ${resolved}: ${err.message}`
|
|
2523
|
-
);
|
|
2524
|
-
}
|
|
2525
|
-
const userPools = extractUserPools(raw);
|
|
2526
|
-
for (const [name, userDef] of Object.entries(userPools)) {
|
|
2527
|
-
const existing = merged.get(name);
|
|
2528
|
-
if (existing) {
|
|
2529
|
-
const next = {
|
|
2530
|
-
queue: existing.queue,
|
|
2531
|
-
concurrency: typeof userDef.concurrency === "number" ? userDef.concurrency : existing.concurrency,
|
|
2532
|
-
reserved: existing.reserved,
|
|
2533
|
-
description: userDef.description ?? existing.description
|
|
2534
|
-
};
|
|
2535
|
-
merged.set(name, next);
|
|
2536
|
-
continue;
|
|
2537
|
-
}
|
|
2538
|
-
if (typeof userDef.queue !== "string" || userDef.queue.length === 0) {
|
|
2539
|
-
throw new Error(
|
|
2540
|
-
`pool-config.loader: pool '${name}' must declare a non-empty 'queue'.`
|
|
2541
|
-
);
|
|
2542
|
-
}
|
|
2543
|
-
if (typeof userDef.concurrency !== "number" || userDef.concurrency <= 0) {
|
|
2544
|
-
throw new Error(
|
|
2545
|
-
`pool-config.loader: pool '${name}' must declare a positive 'concurrency'.`
|
|
2546
|
-
);
|
|
2547
|
-
}
|
|
2548
|
-
if (userDef.reserved === true) {
|
|
2549
|
-
throw new Error(
|
|
2550
|
-
`pool-config.loader: user-defined pool '${name}' cannot set 'reserved: true' \u2014 reserved is framework-only.`
|
|
2551
|
-
);
|
|
2552
|
-
}
|
|
2553
|
-
merged.set(name, {
|
|
2554
|
-
queue: userDef.queue,
|
|
2555
|
-
concurrency: userDef.concurrency,
|
|
2556
|
-
reserved: false,
|
|
2557
|
-
description: userDef.description
|
|
2558
|
-
});
|
|
2559
|
-
}
|
|
2560
|
-
cache.set(resolved, merged);
|
|
2561
|
-
return merged;
|
|
2562
|
-
}
|
|
2563
|
-
function allNonReservedPoolNames(config) {
|
|
2564
|
-
const out = [];
|
|
2565
|
-
for (const [name, def] of config) {
|
|
2566
|
-
if (!def.reserved) out.push(name);
|
|
2567
|
-
}
|
|
2568
|
-
return out;
|
|
2569
|
-
}
|
|
2570
|
-
function extractUserPools(raw) {
|
|
2571
|
-
if (!raw || typeof raw !== "object") return {};
|
|
2572
|
-
const jobs2 = raw.jobs;
|
|
2573
|
-
if (!jobs2 || typeof jobs2 !== "object") return {};
|
|
2574
|
-
const pools = jobs2.pools;
|
|
2575
|
-
if (!pools || typeof pools !== "object") return {};
|
|
2576
|
-
const out = {};
|
|
2577
|
-
for (const [name, def] of Object.entries(pools)) {
|
|
2578
|
-
if (!def || typeof def !== "object") continue;
|
|
2579
|
-
out[name] = def;
|
|
2580
|
-
}
|
|
2581
|
-
return out;
|
|
2582
|
-
}
|
|
2583
|
-
|
|
2584
2974
|
// runtime/subsystems/jobs/job-worker.ts
|
|
2585
|
-
import { Inject as
|
|
2586
|
-
import { and as and5, asc as asc2, desc as desc3, eq as
|
|
2975
|
+
import { Inject as Inject11, Injectable as Injectable12, Logger as Logger6 } from "@nestjs/common";
|
|
2976
|
+
import { and as and5, asc as asc2, desc as desc3, eq as eq6, inArray as inArray3, lt as lt2, lte, sql as sql7 } from "drizzle-orm";
|
|
2587
2977
|
var JOB_WORKER_OPTIONS = /* @__PURE__ */ Symbol("JOB_WORKER_OPTIONS");
|
|
2588
2978
|
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
2589
2979
|
var DEFAULT_STALE_SWEEPER_INTERVAL_MS = 6e4;
|
|
@@ -2646,7 +3036,7 @@ var JobWorker = class {
|
|
|
2646
3036
|
stepService;
|
|
2647
3037
|
options;
|
|
2648
3038
|
moduleRef;
|
|
2649
|
-
logger = new
|
|
3039
|
+
logger = new Logger6(JobWorker.name);
|
|
2650
3040
|
shuttingDown = false;
|
|
2651
3041
|
inFlight = /* @__PURE__ */ new Set();
|
|
2652
3042
|
pollTimer = null;
|
|
@@ -2687,7 +3077,7 @@ var JobWorker = class {
|
|
|
2687
3077
|
await this.drainInFlight();
|
|
2688
3078
|
try {
|
|
2689
3079
|
await this.db.update(jobRuns).set({ status: "pending", claimedAt: null, startedAt: null }).where(
|
|
2690
|
-
and5(
|
|
3080
|
+
and5(eq6(jobRuns.status, "running"), eq6(jobRuns.pool, this.options.pool))
|
|
2691
3081
|
);
|
|
2692
3082
|
} catch (err) {
|
|
2693
3083
|
this.logger.error(`shutdown reset failed: ${err.message}`);
|
|
@@ -2737,8 +3127,8 @@ var JobWorker = class {
|
|
|
2737
3127
|
return this.db.transaction(async (tx) => {
|
|
2738
3128
|
const candidates = await tx.select({ id: jobRuns.id }).from(jobRuns).where(
|
|
2739
3129
|
and5(
|
|
2740
|
-
|
|
2741
|
-
|
|
3130
|
+
eq6(jobRuns.status, "pending"),
|
|
3131
|
+
eq6(jobRuns.pool, pool),
|
|
2742
3132
|
lte(jobRuns.runAt, /* @__PURE__ */ new Date())
|
|
2743
3133
|
)
|
|
2744
3134
|
).orderBy(desc3(jobRuns.priority), asc2(jobRuns.runAt)).limit(1).for("update", { skipLocked: true });
|
|
@@ -2749,7 +3139,7 @@ var JobWorker = class {
|
|
|
2749
3139
|
claimedAt: /* @__PURE__ */ new Date(),
|
|
2750
3140
|
startedAt: /* @__PURE__ */ new Date(),
|
|
2751
3141
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2752
|
-
}).where(
|
|
3142
|
+
}).where(eq6(jobRuns.id, candidate.id)).returning();
|
|
2753
3143
|
return claimed ?? null;
|
|
2754
3144
|
});
|
|
2755
3145
|
}
|
|
@@ -2767,7 +3157,7 @@ var JobWorker = class {
|
|
|
2767
3157
|
await this.db.transaction(async (tx) => {
|
|
2768
3158
|
const threshold = new Date(Date.now() - this.staleThresholdMs);
|
|
2769
3159
|
const stale = await tx.select({ id: jobRuns.id }).from(jobRuns).where(
|
|
2770
|
-
and5(
|
|
3160
|
+
and5(eq6(jobRuns.status, "running"), lt2(jobRuns.claimedAt, threshold))
|
|
2771
3161
|
).for("update", { skipLocked: true });
|
|
2772
3162
|
if (stale.length === 0) return;
|
|
2773
3163
|
const ids = stale.map((r) => r.id);
|
|
@@ -2800,8 +3190,8 @@ var JobWorker = class {
|
|
|
2800
3190
|
if (claimed.concurrencyKey) {
|
|
2801
3191
|
const inflight = await this.db.select({ id: jobRuns.id }).from(jobRuns).where(
|
|
2802
3192
|
and5(
|
|
2803
|
-
|
|
2804
|
-
|
|
3193
|
+
eq6(jobRuns.concurrencyKey, claimed.concurrencyKey),
|
|
3194
|
+
eq6(jobRuns.status, "running")
|
|
2805
3195
|
)
|
|
2806
3196
|
);
|
|
2807
3197
|
const other = inflight.find((r) => r.id !== claimed.id);
|
|
@@ -2811,7 +3201,7 @@ var JobWorker = class {
|
|
|
2811
3201
|
claimedAt: null,
|
|
2812
3202
|
startedAt: null,
|
|
2813
3203
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2814
|
-
}).where(
|
|
3204
|
+
}).where(eq6(jobRuns.id, claimed.id));
|
|
2815
3205
|
return;
|
|
2816
3206
|
}
|
|
2817
3207
|
}
|
|
@@ -2826,7 +3216,7 @@ var JobWorker = class {
|
|
|
2826
3216
|
run: claimed,
|
|
2827
3217
|
step: this.makeStepFn(claimed),
|
|
2828
3218
|
spawnChild: this.makeSpawnFn(claimed),
|
|
2829
|
-
logger: new
|
|
3219
|
+
logger: new Logger6(`JobRun:${claimed.id}`)
|
|
2830
3220
|
};
|
|
2831
3221
|
const attemptsBefore = claimed.attempts ?? 0;
|
|
2832
3222
|
try {
|
|
@@ -2837,7 +3227,7 @@ var JobWorker = class {
|
|
|
2837
3227
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
2838
3228
|
updatedAt: /* @__PURE__ */ new Date(),
|
|
2839
3229
|
attempts: attemptsBefore + 1
|
|
2840
|
-
}).where(
|
|
3230
|
+
}).where(eq6(jobRuns.id, claimed.id));
|
|
2841
3231
|
} catch (err) {
|
|
2842
3232
|
const policy = meta.retry;
|
|
2843
3233
|
const decision = classifyError2(err, policy, attemptsBefore);
|
|
@@ -2852,7 +3242,7 @@ var JobWorker = class {
|
|
|
2852
3242
|
claimedAt: null,
|
|
2853
3243
|
error: serialiseError2(err, nextAttempts, true),
|
|
2854
3244
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2855
|
-
}).where(
|
|
3245
|
+
}).where(eq6(jobRuns.id, claimed.id));
|
|
2856
3246
|
} else {
|
|
2857
3247
|
await this.markFailed(claimed, err, nextAttempts);
|
|
2858
3248
|
}
|
|
@@ -2865,7 +3255,7 @@ var JobWorker = class {
|
|
|
2865
3255
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
2866
3256
|
error: serialiseError2(err, finalAttempts, false),
|
|
2867
3257
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2868
|
-
}).where(
|
|
3258
|
+
}).where(eq6(jobRuns.id, claimed.id));
|
|
2869
3259
|
if (claimed.parentClosePolicy === "terminate") {
|
|
2870
3260
|
try {
|
|
2871
3261
|
await this.orchestrator.cancel(claimed.id, {
|
|
@@ -2968,25 +3358,228 @@ var JobWorker = class {
|
|
|
2968
3358
|
// ============================================================================
|
|
2969
3359
|
};
|
|
2970
3360
|
JobWorker = __decorateClass([
|
|
2971
|
-
|
|
2972
|
-
__decorateParam(0,
|
|
2973
|
-
__decorateParam(1,
|
|
2974
|
-
__decorateParam(2,
|
|
2975
|
-
__decorateParam(3,
|
|
2976
|
-
__decorateParam(4,
|
|
3361
|
+
Injectable12(),
|
|
3362
|
+
__decorateParam(0, Inject11(DRIZZLE)),
|
|
3363
|
+
__decorateParam(1, Inject11(JOB_ORCHESTRATOR)),
|
|
3364
|
+
__decorateParam(2, Inject11(JOB_RUN_SERVICE)),
|
|
3365
|
+
__decorateParam(3, Inject11(JOB_STEP_SERVICE)),
|
|
3366
|
+
__decorateParam(4, Inject11(JOB_WORKER_OPTIONS))
|
|
2977
3367
|
], JobWorker);
|
|
2978
3368
|
|
|
3369
|
+
// runtime/subsystems/jobs/job-worker.bullmq-backend.ts
|
|
3370
|
+
import { Logger as Logger7 } from "@nestjs/common";
|
|
3371
|
+
import { eq as eq7 } from "drizzle-orm";
|
|
3372
|
+
function serialiseError3(err, attempt, retryable) {
|
|
3373
|
+
const e = err;
|
|
3374
|
+
return {
|
|
3375
|
+
message: e?.message ?? String(err),
|
|
3376
|
+
stack: e?.stack,
|
|
3377
|
+
retryable,
|
|
3378
|
+
attempt
|
|
3379
|
+
};
|
|
3380
|
+
}
|
|
3381
|
+
var BullMQJobWorker = class _BullMQJobWorker {
|
|
3382
|
+
constructor(db, orchestrator, stepService, options, moduleRef) {
|
|
3383
|
+
this.db = db;
|
|
3384
|
+
this.orchestrator = orchestrator;
|
|
3385
|
+
this.stepService = stepService;
|
|
3386
|
+
this.options = options;
|
|
3387
|
+
this.moduleRef = moduleRef;
|
|
3388
|
+
}
|
|
3389
|
+
db;
|
|
3390
|
+
orchestrator;
|
|
3391
|
+
stepService;
|
|
3392
|
+
options;
|
|
3393
|
+
moduleRef;
|
|
3394
|
+
logger = new Logger7(_BullMQJobWorker.name);
|
|
3395
|
+
worker = null;
|
|
3396
|
+
async onModuleInit() {
|
|
3397
|
+
let WorkerCtor;
|
|
3398
|
+
try {
|
|
3399
|
+
const mod = await import("bullmq");
|
|
3400
|
+
WorkerCtor = mod.Worker;
|
|
3401
|
+
} catch {
|
|
3402
|
+
throw new Error(
|
|
3403
|
+
'BullMQ backend requires the "bullmq" package. Install it with: npm install bullmq'
|
|
3404
|
+
);
|
|
3405
|
+
}
|
|
3406
|
+
this.worker = new WorkerCtor(
|
|
3407
|
+
this.options.queueName,
|
|
3408
|
+
(job) => this.process(job),
|
|
3409
|
+
{
|
|
3410
|
+
connection: this.options.connection,
|
|
3411
|
+
concurrency: this.options.concurrency
|
|
3412
|
+
}
|
|
3413
|
+
);
|
|
3414
|
+
this.worker.on("failed", (job, err) => {
|
|
3415
|
+
if (!job) return;
|
|
3416
|
+
const attemptsMade = job.attemptsMade;
|
|
3417
|
+
const maxAttempts = job.opts.attempts ?? 1;
|
|
3418
|
+
if (attemptsMade >= maxAttempts) {
|
|
3419
|
+
void this.markFailed(job.data.runId, err, attemptsMade);
|
|
3420
|
+
}
|
|
3421
|
+
});
|
|
3422
|
+
this.logger.log(
|
|
3423
|
+
`BullMQ worker started: pool='${this.options.pool}' queue='${this.options.queueName}' concurrency=${this.options.concurrency}`
|
|
3424
|
+
);
|
|
3425
|
+
}
|
|
3426
|
+
async onModuleDestroy() {
|
|
3427
|
+
if (this.worker) {
|
|
3428
|
+
await this.worker.close();
|
|
3429
|
+
this.worker = null;
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
/**
|
|
3433
|
+
* Process one BullMQ job. Returns the handler output (stored by BullMQ as
|
|
3434
|
+
* the job return value AND written to `job_run.output`). Throws on handler
|
|
3435
|
+
* failure so BullMQ applies the retry policy.
|
|
3436
|
+
*/
|
|
3437
|
+
async process(job) {
|
|
3438
|
+
const { runId } = job.data;
|
|
3439
|
+
const [row] = await this.db.select().from(jobRuns).where(eq7(jobRuns.id, runId)).limit(1);
|
|
3440
|
+
if (!row) {
|
|
3441
|
+
this.logger.warn(`process: job_run ${runId} not found; skipping`);
|
|
3442
|
+
return {};
|
|
3443
|
+
}
|
|
3444
|
+
const run = row;
|
|
3445
|
+
if (run.status === "canceled") {
|
|
3446
|
+
return {};
|
|
3447
|
+
}
|
|
3448
|
+
const registryEntry = JOB_HANDLER_REGISTRY.get(run.jobType);
|
|
3449
|
+
if (!registryEntry) {
|
|
3450
|
+
throw new Error(
|
|
3451
|
+
`No handler registered for jobType='${run.jobType}' (run ${run.id})`
|
|
3452
|
+
);
|
|
3453
|
+
}
|
|
3454
|
+
await this.db.update(jobRuns).set({
|
|
3455
|
+
status: "running",
|
|
3456
|
+
claimedAt: /* @__PURE__ */ new Date(),
|
|
3457
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
3458
|
+
attempts: job.attemptsMade + 1,
|
|
3459
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3460
|
+
}).where(eq7(jobRuns.id, run.id));
|
|
3461
|
+
const HandlerClass = registryEntry.handlerClass;
|
|
3462
|
+
const handler = this.moduleRef.get(
|
|
3463
|
+
HandlerClass,
|
|
3464
|
+
{ strict: false }
|
|
3465
|
+
);
|
|
3466
|
+
const ctx = {
|
|
3467
|
+
input: run.input,
|
|
3468
|
+
run,
|
|
3469
|
+
step: this.makeStepFn(run),
|
|
3470
|
+
spawnChild: this.makeSpawnFn(run),
|
|
3471
|
+
logger: new Logger7(`JobRun:${run.id}`)
|
|
3472
|
+
};
|
|
3473
|
+
const output = await handler.run(ctx);
|
|
3474
|
+
await this.db.update(jobRuns).set({
|
|
3475
|
+
status: "completed",
|
|
3476
|
+
output: output ?? {},
|
|
3477
|
+
finishedAt: /* @__PURE__ */ new Date(),
|
|
3478
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3479
|
+
}).where(eq7(jobRuns.id, run.id));
|
|
3480
|
+
return output ?? {};
|
|
3481
|
+
}
|
|
3482
|
+
async markFailed(runId, err, finalAttempts) {
|
|
3483
|
+
const [row] = await this.db.select().from(jobRuns).where(eq7(jobRuns.id, runId)).limit(1);
|
|
3484
|
+
if (!row) return;
|
|
3485
|
+
const run = row;
|
|
3486
|
+
await this.db.update(jobRuns).set({
|
|
3487
|
+
status: "failed",
|
|
3488
|
+
attempts: finalAttempts,
|
|
3489
|
+
finishedAt: /* @__PURE__ */ new Date(),
|
|
3490
|
+
error: serialiseError3(err, finalAttempts, false),
|
|
3491
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3492
|
+
}).where(eq7(jobRuns.id, runId));
|
|
3493
|
+
if (run.parentClosePolicy === "terminate") {
|
|
3494
|
+
try {
|
|
3495
|
+
await this.orchestrator.cancel(run.id, {
|
|
3496
|
+
cascade: true,
|
|
3497
|
+
reason: "parent-failed",
|
|
3498
|
+
tenantId: run.tenantId
|
|
3499
|
+
});
|
|
3500
|
+
} catch (cascadeErr) {
|
|
3501
|
+
this.logger.warn(
|
|
3502
|
+
`cascade on failed run ${run.id}: ${cascadeErr.message}`
|
|
3503
|
+
);
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
// ── ctx.step / ctx.spawnChild (mirror JobWorker) ──────────────────────────
|
|
3508
|
+
makeStepFn(run) {
|
|
3509
|
+
return async (stepId, fn, _opts) => {
|
|
3510
|
+
void _opts;
|
|
3511
|
+
const existing = await this.stepService.findStep(run.id, stepId);
|
|
3512
|
+
if (existing?.status === "completed") {
|
|
3513
|
+
return existing.output;
|
|
3514
|
+
}
|
|
3515
|
+
const nextAttempts = (existing?.attempts ?? 0) + 1;
|
|
3516
|
+
const seq = nextAttempts;
|
|
3517
|
+
await this.stepService.recordStep({
|
|
3518
|
+
jobRunId: run.id,
|
|
3519
|
+
stepId,
|
|
3520
|
+
kind: "task",
|
|
3521
|
+
seq,
|
|
3522
|
+
status: "running",
|
|
3523
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
3524
|
+
attempts: nextAttempts
|
|
3525
|
+
});
|
|
3526
|
+
try {
|
|
3527
|
+
const output = await fn();
|
|
3528
|
+
await this.stepService.recordStep({
|
|
3529
|
+
jobRunId: run.id,
|
|
3530
|
+
stepId,
|
|
3531
|
+
kind: "task",
|
|
3532
|
+
seq,
|
|
3533
|
+
status: "completed",
|
|
3534
|
+
output,
|
|
3535
|
+
finishedAt: /* @__PURE__ */ new Date(),
|
|
3536
|
+
attempts: nextAttempts
|
|
3537
|
+
});
|
|
3538
|
+
return output;
|
|
3539
|
+
} catch (err) {
|
|
3540
|
+
await this.stepService.recordStep({
|
|
3541
|
+
jobRunId: run.id,
|
|
3542
|
+
stepId,
|
|
3543
|
+
kind: "task",
|
|
3544
|
+
seq,
|
|
3545
|
+
status: "failed",
|
|
3546
|
+
error: serialiseError3(err, nextAttempts, false),
|
|
3547
|
+
finishedAt: /* @__PURE__ */ new Date(),
|
|
3548
|
+
attempts: nextAttempts
|
|
3549
|
+
});
|
|
3550
|
+
throw err;
|
|
3551
|
+
}
|
|
3552
|
+
};
|
|
3553
|
+
}
|
|
3554
|
+
makeSpawnFn(run) {
|
|
3555
|
+
return async (type, input, opts) => {
|
|
3556
|
+
return this.orchestrator.start(type, input, {
|
|
3557
|
+
parentRunId: run.id,
|
|
3558
|
+
parentClosePolicy: opts?.closePolicy,
|
|
3559
|
+
runAt: opts?.runAt,
|
|
3560
|
+
priority: opts?.priority,
|
|
3561
|
+
tags: opts?.tags,
|
|
3562
|
+
triggerSource: "parent",
|
|
3563
|
+
triggerRef: run.id,
|
|
3564
|
+
tenantId: run.tenantId
|
|
3565
|
+
});
|
|
3566
|
+
};
|
|
3567
|
+
}
|
|
3568
|
+
};
|
|
3569
|
+
|
|
2979
3570
|
// runtime/subsystems/jobs/job-worker.module.ts
|
|
2980
3571
|
var DEFAULT_SHUTDOWN_TIMEOUT_MS2 = 3e4;
|
|
2981
3572
|
var JOB_WORKER_MODULE_OPTIONS = /* @__PURE__ */ Symbol("JOB_WORKER_MODULE_OPTIONS");
|
|
2982
3573
|
var JobWorkerOrchestrator = class {
|
|
2983
|
-
constructor(orchestrator, runService, stepService, options, db = null, moduleRef) {
|
|
3574
|
+
constructor(orchestrator, runService, stepService, options, db = null, moduleRef, bullConnection = null, bullConfig = null) {
|
|
2984
3575
|
this.orchestrator = orchestrator;
|
|
2985
3576
|
this.runService = runService;
|
|
2986
3577
|
this.stepService = stepService;
|
|
2987
3578
|
this.options = options;
|
|
2988
3579
|
this.db = db;
|
|
2989
3580
|
this.moduleRef = moduleRef;
|
|
3581
|
+
this.bullConnection = bullConnection;
|
|
3582
|
+
this.bullConfig = bullConfig;
|
|
2990
3583
|
}
|
|
2991
3584
|
orchestrator;
|
|
2992
3585
|
runService;
|
|
@@ -2994,7 +3587,9 @@ var JobWorkerOrchestrator = class {
|
|
|
2994
3587
|
options;
|
|
2995
3588
|
db;
|
|
2996
3589
|
moduleRef;
|
|
2997
|
-
|
|
3590
|
+
bullConnection;
|
|
3591
|
+
bullConfig;
|
|
3592
|
+
logger = new Logger8(JobWorkerOrchestrator.name);
|
|
2998
3593
|
workers = [];
|
|
2999
3594
|
// ============================================================================
|
|
3000
3595
|
// Lifecycle
|
|
@@ -3011,7 +3606,7 @@ var JobWorkerOrchestrator = class {
|
|
|
3011
3606
|
if (backend !== "memory" && orphaned.length > 0) {
|
|
3012
3607
|
throw new BootValidationError(orphaned);
|
|
3013
3608
|
}
|
|
3014
|
-
const activePools = this.options.pools
|
|
3609
|
+
const activePools = this.options.pools ? this.options.pools : this.options.allPools ? allPoolNames(poolConfig) : allNonReservedPoolNames(poolConfig);
|
|
3015
3610
|
for (const poolName of activePools) {
|
|
3016
3611
|
const def = poolConfig.get(poolName);
|
|
3017
3612
|
if (!def) {
|
|
@@ -3024,11 +3619,11 @@ var JobWorkerOrchestrator = class {
|
|
|
3024
3619
|
concurrency: def.concurrency,
|
|
3025
3620
|
shutdownTimeoutMs: this.options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS2
|
|
3026
3621
|
};
|
|
3027
|
-
const worker = this.options.workerFactory ? this.options.workerFactory(workerOptions) : this.spawnWorker(workerOptions);
|
|
3028
|
-
worker.onModuleInit();
|
|
3622
|
+
const worker = this.options.workerFactory ? this.options.workerFactory(workerOptions) : backend === "bullmq" ? this.spawnBullMQWorker(poolName, def.queue, def.concurrency, poolConfig) : this.spawnWorker(workerOptions);
|
|
3623
|
+
await worker.onModuleInit();
|
|
3029
3624
|
this.workers.push(worker);
|
|
3030
3625
|
this.logger.log(
|
|
3031
|
-
`JobWorker started: pool='${poolName}' (queue='${def.queue}') concurrency=${def.concurrency}`
|
|
3626
|
+
`JobWorker started: pool='${poolName}' (queue='${def.queue}') concurrency=${def.concurrency} backend='${backend}'`
|
|
3032
3627
|
);
|
|
3033
3628
|
}
|
|
3034
3629
|
}
|
|
@@ -3045,6 +3640,16 @@ var JobWorkerOrchestrator = class {
|
|
|
3045
3640
|
}
|
|
3046
3641
|
}
|
|
3047
3642
|
this.workers.length = 0;
|
|
3643
|
+
const orch = this.orchestrator;
|
|
3644
|
+
if (typeof orch.closeConnections === "function") {
|
|
3645
|
+
try {
|
|
3646
|
+
await orch.closeConnections();
|
|
3647
|
+
} catch (err) {
|
|
3648
|
+
this.logger.error(
|
|
3649
|
+
`BullMQ orchestrator connection close failed: ${err.message}`
|
|
3650
|
+
);
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3048
3653
|
}
|
|
3049
3654
|
// ============================================================================
|
|
3050
3655
|
// Internals
|
|
@@ -3106,15 +3711,57 @@ var JobWorkerOrchestrator = class {
|
|
|
3106
3711
|
this.moduleRef
|
|
3107
3712
|
);
|
|
3108
3713
|
}
|
|
3714
|
+
/**
|
|
3715
|
+
* BULLMQ-1 — spawn a per-pool `BullMQJobWorker`. Requires the Drizzle
|
|
3716
|
+
* client (the worker drives `job_run` as the source of truth) AND the
|
|
3717
|
+
* resolved BullMQ connection (bound by `JobsDomainModule` when
|
|
3718
|
+
* `backend: 'bullmq'`). The queue name is derived identically to the
|
|
3719
|
+
* orchestrator's `dispatch` via `resolvePoolQueueName(pool, …)` so producer
|
|
3720
|
+
* and consumer agree.
|
|
3721
|
+
*/
|
|
3722
|
+
spawnBullMQWorker(pool, _queueAlias, concurrency, poolConfig) {
|
|
3723
|
+
if (!this.db) {
|
|
3724
|
+
throw new Error(
|
|
3725
|
+
`JobWorkerModule: BullMQ worker spawning requires the Drizzle client (no DRIZZLE provider available) \u2014 job_run remains the source of truth.`
|
|
3726
|
+
);
|
|
3727
|
+
}
|
|
3728
|
+
if (!this.bullConnection) {
|
|
3729
|
+
throw new Error(
|
|
3730
|
+
`JobWorkerModule: BullMQ worker spawning requires a resolved BULLMQ_CONNECTION. Ensure JobsDomainModule was booted with backend: 'bullmq'.`
|
|
3731
|
+
);
|
|
3732
|
+
}
|
|
3733
|
+
if (!this.moduleRef) {
|
|
3734
|
+
throw new Error(
|
|
3735
|
+
`JobWorkerModule: ModuleRef not available \u2014 cannot construct BullMQJobWorker with handler DI support.`
|
|
3736
|
+
);
|
|
3737
|
+
}
|
|
3738
|
+
const queueName = resolvePoolQueueName(pool, this.bullConfig, poolConfig);
|
|
3739
|
+
return new BullMQJobWorker(
|
|
3740
|
+
this.db,
|
|
3741
|
+
this.orchestrator,
|
|
3742
|
+
this.stepService,
|
|
3743
|
+
{
|
|
3744
|
+
pool,
|
|
3745
|
+
queueName,
|
|
3746
|
+
concurrency,
|
|
3747
|
+
connection: this.bullConnection
|
|
3748
|
+
},
|
|
3749
|
+
this.moduleRef
|
|
3750
|
+
);
|
|
3751
|
+
}
|
|
3109
3752
|
};
|
|
3110
3753
|
JobWorkerOrchestrator = __decorateClass([
|
|
3111
|
-
|
|
3112
|
-
__decorateParam(0,
|
|
3113
|
-
__decorateParam(1,
|
|
3114
|
-
__decorateParam(2,
|
|
3115
|
-
__decorateParam(3,
|
|
3116
|
-
__decorateParam(4,
|
|
3117
|
-
__decorateParam(4,
|
|
3754
|
+
Injectable13(),
|
|
3755
|
+
__decorateParam(0, Inject12(JOB_ORCHESTRATOR)),
|
|
3756
|
+
__decorateParam(1, Inject12(JOB_RUN_SERVICE)),
|
|
3757
|
+
__decorateParam(2, Inject12(JOB_STEP_SERVICE)),
|
|
3758
|
+
__decorateParam(3, Inject12(JOB_WORKER_MODULE_OPTIONS)),
|
|
3759
|
+
__decorateParam(4, Optional7()),
|
|
3760
|
+
__decorateParam(4, Inject12(DRIZZLE)),
|
|
3761
|
+
__decorateParam(6, Optional7()),
|
|
3762
|
+
__decorateParam(6, Inject12(BULLMQ_CONNECTION)),
|
|
3763
|
+
__decorateParam(7, Optional7()),
|
|
3764
|
+
__decorateParam(7, Inject12(BULLMQ_RESOLVED_CONFIG))
|
|
3118
3765
|
], JobWorkerOrchestrator);
|
|
3119
3766
|
var JobWorkerModule = class {
|
|
3120
3767
|
static forRoot(opts) {
|
|
@@ -3131,7 +3778,14 @@ var JobWorkerModule = class {
|
|
|
3131
3778
|
{ provide: JOB_WORKER_MODULE_OPTIONS, useValue: opts },
|
|
3132
3779
|
JobWorkerOrchestrator
|
|
3133
3780
|
],
|
|
3134
|
-
|
|
3781
|
+
// BULLMQ-1 Phase 1 — export the options token so `BridgeModule`'s
|
|
3782
|
+
// reserved-pool guard (`onModuleInit`) can actually inject it.
|
|
3783
|
+
// Previously `exports: []` left the `@Optional()` inject resolving to
|
|
3784
|
+
// `undefined` and the guard silently no-opped (a dead check). With the
|
|
3785
|
+
// token exported the guard fires for real; consumers that omit the
|
|
3786
|
+
// reserved pools (and don't set `allPools`) now fail fast with
|
|
3787
|
+
// `BridgeReservedPoolsNotPolledError` — which is correct.
|
|
3788
|
+
exports: [JOB_WORKER_MODULE_OPTIONS]
|
|
3135
3789
|
};
|
|
3136
3790
|
}
|
|
3137
3791
|
};
|
|
@@ -3195,6 +3849,7 @@ var BridgeModule = class {
|
|
|
3195
3849
|
}
|
|
3196
3850
|
async onModuleInit() {
|
|
3197
3851
|
if (!this.workerOpts) return;
|
|
3852
|
+
if (this.workerOpts.allPools) return;
|
|
3198
3853
|
const activePools = this.workerOpts.pools ?? [];
|
|
3199
3854
|
const missing = BRIDGE_RESERVED_POOLS.filter(
|
|
3200
3855
|
(p) => !activePools.includes(p)
|
|
@@ -3206,8 +3861,8 @@ var BridgeModule = class {
|
|
|
3206
3861
|
};
|
|
3207
3862
|
BridgeModule = __decorateClass([
|
|
3208
3863
|
Module3({}),
|
|
3209
|
-
__decorateParam(0,
|
|
3210
|
-
__decorateParam(0,
|
|
3864
|
+
__decorateParam(0, Optional8()),
|
|
3865
|
+
__decorateParam(0, Inject13(JOB_WORKER_MODULE_OPTIONS))
|
|
3211
3866
|
], BridgeModule);
|
|
3212
3867
|
export {
|
|
3213
3868
|
BRIDGE_DELIVERY_JOB_TYPE,
|