@pattern-stack/codegen 0.8.1 → 0.9.0
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/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 +3 -0
- 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.js.map +1 -1
- package/dist/runtime/subsystems/bridge/index.d.ts +3 -0
- 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 +1 -0
- 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 +6 -2
- 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-orchestrator.bullmq-backend.d.ts +107 -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-run-keyset-cursor.d.ts +52 -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 +2 -1
- 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 +2 -1
- 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 +74 -1
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.d.ts +48 -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.module.d.ts +42 -4
- 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/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 +63 -2
- package/dist/runtime/subsystems/observability/observability.service.d.ts +21 -3
- 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 +1 -0
- package/dist/runtime/subsystems/observability/reporters/index.d.ts +1 -0
- package/dist/src/cli/index.js +30 -6
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +1 -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-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
|
@@ -12,11 +12,11 @@ var __decorateParam = (index2, decorator) => (target, key) => decorator(target,
|
|
|
12
12
|
|
|
13
13
|
// runtime/subsystems/jobs/job-worker.module.ts
|
|
14
14
|
import {
|
|
15
|
-
Inject as
|
|
16
|
-
Injectable as
|
|
17
|
-
Logger as
|
|
15
|
+
Inject as Inject8,
|
|
16
|
+
Injectable as Injectable9,
|
|
17
|
+
Logger as Logger6,
|
|
18
18
|
Module as Module2,
|
|
19
|
-
Optional as
|
|
19
|
+
Optional as Optional3
|
|
20
20
|
} from "@nestjs/common";
|
|
21
21
|
|
|
22
22
|
// runtime/constants/tokens.ts
|
|
@@ -609,7 +609,58 @@ function notInStatus(statuses) {
|
|
|
609
609
|
|
|
610
610
|
// runtime/subsystems/jobs/job-run-service.drizzle-backend.ts
|
|
611
611
|
import { Inject as Inject2, Injectable as Injectable2 } from "@nestjs/common";
|
|
612
|
-
import { and as and2, asc, desc as desc2, eq as eq2, inArray as inArray2, isNull, sql as sql3 } from "drizzle-orm";
|
|
612
|
+
import { and as and2, asc, desc as desc2, eq as eq2, gte, inArray as inArray2, isNull, lt, or, sql as sql3 } from "drizzle-orm";
|
|
613
|
+
|
|
614
|
+
// runtime/subsystems/jobs/job-run-keyset-cursor.ts
|
|
615
|
+
var DEFAULT_LIST_LIMIT = 50;
|
|
616
|
+
var MAX_LIST_LIMIT = 200;
|
|
617
|
+
function clampLimit(limit) {
|
|
618
|
+
if (typeof limit !== "number" || !Number.isFinite(limit)) {
|
|
619
|
+
return DEFAULT_LIST_LIMIT;
|
|
620
|
+
}
|
|
621
|
+
const floored = Math.floor(limit);
|
|
622
|
+
if (floored < 1) return 1;
|
|
623
|
+
if (floored > MAX_LIST_LIMIT) return MAX_LIST_LIMIT;
|
|
624
|
+
return floored;
|
|
625
|
+
}
|
|
626
|
+
function encodeKeysetCursor(keyset) {
|
|
627
|
+
const tuple = [keyset.createdAt.toISOString(), keyset.id];
|
|
628
|
+
return Buffer.from(JSON.stringify(tuple), "utf8").toString("base64url");
|
|
629
|
+
}
|
|
630
|
+
function decodeKeysetCursor(cursor) {
|
|
631
|
+
try {
|
|
632
|
+
const json = Buffer.from(cursor, "base64url").toString("utf8");
|
|
633
|
+
const parsed = JSON.parse(json);
|
|
634
|
+
if (!Array.isArray(parsed) || parsed.length !== 2) return null;
|
|
635
|
+
const [iso, id] = parsed;
|
|
636
|
+
if (typeof iso !== "string" || typeof id !== "string") return null;
|
|
637
|
+
const createdAt = new Date(iso);
|
|
638
|
+
if (Number.isNaN(createdAt.getTime())) return null;
|
|
639
|
+
return { createdAt, id };
|
|
640
|
+
} catch {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function toJobRunSummary(r) {
|
|
645
|
+
return {
|
|
646
|
+
runId: r.id,
|
|
647
|
+
rootRunId: r.rootRunId,
|
|
648
|
+
jobType: r.jobType,
|
|
649
|
+
pool: r.pool,
|
|
650
|
+
status: r.status,
|
|
651
|
+
scopeEntityType: r.scopeEntityType,
|
|
652
|
+
scopeEntityId: r.scopeEntityId,
|
|
653
|
+
tenantId: r.tenantId,
|
|
654
|
+
attempts: r.attempts,
|
|
655
|
+
errorMessage: r.error?.message ?? null,
|
|
656
|
+
runAt: r.runAt,
|
|
657
|
+
startedAt: r.startedAt,
|
|
658
|
+
finishedAt: r.finishedAt,
|
|
659
|
+
createdAt: r.createdAt
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// runtime/subsystems/jobs/job-run-service.drizzle-backend.ts
|
|
613
664
|
var NON_TERMINAL_STATUSES = [
|
|
614
665
|
"pending",
|
|
615
666
|
"running",
|
|
@@ -732,6 +783,37 @@ var DrizzleJobRunService = class {
|
|
|
732
783
|
createdAt: r.createdAt
|
|
733
784
|
}));
|
|
734
785
|
}
|
|
786
|
+
async listJobRuns(query = {}) {
|
|
787
|
+
const limit = clampLimit(query.limit);
|
|
788
|
+
const conditions = [];
|
|
789
|
+
const tenantCond = this.tenantCondition("listJobRuns", query.tenantId);
|
|
790
|
+
if (tenantCond) conditions.push(tenantCond);
|
|
791
|
+
if (query.poolId) conditions.push(eq2(jobRuns.pool, query.poolId));
|
|
792
|
+
if (query.rootRunId) conditions.push(eq2(jobRuns.rootRunId, query.rootRunId));
|
|
793
|
+
if (query.status) conditions.push(eq2(jobRuns.status, query.status));
|
|
794
|
+
if (query.since) conditions.push(gte(jobRuns.createdAt, query.since));
|
|
795
|
+
if (query.cursor) {
|
|
796
|
+
const keyset = decodeKeysetCursor(query.cursor);
|
|
797
|
+
if (keyset) {
|
|
798
|
+
conditions.push(
|
|
799
|
+
or(
|
|
800
|
+
lt(jobRuns.createdAt, keyset.createdAt),
|
|
801
|
+
and2(
|
|
802
|
+
eq2(jobRuns.createdAt, keyset.createdAt),
|
|
803
|
+
lt(jobRuns.id, keyset.id)
|
|
804
|
+
)
|
|
805
|
+
)
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
const rows = await this.db.select().from(jobRuns).where(conditions.length > 0 ? and2(...conditions) : void 0).orderBy(desc2(jobRuns.createdAt), desc2(jobRuns.id)).limit(limit + 1);
|
|
810
|
+
const hasMore = rows.length > limit;
|
|
811
|
+
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
812
|
+
const items = page.map(toJobRunSummary);
|
|
813
|
+
const last = page[page.length - 1];
|
|
814
|
+
const nextCursor = hasMore && last ? encodeKeysetCursor({ createdAt: last.createdAt, id: last.id }) : null;
|
|
815
|
+
return { items, nextCursor };
|
|
816
|
+
}
|
|
735
817
|
/**
|
|
736
818
|
* Internal helper used by cascade paths (not on the public protocol).
|
|
737
819
|
* Exposed as a public method on the concrete class so infrastructure
|
|
@@ -793,9 +875,394 @@ DrizzleJobStepService = __decorateClass([
|
|
|
793
875
|
__decorateParam(0, Inject3(DRIZZLE))
|
|
794
876
|
], DrizzleJobStepService);
|
|
795
877
|
|
|
878
|
+
// runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts
|
|
879
|
+
import { createHash } from "crypto";
|
|
880
|
+
import { Inject as Inject4, Injectable as Injectable4, Logger as Logger2, Optional } from "@nestjs/common";
|
|
881
|
+
import { eq as eq4 } from "drizzle-orm";
|
|
882
|
+
|
|
883
|
+
// runtime/subsystems/jobs/pool-config.loader.ts
|
|
884
|
+
import { existsSync, readFileSync } from "fs";
|
|
885
|
+
import { resolve } from "path";
|
|
886
|
+
import { parse as parseYaml } from "yaml";
|
|
887
|
+
var FRAMEWORK_POOLS = Object.freeze({
|
|
888
|
+
events_inbound: Object.freeze({
|
|
889
|
+
queue: "jobs-events-inbound",
|
|
890
|
+
concurrency: 20,
|
|
891
|
+
reserved: true,
|
|
892
|
+
description: "Inbound events drain (events subsystem outbox)."
|
|
893
|
+
}),
|
|
894
|
+
events_change: Object.freeze({
|
|
895
|
+
queue: "jobs-events-change",
|
|
896
|
+
concurrency: 30,
|
|
897
|
+
reserved: true,
|
|
898
|
+
description: "Change events drain (events subsystem outbox)."
|
|
899
|
+
}),
|
|
900
|
+
events_outbound: Object.freeze({
|
|
901
|
+
queue: "jobs-events-outbound",
|
|
902
|
+
concurrency: 10,
|
|
903
|
+
reserved: true,
|
|
904
|
+
description: "Outbound events drain (events subsystem outbox)."
|
|
905
|
+
}),
|
|
906
|
+
interactive: Object.freeze({
|
|
907
|
+
queue: "jobs-interactive",
|
|
908
|
+
concurrency: 20,
|
|
909
|
+
reserved: false,
|
|
910
|
+
description: "User-facing latency-sensitive jobs."
|
|
911
|
+
}),
|
|
912
|
+
batch: Object.freeze({
|
|
913
|
+
queue: "jobs-batch",
|
|
914
|
+
concurrency: 5,
|
|
915
|
+
reserved: false,
|
|
916
|
+
description: "Default pool for background jobs."
|
|
917
|
+
})
|
|
918
|
+
});
|
|
919
|
+
var RESERVED_POOL_NAMES = new Set(
|
|
920
|
+
Object.entries(FRAMEWORK_POOLS).filter(([, def]) => def.reserved).map(([name]) => name)
|
|
921
|
+
);
|
|
922
|
+
var cache = /* @__PURE__ */ new Map();
|
|
923
|
+
function loadPoolConfig(configPath) {
|
|
924
|
+
const resolved = resolve(configPath ?? `${process.cwd()}/codegen.config.yaml`);
|
|
925
|
+
const cached = cache.get(resolved);
|
|
926
|
+
if (cached) return cached;
|
|
927
|
+
const merged = /* @__PURE__ */ new Map();
|
|
928
|
+
for (const [name, def] of Object.entries(FRAMEWORK_POOLS)) {
|
|
929
|
+
merged.set(name, { ...def });
|
|
930
|
+
}
|
|
931
|
+
if (!existsSync(resolved)) {
|
|
932
|
+
cache.set(resolved, merged);
|
|
933
|
+
return merged;
|
|
934
|
+
}
|
|
935
|
+
let raw;
|
|
936
|
+
try {
|
|
937
|
+
raw = parseYaml(readFileSync(resolved, "utf8"));
|
|
938
|
+
} catch (err) {
|
|
939
|
+
throw new Error(
|
|
940
|
+
`pool-config.loader: failed to parse YAML at ${resolved}: ${err.message}`
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
const userPools = extractUserPools(raw);
|
|
944
|
+
for (const [name, userDef] of Object.entries(userPools)) {
|
|
945
|
+
const existing = merged.get(name);
|
|
946
|
+
if (existing) {
|
|
947
|
+
const next = {
|
|
948
|
+
queue: existing.queue,
|
|
949
|
+
concurrency: typeof userDef.concurrency === "number" ? userDef.concurrency : existing.concurrency,
|
|
950
|
+
reserved: existing.reserved,
|
|
951
|
+
description: userDef.description ?? existing.description
|
|
952
|
+
};
|
|
953
|
+
merged.set(name, next);
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
if (typeof userDef.queue !== "string" || userDef.queue.length === 0) {
|
|
957
|
+
throw new Error(
|
|
958
|
+
`pool-config.loader: pool '${name}' must declare a non-empty 'queue'.`
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
if (typeof userDef.concurrency !== "number" || userDef.concurrency <= 0) {
|
|
962
|
+
throw new Error(
|
|
963
|
+
`pool-config.loader: pool '${name}' must declare a positive 'concurrency'.`
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
if (userDef.reserved === true) {
|
|
967
|
+
throw new Error(
|
|
968
|
+
`pool-config.loader: user-defined pool '${name}' cannot set 'reserved: true' \u2014 reserved is framework-only.`
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
merged.set(name, {
|
|
972
|
+
queue: userDef.queue,
|
|
973
|
+
concurrency: userDef.concurrency,
|
|
974
|
+
reserved: false,
|
|
975
|
+
description: userDef.description
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
cache.set(resolved, merged);
|
|
979
|
+
return merged;
|
|
980
|
+
}
|
|
981
|
+
function allNonReservedPoolNames(config) {
|
|
982
|
+
const out = [];
|
|
983
|
+
for (const [name, def] of config) {
|
|
984
|
+
if (!def.reserved) out.push(name);
|
|
985
|
+
}
|
|
986
|
+
return out;
|
|
987
|
+
}
|
|
988
|
+
function allPoolNames(config) {
|
|
989
|
+
return [...config.keys()];
|
|
990
|
+
}
|
|
991
|
+
function extractUserPools(raw) {
|
|
992
|
+
if (!raw || typeof raw !== "object") return {};
|
|
993
|
+
const jobs2 = raw.jobs;
|
|
994
|
+
if (!jobs2 || typeof jobs2 !== "object") return {};
|
|
995
|
+
const pools = jobs2.pools;
|
|
996
|
+
if (!pools || typeof pools !== "object") return {};
|
|
997
|
+
const out = {};
|
|
998
|
+
for (const [name, def] of Object.entries(pools)) {
|
|
999
|
+
if (!def || typeof def !== "object") continue;
|
|
1000
|
+
out[name] = def;
|
|
1001
|
+
}
|
|
1002
|
+
return out;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// runtime/subsystems/jobs/bullmq.config.ts
|
|
1006
|
+
var BULLMQ_CONNECTION = /* @__PURE__ */ Symbol("BULLMQ_CONNECTION");
|
|
1007
|
+
var BULLMQ_RESOLVED_CONFIG = /* @__PURE__ */ Symbol("BULLMQ_RESOLVED_CONFIG");
|
|
1008
|
+
var DEFAULT_REDIS_URL = "redis://localhost:6379";
|
|
1009
|
+
var DEFAULT_BULL_BOARD_MOUNT = "/admin/queues";
|
|
1010
|
+
function resolveBullMqConfig(ext) {
|
|
1011
|
+
const url = ext?.redis_url ?? process.env.REDIS_URL ?? DEFAULT_REDIS_URL;
|
|
1012
|
+
const resolved = {
|
|
1013
|
+
connection: { url },
|
|
1014
|
+
queuePrefix: ext?.queue_prefix
|
|
1015
|
+
};
|
|
1016
|
+
if (ext?.bull_board?.enabled) {
|
|
1017
|
+
resolved.bullBoard = {
|
|
1018
|
+
enabled: true,
|
|
1019
|
+
mountPath: ext.bull_board.mount_path ?? DEFAULT_BULL_BOARD_MOUNT
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
return resolved;
|
|
1023
|
+
}
|
|
1024
|
+
function resolvePoolQueueName(pool, config, poolConfig = loadPoolConfig()) {
|
|
1025
|
+
const alias = poolConfig.get(pool)?.queue ?? pool;
|
|
1026
|
+
const prefix = config?.queuePrefix;
|
|
1027
|
+
return prefix ? `${prefix}:${alias}` : alias;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts
|
|
1031
|
+
function sha1JobId(idempotencyKey) {
|
|
1032
|
+
return createHash("sha1").update(idempotencyKey).digest("hex");
|
|
1033
|
+
}
|
|
1034
|
+
var BullMQJobOrchestrator = class extends DrizzleJobOrchestrator {
|
|
1035
|
+
constructor(db, multiTenant, connection, bullConfig = null) {
|
|
1036
|
+
super(db, multiTenant);
|
|
1037
|
+
this.connection = connection;
|
|
1038
|
+
this.bullConfig = bullConfig;
|
|
1039
|
+
this.bullDb = db;
|
|
1040
|
+
}
|
|
1041
|
+
connection;
|
|
1042
|
+
bullConfig;
|
|
1043
|
+
// TODO(logging-subsystem): swap to ILogger once ADR-028 lands
|
|
1044
|
+
bullLogger = new Logger2(BullMQJobOrchestrator.name);
|
|
1045
|
+
/** Lazily-opened `Queue` handles, one per pool. */
|
|
1046
|
+
queues = /* @__PURE__ */ new Map();
|
|
1047
|
+
/** Single FlowProducer for parent/child hierarchies. Lazily opened. */
|
|
1048
|
+
_flow = null;
|
|
1049
|
+
/**
|
|
1050
|
+
* Cached `bullmq` value constructors, populated by `loadBullMq()` on first
|
|
1051
|
+
* use (the `start`/`cancel`/`replay` entrypoints `await` it before touching
|
|
1052
|
+
* a queue). Kept off the import graph so a `drizzle`-only consumer never
|
|
1053
|
+
* resolves the optional `'bullmq'` package.
|
|
1054
|
+
*/
|
|
1055
|
+
QueueCtor = null;
|
|
1056
|
+
FlowProducerCtor = null;
|
|
1057
|
+
bullMqLoad = null;
|
|
1058
|
+
/**
|
|
1059
|
+
* Own reference to the Drizzle client. `DrizzleJobOrchestrator.db` is
|
|
1060
|
+
* `private` (can't be redeclared even privately in a subclass), and the
|
|
1061
|
+
* spec forbids touching that file — so the subclass keeps its own handle
|
|
1062
|
+
* under a distinct name (same instance, passed through to `super`) for the
|
|
1063
|
+
* cancel-cascade snapshot + definition/run loads below.
|
|
1064
|
+
*/
|
|
1065
|
+
bullDb;
|
|
1066
|
+
/**
|
|
1067
|
+
* Lazily load the optional `bullmq` package and cache its value
|
|
1068
|
+
* constructors. Idempotent (single in-flight promise). Throws a friendly,
|
|
1069
|
+
* actionable error when the consumer selected `backend: 'bullmq'` but did
|
|
1070
|
+
* not install the package — mirrors `createRedisClient` in the redis event
|
|
1071
|
+
* backend. Must be `await`ed before any `queueFor`/`flow` access.
|
|
1072
|
+
*/
|
|
1073
|
+
async loadBullMq() {
|
|
1074
|
+
if (this.QueueCtor && this.FlowProducerCtor) return;
|
|
1075
|
+
if (!this.bullMqLoad) {
|
|
1076
|
+
this.bullMqLoad = (async () => {
|
|
1077
|
+
try {
|
|
1078
|
+
const mod = await import("bullmq");
|
|
1079
|
+
this.QueueCtor = mod.Queue;
|
|
1080
|
+
this.FlowProducerCtor = mod.FlowProducer;
|
|
1081
|
+
} catch {
|
|
1082
|
+
throw new Error(
|
|
1083
|
+
'BullMQ backend requires the "bullmq" package. Install it with: npm install bullmq'
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
})();
|
|
1087
|
+
}
|
|
1088
|
+
await this.bullMqLoad;
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Open (or reuse) the `Queue` for a pool. Synchronous — callers `await
|
|
1092
|
+
* loadBullMq()` first so `QueueCtor` is populated.
|
|
1093
|
+
*/
|
|
1094
|
+
queueFor(pool) {
|
|
1095
|
+
if (!this.QueueCtor) {
|
|
1096
|
+
throw new Error("BullMQJobOrchestrator: queueFor called before loadBullMq()");
|
|
1097
|
+
}
|
|
1098
|
+
const name = resolvePoolQueueName(pool, this.bullConfig);
|
|
1099
|
+
let q = this.queues.get(name);
|
|
1100
|
+
if (!q) {
|
|
1101
|
+
q = new this.QueueCtor(name, { connection: this.connection });
|
|
1102
|
+
this.queues.set(name, q);
|
|
1103
|
+
}
|
|
1104
|
+
return q;
|
|
1105
|
+
}
|
|
1106
|
+
flow() {
|
|
1107
|
+
if (!this.FlowProducerCtor) {
|
|
1108
|
+
throw new Error("BullMQJobOrchestrator: flow called before loadBullMq()");
|
|
1109
|
+
}
|
|
1110
|
+
if (!this._flow) {
|
|
1111
|
+
this._flow = new this.FlowProducerCtor({ connection: this.connection });
|
|
1112
|
+
}
|
|
1113
|
+
return this._flow;
|
|
1114
|
+
}
|
|
1115
|
+
// ==========================================================================
|
|
1116
|
+
// start — Postgres insert (super) + BullMQ dispatch
|
|
1117
|
+
// ==========================================================================
|
|
1118
|
+
async start(type, input, opts = {}, tx) {
|
|
1119
|
+
const run = await super.start(type, input, opts, tx);
|
|
1120
|
+
await this.dispatch(run, type);
|
|
1121
|
+
return run;
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Map a `job_run` row onto a BullMQ job via `queue.add`. When the run has a
|
|
1125
|
+
* `parentRunId` we attach it to the parent's existing BullMQ job through the
|
|
1126
|
+
* `parent: { id, queue }` opt — BullMQ then tracks the parent/child link in
|
|
1127
|
+
* its own graph. (The FlowProducer is reserved for whole-tree atomic
|
|
1128
|
+
* submits, exposed as an opt-in extension via `flowProducer()`; runtime
|
|
1129
|
+
* `ctx.spawnChild` is incremental, so `queue.add` with a parent ref is the
|
|
1130
|
+
* correct primitive here.)
|
|
1131
|
+
*
|
|
1132
|
+
* The `jobId` is colon-safe + stable: `sha1(dedupeKey)` when a dedupe key is
|
|
1133
|
+
* present (so the same logical key dedups), else the `job_run.id` UUID
|
|
1134
|
+
* (already colon-free).
|
|
1135
|
+
*
|
|
1136
|
+
* The domain `parentClosePolicy` cascade is still enforced in Postgres by
|
|
1137
|
+
* the shared `cancel` path — BullMQ's parent link is dispatch bookkeeping,
|
|
1138
|
+
* not the authority.
|
|
1139
|
+
*/
|
|
1140
|
+
async dispatch(run, type) {
|
|
1141
|
+
await this.loadBullMq();
|
|
1142
|
+
const def = await this.loadDefinition(type);
|
|
1143
|
+
const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;
|
|
1144
|
+
const jobOpts = {
|
|
1145
|
+
jobId,
|
|
1146
|
+
...this.retryOpts(def),
|
|
1147
|
+
...this.dedupeOpts(run, def)
|
|
1148
|
+
};
|
|
1149
|
+
if (run.parentRunId) {
|
|
1150
|
+
const parentRow = await this.loadRun(run.parentRunId);
|
|
1151
|
+
if (parentRow) {
|
|
1152
|
+
const parentJobId = parentRow.dedupeKey ? sha1JobId(parentRow.dedupeKey) : parentRow.id;
|
|
1153
|
+
jobOpts.parent = {
|
|
1154
|
+
id: parentJobId,
|
|
1155
|
+
queue: resolvePoolQueueName(parentRow.pool, this.bullConfig)
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
const payload = { runId: run.id, type, input: run.input };
|
|
1160
|
+
await this.queueFor(run.pool).add(type, payload, jobOpts);
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Opt-in extension (spec §Extensions): expose the FlowProducer for
|
|
1164
|
+
* consumers that want to submit a whole parent/child DAG atomically up
|
|
1165
|
+
* front, rather than incrementally via `ctx.spawnChild`. Backend-specific —
|
|
1166
|
+
* code using it is not portable to the Drizzle backend. Async because it
|
|
1167
|
+
* lazily loads the optional `bullmq` package on first use.
|
|
1168
|
+
*/
|
|
1169
|
+
async flowProducer() {
|
|
1170
|
+
await this.loadBullMq();
|
|
1171
|
+
return this.flow();
|
|
1172
|
+
}
|
|
1173
|
+
retryOpts(def) {
|
|
1174
|
+
const policy = def.retryPolicy;
|
|
1175
|
+
if (!policy) return {};
|
|
1176
|
+
return {
|
|
1177
|
+
attempts: policy.attempts,
|
|
1178
|
+
backoff: {
|
|
1179
|
+
type: policy.backoff === "exponential" ? "exponential" : "fixed",
|
|
1180
|
+
delay: policy.baseMs
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
dedupeOpts(run, def) {
|
|
1185
|
+
if (!run.dedupeKey || !def.dedupeWindowMs) return {};
|
|
1186
|
+
return {
|
|
1187
|
+
deduplication: {
|
|
1188
|
+
id: sha1JobId(run.dedupeKey),
|
|
1189
|
+
ttl: def.dedupeWindowMs
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
// ==========================================================================
|
|
1194
|
+
// cancel — Postgres cascade (super) + remove from queue
|
|
1195
|
+
// ==========================================================================
|
|
1196
|
+
async cancel(runId, opts = {}) {
|
|
1197
|
+
const target = await this.loadRun(runId);
|
|
1198
|
+
await super.cancel(runId, opts);
|
|
1199
|
+
if (!target) return;
|
|
1200
|
+
await this.loadBullMq();
|
|
1201
|
+
await this.removeFromQueue(target);
|
|
1202
|
+
if (opts.cascade === false) return;
|
|
1203
|
+
const descendants = await this.bullDb.select().from(jobRuns).where(eq4(jobRuns.rootRunId, target.rootRunId));
|
|
1204
|
+
for (const child of descendants) {
|
|
1205
|
+
if (child.id === runId) continue;
|
|
1206
|
+
await this.removeFromQueue(child);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
async removeFromQueue(run) {
|
|
1210
|
+
const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;
|
|
1211
|
+
try {
|
|
1212
|
+
const job = await this.queueFor(run.pool).getJob(jobId);
|
|
1213
|
+
if (job) await job.remove();
|
|
1214
|
+
} catch (err) {
|
|
1215
|
+
this.bullLogger.warn(
|
|
1216
|
+
`cancel: could not remove BullMQ job ${jobId} (pool=${run.pool}): ${err.message}`
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
// ==========================================================================
|
|
1221
|
+
// replay — Postgres reset (super) + re-enqueue
|
|
1222
|
+
// ==========================================================================
|
|
1223
|
+
async replay(runId) {
|
|
1224
|
+
const run = await super.replay(runId);
|
|
1225
|
+
await this.dispatch(run, run.jobType);
|
|
1226
|
+
return run;
|
|
1227
|
+
}
|
|
1228
|
+
// ==========================================================================
|
|
1229
|
+
// Internals
|
|
1230
|
+
// ==========================================================================
|
|
1231
|
+
async loadDefinition(type) {
|
|
1232
|
+
const [def] = await this.bullDb.select().from(jobs).where(eq4(jobs.type, type)).limit(1);
|
|
1233
|
+
if (!def) {
|
|
1234
|
+
throw new Error(`BullMQJobOrchestrator: no job definition for '${type}'`);
|
|
1235
|
+
}
|
|
1236
|
+
return def;
|
|
1237
|
+
}
|
|
1238
|
+
async loadRun(id) {
|
|
1239
|
+
const [row] = await this.bullDb.select().from(jobRuns).where(eq4(jobRuns.id, id)).limit(1);
|
|
1240
|
+
return row ?? null;
|
|
1241
|
+
}
|
|
1242
|
+
/** Close all open queue + flow connections. Called on module destroy. */
|
|
1243
|
+
async closeConnections() {
|
|
1244
|
+
for (const q of this.queues.values()) {
|
|
1245
|
+
await q.close().catch(() => void 0);
|
|
1246
|
+
}
|
|
1247
|
+
this.queues.clear();
|
|
1248
|
+
if (this._flow) {
|
|
1249
|
+
await this._flow.close().catch(() => void 0);
|
|
1250
|
+
this._flow = null;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
BullMQJobOrchestrator = __decorateClass([
|
|
1255
|
+
Injectable4(),
|
|
1256
|
+
__decorateParam(0, Inject4(DRIZZLE)),
|
|
1257
|
+
__decorateParam(1, Inject4(JOBS_MULTI_TENANT)),
|
|
1258
|
+
__decorateParam(2, Inject4(BULLMQ_CONNECTION)),
|
|
1259
|
+
__decorateParam(3, Optional()),
|
|
1260
|
+
__decorateParam(3, Inject4(BULLMQ_RESOLVED_CONFIG))
|
|
1261
|
+
], BullMQJobOrchestrator);
|
|
1262
|
+
|
|
796
1263
|
// runtime/subsystems/jobs/job-orchestrator.memory-backend.ts
|
|
797
1264
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
798
|
-
import { Inject as
|
|
1265
|
+
import { Inject as Inject5, Injectable as Injectable5, Logger as Logger3, Optional as Optional2 } from "@nestjs/common";
|
|
799
1266
|
var QUEUED_RUN_AT = /* @__PURE__ */ new Date(864e13);
|
|
800
1267
|
var TERMINAL_STATUSES2 = [
|
|
801
1268
|
"completed",
|
|
@@ -842,7 +1309,7 @@ var MemoryJobOrchestrator = class {
|
|
|
842
1309
|
stepService;
|
|
843
1310
|
multiTenant;
|
|
844
1311
|
moduleRef;
|
|
845
|
-
logger = new
|
|
1312
|
+
logger = new Logger3(MemoryJobOrchestrator.name);
|
|
846
1313
|
mutex = new PromiseMutex();
|
|
847
1314
|
handlerRegistry = /* @__PURE__ */ new Map();
|
|
848
1315
|
/**
|
|
@@ -1191,7 +1658,7 @@ var MemoryJobOrchestrator = class {
|
|
|
1191
1658
|
run,
|
|
1192
1659
|
step: this.makeStepFn(run),
|
|
1193
1660
|
spawnChild: this.makeSpawnFn(run),
|
|
1194
|
-
logger: new
|
|
1661
|
+
logger: new Logger3(`JobRun:${run.id}`)
|
|
1195
1662
|
};
|
|
1196
1663
|
const attemptsBefore = run.attempts ?? 0;
|
|
1197
1664
|
try {
|
|
@@ -1364,9 +1831,9 @@ var MemoryJobOrchestrator = class {
|
|
|
1364
1831
|
}
|
|
1365
1832
|
};
|
|
1366
1833
|
MemoryJobOrchestrator = __decorateClass([
|
|
1367
|
-
|
|
1368
|
-
__decorateParam(2,
|
|
1369
|
-
__decorateParam(3,
|
|
1834
|
+
Injectable5(),
|
|
1835
|
+
__decorateParam(2, Inject5(JOBS_MULTI_TENANT)),
|
|
1836
|
+
__decorateParam(3, Optional2())
|
|
1370
1837
|
], MemoryJobOrchestrator);
|
|
1371
1838
|
function classifyError(err, policy, currentAttempts) {
|
|
1372
1839
|
if (!policy) return "fail";
|
|
@@ -1400,7 +1867,7 @@ function serialiseError(err, attempt, retryable) {
|
|
|
1400
1867
|
}
|
|
1401
1868
|
|
|
1402
1869
|
// runtime/subsystems/jobs/job-run-service.memory-backend.ts
|
|
1403
|
-
import { Inject as
|
|
1870
|
+
import { Inject as Inject6, Injectable as Injectable6 } from "@nestjs/common";
|
|
1404
1871
|
var NON_TERMINAL_STATUSES2 = [
|
|
1405
1872
|
"pending",
|
|
1406
1873
|
"running",
|
|
@@ -1517,6 +1984,38 @@ var MemoryJobRunService = class {
|
|
|
1517
1984
|
createdAt: r.createdAt
|
|
1518
1985
|
}));
|
|
1519
1986
|
}
|
|
1987
|
+
async listJobRuns(query = {}) {
|
|
1988
|
+
const limit = clampLimit(query.limit);
|
|
1989
|
+
const tenantCheck = this.tenantPredicate("listJobRuns", query.tenantId);
|
|
1990
|
+
const keyset = query.cursor ? decodeKeysetCursor(query.cursor) : null;
|
|
1991
|
+
const matched = [];
|
|
1992
|
+
for (const r of this.store.runs.values()) {
|
|
1993
|
+
if (tenantCheck && !tenantCheck(r)) continue;
|
|
1994
|
+
if (query.poolId && r.pool !== query.poolId) continue;
|
|
1995
|
+
if (query.rootRunId && r.rootRunId !== query.rootRunId) continue;
|
|
1996
|
+
if (query.status && r.status !== query.status) continue;
|
|
1997
|
+
if (query.since && r.createdAt.getTime() < query.since.getTime()) continue;
|
|
1998
|
+
matched.push(r);
|
|
1999
|
+
}
|
|
2000
|
+
matched.sort((a, b) => {
|
|
2001
|
+
const dt = b.createdAt.getTime() - a.createdAt.getTime();
|
|
2002
|
+
if (dt !== 0) return dt;
|
|
2003
|
+
return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
|
|
2004
|
+
});
|
|
2005
|
+
const seeked = keyset ? matched.filter((r) => {
|
|
2006
|
+
const ct = r.createdAt.getTime();
|
|
2007
|
+
const kt = keyset.createdAt.getTime();
|
|
2008
|
+
if (ct < kt) return true;
|
|
2009
|
+
if (ct > kt) return false;
|
|
2010
|
+
return r.id < keyset.id;
|
|
2011
|
+
}) : matched;
|
|
2012
|
+
const hasMore = seeked.length > limit;
|
|
2013
|
+
const page = hasMore ? seeked.slice(0, limit) : seeked;
|
|
2014
|
+
const items = page.map(toJobRunSummary);
|
|
2015
|
+
const last = page[page.length - 1];
|
|
2016
|
+
const nextCursor = hasMore && last ? encodeKeysetCursor({ createdAt: last.createdAt, id: last.id }) : null;
|
|
2017
|
+
return { items, nextCursor };
|
|
2018
|
+
}
|
|
1520
2019
|
/**
|
|
1521
2020
|
* Direct lookup. Not on the protocol — concrete-class convenience for
|
|
1522
2021
|
* tests. Matches `DrizzleJobRunService.findByRootRunId` in spirit; both
|
|
@@ -1535,9 +2034,9 @@ var MemoryJobRunService = class {
|
|
|
1535
2034
|
}
|
|
1536
2035
|
};
|
|
1537
2036
|
MemoryJobRunService = __decorateClass([
|
|
1538
|
-
|
|
1539
|
-
__decorateParam(1,
|
|
1540
|
-
__decorateParam(2,
|
|
2037
|
+
Injectable6(),
|
|
2038
|
+
__decorateParam(1, Inject6(JOB_ORCHESTRATOR)),
|
|
2039
|
+
__decorateParam(2, Inject6(JOBS_MULTI_TENANT))
|
|
1541
2040
|
], MemoryJobRunService);
|
|
1542
2041
|
function compareBy(a, b, order) {
|
|
1543
2042
|
switch (order) {
|
|
@@ -1555,7 +2054,7 @@ function compareBy(a, b, order) {
|
|
|
1555
2054
|
|
|
1556
2055
|
// runtime/subsystems/jobs/job-step-service.memory-backend.ts
|
|
1557
2056
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
1558
|
-
import { Injectable as
|
|
2057
|
+
import { Injectable as Injectable7 } from "@nestjs/common";
|
|
1559
2058
|
var MemoryJobStepService = class {
|
|
1560
2059
|
constructor(store) {
|
|
1561
2060
|
this.store = store;
|
|
@@ -1648,7 +2147,7 @@ var MemoryJobStepService = class {
|
|
|
1648
2147
|
}
|
|
1649
2148
|
};
|
|
1650
2149
|
MemoryJobStepService = __decorateClass([
|
|
1651
|
-
|
|
2150
|
+
Injectable7()
|
|
1652
2151
|
], MemoryJobStepService);
|
|
1653
2152
|
|
|
1654
2153
|
// runtime/subsystems/jobs/memory-job-store.ts
|
|
@@ -1670,7 +2169,6 @@ var MemoryJobStore = class {
|
|
|
1670
2169
|
// runtime/subsystems/jobs/jobs-domain.module.ts
|
|
1671
2170
|
var JobsDomainModule = class {
|
|
1672
2171
|
static forRoot(opts) {
|
|
1673
|
-
void opts.extensions;
|
|
1674
2172
|
const multiTenant = opts.multiTenant ?? false;
|
|
1675
2173
|
const providers = [
|
|
1676
2174
|
// JOB-8 — boolean provider consumed by the four service-layer backends.
|
|
@@ -1689,21 +2187,32 @@ var JobsDomainModule = class {
|
|
|
1689
2187
|
providers.push({ provide: JOB_ORCHESTRATOR, useExisting: MemoryJobOrchestrator });
|
|
1690
2188
|
providers.push(MemoryJobRunService);
|
|
1691
2189
|
providers.push({ provide: JOB_RUN_SERVICE, useExisting: MemoryJobRunService });
|
|
2190
|
+
} else if (opts.backend === "bullmq") {
|
|
2191
|
+
const resolved = resolveBullMqConfig(opts.extensions?.bullmq);
|
|
2192
|
+
providers.push({ provide: BULLMQ_CONNECTION, useValue: resolved.connection });
|
|
2193
|
+
providers.push({ provide: BULLMQ_RESOLVED_CONFIG, useValue: resolved });
|
|
2194
|
+
providers.push({ provide: JOB_ORCHESTRATOR, useClass: BullMQJobOrchestrator });
|
|
2195
|
+
providers.push({ provide: JOB_RUN_SERVICE, useClass: DrizzleJobRunService });
|
|
2196
|
+
providers.push({ provide: JOB_STEP_SERVICE, useClass: DrizzleJobStepService });
|
|
1692
2197
|
} else {
|
|
1693
2198
|
providers.push({ provide: JOB_ORCHESTRATOR, useClass: DrizzleJobOrchestrator });
|
|
1694
2199
|
providers.push({ provide: JOB_RUN_SERVICE, useClass: DrizzleJobRunService });
|
|
1695
2200
|
providers.push({ provide: JOB_STEP_SERVICE, useClass: DrizzleJobStepService });
|
|
1696
2201
|
}
|
|
2202
|
+
const exports = [
|
|
2203
|
+
JOB_ORCHESTRATOR,
|
|
2204
|
+
JOB_RUN_SERVICE,
|
|
2205
|
+
JOB_STEP_SERVICE,
|
|
2206
|
+
JOBS_MULTI_TENANT
|
|
2207
|
+
];
|
|
2208
|
+
if (opts.backend === "bullmq") {
|
|
2209
|
+
exports.push(BULLMQ_CONNECTION, BULLMQ_RESOLVED_CONFIG);
|
|
2210
|
+
}
|
|
1697
2211
|
return {
|
|
1698
2212
|
module: JobsDomainModule,
|
|
1699
2213
|
global: true,
|
|
1700
2214
|
providers,
|
|
1701
|
-
exports
|
|
1702
|
-
JOB_ORCHESTRATOR,
|
|
1703
|
-
JOB_RUN_SERVICE,
|
|
1704
|
-
JOB_STEP_SERVICE,
|
|
1705
|
-
JOBS_MULTI_TENANT
|
|
1706
|
-
]
|
|
2215
|
+
exports
|
|
1707
2216
|
};
|
|
1708
2217
|
}
|
|
1709
2218
|
};
|
|
@@ -1711,128 +2220,9 @@ JobsDomainModule = __decorateClass([
|
|
|
1711
2220
|
Module({})
|
|
1712
2221
|
], JobsDomainModule);
|
|
1713
2222
|
|
|
1714
|
-
// runtime/subsystems/jobs/pool-config.loader.ts
|
|
1715
|
-
import { existsSync, readFileSync } from "fs";
|
|
1716
|
-
import { resolve } from "path";
|
|
1717
|
-
import { parse as parseYaml } from "yaml";
|
|
1718
|
-
var FRAMEWORK_POOLS = Object.freeze({
|
|
1719
|
-
events_inbound: Object.freeze({
|
|
1720
|
-
queue: "jobs-events-inbound",
|
|
1721
|
-
concurrency: 20,
|
|
1722
|
-
reserved: true,
|
|
1723
|
-
description: "Inbound events drain (events subsystem outbox)."
|
|
1724
|
-
}),
|
|
1725
|
-
events_change: Object.freeze({
|
|
1726
|
-
queue: "jobs-events-change",
|
|
1727
|
-
concurrency: 30,
|
|
1728
|
-
reserved: true,
|
|
1729
|
-
description: "Change events drain (events subsystem outbox)."
|
|
1730
|
-
}),
|
|
1731
|
-
events_outbound: Object.freeze({
|
|
1732
|
-
queue: "jobs-events-outbound",
|
|
1733
|
-
concurrency: 10,
|
|
1734
|
-
reserved: true,
|
|
1735
|
-
description: "Outbound events drain (events subsystem outbox)."
|
|
1736
|
-
}),
|
|
1737
|
-
interactive: Object.freeze({
|
|
1738
|
-
queue: "jobs-interactive",
|
|
1739
|
-
concurrency: 20,
|
|
1740
|
-
reserved: false,
|
|
1741
|
-
description: "User-facing latency-sensitive jobs."
|
|
1742
|
-
}),
|
|
1743
|
-
batch: Object.freeze({
|
|
1744
|
-
queue: "jobs-batch",
|
|
1745
|
-
concurrency: 5,
|
|
1746
|
-
reserved: false,
|
|
1747
|
-
description: "Default pool for background jobs."
|
|
1748
|
-
})
|
|
1749
|
-
});
|
|
1750
|
-
var RESERVED_POOL_NAMES = new Set(
|
|
1751
|
-
Object.entries(FRAMEWORK_POOLS).filter(([, def]) => def.reserved).map(([name]) => name)
|
|
1752
|
-
);
|
|
1753
|
-
var cache = /* @__PURE__ */ new Map();
|
|
1754
|
-
function loadPoolConfig(configPath) {
|
|
1755
|
-
const resolved = resolve(configPath ?? `${process.cwd()}/codegen.config.yaml`);
|
|
1756
|
-
const cached = cache.get(resolved);
|
|
1757
|
-
if (cached) return cached;
|
|
1758
|
-
const merged = /* @__PURE__ */ new Map();
|
|
1759
|
-
for (const [name, def] of Object.entries(FRAMEWORK_POOLS)) {
|
|
1760
|
-
merged.set(name, { ...def });
|
|
1761
|
-
}
|
|
1762
|
-
if (!existsSync(resolved)) {
|
|
1763
|
-
cache.set(resolved, merged);
|
|
1764
|
-
return merged;
|
|
1765
|
-
}
|
|
1766
|
-
let raw;
|
|
1767
|
-
try {
|
|
1768
|
-
raw = parseYaml(readFileSync(resolved, "utf8"));
|
|
1769
|
-
} catch (err) {
|
|
1770
|
-
throw new Error(
|
|
1771
|
-
`pool-config.loader: failed to parse YAML at ${resolved}: ${err.message}`
|
|
1772
|
-
);
|
|
1773
|
-
}
|
|
1774
|
-
const userPools = extractUserPools(raw);
|
|
1775
|
-
for (const [name, userDef] of Object.entries(userPools)) {
|
|
1776
|
-
const existing = merged.get(name);
|
|
1777
|
-
if (existing) {
|
|
1778
|
-
const next = {
|
|
1779
|
-
queue: existing.queue,
|
|
1780
|
-
concurrency: typeof userDef.concurrency === "number" ? userDef.concurrency : existing.concurrency,
|
|
1781
|
-
reserved: existing.reserved,
|
|
1782
|
-
description: userDef.description ?? existing.description
|
|
1783
|
-
};
|
|
1784
|
-
merged.set(name, next);
|
|
1785
|
-
continue;
|
|
1786
|
-
}
|
|
1787
|
-
if (typeof userDef.queue !== "string" || userDef.queue.length === 0) {
|
|
1788
|
-
throw new Error(
|
|
1789
|
-
`pool-config.loader: pool '${name}' must declare a non-empty 'queue'.`
|
|
1790
|
-
);
|
|
1791
|
-
}
|
|
1792
|
-
if (typeof userDef.concurrency !== "number" || userDef.concurrency <= 0) {
|
|
1793
|
-
throw new Error(
|
|
1794
|
-
`pool-config.loader: pool '${name}' must declare a positive 'concurrency'.`
|
|
1795
|
-
);
|
|
1796
|
-
}
|
|
1797
|
-
if (userDef.reserved === true) {
|
|
1798
|
-
throw new Error(
|
|
1799
|
-
`pool-config.loader: user-defined pool '${name}' cannot set 'reserved: true' \u2014 reserved is framework-only.`
|
|
1800
|
-
);
|
|
1801
|
-
}
|
|
1802
|
-
merged.set(name, {
|
|
1803
|
-
queue: userDef.queue,
|
|
1804
|
-
concurrency: userDef.concurrency,
|
|
1805
|
-
reserved: false,
|
|
1806
|
-
description: userDef.description
|
|
1807
|
-
});
|
|
1808
|
-
}
|
|
1809
|
-
cache.set(resolved, merged);
|
|
1810
|
-
return merged;
|
|
1811
|
-
}
|
|
1812
|
-
function allNonReservedPoolNames(config) {
|
|
1813
|
-
const out = [];
|
|
1814
|
-
for (const [name, def] of config) {
|
|
1815
|
-
if (!def.reserved) out.push(name);
|
|
1816
|
-
}
|
|
1817
|
-
return out;
|
|
1818
|
-
}
|
|
1819
|
-
function extractUserPools(raw) {
|
|
1820
|
-
if (!raw || typeof raw !== "object") return {};
|
|
1821
|
-
const jobs2 = raw.jobs;
|
|
1822
|
-
if (!jobs2 || typeof jobs2 !== "object") return {};
|
|
1823
|
-
const pools = jobs2.pools;
|
|
1824
|
-
if (!pools || typeof pools !== "object") return {};
|
|
1825
|
-
const out = {};
|
|
1826
|
-
for (const [name, def] of Object.entries(pools)) {
|
|
1827
|
-
if (!def || typeof def !== "object") continue;
|
|
1828
|
-
out[name] = def;
|
|
1829
|
-
}
|
|
1830
|
-
return out;
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
2223
|
// runtime/subsystems/jobs/job-worker.ts
|
|
1834
|
-
import { Inject as
|
|
1835
|
-
import { and as and4, asc as asc2, desc as desc3, eq as
|
|
2224
|
+
import { Inject as Inject7, Injectable as Injectable8, Logger as Logger4 } from "@nestjs/common";
|
|
2225
|
+
import { and as and4, asc as asc2, desc as desc3, eq as eq5, inArray as inArray3, lt as lt2, lte, sql as sql4 } from "drizzle-orm";
|
|
1836
2226
|
var JOB_WORKER_OPTIONS = /* @__PURE__ */ Symbol("JOB_WORKER_OPTIONS");
|
|
1837
2227
|
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
1838
2228
|
var DEFAULT_STALE_SWEEPER_INTERVAL_MS = 6e4;
|
|
@@ -1895,7 +2285,7 @@ var JobWorker = class {
|
|
|
1895
2285
|
stepService;
|
|
1896
2286
|
options;
|
|
1897
2287
|
moduleRef;
|
|
1898
|
-
logger = new
|
|
2288
|
+
logger = new Logger4(JobWorker.name);
|
|
1899
2289
|
shuttingDown = false;
|
|
1900
2290
|
inFlight = /* @__PURE__ */ new Set();
|
|
1901
2291
|
pollTimer = null;
|
|
@@ -1936,7 +2326,7 @@ var JobWorker = class {
|
|
|
1936
2326
|
await this.drainInFlight();
|
|
1937
2327
|
try {
|
|
1938
2328
|
await this.db.update(jobRuns).set({ status: "pending", claimedAt: null, startedAt: null }).where(
|
|
1939
|
-
and4(
|
|
2329
|
+
and4(eq5(jobRuns.status, "running"), eq5(jobRuns.pool, this.options.pool))
|
|
1940
2330
|
);
|
|
1941
2331
|
} catch (err) {
|
|
1942
2332
|
this.logger.error(`shutdown reset failed: ${err.message}`);
|
|
@@ -1986,8 +2376,8 @@ var JobWorker = class {
|
|
|
1986
2376
|
return this.db.transaction(async (tx) => {
|
|
1987
2377
|
const candidates = await tx.select({ id: jobRuns.id }).from(jobRuns).where(
|
|
1988
2378
|
and4(
|
|
1989
|
-
|
|
1990
|
-
|
|
2379
|
+
eq5(jobRuns.status, "pending"),
|
|
2380
|
+
eq5(jobRuns.pool, pool),
|
|
1991
2381
|
lte(jobRuns.runAt, /* @__PURE__ */ new Date())
|
|
1992
2382
|
)
|
|
1993
2383
|
).orderBy(desc3(jobRuns.priority), asc2(jobRuns.runAt)).limit(1).for("update", { skipLocked: true });
|
|
@@ -1998,7 +2388,7 @@ var JobWorker = class {
|
|
|
1998
2388
|
claimedAt: /* @__PURE__ */ new Date(),
|
|
1999
2389
|
startedAt: /* @__PURE__ */ new Date(),
|
|
2000
2390
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2001
|
-
}).where(
|
|
2391
|
+
}).where(eq5(jobRuns.id, candidate.id)).returning();
|
|
2002
2392
|
return claimed ?? null;
|
|
2003
2393
|
});
|
|
2004
2394
|
}
|
|
@@ -2016,7 +2406,7 @@ var JobWorker = class {
|
|
|
2016
2406
|
await this.db.transaction(async (tx) => {
|
|
2017
2407
|
const threshold = new Date(Date.now() - this.staleThresholdMs);
|
|
2018
2408
|
const stale = await tx.select({ id: jobRuns.id }).from(jobRuns).where(
|
|
2019
|
-
and4(
|
|
2409
|
+
and4(eq5(jobRuns.status, "running"), lt2(jobRuns.claimedAt, threshold))
|
|
2020
2410
|
).for("update", { skipLocked: true });
|
|
2021
2411
|
if (stale.length === 0) return;
|
|
2022
2412
|
const ids = stale.map((r) => r.id);
|
|
@@ -2049,8 +2439,8 @@ var JobWorker = class {
|
|
|
2049
2439
|
if (claimed.concurrencyKey) {
|
|
2050
2440
|
const inflight = await this.db.select({ id: jobRuns.id }).from(jobRuns).where(
|
|
2051
2441
|
and4(
|
|
2052
|
-
|
|
2053
|
-
|
|
2442
|
+
eq5(jobRuns.concurrencyKey, claimed.concurrencyKey),
|
|
2443
|
+
eq5(jobRuns.status, "running")
|
|
2054
2444
|
)
|
|
2055
2445
|
);
|
|
2056
2446
|
const other = inflight.find((r) => r.id !== claimed.id);
|
|
@@ -2060,7 +2450,7 @@ var JobWorker = class {
|
|
|
2060
2450
|
claimedAt: null,
|
|
2061
2451
|
startedAt: null,
|
|
2062
2452
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2063
|
-
}).where(
|
|
2453
|
+
}).where(eq5(jobRuns.id, claimed.id));
|
|
2064
2454
|
return;
|
|
2065
2455
|
}
|
|
2066
2456
|
}
|
|
@@ -2075,7 +2465,7 @@ var JobWorker = class {
|
|
|
2075
2465
|
run: claimed,
|
|
2076
2466
|
step: this.makeStepFn(claimed),
|
|
2077
2467
|
spawnChild: this.makeSpawnFn(claimed),
|
|
2078
|
-
logger: new
|
|
2468
|
+
logger: new Logger4(`JobRun:${claimed.id}`)
|
|
2079
2469
|
};
|
|
2080
2470
|
const attemptsBefore = claimed.attempts ?? 0;
|
|
2081
2471
|
try {
|
|
@@ -2086,7 +2476,7 @@ var JobWorker = class {
|
|
|
2086
2476
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
2087
2477
|
updatedAt: /* @__PURE__ */ new Date(),
|
|
2088
2478
|
attempts: attemptsBefore + 1
|
|
2089
|
-
}).where(
|
|
2479
|
+
}).where(eq5(jobRuns.id, claimed.id));
|
|
2090
2480
|
} catch (err) {
|
|
2091
2481
|
const policy = meta.retry;
|
|
2092
2482
|
const decision = classifyError2(err, policy, attemptsBefore);
|
|
@@ -2101,7 +2491,7 @@ var JobWorker = class {
|
|
|
2101
2491
|
claimedAt: null,
|
|
2102
2492
|
error: serialiseError2(err, nextAttempts, true),
|
|
2103
2493
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2104
|
-
}).where(
|
|
2494
|
+
}).where(eq5(jobRuns.id, claimed.id));
|
|
2105
2495
|
} else {
|
|
2106
2496
|
await this.markFailed(claimed, err, nextAttempts);
|
|
2107
2497
|
}
|
|
@@ -2114,7 +2504,7 @@ var JobWorker = class {
|
|
|
2114
2504
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
2115
2505
|
error: serialiseError2(err, finalAttempts, false),
|
|
2116
2506
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2117
|
-
}).where(
|
|
2507
|
+
}).where(eq5(jobRuns.id, claimed.id));
|
|
2118
2508
|
if (claimed.parentClosePolicy === "terminate") {
|
|
2119
2509
|
try {
|
|
2120
2510
|
await this.orchestrator.cancel(claimed.id, {
|
|
@@ -2217,25 +2607,228 @@ var JobWorker = class {
|
|
|
2217
2607
|
// ============================================================================
|
|
2218
2608
|
};
|
|
2219
2609
|
JobWorker = __decorateClass([
|
|
2220
|
-
|
|
2221
|
-
__decorateParam(0,
|
|
2222
|
-
__decorateParam(1,
|
|
2223
|
-
__decorateParam(2,
|
|
2224
|
-
__decorateParam(3,
|
|
2225
|
-
__decorateParam(4,
|
|
2610
|
+
Injectable8(),
|
|
2611
|
+
__decorateParam(0, Inject7(DRIZZLE)),
|
|
2612
|
+
__decorateParam(1, Inject7(JOB_ORCHESTRATOR)),
|
|
2613
|
+
__decorateParam(2, Inject7(JOB_RUN_SERVICE)),
|
|
2614
|
+
__decorateParam(3, Inject7(JOB_STEP_SERVICE)),
|
|
2615
|
+
__decorateParam(4, Inject7(JOB_WORKER_OPTIONS))
|
|
2226
2616
|
], JobWorker);
|
|
2227
2617
|
|
|
2618
|
+
// runtime/subsystems/jobs/job-worker.bullmq-backend.ts
|
|
2619
|
+
import { Logger as Logger5 } from "@nestjs/common";
|
|
2620
|
+
import { eq as eq6 } from "drizzle-orm";
|
|
2621
|
+
function serialiseError3(err, attempt, retryable) {
|
|
2622
|
+
const e = err;
|
|
2623
|
+
return {
|
|
2624
|
+
message: e?.message ?? String(err),
|
|
2625
|
+
stack: e?.stack,
|
|
2626
|
+
retryable,
|
|
2627
|
+
attempt
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
var BullMQJobWorker = class _BullMQJobWorker {
|
|
2631
|
+
constructor(db, orchestrator, stepService, options, moduleRef) {
|
|
2632
|
+
this.db = db;
|
|
2633
|
+
this.orchestrator = orchestrator;
|
|
2634
|
+
this.stepService = stepService;
|
|
2635
|
+
this.options = options;
|
|
2636
|
+
this.moduleRef = moduleRef;
|
|
2637
|
+
}
|
|
2638
|
+
db;
|
|
2639
|
+
orchestrator;
|
|
2640
|
+
stepService;
|
|
2641
|
+
options;
|
|
2642
|
+
moduleRef;
|
|
2643
|
+
logger = new Logger5(_BullMQJobWorker.name);
|
|
2644
|
+
worker = null;
|
|
2645
|
+
async onModuleInit() {
|
|
2646
|
+
let WorkerCtor;
|
|
2647
|
+
try {
|
|
2648
|
+
const mod = await import("bullmq");
|
|
2649
|
+
WorkerCtor = mod.Worker;
|
|
2650
|
+
} catch {
|
|
2651
|
+
throw new Error(
|
|
2652
|
+
'BullMQ backend requires the "bullmq" package. Install it with: npm install bullmq'
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2655
|
+
this.worker = new WorkerCtor(
|
|
2656
|
+
this.options.queueName,
|
|
2657
|
+
(job) => this.process(job),
|
|
2658
|
+
{
|
|
2659
|
+
connection: this.options.connection,
|
|
2660
|
+
concurrency: this.options.concurrency
|
|
2661
|
+
}
|
|
2662
|
+
);
|
|
2663
|
+
this.worker.on("failed", (job, err) => {
|
|
2664
|
+
if (!job) return;
|
|
2665
|
+
const attemptsMade = job.attemptsMade;
|
|
2666
|
+
const maxAttempts = job.opts.attempts ?? 1;
|
|
2667
|
+
if (attemptsMade >= maxAttempts) {
|
|
2668
|
+
void this.markFailed(job.data.runId, err, attemptsMade);
|
|
2669
|
+
}
|
|
2670
|
+
});
|
|
2671
|
+
this.logger.log(
|
|
2672
|
+
`BullMQ worker started: pool='${this.options.pool}' queue='${this.options.queueName}' concurrency=${this.options.concurrency}`
|
|
2673
|
+
);
|
|
2674
|
+
}
|
|
2675
|
+
async onModuleDestroy() {
|
|
2676
|
+
if (this.worker) {
|
|
2677
|
+
await this.worker.close();
|
|
2678
|
+
this.worker = null;
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
/**
|
|
2682
|
+
* Process one BullMQ job. Returns the handler output (stored by BullMQ as
|
|
2683
|
+
* the job return value AND written to `job_run.output`). Throws on handler
|
|
2684
|
+
* failure so BullMQ applies the retry policy.
|
|
2685
|
+
*/
|
|
2686
|
+
async process(job) {
|
|
2687
|
+
const { runId } = job.data;
|
|
2688
|
+
const [row] = await this.db.select().from(jobRuns).where(eq6(jobRuns.id, runId)).limit(1);
|
|
2689
|
+
if (!row) {
|
|
2690
|
+
this.logger.warn(`process: job_run ${runId} not found; skipping`);
|
|
2691
|
+
return {};
|
|
2692
|
+
}
|
|
2693
|
+
const run = row;
|
|
2694
|
+
if (run.status === "canceled") {
|
|
2695
|
+
return {};
|
|
2696
|
+
}
|
|
2697
|
+
const registryEntry = JOB_HANDLER_REGISTRY.get(run.jobType);
|
|
2698
|
+
if (!registryEntry) {
|
|
2699
|
+
throw new Error(
|
|
2700
|
+
`No handler registered for jobType='${run.jobType}' (run ${run.id})`
|
|
2701
|
+
);
|
|
2702
|
+
}
|
|
2703
|
+
await this.db.update(jobRuns).set({
|
|
2704
|
+
status: "running",
|
|
2705
|
+
claimedAt: /* @__PURE__ */ new Date(),
|
|
2706
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
2707
|
+
attempts: job.attemptsMade + 1,
|
|
2708
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2709
|
+
}).where(eq6(jobRuns.id, run.id));
|
|
2710
|
+
const HandlerClass = registryEntry.handlerClass;
|
|
2711
|
+
const handler = this.moduleRef.get(
|
|
2712
|
+
HandlerClass,
|
|
2713
|
+
{ strict: false }
|
|
2714
|
+
);
|
|
2715
|
+
const ctx = {
|
|
2716
|
+
input: run.input,
|
|
2717
|
+
run,
|
|
2718
|
+
step: this.makeStepFn(run),
|
|
2719
|
+
spawnChild: this.makeSpawnFn(run),
|
|
2720
|
+
logger: new Logger5(`JobRun:${run.id}`)
|
|
2721
|
+
};
|
|
2722
|
+
const output = await handler.run(ctx);
|
|
2723
|
+
await this.db.update(jobRuns).set({
|
|
2724
|
+
status: "completed",
|
|
2725
|
+
output: output ?? {},
|
|
2726
|
+
finishedAt: /* @__PURE__ */ new Date(),
|
|
2727
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2728
|
+
}).where(eq6(jobRuns.id, run.id));
|
|
2729
|
+
return output ?? {};
|
|
2730
|
+
}
|
|
2731
|
+
async markFailed(runId, err, finalAttempts) {
|
|
2732
|
+
const [row] = await this.db.select().from(jobRuns).where(eq6(jobRuns.id, runId)).limit(1);
|
|
2733
|
+
if (!row) return;
|
|
2734
|
+
const run = row;
|
|
2735
|
+
await this.db.update(jobRuns).set({
|
|
2736
|
+
status: "failed",
|
|
2737
|
+
attempts: finalAttempts,
|
|
2738
|
+
finishedAt: /* @__PURE__ */ new Date(),
|
|
2739
|
+
error: serialiseError3(err, finalAttempts, false),
|
|
2740
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2741
|
+
}).where(eq6(jobRuns.id, runId));
|
|
2742
|
+
if (run.parentClosePolicy === "terminate") {
|
|
2743
|
+
try {
|
|
2744
|
+
await this.orchestrator.cancel(run.id, {
|
|
2745
|
+
cascade: true,
|
|
2746
|
+
reason: "parent-failed",
|
|
2747
|
+
tenantId: run.tenantId
|
|
2748
|
+
});
|
|
2749
|
+
} catch (cascadeErr) {
|
|
2750
|
+
this.logger.warn(
|
|
2751
|
+
`cascade on failed run ${run.id}: ${cascadeErr.message}`
|
|
2752
|
+
);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
// ── ctx.step / ctx.spawnChild (mirror JobWorker) ──────────────────────────
|
|
2757
|
+
makeStepFn(run) {
|
|
2758
|
+
return async (stepId, fn, _opts) => {
|
|
2759
|
+
void _opts;
|
|
2760
|
+
const existing = await this.stepService.findStep(run.id, stepId);
|
|
2761
|
+
if (existing?.status === "completed") {
|
|
2762
|
+
return existing.output;
|
|
2763
|
+
}
|
|
2764
|
+
const nextAttempts = (existing?.attempts ?? 0) + 1;
|
|
2765
|
+
const seq = nextAttempts;
|
|
2766
|
+
await this.stepService.recordStep({
|
|
2767
|
+
jobRunId: run.id,
|
|
2768
|
+
stepId,
|
|
2769
|
+
kind: "task",
|
|
2770
|
+
seq,
|
|
2771
|
+
status: "running",
|
|
2772
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
2773
|
+
attempts: nextAttempts
|
|
2774
|
+
});
|
|
2775
|
+
try {
|
|
2776
|
+
const output = await fn();
|
|
2777
|
+
await this.stepService.recordStep({
|
|
2778
|
+
jobRunId: run.id,
|
|
2779
|
+
stepId,
|
|
2780
|
+
kind: "task",
|
|
2781
|
+
seq,
|
|
2782
|
+
status: "completed",
|
|
2783
|
+
output,
|
|
2784
|
+
finishedAt: /* @__PURE__ */ new Date(),
|
|
2785
|
+
attempts: nextAttempts
|
|
2786
|
+
});
|
|
2787
|
+
return output;
|
|
2788
|
+
} catch (err) {
|
|
2789
|
+
await this.stepService.recordStep({
|
|
2790
|
+
jobRunId: run.id,
|
|
2791
|
+
stepId,
|
|
2792
|
+
kind: "task",
|
|
2793
|
+
seq,
|
|
2794
|
+
status: "failed",
|
|
2795
|
+
error: serialiseError3(err, nextAttempts, false),
|
|
2796
|
+
finishedAt: /* @__PURE__ */ new Date(),
|
|
2797
|
+
attempts: nextAttempts
|
|
2798
|
+
});
|
|
2799
|
+
throw err;
|
|
2800
|
+
}
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2803
|
+
makeSpawnFn(run) {
|
|
2804
|
+
return async (type, input, opts) => {
|
|
2805
|
+
return this.orchestrator.start(type, input, {
|
|
2806
|
+
parentRunId: run.id,
|
|
2807
|
+
parentClosePolicy: opts?.closePolicy,
|
|
2808
|
+
runAt: opts?.runAt,
|
|
2809
|
+
priority: opts?.priority,
|
|
2810
|
+
tags: opts?.tags,
|
|
2811
|
+
triggerSource: "parent",
|
|
2812
|
+
triggerRef: run.id,
|
|
2813
|
+
tenantId: run.tenantId
|
|
2814
|
+
});
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
};
|
|
2818
|
+
|
|
2228
2819
|
// runtime/subsystems/jobs/job-worker.module.ts
|
|
2229
2820
|
var DEFAULT_SHUTDOWN_TIMEOUT_MS2 = 3e4;
|
|
2230
2821
|
var JOB_WORKER_MODULE_OPTIONS = /* @__PURE__ */ Symbol("JOB_WORKER_MODULE_OPTIONS");
|
|
2231
2822
|
var JobWorkerOrchestrator = class {
|
|
2232
|
-
constructor(orchestrator, runService, stepService, options, db = null, moduleRef) {
|
|
2823
|
+
constructor(orchestrator, runService, stepService, options, db = null, moduleRef, bullConnection = null, bullConfig = null) {
|
|
2233
2824
|
this.orchestrator = orchestrator;
|
|
2234
2825
|
this.runService = runService;
|
|
2235
2826
|
this.stepService = stepService;
|
|
2236
2827
|
this.options = options;
|
|
2237
2828
|
this.db = db;
|
|
2238
2829
|
this.moduleRef = moduleRef;
|
|
2830
|
+
this.bullConnection = bullConnection;
|
|
2831
|
+
this.bullConfig = bullConfig;
|
|
2239
2832
|
}
|
|
2240
2833
|
orchestrator;
|
|
2241
2834
|
runService;
|
|
@@ -2243,7 +2836,9 @@ var JobWorkerOrchestrator = class {
|
|
|
2243
2836
|
options;
|
|
2244
2837
|
db;
|
|
2245
2838
|
moduleRef;
|
|
2246
|
-
|
|
2839
|
+
bullConnection;
|
|
2840
|
+
bullConfig;
|
|
2841
|
+
logger = new Logger6(JobWorkerOrchestrator.name);
|
|
2247
2842
|
workers = [];
|
|
2248
2843
|
// ============================================================================
|
|
2249
2844
|
// Lifecycle
|
|
@@ -2260,7 +2855,7 @@ var JobWorkerOrchestrator = class {
|
|
|
2260
2855
|
if (backend !== "memory" && orphaned.length > 0) {
|
|
2261
2856
|
throw new BootValidationError(orphaned);
|
|
2262
2857
|
}
|
|
2263
|
-
const activePools = this.options.pools
|
|
2858
|
+
const activePools = this.options.pools ? this.options.pools : this.options.allPools ? allPoolNames(poolConfig) : allNonReservedPoolNames(poolConfig);
|
|
2264
2859
|
for (const poolName of activePools) {
|
|
2265
2860
|
const def = poolConfig.get(poolName);
|
|
2266
2861
|
if (!def) {
|
|
@@ -2273,11 +2868,11 @@ var JobWorkerOrchestrator = class {
|
|
|
2273
2868
|
concurrency: def.concurrency,
|
|
2274
2869
|
shutdownTimeoutMs: this.options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS2
|
|
2275
2870
|
};
|
|
2276
|
-
const worker = this.options.workerFactory ? this.options.workerFactory(workerOptions) : this.spawnWorker(workerOptions);
|
|
2277
|
-
worker.onModuleInit();
|
|
2871
|
+
const worker = this.options.workerFactory ? this.options.workerFactory(workerOptions) : backend === "bullmq" ? this.spawnBullMQWorker(poolName, def.queue, def.concurrency, poolConfig) : this.spawnWorker(workerOptions);
|
|
2872
|
+
await worker.onModuleInit();
|
|
2278
2873
|
this.workers.push(worker);
|
|
2279
2874
|
this.logger.log(
|
|
2280
|
-
`JobWorker started: pool='${poolName}' (queue='${def.queue}') concurrency=${def.concurrency}`
|
|
2875
|
+
`JobWorker started: pool='${poolName}' (queue='${def.queue}') concurrency=${def.concurrency} backend='${backend}'`
|
|
2281
2876
|
);
|
|
2282
2877
|
}
|
|
2283
2878
|
}
|
|
@@ -2294,6 +2889,16 @@ var JobWorkerOrchestrator = class {
|
|
|
2294
2889
|
}
|
|
2295
2890
|
}
|
|
2296
2891
|
this.workers.length = 0;
|
|
2892
|
+
const orch = this.orchestrator;
|
|
2893
|
+
if (typeof orch.closeConnections === "function") {
|
|
2894
|
+
try {
|
|
2895
|
+
await orch.closeConnections();
|
|
2896
|
+
} catch (err) {
|
|
2897
|
+
this.logger.error(
|
|
2898
|
+
`BullMQ orchestrator connection close failed: ${err.message}`
|
|
2899
|
+
);
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2297
2902
|
}
|
|
2298
2903
|
// ============================================================================
|
|
2299
2904
|
// Internals
|
|
@@ -2355,15 +2960,57 @@ var JobWorkerOrchestrator = class {
|
|
|
2355
2960
|
this.moduleRef
|
|
2356
2961
|
);
|
|
2357
2962
|
}
|
|
2963
|
+
/**
|
|
2964
|
+
* BULLMQ-1 — spawn a per-pool `BullMQJobWorker`. Requires the Drizzle
|
|
2965
|
+
* client (the worker drives `job_run` as the source of truth) AND the
|
|
2966
|
+
* resolved BullMQ connection (bound by `JobsDomainModule` when
|
|
2967
|
+
* `backend: 'bullmq'`). The queue name is derived identically to the
|
|
2968
|
+
* orchestrator's `dispatch` via `resolvePoolQueueName(pool, …)` so producer
|
|
2969
|
+
* and consumer agree.
|
|
2970
|
+
*/
|
|
2971
|
+
spawnBullMQWorker(pool, _queueAlias, concurrency, poolConfig) {
|
|
2972
|
+
if (!this.db) {
|
|
2973
|
+
throw new Error(
|
|
2974
|
+
`JobWorkerModule: BullMQ worker spawning requires the Drizzle client (no DRIZZLE provider available) \u2014 job_run remains the source of truth.`
|
|
2975
|
+
);
|
|
2976
|
+
}
|
|
2977
|
+
if (!this.bullConnection) {
|
|
2978
|
+
throw new Error(
|
|
2979
|
+
`JobWorkerModule: BullMQ worker spawning requires a resolved BULLMQ_CONNECTION. Ensure JobsDomainModule was booted with backend: 'bullmq'.`
|
|
2980
|
+
);
|
|
2981
|
+
}
|
|
2982
|
+
if (!this.moduleRef) {
|
|
2983
|
+
throw new Error(
|
|
2984
|
+
`JobWorkerModule: ModuleRef not available \u2014 cannot construct BullMQJobWorker with handler DI support.`
|
|
2985
|
+
);
|
|
2986
|
+
}
|
|
2987
|
+
const queueName = resolvePoolQueueName(pool, this.bullConfig, poolConfig);
|
|
2988
|
+
return new BullMQJobWorker(
|
|
2989
|
+
this.db,
|
|
2990
|
+
this.orchestrator,
|
|
2991
|
+
this.stepService,
|
|
2992
|
+
{
|
|
2993
|
+
pool,
|
|
2994
|
+
queueName,
|
|
2995
|
+
concurrency,
|
|
2996
|
+
connection: this.bullConnection
|
|
2997
|
+
},
|
|
2998
|
+
this.moduleRef
|
|
2999
|
+
);
|
|
3000
|
+
}
|
|
2358
3001
|
};
|
|
2359
3002
|
JobWorkerOrchestrator = __decorateClass([
|
|
2360
|
-
|
|
2361
|
-
__decorateParam(0,
|
|
2362
|
-
__decorateParam(1,
|
|
2363
|
-
__decorateParam(2,
|
|
2364
|
-
__decorateParam(3,
|
|
2365
|
-
__decorateParam(4,
|
|
2366
|
-
__decorateParam(4,
|
|
3003
|
+
Injectable9(),
|
|
3004
|
+
__decorateParam(0, Inject8(JOB_ORCHESTRATOR)),
|
|
3005
|
+
__decorateParam(1, Inject8(JOB_RUN_SERVICE)),
|
|
3006
|
+
__decorateParam(2, Inject8(JOB_STEP_SERVICE)),
|
|
3007
|
+
__decorateParam(3, Inject8(JOB_WORKER_MODULE_OPTIONS)),
|
|
3008
|
+
__decorateParam(4, Optional3()),
|
|
3009
|
+
__decorateParam(4, Inject8(DRIZZLE)),
|
|
3010
|
+
__decorateParam(6, Optional3()),
|
|
3011
|
+
__decorateParam(6, Inject8(BULLMQ_CONNECTION)),
|
|
3012
|
+
__decorateParam(7, Optional3()),
|
|
3013
|
+
__decorateParam(7, Inject8(BULLMQ_RESOLVED_CONFIG))
|
|
2367
3014
|
], JobWorkerOrchestrator);
|
|
2368
3015
|
var JobWorkerModule = class {
|
|
2369
3016
|
static forRoot(opts) {
|
|
@@ -2380,7 +3027,14 @@ var JobWorkerModule = class {
|
|
|
2380
3027
|
{ provide: JOB_WORKER_MODULE_OPTIONS, useValue: opts },
|
|
2381
3028
|
JobWorkerOrchestrator
|
|
2382
3029
|
],
|
|
2383
|
-
|
|
3030
|
+
// BULLMQ-1 Phase 1 — export the options token so `BridgeModule`'s
|
|
3031
|
+
// reserved-pool guard (`onModuleInit`) can actually inject it.
|
|
3032
|
+
// Previously `exports: []` left the `@Optional()` inject resolving to
|
|
3033
|
+
// `undefined` and the guard silently no-opped (a dead check). With the
|
|
3034
|
+
// token exported the guard fires for real; consumers that omit the
|
|
3035
|
+
// reserved pools (and don't set `allPools`) now fail fast with
|
|
3036
|
+
// `BridgeReservedPoolsNotPolledError` — which is correct.
|
|
3037
|
+
exports: [JOB_WORKER_MODULE_OPTIONS]
|
|
2384
3038
|
};
|
|
2385
3039
|
}
|
|
2386
3040
|
};
|