@pattern-stack/codegen 0.8.0 → 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 +70 -0
- package/dist/runtime/subsystems/auth/controllers/auth.controller.d.ts +1 -0
- package/dist/runtime/subsystems/auth/index.d.ts +2 -0
- package/dist/runtime/subsystems/auth/index.js +55 -0
- package/dist/runtime/subsystems/auth/index.js.map +1 -1
- package/dist/runtime/subsystems/auth/middleware/requester-context.d.ts +81 -0
- package/dist/runtime/subsystems/auth/middleware/requester-context.js +60 -0
- package/dist/runtime/subsystems/auth/middleware/requester-context.js.map +1 -0
- package/dist/runtime/subsystems/auth/protocols/user-context.d.ts +18 -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 +2 -0
- package/dist/runtime/subsystems/index.js +1198 -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 +43 -7
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/runtime/subsystems/auth/index.ts +8 -0
- package/runtime/subsystems/auth/middleware/requester-context.ts +141 -0
- package/runtime/subsystems/auth/protocols/user-context.ts +17 -0
- 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
|
@@ -629,7 +629,58 @@ function notInStatus(statuses) {
|
|
|
629
629
|
|
|
630
630
|
// runtime/subsystems/jobs/job-run-service.drizzle-backend.ts
|
|
631
631
|
import { Inject as Inject2, Injectable as Injectable2 } from "@nestjs/common";
|
|
632
|
-
import { and as and2, asc, desc as desc2, eq as eq2, inArray as inArray2, isNull, sql as sql3 } from "drizzle-orm";
|
|
632
|
+
import { and as and2, asc, desc as desc2, eq as eq2, gte, inArray as inArray2, isNull, lt, or, sql as sql3 } from "drizzle-orm";
|
|
633
|
+
|
|
634
|
+
// runtime/subsystems/jobs/job-run-keyset-cursor.ts
|
|
635
|
+
var DEFAULT_LIST_LIMIT = 50;
|
|
636
|
+
var MAX_LIST_LIMIT = 200;
|
|
637
|
+
function clampLimit(limit) {
|
|
638
|
+
if (typeof limit !== "number" || !Number.isFinite(limit)) {
|
|
639
|
+
return DEFAULT_LIST_LIMIT;
|
|
640
|
+
}
|
|
641
|
+
const floored = Math.floor(limit);
|
|
642
|
+
if (floored < 1) return 1;
|
|
643
|
+
if (floored > MAX_LIST_LIMIT) return MAX_LIST_LIMIT;
|
|
644
|
+
return floored;
|
|
645
|
+
}
|
|
646
|
+
function encodeKeysetCursor(keyset) {
|
|
647
|
+
const tuple = [keyset.createdAt.toISOString(), keyset.id];
|
|
648
|
+
return Buffer.from(JSON.stringify(tuple), "utf8").toString("base64url");
|
|
649
|
+
}
|
|
650
|
+
function decodeKeysetCursor(cursor) {
|
|
651
|
+
try {
|
|
652
|
+
const json = Buffer.from(cursor, "base64url").toString("utf8");
|
|
653
|
+
const parsed = JSON.parse(json);
|
|
654
|
+
if (!Array.isArray(parsed) || parsed.length !== 2) return null;
|
|
655
|
+
const [iso, id] = parsed;
|
|
656
|
+
if (typeof iso !== "string" || typeof id !== "string") return null;
|
|
657
|
+
const createdAt = new Date(iso);
|
|
658
|
+
if (Number.isNaN(createdAt.getTime())) return null;
|
|
659
|
+
return { createdAt, id };
|
|
660
|
+
} catch {
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
function toJobRunSummary(r) {
|
|
665
|
+
return {
|
|
666
|
+
runId: r.id,
|
|
667
|
+
rootRunId: r.rootRunId,
|
|
668
|
+
jobType: r.jobType,
|
|
669
|
+
pool: r.pool,
|
|
670
|
+
status: r.status,
|
|
671
|
+
scopeEntityType: r.scopeEntityType,
|
|
672
|
+
scopeEntityId: r.scopeEntityId,
|
|
673
|
+
tenantId: r.tenantId,
|
|
674
|
+
attempts: r.attempts,
|
|
675
|
+
errorMessage: r.error?.message ?? null,
|
|
676
|
+
runAt: r.runAt,
|
|
677
|
+
startedAt: r.startedAt,
|
|
678
|
+
finishedAt: r.finishedAt,
|
|
679
|
+
createdAt: r.createdAt
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// runtime/subsystems/jobs/job-run-service.drizzle-backend.ts
|
|
633
684
|
var NON_TERMINAL_STATUSES = [
|
|
634
685
|
"pending",
|
|
635
686
|
"running",
|
|
@@ -752,6 +803,37 @@ var DrizzleJobRunService = class {
|
|
|
752
803
|
createdAt: r.createdAt
|
|
753
804
|
}));
|
|
754
805
|
}
|
|
806
|
+
async listJobRuns(query = {}) {
|
|
807
|
+
const limit = clampLimit(query.limit);
|
|
808
|
+
const conditions = [];
|
|
809
|
+
const tenantCond = this.tenantCondition("listJobRuns", query.tenantId);
|
|
810
|
+
if (tenantCond) conditions.push(tenantCond);
|
|
811
|
+
if (query.poolId) conditions.push(eq2(jobRuns.pool, query.poolId));
|
|
812
|
+
if (query.rootRunId) conditions.push(eq2(jobRuns.rootRunId, query.rootRunId));
|
|
813
|
+
if (query.status) conditions.push(eq2(jobRuns.status, query.status));
|
|
814
|
+
if (query.since) conditions.push(gte(jobRuns.createdAt, query.since));
|
|
815
|
+
if (query.cursor) {
|
|
816
|
+
const keyset = decodeKeysetCursor(query.cursor);
|
|
817
|
+
if (keyset) {
|
|
818
|
+
conditions.push(
|
|
819
|
+
or(
|
|
820
|
+
lt(jobRuns.createdAt, keyset.createdAt),
|
|
821
|
+
and2(
|
|
822
|
+
eq2(jobRuns.createdAt, keyset.createdAt),
|
|
823
|
+
lt(jobRuns.id, keyset.id)
|
|
824
|
+
)
|
|
825
|
+
)
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
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);
|
|
830
|
+
const hasMore = rows.length > limit;
|
|
831
|
+
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
832
|
+
const items = page.map(toJobRunSummary);
|
|
833
|
+
const last = page[page.length - 1];
|
|
834
|
+
const nextCursor = hasMore && last ? encodeKeysetCursor({ createdAt: last.createdAt, id: last.id }) : null;
|
|
835
|
+
return { items, nextCursor };
|
|
836
|
+
}
|
|
755
837
|
/**
|
|
756
838
|
* Internal helper used by cascade paths (not on the public protocol).
|
|
757
839
|
* Exposed as a public method on the concrete class so infrastructure
|
|
@@ -800,22 +882,608 @@ var DrizzleJobStepService = class {
|
|
|
800
882
|
finishedAt: values.finishedAt,
|
|
801
883
|
attempts: values.attempts
|
|
802
884
|
}
|
|
803
|
-
}).returning();
|
|
804
|
-
return row;
|
|
885
|
+
}).returning();
|
|
886
|
+
return row;
|
|
887
|
+
}
|
|
888
|
+
async findStep(runId, stepId) {
|
|
889
|
+
const [row] = await this.db.select().from(jobSteps).where(and3(eq3(jobSteps.jobRunId, runId), eq3(jobSteps.stepId, stepId))).limit(1);
|
|
890
|
+
return row ?? null;
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
DrizzleJobStepService = __decorateClass([
|
|
894
|
+
Injectable3(),
|
|
895
|
+
__decorateParam(0, Inject3(DRIZZLE))
|
|
896
|
+
], DrizzleJobStepService);
|
|
897
|
+
|
|
898
|
+
// runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts
|
|
899
|
+
import { createHash } from "crypto";
|
|
900
|
+
import { Inject as Inject4, Injectable as Injectable4, Logger as Logger2, Optional } from "@nestjs/common";
|
|
901
|
+
import { eq as eq4 } from "drizzle-orm";
|
|
902
|
+
|
|
903
|
+
// runtime/subsystems/jobs/pool-config.loader.ts
|
|
904
|
+
import { existsSync, readFileSync } from "fs";
|
|
905
|
+
import { resolve } from "path";
|
|
906
|
+
import { parse as parseYaml } from "yaml";
|
|
907
|
+
var FRAMEWORK_POOLS = Object.freeze({
|
|
908
|
+
events_inbound: Object.freeze({
|
|
909
|
+
queue: "jobs-events-inbound",
|
|
910
|
+
concurrency: 20,
|
|
911
|
+
reserved: true,
|
|
912
|
+
description: "Inbound events drain (events subsystem outbox)."
|
|
913
|
+
}),
|
|
914
|
+
events_change: Object.freeze({
|
|
915
|
+
queue: "jobs-events-change",
|
|
916
|
+
concurrency: 30,
|
|
917
|
+
reserved: true,
|
|
918
|
+
description: "Change events drain (events subsystem outbox)."
|
|
919
|
+
}),
|
|
920
|
+
events_outbound: Object.freeze({
|
|
921
|
+
queue: "jobs-events-outbound",
|
|
922
|
+
concurrency: 10,
|
|
923
|
+
reserved: true,
|
|
924
|
+
description: "Outbound events drain (events subsystem outbox)."
|
|
925
|
+
}),
|
|
926
|
+
interactive: Object.freeze({
|
|
927
|
+
queue: "jobs-interactive",
|
|
928
|
+
concurrency: 20,
|
|
929
|
+
reserved: false,
|
|
930
|
+
description: "User-facing latency-sensitive jobs."
|
|
931
|
+
}),
|
|
932
|
+
batch: Object.freeze({
|
|
933
|
+
queue: "jobs-batch",
|
|
934
|
+
concurrency: 5,
|
|
935
|
+
reserved: false,
|
|
936
|
+
description: "Default pool for background jobs."
|
|
937
|
+
})
|
|
938
|
+
});
|
|
939
|
+
var RESERVED_POOL_NAMES = new Set(
|
|
940
|
+
Object.entries(FRAMEWORK_POOLS).filter(([, def]) => def.reserved).map(([name]) => name)
|
|
941
|
+
);
|
|
942
|
+
var cache = /* @__PURE__ */ new Map();
|
|
943
|
+
function loadPoolConfig(configPath) {
|
|
944
|
+
const resolved = resolve(configPath ?? `${process.cwd()}/codegen.config.yaml`);
|
|
945
|
+
const cached = cache.get(resolved);
|
|
946
|
+
if (cached) return cached;
|
|
947
|
+
const merged = /* @__PURE__ */ new Map();
|
|
948
|
+
for (const [name, def] of Object.entries(FRAMEWORK_POOLS)) {
|
|
949
|
+
merged.set(name, { ...def });
|
|
950
|
+
}
|
|
951
|
+
if (!existsSync(resolved)) {
|
|
952
|
+
cache.set(resolved, merged);
|
|
953
|
+
return merged;
|
|
954
|
+
}
|
|
955
|
+
let raw;
|
|
956
|
+
try {
|
|
957
|
+
raw = parseYaml(readFileSync(resolved, "utf8"));
|
|
958
|
+
} catch (err) {
|
|
959
|
+
throw new Error(
|
|
960
|
+
`pool-config.loader: failed to parse YAML at ${resolved}: ${err.message}`
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
const userPools = extractUserPools(raw);
|
|
964
|
+
for (const [name, userDef] of Object.entries(userPools)) {
|
|
965
|
+
const existing = merged.get(name);
|
|
966
|
+
if (existing) {
|
|
967
|
+
const next = {
|
|
968
|
+
queue: existing.queue,
|
|
969
|
+
concurrency: typeof userDef.concurrency === "number" ? userDef.concurrency : existing.concurrency,
|
|
970
|
+
reserved: existing.reserved,
|
|
971
|
+
description: userDef.description ?? existing.description
|
|
972
|
+
};
|
|
973
|
+
merged.set(name, next);
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
if (typeof userDef.queue !== "string" || userDef.queue.length === 0) {
|
|
977
|
+
throw new Error(
|
|
978
|
+
`pool-config.loader: pool '${name}' must declare a non-empty 'queue'.`
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
if (typeof userDef.concurrency !== "number" || userDef.concurrency <= 0) {
|
|
982
|
+
throw new Error(
|
|
983
|
+
`pool-config.loader: pool '${name}' must declare a positive 'concurrency'.`
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
if (userDef.reserved === true) {
|
|
987
|
+
throw new Error(
|
|
988
|
+
`pool-config.loader: user-defined pool '${name}' cannot set 'reserved: true' \u2014 reserved is framework-only.`
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
merged.set(name, {
|
|
992
|
+
queue: userDef.queue,
|
|
993
|
+
concurrency: userDef.concurrency,
|
|
994
|
+
reserved: false,
|
|
995
|
+
description: userDef.description
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
cache.set(resolved, merged);
|
|
999
|
+
return merged;
|
|
1000
|
+
}
|
|
1001
|
+
function allNonReservedPoolNames(config) {
|
|
1002
|
+
const out = [];
|
|
1003
|
+
for (const [name, def] of config) {
|
|
1004
|
+
if (!def.reserved) out.push(name);
|
|
1005
|
+
}
|
|
1006
|
+
return out;
|
|
1007
|
+
}
|
|
1008
|
+
function allPoolNames(config) {
|
|
1009
|
+
return [...config.keys()];
|
|
1010
|
+
}
|
|
1011
|
+
function extractUserPools(raw) {
|
|
1012
|
+
if (!raw || typeof raw !== "object") return {};
|
|
1013
|
+
const jobs2 = raw.jobs;
|
|
1014
|
+
if (!jobs2 || typeof jobs2 !== "object") return {};
|
|
1015
|
+
const pools = jobs2.pools;
|
|
1016
|
+
if (!pools || typeof pools !== "object") return {};
|
|
1017
|
+
const out = {};
|
|
1018
|
+
for (const [name, def] of Object.entries(pools)) {
|
|
1019
|
+
if (!def || typeof def !== "object") continue;
|
|
1020
|
+
out[name] = def;
|
|
1021
|
+
}
|
|
1022
|
+
return out;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// runtime/subsystems/jobs/bullmq.config.ts
|
|
1026
|
+
var BULLMQ_CONNECTION = /* @__PURE__ */ Symbol("BULLMQ_CONNECTION");
|
|
1027
|
+
var BULLMQ_RESOLVED_CONFIG = /* @__PURE__ */ Symbol("BULLMQ_RESOLVED_CONFIG");
|
|
1028
|
+
var DEFAULT_REDIS_URL = "redis://localhost:6379";
|
|
1029
|
+
var DEFAULT_BULL_BOARD_MOUNT = "/admin/queues";
|
|
1030
|
+
function resolveBullMqConfig(ext) {
|
|
1031
|
+
const url = ext?.redis_url ?? process.env.REDIS_URL ?? DEFAULT_REDIS_URL;
|
|
1032
|
+
const resolved = {
|
|
1033
|
+
connection: { url },
|
|
1034
|
+
queuePrefix: ext?.queue_prefix
|
|
1035
|
+
};
|
|
1036
|
+
if (ext?.bull_board?.enabled) {
|
|
1037
|
+
resolved.bullBoard = {
|
|
1038
|
+
enabled: true,
|
|
1039
|
+
mountPath: ext.bull_board.mount_path ?? DEFAULT_BULL_BOARD_MOUNT
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
return resolved;
|
|
1043
|
+
}
|
|
1044
|
+
function resolvePoolQueueName(pool, config, poolConfig = loadPoolConfig()) {
|
|
1045
|
+
const alias = poolConfig.get(pool)?.queue ?? pool;
|
|
1046
|
+
const prefix = config?.queuePrefix;
|
|
1047
|
+
return prefix ? `${prefix}:${alias}` : alias;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts
|
|
1051
|
+
function sha1JobId(idempotencyKey) {
|
|
1052
|
+
return createHash("sha1").update(idempotencyKey).digest("hex");
|
|
1053
|
+
}
|
|
1054
|
+
var BullMQJobOrchestrator = class extends DrizzleJobOrchestrator {
|
|
1055
|
+
constructor(db, multiTenant, connection, bullConfig = null) {
|
|
1056
|
+
super(db, multiTenant);
|
|
1057
|
+
this.connection = connection;
|
|
1058
|
+
this.bullConfig = bullConfig;
|
|
1059
|
+
this.bullDb = db;
|
|
1060
|
+
}
|
|
1061
|
+
connection;
|
|
1062
|
+
bullConfig;
|
|
1063
|
+
// TODO(logging-subsystem): swap to ILogger once ADR-028 lands
|
|
1064
|
+
bullLogger = new Logger2(BullMQJobOrchestrator.name);
|
|
1065
|
+
/** Lazily-opened `Queue` handles, one per pool. */
|
|
1066
|
+
queues = /* @__PURE__ */ new Map();
|
|
1067
|
+
/** Single FlowProducer for parent/child hierarchies. Lazily opened. */
|
|
1068
|
+
_flow = null;
|
|
1069
|
+
/**
|
|
1070
|
+
* Cached `bullmq` value constructors, populated by `loadBullMq()` on first
|
|
1071
|
+
* use (the `start`/`cancel`/`replay` entrypoints `await` it before touching
|
|
1072
|
+
* a queue). Kept off the import graph so a `drizzle`-only consumer never
|
|
1073
|
+
* resolves the optional `'bullmq'` package.
|
|
1074
|
+
*/
|
|
1075
|
+
QueueCtor = null;
|
|
1076
|
+
FlowProducerCtor = null;
|
|
1077
|
+
bullMqLoad = null;
|
|
1078
|
+
/**
|
|
1079
|
+
* Own reference to the Drizzle client. `DrizzleJobOrchestrator.db` is
|
|
1080
|
+
* `private` (can't be redeclared even privately in a subclass), and the
|
|
1081
|
+
* spec forbids touching that file — so the subclass keeps its own handle
|
|
1082
|
+
* under a distinct name (same instance, passed through to `super`) for the
|
|
1083
|
+
* cancel-cascade snapshot + definition/run loads below.
|
|
1084
|
+
*/
|
|
1085
|
+
bullDb;
|
|
1086
|
+
/**
|
|
1087
|
+
* Lazily load the optional `bullmq` package and cache its value
|
|
1088
|
+
* constructors. Idempotent (single in-flight promise). Throws a friendly,
|
|
1089
|
+
* actionable error when the consumer selected `backend: 'bullmq'` but did
|
|
1090
|
+
* not install the package — mirrors `createRedisClient` in the redis event
|
|
1091
|
+
* backend. Must be `await`ed before any `queueFor`/`flow` access.
|
|
1092
|
+
*/
|
|
1093
|
+
async loadBullMq() {
|
|
1094
|
+
if (this.QueueCtor && this.FlowProducerCtor) return;
|
|
1095
|
+
if (!this.bullMqLoad) {
|
|
1096
|
+
this.bullMqLoad = (async () => {
|
|
1097
|
+
try {
|
|
1098
|
+
const mod = await import("bullmq");
|
|
1099
|
+
this.QueueCtor = mod.Queue;
|
|
1100
|
+
this.FlowProducerCtor = mod.FlowProducer;
|
|
1101
|
+
} catch {
|
|
1102
|
+
throw new Error(
|
|
1103
|
+
'BullMQ backend requires the "bullmq" package. Install it with: npm install bullmq'
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
})();
|
|
1107
|
+
}
|
|
1108
|
+
await this.bullMqLoad;
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Open (or reuse) the `Queue` for a pool. Synchronous — callers `await
|
|
1112
|
+
* loadBullMq()` first so `QueueCtor` is populated.
|
|
1113
|
+
*/
|
|
1114
|
+
queueFor(pool) {
|
|
1115
|
+
if (!this.QueueCtor) {
|
|
1116
|
+
throw new Error("BullMQJobOrchestrator: queueFor called before loadBullMq()");
|
|
1117
|
+
}
|
|
1118
|
+
const name = resolvePoolQueueName(pool, this.bullConfig);
|
|
1119
|
+
let q = this.queues.get(name);
|
|
1120
|
+
if (!q) {
|
|
1121
|
+
q = new this.QueueCtor(name, { connection: this.connection });
|
|
1122
|
+
this.queues.set(name, q);
|
|
1123
|
+
}
|
|
1124
|
+
return q;
|
|
1125
|
+
}
|
|
1126
|
+
flow() {
|
|
1127
|
+
if (!this.FlowProducerCtor) {
|
|
1128
|
+
throw new Error("BullMQJobOrchestrator: flow called before loadBullMq()");
|
|
1129
|
+
}
|
|
1130
|
+
if (!this._flow) {
|
|
1131
|
+
this._flow = new this.FlowProducerCtor({ connection: this.connection });
|
|
1132
|
+
}
|
|
1133
|
+
return this._flow;
|
|
1134
|
+
}
|
|
1135
|
+
// ==========================================================================
|
|
1136
|
+
// start — Postgres insert (super) + BullMQ dispatch
|
|
1137
|
+
// ==========================================================================
|
|
1138
|
+
async start(type, input, opts = {}, tx) {
|
|
1139
|
+
const run = await super.start(type, input, opts, tx);
|
|
1140
|
+
await this.dispatch(run, type);
|
|
1141
|
+
return run;
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Map a `job_run` row onto a BullMQ job via `queue.add`. When the run has a
|
|
1145
|
+
* `parentRunId` we attach it to the parent's existing BullMQ job through the
|
|
1146
|
+
* `parent: { id, queue }` opt — BullMQ then tracks the parent/child link in
|
|
1147
|
+
* its own graph. (The FlowProducer is reserved for whole-tree atomic
|
|
1148
|
+
* submits, exposed as an opt-in extension via `flowProducer()`; runtime
|
|
1149
|
+
* `ctx.spawnChild` is incremental, so `queue.add` with a parent ref is the
|
|
1150
|
+
* correct primitive here.)
|
|
1151
|
+
*
|
|
1152
|
+
* The `jobId` is colon-safe + stable: `sha1(dedupeKey)` when a dedupe key is
|
|
1153
|
+
* present (so the same logical key dedups), else the `job_run.id` UUID
|
|
1154
|
+
* (already colon-free).
|
|
1155
|
+
*
|
|
1156
|
+
* The domain `parentClosePolicy` cascade is still enforced in Postgres by
|
|
1157
|
+
* the shared `cancel` path — BullMQ's parent link is dispatch bookkeeping,
|
|
1158
|
+
* not the authority.
|
|
1159
|
+
*/
|
|
1160
|
+
async dispatch(run, type) {
|
|
1161
|
+
await this.loadBullMq();
|
|
1162
|
+
const def = await this.loadDefinition(type);
|
|
1163
|
+
const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;
|
|
1164
|
+
const jobOpts = {
|
|
1165
|
+
jobId,
|
|
1166
|
+
...this.retryOpts(def),
|
|
1167
|
+
...this.dedupeOpts(run, def)
|
|
1168
|
+
};
|
|
1169
|
+
if (run.parentRunId) {
|
|
1170
|
+
const parentRow = await this.loadRun(run.parentRunId);
|
|
1171
|
+
if (parentRow) {
|
|
1172
|
+
const parentJobId = parentRow.dedupeKey ? sha1JobId(parentRow.dedupeKey) : parentRow.id;
|
|
1173
|
+
jobOpts.parent = {
|
|
1174
|
+
id: parentJobId,
|
|
1175
|
+
queue: resolvePoolQueueName(parentRow.pool, this.bullConfig)
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
const payload = { runId: run.id, type, input: run.input };
|
|
1180
|
+
await this.queueFor(run.pool).add(type, payload, jobOpts);
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Opt-in extension (spec §Extensions): expose the FlowProducer for
|
|
1184
|
+
* consumers that want to submit a whole parent/child DAG atomically up
|
|
1185
|
+
* front, rather than incrementally via `ctx.spawnChild`. Backend-specific —
|
|
1186
|
+
* code using it is not portable to the Drizzle backend. Async because it
|
|
1187
|
+
* lazily loads the optional `bullmq` package on first use.
|
|
1188
|
+
*/
|
|
1189
|
+
async flowProducer() {
|
|
1190
|
+
await this.loadBullMq();
|
|
1191
|
+
return this.flow();
|
|
1192
|
+
}
|
|
1193
|
+
retryOpts(def) {
|
|
1194
|
+
const policy = def.retryPolicy;
|
|
1195
|
+
if (!policy) return {};
|
|
1196
|
+
return {
|
|
1197
|
+
attempts: policy.attempts,
|
|
1198
|
+
backoff: {
|
|
1199
|
+
type: policy.backoff === "exponential" ? "exponential" : "fixed",
|
|
1200
|
+
delay: policy.baseMs
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
dedupeOpts(run, def) {
|
|
1205
|
+
if (!run.dedupeKey || !def.dedupeWindowMs) return {};
|
|
1206
|
+
return {
|
|
1207
|
+
deduplication: {
|
|
1208
|
+
id: sha1JobId(run.dedupeKey),
|
|
1209
|
+
ttl: def.dedupeWindowMs
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
// ==========================================================================
|
|
1214
|
+
// cancel — Postgres cascade (super) + remove from queue
|
|
1215
|
+
// ==========================================================================
|
|
1216
|
+
async cancel(runId, opts = {}) {
|
|
1217
|
+
const target = await this.loadRun(runId);
|
|
1218
|
+
await super.cancel(runId, opts);
|
|
1219
|
+
if (!target) return;
|
|
1220
|
+
await this.loadBullMq();
|
|
1221
|
+
await this.removeFromQueue(target);
|
|
1222
|
+
if (opts.cascade === false) return;
|
|
1223
|
+
const descendants = await this.bullDb.select().from(jobRuns).where(eq4(jobRuns.rootRunId, target.rootRunId));
|
|
1224
|
+
for (const child of descendants) {
|
|
1225
|
+
if (child.id === runId) continue;
|
|
1226
|
+
await this.removeFromQueue(child);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
async removeFromQueue(run) {
|
|
1230
|
+
const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;
|
|
1231
|
+
try {
|
|
1232
|
+
const job = await this.queueFor(run.pool).getJob(jobId);
|
|
1233
|
+
if (job) await job.remove();
|
|
1234
|
+
} catch (err) {
|
|
1235
|
+
this.bullLogger.warn(
|
|
1236
|
+
`cancel: could not remove BullMQ job ${jobId} (pool=${run.pool}): ${err.message}`
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
// ==========================================================================
|
|
1241
|
+
// replay — Postgres reset (super) + re-enqueue
|
|
1242
|
+
// ==========================================================================
|
|
1243
|
+
async replay(runId) {
|
|
1244
|
+
const run = await super.replay(runId);
|
|
1245
|
+
await this.dispatch(run, run.jobType);
|
|
1246
|
+
return run;
|
|
1247
|
+
}
|
|
1248
|
+
// ==========================================================================
|
|
1249
|
+
// Internals
|
|
1250
|
+
// ==========================================================================
|
|
1251
|
+
async loadDefinition(type) {
|
|
1252
|
+
const [def] = await this.bullDb.select().from(jobs).where(eq4(jobs.type, type)).limit(1);
|
|
1253
|
+
if (!def) {
|
|
1254
|
+
throw new Error(`BullMQJobOrchestrator: no job definition for '${type}'`);
|
|
1255
|
+
}
|
|
1256
|
+
return def;
|
|
1257
|
+
}
|
|
1258
|
+
async loadRun(id) {
|
|
1259
|
+
const [row] = await this.bullDb.select().from(jobRuns).where(eq4(jobRuns.id, id)).limit(1);
|
|
1260
|
+
return row ?? null;
|
|
1261
|
+
}
|
|
1262
|
+
/** Close all open queue + flow connections. Called on module destroy. */
|
|
1263
|
+
async closeConnections() {
|
|
1264
|
+
for (const q of this.queues.values()) {
|
|
1265
|
+
await q.close().catch(() => void 0);
|
|
1266
|
+
}
|
|
1267
|
+
this.queues.clear();
|
|
1268
|
+
if (this._flow) {
|
|
1269
|
+
await this._flow.close().catch(() => void 0);
|
|
1270
|
+
this._flow = null;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
BullMQJobOrchestrator = __decorateClass([
|
|
1275
|
+
Injectable4(),
|
|
1276
|
+
__decorateParam(0, Inject4(DRIZZLE)),
|
|
1277
|
+
__decorateParam(1, Inject4(JOBS_MULTI_TENANT)),
|
|
1278
|
+
__decorateParam(2, Inject4(BULLMQ_CONNECTION)),
|
|
1279
|
+
__decorateParam(3, Optional()),
|
|
1280
|
+
__decorateParam(3, Inject4(BULLMQ_RESOLVED_CONFIG))
|
|
1281
|
+
], BullMQJobOrchestrator);
|
|
1282
|
+
|
|
1283
|
+
// runtime/subsystems/jobs/job-worker.bullmq-backend.ts
|
|
1284
|
+
import { Logger as Logger3 } from "@nestjs/common";
|
|
1285
|
+
import { eq as eq5 } from "drizzle-orm";
|
|
1286
|
+
function serialiseError(err, attempt, retryable) {
|
|
1287
|
+
const e = err;
|
|
1288
|
+
return {
|
|
1289
|
+
message: e?.message ?? String(err),
|
|
1290
|
+
stack: e?.stack,
|
|
1291
|
+
retryable,
|
|
1292
|
+
attempt
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
var BullMQJobWorker = class _BullMQJobWorker {
|
|
1296
|
+
constructor(db, orchestrator, stepService, options, moduleRef) {
|
|
1297
|
+
this.db = db;
|
|
1298
|
+
this.orchestrator = orchestrator;
|
|
1299
|
+
this.stepService = stepService;
|
|
1300
|
+
this.options = options;
|
|
1301
|
+
this.moduleRef = moduleRef;
|
|
1302
|
+
}
|
|
1303
|
+
db;
|
|
1304
|
+
orchestrator;
|
|
1305
|
+
stepService;
|
|
1306
|
+
options;
|
|
1307
|
+
moduleRef;
|
|
1308
|
+
logger = new Logger3(_BullMQJobWorker.name);
|
|
1309
|
+
worker = null;
|
|
1310
|
+
async onModuleInit() {
|
|
1311
|
+
let WorkerCtor;
|
|
1312
|
+
try {
|
|
1313
|
+
const mod = await import("bullmq");
|
|
1314
|
+
WorkerCtor = mod.Worker;
|
|
1315
|
+
} catch {
|
|
1316
|
+
throw new Error(
|
|
1317
|
+
'BullMQ backend requires the "bullmq" package. Install it with: npm install bullmq'
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
this.worker = new WorkerCtor(
|
|
1321
|
+
this.options.queueName,
|
|
1322
|
+
(job) => this.process(job),
|
|
1323
|
+
{
|
|
1324
|
+
connection: this.options.connection,
|
|
1325
|
+
concurrency: this.options.concurrency
|
|
1326
|
+
}
|
|
1327
|
+
);
|
|
1328
|
+
this.worker.on("failed", (job, err) => {
|
|
1329
|
+
if (!job) return;
|
|
1330
|
+
const attemptsMade = job.attemptsMade;
|
|
1331
|
+
const maxAttempts = job.opts.attempts ?? 1;
|
|
1332
|
+
if (attemptsMade >= maxAttempts) {
|
|
1333
|
+
void this.markFailed(job.data.runId, err, attemptsMade);
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
this.logger.log(
|
|
1337
|
+
`BullMQ worker started: pool='${this.options.pool}' queue='${this.options.queueName}' concurrency=${this.options.concurrency}`
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
async onModuleDestroy() {
|
|
1341
|
+
if (this.worker) {
|
|
1342
|
+
await this.worker.close();
|
|
1343
|
+
this.worker = null;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Process one BullMQ job. Returns the handler output (stored by BullMQ as
|
|
1348
|
+
* the job return value AND written to `job_run.output`). Throws on handler
|
|
1349
|
+
* failure so BullMQ applies the retry policy.
|
|
1350
|
+
*/
|
|
1351
|
+
async process(job) {
|
|
1352
|
+
const { runId } = job.data;
|
|
1353
|
+
const [row] = await this.db.select().from(jobRuns).where(eq5(jobRuns.id, runId)).limit(1);
|
|
1354
|
+
if (!row) {
|
|
1355
|
+
this.logger.warn(`process: job_run ${runId} not found; skipping`);
|
|
1356
|
+
return {};
|
|
1357
|
+
}
|
|
1358
|
+
const run = row;
|
|
1359
|
+
if (run.status === "canceled") {
|
|
1360
|
+
return {};
|
|
1361
|
+
}
|
|
1362
|
+
const registryEntry = JOB_HANDLER_REGISTRY.get(run.jobType);
|
|
1363
|
+
if (!registryEntry) {
|
|
1364
|
+
throw new Error(
|
|
1365
|
+
`No handler registered for jobType='${run.jobType}' (run ${run.id})`
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
await this.db.update(jobRuns).set({
|
|
1369
|
+
status: "running",
|
|
1370
|
+
claimedAt: /* @__PURE__ */ new Date(),
|
|
1371
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
1372
|
+
attempts: job.attemptsMade + 1,
|
|
1373
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1374
|
+
}).where(eq5(jobRuns.id, run.id));
|
|
1375
|
+
const HandlerClass = registryEntry.handlerClass;
|
|
1376
|
+
const handler = this.moduleRef.get(
|
|
1377
|
+
HandlerClass,
|
|
1378
|
+
{ strict: false }
|
|
1379
|
+
);
|
|
1380
|
+
const ctx = {
|
|
1381
|
+
input: run.input,
|
|
1382
|
+
run,
|
|
1383
|
+
step: this.makeStepFn(run),
|
|
1384
|
+
spawnChild: this.makeSpawnFn(run),
|
|
1385
|
+
logger: new Logger3(`JobRun:${run.id}`)
|
|
1386
|
+
};
|
|
1387
|
+
const output = await handler.run(ctx);
|
|
1388
|
+
await this.db.update(jobRuns).set({
|
|
1389
|
+
status: "completed",
|
|
1390
|
+
output: output ?? {},
|
|
1391
|
+
finishedAt: /* @__PURE__ */ new Date(),
|
|
1392
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1393
|
+
}).where(eq5(jobRuns.id, run.id));
|
|
1394
|
+
return output ?? {};
|
|
1395
|
+
}
|
|
1396
|
+
async markFailed(runId, err, finalAttempts) {
|
|
1397
|
+
const [row] = await this.db.select().from(jobRuns).where(eq5(jobRuns.id, runId)).limit(1);
|
|
1398
|
+
if (!row) return;
|
|
1399
|
+
const run = row;
|
|
1400
|
+
await this.db.update(jobRuns).set({
|
|
1401
|
+
status: "failed",
|
|
1402
|
+
attempts: finalAttempts,
|
|
1403
|
+
finishedAt: /* @__PURE__ */ new Date(),
|
|
1404
|
+
error: serialiseError(err, finalAttempts, false),
|
|
1405
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1406
|
+
}).where(eq5(jobRuns.id, runId));
|
|
1407
|
+
if (run.parentClosePolicy === "terminate") {
|
|
1408
|
+
try {
|
|
1409
|
+
await this.orchestrator.cancel(run.id, {
|
|
1410
|
+
cascade: true,
|
|
1411
|
+
reason: "parent-failed",
|
|
1412
|
+
tenantId: run.tenantId
|
|
1413
|
+
});
|
|
1414
|
+
} catch (cascadeErr) {
|
|
1415
|
+
this.logger.warn(
|
|
1416
|
+
`cascade on failed run ${run.id}: ${cascadeErr.message}`
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
805
1420
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
return
|
|
1421
|
+
// ── ctx.step / ctx.spawnChild (mirror JobWorker) ──────────────────────────
|
|
1422
|
+
makeStepFn(run) {
|
|
1423
|
+
return async (stepId, fn, _opts) => {
|
|
1424
|
+
void _opts;
|
|
1425
|
+
const existing = await this.stepService.findStep(run.id, stepId);
|
|
1426
|
+
if (existing?.status === "completed") {
|
|
1427
|
+
return existing.output;
|
|
1428
|
+
}
|
|
1429
|
+
const nextAttempts = (existing?.attempts ?? 0) + 1;
|
|
1430
|
+
const seq = nextAttempts;
|
|
1431
|
+
await this.stepService.recordStep({
|
|
1432
|
+
jobRunId: run.id,
|
|
1433
|
+
stepId,
|
|
1434
|
+
kind: "task",
|
|
1435
|
+
seq,
|
|
1436
|
+
status: "running",
|
|
1437
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
1438
|
+
attempts: nextAttempts
|
|
1439
|
+
});
|
|
1440
|
+
try {
|
|
1441
|
+
const output = await fn();
|
|
1442
|
+
await this.stepService.recordStep({
|
|
1443
|
+
jobRunId: run.id,
|
|
1444
|
+
stepId,
|
|
1445
|
+
kind: "task",
|
|
1446
|
+
seq,
|
|
1447
|
+
status: "completed",
|
|
1448
|
+
output,
|
|
1449
|
+
finishedAt: /* @__PURE__ */ new Date(),
|
|
1450
|
+
attempts: nextAttempts
|
|
1451
|
+
});
|
|
1452
|
+
return output;
|
|
1453
|
+
} catch (err) {
|
|
1454
|
+
await this.stepService.recordStep({
|
|
1455
|
+
jobRunId: run.id,
|
|
1456
|
+
stepId,
|
|
1457
|
+
kind: "task",
|
|
1458
|
+
seq,
|
|
1459
|
+
status: "failed",
|
|
1460
|
+
error: serialiseError(err, nextAttempts, false),
|
|
1461
|
+
finishedAt: /* @__PURE__ */ new Date(),
|
|
1462
|
+
attempts: nextAttempts
|
|
1463
|
+
});
|
|
1464
|
+
throw err;
|
|
1465
|
+
}
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
makeSpawnFn(run) {
|
|
1469
|
+
return async (type, input, opts) => {
|
|
1470
|
+
return this.orchestrator.start(type, input, {
|
|
1471
|
+
parentRunId: run.id,
|
|
1472
|
+
parentClosePolicy: opts?.closePolicy,
|
|
1473
|
+
runAt: opts?.runAt,
|
|
1474
|
+
priority: opts?.priority,
|
|
1475
|
+
tags: opts?.tags,
|
|
1476
|
+
triggerSource: "parent",
|
|
1477
|
+
triggerRef: run.id,
|
|
1478
|
+
tenantId: run.tenantId
|
|
1479
|
+
});
|
|
1480
|
+
};
|
|
809
1481
|
}
|
|
810
1482
|
};
|
|
811
|
-
DrizzleJobStepService = __decorateClass([
|
|
812
|
-
Injectable3(),
|
|
813
|
-
__decorateParam(0, Inject3(DRIZZLE))
|
|
814
|
-
], DrizzleJobStepService);
|
|
815
1483
|
|
|
816
1484
|
// runtime/subsystems/jobs/job-worker.ts
|
|
817
|
-
import { Inject as
|
|
818
|
-
import { and as and4, asc as asc2, desc as desc3, eq as
|
|
1485
|
+
import { Inject as Inject5, Injectable as Injectable5, Logger as Logger4 } from "@nestjs/common";
|
|
1486
|
+
import { and as and4, asc as asc2, desc as desc3, eq as eq6, inArray as inArray3, lt as lt2, lte, sql as sql4 } from "drizzle-orm";
|
|
819
1487
|
var JOB_WORKER_OPTIONS = /* @__PURE__ */ Symbol("JOB_WORKER_OPTIONS");
|
|
820
1488
|
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
821
1489
|
var DEFAULT_STALE_SWEEPER_INTERVAL_MS = 6e4;
|
|
@@ -847,8 +1515,8 @@ function classifyError(err, policy, currentAttempts) {
|
|
|
847
1515
|
function buildClaimQuery(db, pool) {
|
|
848
1516
|
return db.select({ id: jobRuns.id }).from(jobRuns).where(
|
|
849
1517
|
and4(
|
|
850
|
-
|
|
851
|
-
|
|
1518
|
+
eq6(jobRuns.status, "pending"),
|
|
1519
|
+
eq6(jobRuns.pool, pool),
|
|
852
1520
|
lte(jobRuns.runAt, /* @__PURE__ */ new Date())
|
|
853
1521
|
)
|
|
854
1522
|
).orderBy(desc3(jobRuns.priority), asc2(jobRuns.runAt)).limit(1).for("update", { skipLocked: true });
|
|
@@ -857,12 +1525,12 @@ function buildStaleSweepQuery(db, staleThresholdMs) {
|
|
|
857
1525
|
const threshold = new Date(Date.now() - staleThresholdMs);
|
|
858
1526
|
return db.select({ id: jobRuns.id }).from(jobRuns).where(
|
|
859
1527
|
and4(
|
|
860
|
-
|
|
861
|
-
|
|
1528
|
+
eq6(jobRuns.status, "running"),
|
|
1529
|
+
lt2(jobRuns.claimedAt, threshold)
|
|
862
1530
|
)
|
|
863
1531
|
).for("update", { skipLocked: true });
|
|
864
1532
|
}
|
|
865
|
-
function
|
|
1533
|
+
function serialiseError2(err, attempt, retryable) {
|
|
866
1534
|
const e = err;
|
|
867
1535
|
return {
|
|
868
1536
|
message: e?.message ?? String(err),
|
|
@@ -896,7 +1564,7 @@ var JobWorker = class {
|
|
|
896
1564
|
stepService;
|
|
897
1565
|
options;
|
|
898
1566
|
moduleRef;
|
|
899
|
-
logger = new
|
|
1567
|
+
logger = new Logger4(JobWorker.name);
|
|
900
1568
|
shuttingDown = false;
|
|
901
1569
|
inFlight = /* @__PURE__ */ new Set();
|
|
902
1570
|
pollTimer = null;
|
|
@@ -937,7 +1605,7 @@ var JobWorker = class {
|
|
|
937
1605
|
await this.drainInFlight();
|
|
938
1606
|
try {
|
|
939
1607
|
await this.db.update(jobRuns).set({ status: "pending", claimedAt: null, startedAt: null }).where(
|
|
940
|
-
and4(
|
|
1608
|
+
and4(eq6(jobRuns.status, "running"), eq6(jobRuns.pool, this.options.pool))
|
|
941
1609
|
);
|
|
942
1610
|
} catch (err) {
|
|
943
1611
|
this.logger.error(`shutdown reset failed: ${err.message}`);
|
|
@@ -987,8 +1655,8 @@ var JobWorker = class {
|
|
|
987
1655
|
return this.db.transaction(async (tx) => {
|
|
988
1656
|
const candidates = await tx.select({ id: jobRuns.id }).from(jobRuns).where(
|
|
989
1657
|
and4(
|
|
990
|
-
|
|
991
|
-
|
|
1658
|
+
eq6(jobRuns.status, "pending"),
|
|
1659
|
+
eq6(jobRuns.pool, pool),
|
|
992
1660
|
lte(jobRuns.runAt, /* @__PURE__ */ new Date())
|
|
993
1661
|
)
|
|
994
1662
|
).orderBy(desc3(jobRuns.priority), asc2(jobRuns.runAt)).limit(1).for("update", { skipLocked: true });
|
|
@@ -999,7 +1667,7 @@ var JobWorker = class {
|
|
|
999
1667
|
claimedAt: /* @__PURE__ */ new Date(),
|
|
1000
1668
|
startedAt: /* @__PURE__ */ new Date(),
|
|
1001
1669
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1002
|
-
}).where(
|
|
1670
|
+
}).where(eq6(jobRuns.id, candidate.id)).returning();
|
|
1003
1671
|
return claimed ?? null;
|
|
1004
1672
|
});
|
|
1005
1673
|
}
|
|
@@ -1017,7 +1685,7 @@ var JobWorker = class {
|
|
|
1017
1685
|
await this.db.transaction(async (tx) => {
|
|
1018
1686
|
const threshold = new Date(Date.now() - this.staleThresholdMs);
|
|
1019
1687
|
const stale = await tx.select({ id: jobRuns.id }).from(jobRuns).where(
|
|
1020
|
-
and4(
|
|
1688
|
+
and4(eq6(jobRuns.status, "running"), lt2(jobRuns.claimedAt, threshold))
|
|
1021
1689
|
).for("update", { skipLocked: true });
|
|
1022
1690
|
if (stale.length === 0) return;
|
|
1023
1691
|
const ids = stale.map((r) => r.id);
|
|
@@ -1050,8 +1718,8 @@ var JobWorker = class {
|
|
|
1050
1718
|
if (claimed.concurrencyKey) {
|
|
1051
1719
|
const inflight = await this.db.select({ id: jobRuns.id }).from(jobRuns).where(
|
|
1052
1720
|
and4(
|
|
1053
|
-
|
|
1054
|
-
|
|
1721
|
+
eq6(jobRuns.concurrencyKey, claimed.concurrencyKey),
|
|
1722
|
+
eq6(jobRuns.status, "running")
|
|
1055
1723
|
)
|
|
1056
1724
|
);
|
|
1057
1725
|
const other = inflight.find((r) => r.id !== claimed.id);
|
|
@@ -1061,7 +1729,7 @@ var JobWorker = class {
|
|
|
1061
1729
|
claimedAt: null,
|
|
1062
1730
|
startedAt: null,
|
|
1063
1731
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1064
|
-
}).where(
|
|
1732
|
+
}).where(eq6(jobRuns.id, claimed.id));
|
|
1065
1733
|
return;
|
|
1066
1734
|
}
|
|
1067
1735
|
}
|
|
@@ -1076,7 +1744,7 @@ var JobWorker = class {
|
|
|
1076
1744
|
run: claimed,
|
|
1077
1745
|
step: this.makeStepFn(claimed),
|
|
1078
1746
|
spawnChild: this.makeSpawnFn(claimed),
|
|
1079
|
-
logger: new
|
|
1747
|
+
logger: new Logger4(`JobRun:${claimed.id}`)
|
|
1080
1748
|
};
|
|
1081
1749
|
const attemptsBefore = claimed.attempts ?? 0;
|
|
1082
1750
|
try {
|
|
@@ -1087,7 +1755,7 @@ var JobWorker = class {
|
|
|
1087
1755
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
1088
1756
|
updatedAt: /* @__PURE__ */ new Date(),
|
|
1089
1757
|
attempts: attemptsBefore + 1
|
|
1090
|
-
}).where(
|
|
1758
|
+
}).where(eq6(jobRuns.id, claimed.id));
|
|
1091
1759
|
} catch (err) {
|
|
1092
1760
|
const policy = meta.retry;
|
|
1093
1761
|
const decision = classifyError(err, policy, attemptsBefore);
|
|
@@ -1100,9 +1768,9 @@ var JobWorker = class {
|
|
|
1100
1768
|
runAt: new Date(Date.now() + delay),
|
|
1101
1769
|
startedAt: null,
|
|
1102
1770
|
claimedAt: null,
|
|
1103
|
-
error:
|
|
1771
|
+
error: serialiseError2(err, nextAttempts, true),
|
|
1104
1772
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1105
|
-
}).where(
|
|
1773
|
+
}).where(eq6(jobRuns.id, claimed.id));
|
|
1106
1774
|
} else {
|
|
1107
1775
|
await this.markFailed(claimed, err, nextAttempts);
|
|
1108
1776
|
}
|
|
@@ -1113,9 +1781,9 @@ var JobWorker = class {
|
|
|
1113
1781
|
status: "failed",
|
|
1114
1782
|
attempts: finalAttempts,
|
|
1115
1783
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
1116
|
-
error:
|
|
1784
|
+
error: serialiseError2(err, finalAttempts, false),
|
|
1117
1785
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1118
|
-
}).where(
|
|
1786
|
+
}).where(eq6(jobRuns.id, claimed.id));
|
|
1119
1787
|
if (claimed.parentClosePolicy === "terminate") {
|
|
1120
1788
|
try {
|
|
1121
1789
|
await this.orchestrator.cancel(claimed.id, {
|
|
@@ -1172,7 +1840,7 @@ var JobWorker = class {
|
|
|
1172
1840
|
kind: "task",
|
|
1173
1841
|
seq,
|
|
1174
1842
|
status: "failed",
|
|
1175
|
-
error:
|
|
1843
|
+
error: serialiseError2(err, nextAttempts, false),
|
|
1176
1844
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
1177
1845
|
attempts: nextAttempts
|
|
1178
1846
|
});
|
|
@@ -1218,12 +1886,12 @@ var JobWorker = class {
|
|
|
1218
1886
|
// ============================================================================
|
|
1219
1887
|
};
|
|
1220
1888
|
JobWorker = __decorateClass([
|
|
1221
|
-
|
|
1222
|
-
__decorateParam(0,
|
|
1223
|
-
__decorateParam(1,
|
|
1224
|
-
__decorateParam(2,
|
|
1225
|
-
__decorateParam(3,
|
|
1226
|
-
__decorateParam(4,
|
|
1889
|
+
Injectable5(),
|
|
1890
|
+
__decorateParam(0, Inject5(DRIZZLE)),
|
|
1891
|
+
__decorateParam(1, Inject5(JOB_ORCHESTRATOR)),
|
|
1892
|
+
__decorateParam(2, Inject5(JOB_RUN_SERVICE)),
|
|
1893
|
+
__decorateParam(3, Inject5(JOB_STEP_SERVICE)),
|
|
1894
|
+
__decorateParam(4, Inject5(JOB_WORKER_OPTIONS))
|
|
1227
1895
|
], JobWorker);
|
|
1228
1896
|
|
|
1229
1897
|
// runtime/subsystems/jobs/memory-job-store.ts
|
|
@@ -1244,7 +1912,7 @@ var MemoryJobStore = class {
|
|
|
1244
1912
|
|
|
1245
1913
|
// runtime/subsystems/jobs/job-orchestrator.memory-backend.ts
|
|
1246
1914
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1247
|
-
import { Inject as
|
|
1915
|
+
import { Inject as Inject6, Injectable as Injectable6, Logger as Logger5, Optional as Optional2 } from "@nestjs/common";
|
|
1248
1916
|
var QUEUED_RUN_AT = /* @__PURE__ */ new Date(864e13);
|
|
1249
1917
|
var TERMINAL_STATUSES2 = [
|
|
1250
1918
|
"completed",
|
|
@@ -1291,7 +1959,7 @@ var MemoryJobOrchestrator = class {
|
|
|
1291
1959
|
stepService;
|
|
1292
1960
|
multiTenant;
|
|
1293
1961
|
moduleRef;
|
|
1294
|
-
logger = new
|
|
1962
|
+
logger = new Logger5(MemoryJobOrchestrator.name);
|
|
1295
1963
|
mutex = new PromiseMutex();
|
|
1296
1964
|
handlerRegistry = /* @__PURE__ */ new Map();
|
|
1297
1965
|
/**
|
|
@@ -1640,7 +2308,7 @@ var MemoryJobOrchestrator = class {
|
|
|
1640
2308
|
run,
|
|
1641
2309
|
step: this.makeStepFn(run),
|
|
1642
2310
|
spawnChild: this.makeSpawnFn(run),
|
|
1643
|
-
logger: new
|
|
2311
|
+
logger: new Logger5(`JobRun:${run.id}`)
|
|
1644
2312
|
};
|
|
1645
2313
|
const attemptsBefore = run.attempts ?? 0;
|
|
1646
2314
|
try {
|
|
@@ -1697,7 +2365,7 @@ var MemoryJobOrchestrator = class {
|
|
|
1697
2365
|
kind: "task",
|
|
1698
2366
|
seq,
|
|
1699
2367
|
status: "failed",
|
|
1700
|
-
error:
|
|
2368
|
+
error: serialiseError3(err, nextAttempts, false),
|
|
1701
2369
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
1702
2370
|
attempts: nextAttempts
|
|
1703
2371
|
});
|
|
@@ -1752,7 +2420,7 @@ var MemoryJobOrchestrator = class {
|
|
|
1752
2420
|
finishedAt: now,
|
|
1753
2421
|
updatedAt: now,
|
|
1754
2422
|
attempts,
|
|
1755
|
-
error:
|
|
2423
|
+
error: serialiseError3(err, attempts, false)
|
|
1756
2424
|
});
|
|
1757
2425
|
this.unblockQueuedDependents(run.id);
|
|
1758
2426
|
});
|
|
@@ -1783,7 +2451,7 @@ var MemoryJobOrchestrator = class {
|
|
|
1783
2451
|
startedAt: null,
|
|
1784
2452
|
claimedAt: null,
|
|
1785
2453
|
updatedAt: now,
|
|
1786
|
-
error:
|
|
2454
|
+
error: serialiseError3(err, attempts, true)
|
|
1787
2455
|
});
|
|
1788
2456
|
});
|
|
1789
2457
|
}
|
|
@@ -1813,9 +2481,9 @@ var MemoryJobOrchestrator = class {
|
|
|
1813
2481
|
}
|
|
1814
2482
|
};
|
|
1815
2483
|
MemoryJobOrchestrator = __decorateClass([
|
|
1816
|
-
|
|
1817
|
-
__decorateParam(2,
|
|
1818
|
-
__decorateParam(3,
|
|
2484
|
+
Injectable6(),
|
|
2485
|
+
__decorateParam(2, Inject6(JOBS_MULTI_TENANT)),
|
|
2486
|
+
__decorateParam(3, Optional2())
|
|
1819
2487
|
], MemoryJobOrchestrator);
|
|
1820
2488
|
function classifyError2(err, policy, currentAttempts) {
|
|
1821
2489
|
if (!policy) return "fail";
|
|
@@ -1838,7 +2506,7 @@ function computeBackoff2(policy, attempts) {
|
|
|
1838
2506
|
}
|
|
1839
2507
|
return raw;
|
|
1840
2508
|
}
|
|
1841
|
-
function
|
|
2509
|
+
function serialiseError3(err, attempt, retryable) {
|
|
1842
2510
|
const e = err;
|
|
1843
2511
|
return {
|
|
1844
2512
|
message: e?.message ?? String(err),
|
|
@@ -1849,7 +2517,7 @@ function serialiseError2(err, attempt, retryable) {
|
|
|
1849
2517
|
}
|
|
1850
2518
|
|
|
1851
2519
|
// runtime/subsystems/jobs/job-run-service.memory-backend.ts
|
|
1852
|
-
import { Inject as
|
|
2520
|
+
import { Inject as Inject7, Injectable as Injectable7 } from "@nestjs/common";
|
|
1853
2521
|
var NON_TERMINAL_STATUSES2 = [
|
|
1854
2522
|
"pending",
|
|
1855
2523
|
"running",
|
|
@@ -1966,6 +2634,38 @@ var MemoryJobRunService = class {
|
|
|
1966
2634
|
createdAt: r.createdAt
|
|
1967
2635
|
}));
|
|
1968
2636
|
}
|
|
2637
|
+
async listJobRuns(query = {}) {
|
|
2638
|
+
const limit = clampLimit(query.limit);
|
|
2639
|
+
const tenantCheck = this.tenantPredicate("listJobRuns", query.tenantId);
|
|
2640
|
+
const keyset = query.cursor ? decodeKeysetCursor(query.cursor) : null;
|
|
2641
|
+
const matched = [];
|
|
2642
|
+
for (const r of this.store.runs.values()) {
|
|
2643
|
+
if (tenantCheck && !tenantCheck(r)) continue;
|
|
2644
|
+
if (query.poolId && r.pool !== query.poolId) continue;
|
|
2645
|
+
if (query.rootRunId && r.rootRunId !== query.rootRunId) continue;
|
|
2646
|
+
if (query.status && r.status !== query.status) continue;
|
|
2647
|
+
if (query.since && r.createdAt.getTime() < query.since.getTime()) continue;
|
|
2648
|
+
matched.push(r);
|
|
2649
|
+
}
|
|
2650
|
+
matched.sort((a, b) => {
|
|
2651
|
+
const dt = b.createdAt.getTime() - a.createdAt.getTime();
|
|
2652
|
+
if (dt !== 0) return dt;
|
|
2653
|
+
return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
|
|
2654
|
+
});
|
|
2655
|
+
const seeked = keyset ? matched.filter((r) => {
|
|
2656
|
+
const ct = r.createdAt.getTime();
|
|
2657
|
+
const kt = keyset.createdAt.getTime();
|
|
2658
|
+
if (ct < kt) return true;
|
|
2659
|
+
if (ct > kt) return false;
|
|
2660
|
+
return r.id < keyset.id;
|
|
2661
|
+
}) : matched;
|
|
2662
|
+
const hasMore = seeked.length > limit;
|
|
2663
|
+
const page = hasMore ? seeked.slice(0, limit) : seeked;
|
|
2664
|
+
const items = page.map(toJobRunSummary);
|
|
2665
|
+
const last = page[page.length - 1];
|
|
2666
|
+
const nextCursor = hasMore && last ? encodeKeysetCursor({ createdAt: last.createdAt, id: last.id }) : null;
|
|
2667
|
+
return { items, nextCursor };
|
|
2668
|
+
}
|
|
1969
2669
|
/**
|
|
1970
2670
|
* Direct lookup. Not on the protocol — concrete-class convenience for
|
|
1971
2671
|
* tests. Matches `DrizzleJobRunService.findByRootRunId` in spirit; both
|
|
@@ -1984,9 +2684,9 @@ var MemoryJobRunService = class {
|
|
|
1984
2684
|
}
|
|
1985
2685
|
};
|
|
1986
2686
|
MemoryJobRunService = __decorateClass([
|
|
1987
|
-
|
|
1988
|
-
__decorateParam(1,
|
|
1989
|
-
__decorateParam(2,
|
|
2687
|
+
Injectable7(),
|
|
2688
|
+
__decorateParam(1, Inject7(JOB_ORCHESTRATOR)),
|
|
2689
|
+
__decorateParam(2, Inject7(JOBS_MULTI_TENANT))
|
|
1990
2690
|
], MemoryJobRunService);
|
|
1991
2691
|
function compareBy(a, b, order) {
|
|
1992
2692
|
switch (order) {
|
|
@@ -2004,7 +2704,7 @@ function compareBy(a, b, order) {
|
|
|
2004
2704
|
|
|
2005
2705
|
// runtime/subsystems/jobs/job-step-service.memory-backend.ts
|
|
2006
2706
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
2007
|
-
import { Injectable as
|
|
2707
|
+
import { Injectable as Injectable8 } from "@nestjs/common";
|
|
2008
2708
|
var MemoryJobStepService = class {
|
|
2009
2709
|
constructor(store) {
|
|
2010
2710
|
this.store = store;
|
|
@@ -2097,14 +2797,13 @@ var MemoryJobStepService = class {
|
|
|
2097
2797
|
}
|
|
2098
2798
|
};
|
|
2099
2799
|
MemoryJobStepService = __decorateClass([
|
|
2100
|
-
|
|
2800
|
+
Injectable8()
|
|
2101
2801
|
], MemoryJobStepService);
|
|
2102
2802
|
|
|
2103
2803
|
// runtime/subsystems/jobs/jobs-domain.module.ts
|
|
2104
2804
|
import { Module } from "@nestjs/common";
|
|
2105
2805
|
var JobsDomainModule = class {
|
|
2106
2806
|
static forRoot(opts) {
|
|
2107
|
-
void opts.extensions;
|
|
2108
2807
|
const multiTenant = opts.multiTenant ?? false;
|
|
2109
2808
|
const providers = [
|
|
2110
2809
|
// JOB-8 — boolean provider consumed by the four service-layer backends.
|
|
@@ -2123,21 +2822,32 @@ var JobsDomainModule = class {
|
|
|
2123
2822
|
providers.push({ provide: JOB_ORCHESTRATOR, useExisting: MemoryJobOrchestrator });
|
|
2124
2823
|
providers.push(MemoryJobRunService);
|
|
2125
2824
|
providers.push({ provide: JOB_RUN_SERVICE, useExisting: MemoryJobRunService });
|
|
2825
|
+
} else if (opts.backend === "bullmq") {
|
|
2826
|
+
const resolved = resolveBullMqConfig(opts.extensions?.bullmq);
|
|
2827
|
+
providers.push({ provide: BULLMQ_CONNECTION, useValue: resolved.connection });
|
|
2828
|
+
providers.push({ provide: BULLMQ_RESOLVED_CONFIG, useValue: resolved });
|
|
2829
|
+
providers.push({ provide: JOB_ORCHESTRATOR, useClass: BullMQJobOrchestrator });
|
|
2830
|
+
providers.push({ provide: JOB_RUN_SERVICE, useClass: DrizzleJobRunService });
|
|
2831
|
+
providers.push({ provide: JOB_STEP_SERVICE, useClass: DrizzleJobStepService });
|
|
2126
2832
|
} else {
|
|
2127
2833
|
providers.push({ provide: JOB_ORCHESTRATOR, useClass: DrizzleJobOrchestrator });
|
|
2128
2834
|
providers.push({ provide: JOB_RUN_SERVICE, useClass: DrizzleJobRunService });
|
|
2129
2835
|
providers.push({ provide: JOB_STEP_SERVICE, useClass: DrizzleJobStepService });
|
|
2130
2836
|
}
|
|
2837
|
+
const exports = [
|
|
2838
|
+
JOB_ORCHESTRATOR,
|
|
2839
|
+
JOB_RUN_SERVICE,
|
|
2840
|
+
JOB_STEP_SERVICE,
|
|
2841
|
+
JOBS_MULTI_TENANT
|
|
2842
|
+
];
|
|
2843
|
+
if (opts.backend === "bullmq") {
|
|
2844
|
+
exports.push(BULLMQ_CONNECTION, BULLMQ_RESOLVED_CONFIG);
|
|
2845
|
+
}
|
|
2131
2846
|
return {
|
|
2132
2847
|
module: JobsDomainModule,
|
|
2133
2848
|
global: true,
|
|
2134
2849
|
providers,
|
|
2135
|
-
exports
|
|
2136
|
-
JOB_ORCHESTRATOR,
|
|
2137
|
-
JOB_RUN_SERVICE,
|
|
2138
|
-
JOB_STEP_SERVICE,
|
|
2139
|
-
JOBS_MULTI_TENANT
|
|
2140
|
-
]
|
|
2850
|
+
exports
|
|
2141
2851
|
};
|
|
2142
2852
|
}
|
|
2143
2853
|
};
|
|
@@ -2147,143 +2857,24 @@ JobsDomainModule = __decorateClass([
|
|
|
2147
2857
|
|
|
2148
2858
|
// runtime/subsystems/jobs/job-worker.module.ts
|
|
2149
2859
|
import {
|
|
2150
|
-
Inject as
|
|
2151
|
-
Injectable as
|
|
2152
|
-
Logger as
|
|
2860
|
+
Inject as Inject8,
|
|
2861
|
+
Injectable as Injectable9,
|
|
2862
|
+
Logger as Logger6,
|
|
2153
2863
|
Module as Module2,
|
|
2154
|
-
Optional as
|
|
2864
|
+
Optional as Optional3
|
|
2155
2865
|
} from "@nestjs/common";
|
|
2156
|
-
|
|
2157
|
-
// runtime/subsystems/jobs/pool-config.loader.ts
|
|
2158
|
-
import { existsSync, readFileSync } from "fs";
|
|
2159
|
-
import { resolve } from "path";
|
|
2160
|
-
import { parse as parseYaml } from "yaml";
|
|
2161
|
-
var FRAMEWORK_POOLS = Object.freeze({
|
|
2162
|
-
events_inbound: Object.freeze({
|
|
2163
|
-
queue: "jobs-events-inbound",
|
|
2164
|
-
concurrency: 20,
|
|
2165
|
-
reserved: true,
|
|
2166
|
-
description: "Inbound events drain (events subsystem outbox)."
|
|
2167
|
-
}),
|
|
2168
|
-
events_change: Object.freeze({
|
|
2169
|
-
queue: "jobs-events-change",
|
|
2170
|
-
concurrency: 30,
|
|
2171
|
-
reserved: true,
|
|
2172
|
-
description: "Change events drain (events subsystem outbox)."
|
|
2173
|
-
}),
|
|
2174
|
-
events_outbound: Object.freeze({
|
|
2175
|
-
queue: "jobs-events-outbound",
|
|
2176
|
-
concurrency: 10,
|
|
2177
|
-
reserved: true,
|
|
2178
|
-
description: "Outbound events drain (events subsystem outbox)."
|
|
2179
|
-
}),
|
|
2180
|
-
interactive: Object.freeze({
|
|
2181
|
-
queue: "jobs-interactive",
|
|
2182
|
-
concurrency: 20,
|
|
2183
|
-
reserved: false,
|
|
2184
|
-
description: "User-facing latency-sensitive jobs."
|
|
2185
|
-
}),
|
|
2186
|
-
batch: Object.freeze({
|
|
2187
|
-
queue: "jobs-batch",
|
|
2188
|
-
concurrency: 5,
|
|
2189
|
-
reserved: false,
|
|
2190
|
-
description: "Default pool for background jobs."
|
|
2191
|
-
})
|
|
2192
|
-
});
|
|
2193
|
-
var RESERVED_POOL_NAMES = new Set(
|
|
2194
|
-
Object.entries(FRAMEWORK_POOLS).filter(([, def]) => def.reserved).map(([name]) => name)
|
|
2195
|
-
);
|
|
2196
|
-
var cache = /* @__PURE__ */ new Map();
|
|
2197
|
-
function loadPoolConfig(configPath) {
|
|
2198
|
-
const resolved = resolve(configPath ?? `${process.cwd()}/codegen.config.yaml`);
|
|
2199
|
-
const cached = cache.get(resolved);
|
|
2200
|
-
if (cached) return cached;
|
|
2201
|
-
const merged = /* @__PURE__ */ new Map();
|
|
2202
|
-
for (const [name, def] of Object.entries(FRAMEWORK_POOLS)) {
|
|
2203
|
-
merged.set(name, { ...def });
|
|
2204
|
-
}
|
|
2205
|
-
if (!existsSync(resolved)) {
|
|
2206
|
-
cache.set(resolved, merged);
|
|
2207
|
-
return merged;
|
|
2208
|
-
}
|
|
2209
|
-
let raw;
|
|
2210
|
-
try {
|
|
2211
|
-
raw = parseYaml(readFileSync(resolved, "utf8"));
|
|
2212
|
-
} catch (err) {
|
|
2213
|
-
throw new Error(
|
|
2214
|
-
`pool-config.loader: failed to parse YAML at ${resolved}: ${err.message}`
|
|
2215
|
-
);
|
|
2216
|
-
}
|
|
2217
|
-
const userPools = extractUserPools(raw);
|
|
2218
|
-
for (const [name, userDef] of Object.entries(userPools)) {
|
|
2219
|
-
const existing = merged.get(name);
|
|
2220
|
-
if (existing) {
|
|
2221
|
-
const next = {
|
|
2222
|
-
queue: existing.queue,
|
|
2223
|
-
concurrency: typeof userDef.concurrency === "number" ? userDef.concurrency : existing.concurrency,
|
|
2224
|
-
reserved: existing.reserved,
|
|
2225
|
-
description: userDef.description ?? existing.description
|
|
2226
|
-
};
|
|
2227
|
-
merged.set(name, next);
|
|
2228
|
-
continue;
|
|
2229
|
-
}
|
|
2230
|
-
if (typeof userDef.queue !== "string" || userDef.queue.length === 0) {
|
|
2231
|
-
throw new Error(
|
|
2232
|
-
`pool-config.loader: pool '${name}' must declare a non-empty 'queue'.`
|
|
2233
|
-
);
|
|
2234
|
-
}
|
|
2235
|
-
if (typeof userDef.concurrency !== "number" || userDef.concurrency <= 0) {
|
|
2236
|
-
throw new Error(
|
|
2237
|
-
`pool-config.loader: pool '${name}' must declare a positive 'concurrency'.`
|
|
2238
|
-
);
|
|
2239
|
-
}
|
|
2240
|
-
if (userDef.reserved === true) {
|
|
2241
|
-
throw new Error(
|
|
2242
|
-
`pool-config.loader: user-defined pool '${name}' cannot set 'reserved: true' \u2014 reserved is framework-only.`
|
|
2243
|
-
);
|
|
2244
|
-
}
|
|
2245
|
-
merged.set(name, {
|
|
2246
|
-
queue: userDef.queue,
|
|
2247
|
-
concurrency: userDef.concurrency,
|
|
2248
|
-
reserved: false,
|
|
2249
|
-
description: userDef.description
|
|
2250
|
-
});
|
|
2251
|
-
}
|
|
2252
|
-
cache.set(resolved, merged);
|
|
2253
|
-
return merged;
|
|
2254
|
-
}
|
|
2255
|
-
function allNonReservedPoolNames(config) {
|
|
2256
|
-
const out = [];
|
|
2257
|
-
for (const [name, def] of config) {
|
|
2258
|
-
if (!def.reserved) out.push(name);
|
|
2259
|
-
}
|
|
2260
|
-
return out;
|
|
2261
|
-
}
|
|
2262
|
-
function extractUserPools(raw) {
|
|
2263
|
-
if (!raw || typeof raw !== "object") return {};
|
|
2264
|
-
const jobs2 = raw.jobs;
|
|
2265
|
-
if (!jobs2 || typeof jobs2 !== "object") return {};
|
|
2266
|
-
const pools = jobs2.pools;
|
|
2267
|
-
if (!pools || typeof pools !== "object") return {};
|
|
2268
|
-
const out = {};
|
|
2269
|
-
for (const [name, def] of Object.entries(pools)) {
|
|
2270
|
-
if (!def || typeof def !== "object") continue;
|
|
2271
|
-
out[name] = def;
|
|
2272
|
-
}
|
|
2273
|
-
return out;
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
|
-
// runtime/subsystems/jobs/job-worker.module.ts
|
|
2277
2866
|
var DEFAULT_SHUTDOWN_TIMEOUT_MS2 = 3e4;
|
|
2278
2867
|
var JOB_WORKER_MODULE_OPTIONS = /* @__PURE__ */ Symbol("JOB_WORKER_MODULE_OPTIONS");
|
|
2279
2868
|
var JobWorkerOrchestrator = class {
|
|
2280
|
-
constructor(orchestrator, runService, stepService, options, db = null, moduleRef) {
|
|
2869
|
+
constructor(orchestrator, runService, stepService, options, db = null, moduleRef, bullConnection = null, bullConfig = null) {
|
|
2281
2870
|
this.orchestrator = orchestrator;
|
|
2282
2871
|
this.runService = runService;
|
|
2283
2872
|
this.stepService = stepService;
|
|
2284
2873
|
this.options = options;
|
|
2285
2874
|
this.db = db;
|
|
2286
2875
|
this.moduleRef = moduleRef;
|
|
2876
|
+
this.bullConnection = bullConnection;
|
|
2877
|
+
this.bullConfig = bullConfig;
|
|
2287
2878
|
}
|
|
2288
2879
|
orchestrator;
|
|
2289
2880
|
runService;
|
|
@@ -2291,7 +2882,9 @@ var JobWorkerOrchestrator = class {
|
|
|
2291
2882
|
options;
|
|
2292
2883
|
db;
|
|
2293
2884
|
moduleRef;
|
|
2294
|
-
|
|
2885
|
+
bullConnection;
|
|
2886
|
+
bullConfig;
|
|
2887
|
+
logger = new Logger6(JobWorkerOrchestrator.name);
|
|
2295
2888
|
workers = [];
|
|
2296
2889
|
// ============================================================================
|
|
2297
2890
|
// Lifecycle
|
|
@@ -2308,7 +2901,7 @@ var JobWorkerOrchestrator = class {
|
|
|
2308
2901
|
if (backend !== "memory" && orphaned.length > 0) {
|
|
2309
2902
|
throw new BootValidationError(orphaned);
|
|
2310
2903
|
}
|
|
2311
|
-
const activePools = this.options.pools
|
|
2904
|
+
const activePools = this.options.pools ? this.options.pools : this.options.allPools ? allPoolNames(poolConfig) : allNonReservedPoolNames(poolConfig);
|
|
2312
2905
|
for (const poolName of activePools) {
|
|
2313
2906
|
const def = poolConfig.get(poolName);
|
|
2314
2907
|
if (!def) {
|
|
@@ -2321,11 +2914,11 @@ var JobWorkerOrchestrator = class {
|
|
|
2321
2914
|
concurrency: def.concurrency,
|
|
2322
2915
|
shutdownTimeoutMs: this.options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS2
|
|
2323
2916
|
};
|
|
2324
|
-
const worker = this.options.workerFactory ? this.options.workerFactory(workerOptions) : this.spawnWorker(workerOptions);
|
|
2325
|
-
worker.onModuleInit();
|
|
2917
|
+
const worker = this.options.workerFactory ? this.options.workerFactory(workerOptions) : backend === "bullmq" ? this.spawnBullMQWorker(poolName, def.queue, def.concurrency, poolConfig) : this.spawnWorker(workerOptions);
|
|
2918
|
+
await worker.onModuleInit();
|
|
2326
2919
|
this.workers.push(worker);
|
|
2327
2920
|
this.logger.log(
|
|
2328
|
-
`JobWorker started: pool='${poolName}' (queue='${def.queue}') concurrency=${def.concurrency}`
|
|
2921
|
+
`JobWorker started: pool='${poolName}' (queue='${def.queue}') concurrency=${def.concurrency} backend='${backend}'`
|
|
2329
2922
|
);
|
|
2330
2923
|
}
|
|
2331
2924
|
}
|
|
@@ -2342,6 +2935,16 @@ var JobWorkerOrchestrator = class {
|
|
|
2342
2935
|
}
|
|
2343
2936
|
}
|
|
2344
2937
|
this.workers.length = 0;
|
|
2938
|
+
const orch = this.orchestrator;
|
|
2939
|
+
if (typeof orch.closeConnections === "function") {
|
|
2940
|
+
try {
|
|
2941
|
+
await orch.closeConnections();
|
|
2942
|
+
} catch (err) {
|
|
2943
|
+
this.logger.error(
|
|
2944
|
+
`BullMQ orchestrator connection close failed: ${err.message}`
|
|
2945
|
+
);
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2345
2948
|
}
|
|
2346
2949
|
// ============================================================================
|
|
2347
2950
|
// Internals
|
|
@@ -2403,15 +3006,57 @@ var JobWorkerOrchestrator = class {
|
|
|
2403
3006
|
this.moduleRef
|
|
2404
3007
|
);
|
|
2405
3008
|
}
|
|
3009
|
+
/**
|
|
3010
|
+
* BULLMQ-1 — spawn a per-pool `BullMQJobWorker`. Requires the Drizzle
|
|
3011
|
+
* client (the worker drives `job_run` as the source of truth) AND the
|
|
3012
|
+
* resolved BullMQ connection (bound by `JobsDomainModule` when
|
|
3013
|
+
* `backend: 'bullmq'`). The queue name is derived identically to the
|
|
3014
|
+
* orchestrator's `dispatch` via `resolvePoolQueueName(pool, …)` so producer
|
|
3015
|
+
* and consumer agree.
|
|
3016
|
+
*/
|
|
3017
|
+
spawnBullMQWorker(pool, _queueAlias, concurrency, poolConfig) {
|
|
3018
|
+
if (!this.db) {
|
|
3019
|
+
throw new Error(
|
|
3020
|
+
`JobWorkerModule: BullMQ worker spawning requires the Drizzle client (no DRIZZLE provider available) \u2014 job_run remains the source of truth.`
|
|
3021
|
+
);
|
|
3022
|
+
}
|
|
3023
|
+
if (!this.bullConnection) {
|
|
3024
|
+
throw new Error(
|
|
3025
|
+
`JobWorkerModule: BullMQ worker spawning requires a resolved BULLMQ_CONNECTION. Ensure JobsDomainModule was booted with backend: 'bullmq'.`
|
|
3026
|
+
);
|
|
3027
|
+
}
|
|
3028
|
+
if (!this.moduleRef) {
|
|
3029
|
+
throw new Error(
|
|
3030
|
+
`JobWorkerModule: ModuleRef not available \u2014 cannot construct BullMQJobWorker with handler DI support.`
|
|
3031
|
+
);
|
|
3032
|
+
}
|
|
3033
|
+
const queueName = resolvePoolQueueName(pool, this.bullConfig, poolConfig);
|
|
3034
|
+
return new BullMQJobWorker(
|
|
3035
|
+
this.db,
|
|
3036
|
+
this.orchestrator,
|
|
3037
|
+
this.stepService,
|
|
3038
|
+
{
|
|
3039
|
+
pool,
|
|
3040
|
+
queueName,
|
|
3041
|
+
concurrency,
|
|
3042
|
+
connection: this.bullConnection
|
|
3043
|
+
},
|
|
3044
|
+
this.moduleRef
|
|
3045
|
+
);
|
|
3046
|
+
}
|
|
2406
3047
|
};
|
|
2407
3048
|
JobWorkerOrchestrator = __decorateClass([
|
|
2408
|
-
|
|
2409
|
-
__decorateParam(0,
|
|
2410
|
-
__decorateParam(1,
|
|
2411
|
-
__decorateParam(2,
|
|
2412
|
-
__decorateParam(3,
|
|
2413
|
-
__decorateParam(4,
|
|
2414
|
-
__decorateParam(4,
|
|
3049
|
+
Injectable9(),
|
|
3050
|
+
__decorateParam(0, Inject8(JOB_ORCHESTRATOR)),
|
|
3051
|
+
__decorateParam(1, Inject8(JOB_RUN_SERVICE)),
|
|
3052
|
+
__decorateParam(2, Inject8(JOB_STEP_SERVICE)),
|
|
3053
|
+
__decorateParam(3, Inject8(JOB_WORKER_MODULE_OPTIONS)),
|
|
3054
|
+
__decorateParam(4, Optional3()),
|
|
3055
|
+
__decorateParam(4, Inject8(DRIZZLE)),
|
|
3056
|
+
__decorateParam(6, Optional3()),
|
|
3057
|
+
__decorateParam(6, Inject8(BULLMQ_CONNECTION)),
|
|
3058
|
+
__decorateParam(7, Optional3()),
|
|
3059
|
+
__decorateParam(7, Inject8(BULLMQ_RESOLVED_CONFIG))
|
|
2415
3060
|
], JobWorkerOrchestrator);
|
|
2416
3061
|
var JobWorkerModule = class {
|
|
2417
3062
|
static forRoot(opts) {
|
|
@@ -2428,7 +3073,14 @@ var JobWorkerModule = class {
|
|
|
2428
3073
|
{ provide: JOB_WORKER_MODULE_OPTIONS, useValue: opts },
|
|
2429
3074
|
JobWorkerOrchestrator
|
|
2430
3075
|
],
|
|
2431
|
-
|
|
3076
|
+
// BULLMQ-1 Phase 1 — export the options token so `BridgeModule`'s
|
|
3077
|
+
// reserved-pool guard (`onModuleInit`) can actually inject it.
|
|
3078
|
+
// Previously `exports: []` left the `@Optional()` inject resolving to
|
|
3079
|
+
// `undefined` and the guard silently no-opped (a dead check). With the
|
|
3080
|
+
// token exported the guard fires for real; consumers that omit the
|
|
3081
|
+
// reserved pools (and don't set `allPools`) now fail fast with
|
|
3082
|
+
// `BridgeReservedPoolsNotPolledError` — which is correct.
|
|
3083
|
+
exports: [JOB_WORKER_MODULE_OPTIONS]
|
|
2432
3084
|
};
|
|
2433
3085
|
}
|
|
2434
3086
|
};
|
|
@@ -2436,7 +3088,11 @@ JobWorkerModule = __decorateClass([
|
|
|
2436
3088
|
Module2({})
|
|
2437
3089
|
], JobWorkerModule);
|
|
2438
3090
|
export {
|
|
3091
|
+
BULLMQ_CONNECTION,
|
|
3092
|
+
BULLMQ_RESOLVED_CONFIG,
|
|
2439
3093
|
BootValidationError,
|
|
3094
|
+
BullMQJobOrchestrator,
|
|
3095
|
+
BullMQJobWorker,
|
|
2440
3096
|
DrizzleJobOrchestrator,
|
|
2441
3097
|
DrizzleJobRunService,
|
|
2442
3098
|
DrizzleJobStepService,
|
|
@@ -2468,6 +3124,7 @@ export {
|
|
|
2468
3124
|
RESERVED_POOL_NAMES,
|
|
2469
3125
|
ReservedPoolViolationError,
|
|
2470
3126
|
allNonReservedPoolNames,
|
|
3127
|
+
allPoolNames,
|
|
2471
3128
|
buildClaimQuery,
|
|
2472
3129
|
buildStaleSweepQuery,
|
|
2473
3130
|
classifyError,
|
|
@@ -2482,6 +3139,9 @@ export {
|
|
|
2482
3139
|
loadPoolConfig,
|
|
2483
3140
|
parentClosePolicyEnum,
|
|
2484
3141
|
replayFromEnum,
|
|
3142
|
+
resolveBullMqConfig,
|
|
3143
|
+
resolvePoolQueueName,
|
|
3144
|
+
sha1JobId,
|
|
2485
3145
|
triggerSourceEnum,
|
|
2486
3146
|
waitKindEnum
|
|
2487
3147
|
};
|