@pattern-stack/codegen 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/consumer-skills/events/typed-bus-and-outbox.md +1 -1
- package/consumer-skills/subsystems/SKILL.md +56 -0
- package/dist/runtime/subsystems/bridge/bridge.module.d.ts +0 -1
- package/dist/runtime/subsystems/bridge/bridge.module.js +294 -710
- package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
- package/dist/runtime/subsystems/bridge/index.d.ts +0 -1
- package/dist/runtime/subsystems/bridge/index.js +248 -664
- package/dist/runtime/subsystems/bridge/index.js.map +1 -1
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +18 -10
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js.map +1 -1
- package/dist/runtime/subsystems/events/events.module.js +43 -244
- package/dist/runtime/subsystems/events/events.module.js.map +1 -1
- package/dist/runtime/subsystems/events/index.d.ts +0 -1
- package/dist/runtime/subsystems/events/index.js +39 -241
- package/dist/runtime/subsystems/events/index.js.map +1 -1
- package/dist/runtime/subsystems/index.js +174 -791
- package/dist/runtime/subsystems/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/bullmq.config.d.ts +22 -3
- package/dist/runtime/subsystems/jobs/bullmq.config.js.map +1 -1
- package/dist/runtime/subsystems/jobs/index.d.ts +1 -4
- package/dist/runtime/subsystems/jobs/index.js +87 -506
- package/dist/runtime/subsystems/jobs/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +3 -0
- package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +11 -4
- package/dist/runtime/subsystems/jobs/job-worker.module.js +248 -664
- package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.module.d.ts +0 -1
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js +89 -391
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
- package/dist/src/cli/index.js +152 -35
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/runtime/subsystems/events/event-bus.drizzle-backend.ts +32 -10
- package/runtime/subsystems/events/events.module.ts +38 -6
- package/runtime/subsystems/events/index.ts +7 -1
- package/runtime/subsystems/jobs/bullmq.config.ts +23 -3
- package/runtime/subsystems/jobs/index.ts +13 -8
- package/runtime/subsystems/jobs/job-worker.bullmq-backend.ts +5 -2
- package/runtime/subsystems/jobs/job-worker.module.ts +27 -7
- package/runtime/subsystems/jobs/jobs-domain.module.ts +27 -2
- package/templates/subsystem/events/domain-events.schema.ejs.t +43 -2
|
@@ -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 Inject7,
|
|
16
|
+
Injectable as Injectable8,
|
|
17
|
+
Logger as Logger4,
|
|
18
18
|
Module as Module2,
|
|
19
|
-
Optional as
|
|
19
|
+
Optional as Optional2
|
|
20
20
|
} from "@nestjs/common";
|
|
21
21
|
|
|
22
22
|
// runtime/constants/tokens.ts
|
|
@@ -878,394 +878,9 @@ DrizzleJobStepService = __decorateClass([
|
|
|
878
878
|
__decorateParam(0, Inject3(DRIZZLE))
|
|
879
879
|
], DrizzleJobStepService);
|
|
880
880
|
|
|
881
|
-
// runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts
|
|
882
|
-
import { createHash } from "crypto";
|
|
883
|
-
import { Inject as Inject4, Injectable as Injectable4, Logger as Logger2, Optional } from "@nestjs/common";
|
|
884
|
-
import { eq as eq4 } from "drizzle-orm";
|
|
885
|
-
|
|
886
|
-
// runtime/subsystems/jobs/pool-config.loader.ts
|
|
887
|
-
import { existsSync, readFileSync } from "fs";
|
|
888
|
-
import { resolve } from "path";
|
|
889
|
-
import { parse as parseYaml } from "yaml";
|
|
890
|
-
var FRAMEWORK_POOLS = Object.freeze({
|
|
891
|
-
events_inbound: Object.freeze({
|
|
892
|
-
queue: "jobs-events-inbound",
|
|
893
|
-
concurrency: 20,
|
|
894
|
-
reserved: true,
|
|
895
|
-
description: "Inbound events drain (events subsystem outbox)."
|
|
896
|
-
}),
|
|
897
|
-
events_change: Object.freeze({
|
|
898
|
-
queue: "jobs-events-change",
|
|
899
|
-
concurrency: 30,
|
|
900
|
-
reserved: true,
|
|
901
|
-
description: "Change events drain (events subsystem outbox)."
|
|
902
|
-
}),
|
|
903
|
-
events_outbound: Object.freeze({
|
|
904
|
-
queue: "jobs-events-outbound",
|
|
905
|
-
concurrency: 10,
|
|
906
|
-
reserved: true,
|
|
907
|
-
description: "Outbound events drain (events subsystem outbox)."
|
|
908
|
-
}),
|
|
909
|
-
interactive: Object.freeze({
|
|
910
|
-
queue: "jobs-interactive",
|
|
911
|
-
concurrency: 20,
|
|
912
|
-
reserved: false,
|
|
913
|
-
description: "User-facing latency-sensitive jobs."
|
|
914
|
-
}),
|
|
915
|
-
batch: Object.freeze({
|
|
916
|
-
queue: "jobs-batch",
|
|
917
|
-
concurrency: 5,
|
|
918
|
-
reserved: false,
|
|
919
|
-
description: "Default pool for background jobs."
|
|
920
|
-
})
|
|
921
|
-
});
|
|
922
|
-
var RESERVED_POOL_NAMES = new Set(
|
|
923
|
-
Object.entries(FRAMEWORK_POOLS).filter(([, def]) => def.reserved).map(([name]) => name)
|
|
924
|
-
);
|
|
925
|
-
var cache = /* @__PURE__ */ new Map();
|
|
926
|
-
function loadPoolConfig(configPath) {
|
|
927
|
-
const resolved = resolve(configPath ?? `${process.cwd()}/codegen.config.yaml`);
|
|
928
|
-
const cached = cache.get(resolved);
|
|
929
|
-
if (cached) return cached;
|
|
930
|
-
const merged = /* @__PURE__ */ new Map();
|
|
931
|
-
for (const [name, def] of Object.entries(FRAMEWORK_POOLS)) {
|
|
932
|
-
merged.set(name, { ...def });
|
|
933
|
-
}
|
|
934
|
-
if (!existsSync(resolved)) {
|
|
935
|
-
cache.set(resolved, merged);
|
|
936
|
-
return merged;
|
|
937
|
-
}
|
|
938
|
-
let raw;
|
|
939
|
-
try {
|
|
940
|
-
raw = parseYaml(readFileSync(resolved, "utf8"));
|
|
941
|
-
} catch (err) {
|
|
942
|
-
throw new Error(
|
|
943
|
-
`pool-config.loader: failed to parse YAML at ${resolved}: ${err.message}`
|
|
944
|
-
);
|
|
945
|
-
}
|
|
946
|
-
const userPools = extractUserPools(raw);
|
|
947
|
-
for (const [name, userDef] of Object.entries(userPools)) {
|
|
948
|
-
const existing = merged.get(name);
|
|
949
|
-
if (existing) {
|
|
950
|
-
const next = {
|
|
951
|
-
queue: existing.queue,
|
|
952
|
-
concurrency: typeof userDef.concurrency === "number" ? userDef.concurrency : existing.concurrency,
|
|
953
|
-
reserved: existing.reserved,
|
|
954
|
-
description: userDef.description ?? existing.description
|
|
955
|
-
};
|
|
956
|
-
merged.set(name, next);
|
|
957
|
-
continue;
|
|
958
|
-
}
|
|
959
|
-
if (typeof userDef.queue !== "string" || userDef.queue.length === 0) {
|
|
960
|
-
throw new Error(
|
|
961
|
-
`pool-config.loader: pool '${name}' must declare a non-empty 'queue'.`
|
|
962
|
-
);
|
|
963
|
-
}
|
|
964
|
-
if (typeof userDef.concurrency !== "number" || userDef.concurrency <= 0) {
|
|
965
|
-
throw new Error(
|
|
966
|
-
`pool-config.loader: pool '${name}' must declare a positive 'concurrency'.`
|
|
967
|
-
);
|
|
968
|
-
}
|
|
969
|
-
if (userDef.reserved === true) {
|
|
970
|
-
throw new Error(
|
|
971
|
-
`pool-config.loader: user-defined pool '${name}' cannot set 'reserved: true' \u2014 reserved is framework-only.`
|
|
972
|
-
);
|
|
973
|
-
}
|
|
974
|
-
merged.set(name, {
|
|
975
|
-
queue: userDef.queue,
|
|
976
|
-
concurrency: userDef.concurrency,
|
|
977
|
-
reserved: false,
|
|
978
|
-
description: userDef.description
|
|
979
|
-
});
|
|
980
|
-
}
|
|
981
|
-
cache.set(resolved, merged);
|
|
982
|
-
return merged;
|
|
983
|
-
}
|
|
984
|
-
function allNonReservedPoolNames(config) {
|
|
985
|
-
const out = [];
|
|
986
|
-
for (const [name, def] of config) {
|
|
987
|
-
if (!def.reserved) out.push(name);
|
|
988
|
-
}
|
|
989
|
-
return out;
|
|
990
|
-
}
|
|
991
|
-
function allPoolNames(config) {
|
|
992
|
-
return [...config.keys()];
|
|
993
|
-
}
|
|
994
|
-
function extractUserPools(raw) {
|
|
995
|
-
if (!raw || typeof raw !== "object") return {};
|
|
996
|
-
const jobs2 = raw.jobs;
|
|
997
|
-
if (!jobs2 || typeof jobs2 !== "object") return {};
|
|
998
|
-
const pools = jobs2.pools;
|
|
999
|
-
if (!pools || typeof pools !== "object") return {};
|
|
1000
|
-
const out = {};
|
|
1001
|
-
for (const [name, def] of Object.entries(pools)) {
|
|
1002
|
-
if (!def || typeof def !== "object") continue;
|
|
1003
|
-
out[name] = def;
|
|
1004
|
-
}
|
|
1005
|
-
return out;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
// runtime/subsystems/jobs/bullmq.config.ts
|
|
1009
|
-
var BULLMQ_CONNECTION = /* @__PURE__ */ Symbol("BULLMQ_CONNECTION");
|
|
1010
|
-
var BULLMQ_RESOLVED_CONFIG = /* @__PURE__ */ Symbol("BULLMQ_RESOLVED_CONFIG");
|
|
1011
|
-
var DEFAULT_REDIS_URL = "redis://localhost:6379";
|
|
1012
|
-
var DEFAULT_BULL_BOARD_MOUNT = "/admin/queues";
|
|
1013
|
-
function resolveBullMqConfig(ext) {
|
|
1014
|
-
const url = ext?.redis_url ?? process.env.REDIS_URL ?? DEFAULT_REDIS_URL;
|
|
1015
|
-
const resolved = {
|
|
1016
|
-
connection: { url },
|
|
1017
|
-
queuePrefix: ext?.queue_prefix
|
|
1018
|
-
};
|
|
1019
|
-
if (ext?.bull_board?.enabled) {
|
|
1020
|
-
resolved.bullBoard = {
|
|
1021
|
-
enabled: true,
|
|
1022
|
-
mountPath: ext.bull_board.mount_path ?? DEFAULT_BULL_BOARD_MOUNT
|
|
1023
|
-
};
|
|
1024
|
-
}
|
|
1025
|
-
return resolved;
|
|
1026
|
-
}
|
|
1027
|
-
function resolvePoolQueueName(pool, config, poolConfig = loadPoolConfig()) {
|
|
1028
|
-
const alias = poolConfig.get(pool)?.queue ?? pool;
|
|
1029
|
-
const prefix = config?.queuePrefix;
|
|
1030
|
-
return prefix ? `${prefix}:${alias}` : alias;
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
// runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts
|
|
1034
|
-
function sha1JobId(idempotencyKey) {
|
|
1035
|
-
return createHash("sha1").update(idempotencyKey).digest("hex");
|
|
1036
|
-
}
|
|
1037
|
-
var BullMQJobOrchestrator = class extends DrizzleJobOrchestrator {
|
|
1038
|
-
constructor(db, multiTenant, connection, bullConfig = null) {
|
|
1039
|
-
super(db, multiTenant);
|
|
1040
|
-
this.connection = connection;
|
|
1041
|
-
this.bullConfig = bullConfig;
|
|
1042
|
-
this.bullDb = db;
|
|
1043
|
-
}
|
|
1044
|
-
connection;
|
|
1045
|
-
bullConfig;
|
|
1046
|
-
// TODO(logging-subsystem): swap to ILogger once ADR-028 lands
|
|
1047
|
-
bullLogger = new Logger2(BullMQJobOrchestrator.name);
|
|
1048
|
-
/** Lazily-opened `Queue` handles, one per pool. */
|
|
1049
|
-
queues = /* @__PURE__ */ new Map();
|
|
1050
|
-
/** Single FlowProducer for parent/child hierarchies. Lazily opened. */
|
|
1051
|
-
_flow = null;
|
|
1052
|
-
/**
|
|
1053
|
-
* Cached `bullmq` value constructors, populated by `loadBullMq()` on first
|
|
1054
|
-
* use (the `start`/`cancel`/`replay` entrypoints `await` it before touching
|
|
1055
|
-
* a queue). Kept off the import graph so a `drizzle`-only consumer never
|
|
1056
|
-
* resolves the optional `'bullmq'` package.
|
|
1057
|
-
*/
|
|
1058
|
-
QueueCtor = null;
|
|
1059
|
-
FlowProducerCtor = null;
|
|
1060
|
-
bullMqLoad = null;
|
|
1061
|
-
/**
|
|
1062
|
-
* Own reference to the Drizzle client. `DrizzleJobOrchestrator.db` is
|
|
1063
|
-
* `private` (can't be redeclared even privately in a subclass), and the
|
|
1064
|
-
* spec forbids touching that file — so the subclass keeps its own handle
|
|
1065
|
-
* under a distinct name (same instance, passed through to `super`) for the
|
|
1066
|
-
* cancel-cascade snapshot + definition/run loads below.
|
|
1067
|
-
*/
|
|
1068
|
-
bullDb;
|
|
1069
|
-
/**
|
|
1070
|
-
* Lazily load the optional `bullmq` package and cache its value
|
|
1071
|
-
* constructors. Idempotent (single in-flight promise). Throws a friendly,
|
|
1072
|
-
* actionable error when the consumer selected `backend: 'bullmq'` but did
|
|
1073
|
-
* not install the package — mirrors `createRedisClient` in the redis event
|
|
1074
|
-
* backend. Must be `await`ed before any `queueFor`/`flow` access.
|
|
1075
|
-
*/
|
|
1076
|
-
async loadBullMq() {
|
|
1077
|
-
if (this.QueueCtor && this.FlowProducerCtor) return;
|
|
1078
|
-
if (!this.bullMqLoad) {
|
|
1079
|
-
this.bullMqLoad = (async () => {
|
|
1080
|
-
try {
|
|
1081
|
-
const mod = await import("bullmq");
|
|
1082
|
-
this.QueueCtor = mod.Queue;
|
|
1083
|
-
this.FlowProducerCtor = mod.FlowProducer;
|
|
1084
|
-
} catch {
|
|
1085
|
-
throw new Error(
|
|
1086
|
-
'BullMQ backend requires the "bullmq" package. Install it with: npm install bullmq'
|
|
1087
|
-
);
|
|
1088
|
-
}
|
|
1089
|
-
})();
|
|
1090
|
-
}
|
|
1091
|
-
await this.bullMqLoad;
|
|
1092
|
-
}
|
|
1093
|
-
/**
|
|
1094
|
-
* Open (or reuse) the `Queue` for a pool. Synchronous — callers `await
|
|
1095
|
-
* loadBullMq()` first so `QueueCtor` is populated.
|
|
1096
|
-
*/
|
|
1097
|
-
queueFor(pool) {
|
|
1098
|
-
if (!this.QueueCtor) {
|
|
1099
|
-
throw new Error("BullMQJobOrchestrator: queueFor called before loadBullMq()");
|
|
1100
|
-
}
|
|
1101
|
-
const name = resolvePoolQueueName(pool, this.bullConfig);
|
|
1102
|
-
let q = this.queues.get(name);
|
|
1103
|
-
if (!q) {
|
|
1104
|
-
q = new this.QueueCtor(name, { connection: this.connection });
|
|
1105
|
-
this.queues.set(name, q);
|
|
1106
|
-
}
|
|
1107
|
-
return q;
|
|
1108
|
-
}
|
|
1109
|
-
flow() {
|
|
1110
|
-
if (!this.FlowProducerCtor) {
|
|
1111
|
-
throw new Error("BullMQJobOrchestrator: flow called before loadBullMq()");
|
|
1112
|
-
}
|
|
1113
|
-
if (!this._flow) {
|
|
1114
|
-
this._flow = new this.FlowProducerCtor({ connection: this.connection });
|
|
1115
|
-
}
|
|
1116
|
-
return this._flow;
|
|
1117
|
-
}
|
|
1118
|
-
// ==========================================================================
|
|
1119
|
-
// start — Postgres insert (super) + BullMQ dispatch
|
|
1120
|
-
// ==========================================================================
|
|
1121
|
-
async start(type, input, opts = {}, tx) {
|
|
1122
|
-
const run = await super.start(type, input, opts, tx);
|
|
1123
|
-
await this.dispatch(run, type);
|
|
1124
|
-
return run;
|
|
1125
|
-
}
|
|
1126
|
-
/**
|
|
1127
|
-
* Map a `job_run` row onto a BullMQ job via `queue.add`. When the run has a
|
|
1128
|
-
* `parentRunId` we attach it to the parent's existing BullMQ job through the
|
|
1129
|
-
* `parent: { id, queue }` opt — BullMQ then tracks the parent/child link in
|
|
1130
|
-
* its own graph. (The FlowProducer is reserved for whole-tree atomic
|
|
1131
|
-
* submits, exposed as an opt-in extension via `flowProducer()`; runtime
|
|
1132
|
-
* `ctx.spawnChild` is incremental, so `queue.add` with a parent ref is the
|
|
1133
|
-
* correct primitive here.)
|
|
1134
|
-
*
|
|
1135
|
-
* The `jobId` is colon-safe + stable: `sha1(dedupeKey)` when a dedupe key is
|
|
1136
|
-
* present (so the same logical key dedups), else the `job_run.id` UUID
|
|
1137
|
-
* (already colon-free).
|
|
1138
|
-
*
|
|
1139
|
-
* The domain `parentClosePolicy` cascade is still enforced in Postgres by
|
|
1140
|
-
* the shared `cancel` path — BullMQ's parent link is dispatch bookkeeping,
|
|
1141
|
-
* not the authority.
|
|
1142
|
-
*/
|
|
1143
|
-
async dispatch(run, type) {
|
|
1144
|
-
await this.loadBullMq();
|
|
1145
|
-
const def = await this.loadDefinition(type);
|
|
1146
|
-
const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;
|
|
1147
|
-
const jobOpts = {
|
|
1148
|
-
jobId,
|
|
1149
|
-
...this.retryOpts(def),
|
|
1150
|
-
...this.dedupeOpts(run, def)
|
|
1151
|
-
};
|
|
1152
|
-
if (run.parentRunId) {
|
|
1153
|
-
const parentRow = await this.loadRun(run.parentRunId);
|
|
1154
|
-
if (parentRow) {
|
|
1155
|
-
const parentJobId = parentRow.dedupeKey ? sha1JobId(parentRow.dedupeKey) : parentRow.id;
|
|
1156
|
-
jobOpts.parent = {
|
|
1157
|
-
id: parentJobId,
|
|
1158
|
-
queue: resolvePoolQueueName(parentRow.pool, this.bullConfig)
|
|
1159
|
-
};
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
const payload = { runId: run.id, type, input: run.input };
|
|
1163
|
-
await this.queueFor(run.pool).add(type, payload, jobOpts);
|
|
1164
|
-
}
|
|
1165
|
-
/**
|
|
1166
|
-
* Opt-in extension (spec §Extensions): expose the FlowProducer for
|
|
1167
|
-
* consumers that want to submit a whole parent/child DAG atomically up
|
|
1168
|
-
* front, rather than incrementally via `ctx.spawnChild`. Backend-specific —
|
|
1169
|
-
* code using it is not portable to the Drizzle backend. Async because it
|
|
1170
|
-
* lazily loads the optional `bullmq` package on first use.
|
|
1171
|
-
*/
|
|
1172
|
-
async flowProducer() {
|
|
1173
|
-
await this.loadBullMq();
|
|
1174
|
-
return this.flow();
|
|
1175
|
-
}
|
|
1176
|
-
retryOpts(def) {
|
|
1177
|
-
const policy = def.retryPolicy;
|
|
1178
|
-
if (!policy) return {};
|
|
1179
|
-
return {
|
|
1180
|
-
attempts: policy.attempts,
|
|
1181
|
-
backoff: {
|
|
1182
|
-
type: policy.backoff === "exponential" ? "exponential" : "fixed",
|
|
1183
|
-
delay: policy.baseMs
|
|
1184
|
-
}
|
|
1185
|
-
};
|
|
1186
|
-
}
|
|
1187
|
-
dedupeOpts(run, def) {
|
|
1188
|
-
if (!run.dedupeKey || !def.dedupeWindowMs) return {};
|
|
1189
|
-
return {
|
|
1190
|
-
deduplication: {
|
|
1191
|
-
id: sha1JobId(run.dedupeKey),
|
|
1192
|
-
ttl: def.dedupeWindowMs
|
|
1193
|
-
}
|
|
1194
|
-
};
|
|
1195
|
-
}
|
|
1196
|
-
// ==========================================================================
|
|
1197
|
-
// cancel — Postgres cascade (super) + remove from queue
|
|
1198
|
-
// ==========================================================================
|
|
1199
|
-
async cancel(runId, opts = {}) {
|
|
1200
|
-
const target = await this.loadRun(runId);
|
|
1201
|
-
await super.cancel(runId, opts);
|
|
1202
|
-
if (!target) return;
|
|
1203
|
-
await this.loadBullMq();
|
|
1204
|
-
await this.removeFromQueue(target);
|
|
1205
|
-
if (opts.cascade === false) return;
|
|
1206
|
-
const descendants = await this.bullDb.select().from(jobRuns).where(eq4(jobRuns.rootRunId, target.rootRunId));
|
|
1207
|
-
for (const child of descendants) {
|
|
1208
|
-
if (child.id === runId) continue;
|
|
1209
|
-
await this.removeFromQueue(child);
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
async removeFromQueue(run) {
|
|
1213
|
-
const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;
|
|
1214
|
-
try {
|
|
1215
|
-
const job = await this.queueFor(run.pool).getJob(jobId);
|
|
1216
|
-
if (job) await job.remove();
|
|
1217
|
-
} catch (err) {
|
|
1218
|
-
this.bullLogger.warn(
|
|
1219
|
-
`cancel: could not remove BullMQ job ${jobId} (pool=${run.pool}): ${err.message}`
|
|
1220
|
-
);
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
// ==========================================================================
|
|
1224
|
-
// replay — Postgres reset (super) + re-enqueue
|
|
1225
|
-
// ==========================================================================
|
|
1226
|
-
async replay(runId) {
|
|
1227
|
-
const run = await super.replay(runId);
|
|
1228
|
-
await this.dispatch(run, run.jobType);
|
|
1229
|
-
return run;
|
|
1230
|
-
}
|
|
1231
|
-
// ==========================================================================
|
|
1232
|
-
// Internals
|
|
1233
|
-
// ==========================================================================
|
|
1234
|
-
async loadDefinition(type) {
|
|
1235
|
-
const [def] = await this.bullDb.select().from(jobs).where(eq4(jobs.type, type)).limit(1);
|
|
1236
|
-
if (!def) {
|
|
1237
|
-
throw new Error(`BullMQJobOrchestrator: no job definition for '${type}'`);
|
|
1238
|
-
}
|
|
1239
|
-
return def;
|
|
1240
|
-
}
|
|
1241
|
-
async loadRun(id) {
|
|
1242
|
-
const [row] = await this.bullDb.select().from(jobRuns).where(eq4(jobRuns.id, id)).limit(1);
|
|
1243
|
-
return row ?? null;
|
|
1244
|
-
}
|
|
1245
|
-
/** Close all open queue + flow connections. Called on module destroy. */
|
|
1246
|
-
async closeConnections() {
|
|
1247
|
-
for (const q of this.queues.values()) {
|
|
1248
|
-
await q.close().catch(() => void 0);
|
|
1249
|
-
}
|
|
1250
|
-
this.queues.clear();
|
|
1251
|
-
if (this._flow) {
|
|
1252
|
-
await this._flow.close().catch(() => void 0);
|
|
1253
|
-
this._flow = null;
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
};
|
|
1257
|
-
BullMQJobOrchestrator = __decorateClass([
|
|
1258
|
-
Injectable4(),
|
|
1259
|
-
__decorateParam(0, Inject4(DRIZZLE)),
|
|
1260
|
-
__decorateParam(1, Inject4(JOBS_MULTI_TENANT)),
|
|
1261
|
-
__decorateParam(2, Inject4(BULLMQ_CONNECTION)),
|
|
1262
|
-
__decorateParam(3, Optional()),
|
|
1263
|
-
__decorateParam(3, Inject4(BULLMQ_RESOLVED_CONFIG))
|
|
1264
|
-
], BullMQJobOrchestrator);
|
|
1265
|
-
|
|
1266
881
|
// runtime/subsystems/jobs/job-orchestrator.memory-backend.ts
|
|
1267
882
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1268
|
-
import { Inject as
|
|
883
|
+
import { Inject as Inject4, Injectable as Injectable4, Logger as Logger2, Optional } from "@nestjs/common";
|
|
1269
884
|
var QUEUED_RUN_AT = /* @__PURE__ */ new Date(864e13);
|
|
1270
885
|
var TERMINAL_STATUSES2 = [
|
|
1271
886
|
"completed",
|
|
@@ -1312,7 +927,7 @@ var MemoryJobOrchestrator = class {
|
|
|
1312
927
|
stepService;
|
|
1313
928
|
multiTenant;
|
|
1314
929
|
moduleRef;
|
|
1315
|
-
logger = new
|
|
930
|
+
logger = new Logger2(MemoryJobOrchestrator.name);
|
|
1316
931
|
mutex = new PromiseMutex();
|
|
1317
932
|
handlerRegistry = /* @__PURE__ */ new Map();
|
|
1318
933
|
/**
|
|
@@ -1661,7 +1276,7 @@ var MemoryJobOrchestrator = class {
|
|
|
1661
1276
|
run,
|
|
1662
1277
|
step: this.makeStepFn(run),
|
|
1663
1278
|
spawnChild: this.makeSpawnFn(run),
|
|
1664
|
-
logger: new
|
|
1279
|
+
logger: new Logger2(`JobRun:${run.id}`)
|
|
1665
1280
|
};
|
|
1666
1281
|
const attemptsBefore = run.attempts ?? 0;
|
|
1667
1282
|
try {
|
|
@@ -1834,9 +1449,9 @@ var MemoryJobOrchestrator = class {
|
|
|
1834
1449
|
}
|
|
1835
1450
|
};
|
|
1836
1451
|
MemoryJobOrchestrator = __decorateClass([
|
|
1837
|
-
|
|
1838
|
-
__decorateParam(2,
|
|
1839
|
-
__decorateParam(3,
|
|
1452
|
+
Injectable4(),
|
|
1453
|
+
__decorateParam(2, Inject4(JOBS_MULTI_TENANT)),
|
|
1454
|
+
__decorateParam(3, Optional())
|
|
1840
1455
|
], MemoryJobOrchestrator);
|
|
1841
1456
|
function classifyError(err, policy, currentAttempts) {
|
|
1842
1457
|
if (!policy) return "fail";
|
|
@@ -1870,7 +1485,7 @@ function serialiseError(err, attempt, retryable) {
|
|
|
1870
1485
|
}
|
|
1871
1486
|
|
|
1872
1487
|
// runtime/subsystems/jobs/job-run-service.memory-backend.ts
|
|
1873
|
-
import { Inject as
|
|
1488
|
+
import { Inject as Inject5, Injectable as Injectable5 } from "@nestjs/common";
|
|
1874
1489
|
var NON_TERMINAL_STATUSES2 = [
|
|
1875
1490
|
"pending",
|
|
1876
1491
|
"running",
|
|
@@ -2037,9 +1652,9 @@ var MemoryJobRunService = class {
|
|
|
2037
1652
|
}
|
|
2038
1653
|
};
|
|
2039
1654
|
MemoryJobRunService = __decorateClass([
|
|
2040
|
-
|
|
2041
|
-
__decorateParam(1,
|
|
2042
|
-
__decorateParam(2,
|
|
1655
|
+
Injectable5(),
|
|
1656
|
+
__decorateParam(1, Inject5(JOB_ORCHESTRATOR)),
|
|
1657
|
+
__decorateParam(2, Inject5(JOBS_MULTI_TENANT))
|
|
2043
1658
|
], MemoryJobRunService);
|
|
2044
1659
|
function compareBy(a, b, order) {
|
|
2045
1660
|
switch (order) {
|
|
@@ -2057,7 +1672,7 @@ function compareBy(a, b, order) {
|
|
|
2057
1672
|
|
|
2058
1673
|
// runtime/subsystems/jobs/job-step-service.memory-backend.ts
|
|
2059
1674
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
2060
|
-
import { Injectable as
|
|
1675
|
+
import { Injectable as Injectable6 } from "@nestjs/common";
|
|
2061
1676
|
var MemoryJobStepService = class {
|
|
2062
1677
|
constructor(store) {
|
|
2063
1678
|
this.store = store;
|
|
@@ -2150,7 +1765,7 @@ var MemoryJobStepService = class {
|
|
|
2150
1765
|
}
|
|
2151
1766
|
};
|
|
2152
1767
|
MemoryJobStepService = __decorateClass([
|
|
2153
|
-
|
|
1768
|
+
Injectable6()
|
|
2154
1769
|
], MemoryJobStepService);
|
|
2155
1770
|
|
|
2156
1771
|
// runtime/subsystems/jobs/memory-job-store.ts
|
|
@@ -2169,32 +1784,192 @@ var MemoryJobStore = class {
|
|
|
2169
1784
|
}
|
|
2170
1785
|
};
|
|
2171
1786
|
|
|
2172
|
-
// runtime/subsystems/jobs/
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
1787
|
+
// runtime/subsystems/jobs/pool-config.loader.ts
|
|
1788
|
+
import { existsSync, readFileSync } from "fs";
|
|
1789
|
+
import { resolve } from "path";
|
|
1790
|
+
import { parse as parseYaml } from "yaml";
|
|
1791
|
+
var FRAMEWORK_POOLS = Object.freeze({
|
|
1792
|
+
events_inbound: Object.freeze({
|
|
1793
|
+
queue: "jobs-events-inbound",
|
|
1794
|
+
concurrency: 20,
|
|
1795
|
+
reserved: true,
|
|
1796
|
+
description: "Inbound events drain (events subsystem outbox)."
|
|
1797
|
+
}),
|
|
1798
|
+
events_change: Object.freeze({
|
|
1799
|
+
queue: "jobs-events-change",
|
|
1800
|
+
concurrency: 30,
|
|
1801
|
+
reserved: true,
|
|
1802
|
+
description: "Change events drain (events subsystem outbox)."
|
|
1803
|
+
}),
|
|
1804
|
+
events_outbound: Object.freeze({
|
|
1805
|
+
queue: "jobs-events-outbound",
|
|
1806
|
+
concurrency: 10,
|
|
1807
|
+
reserved: true,
|
|
1808
|
+
description: "Outbound events drain (events subsystem outbox)."
|
|
1809
|
+
}),
|
|
1810
|
+
interactive: Object.freeze({
|
|
1811
|
+
queue: "jobs-interactive",
|
|
1812
|
+
concurrency: 20,
|
|
1813
|
+
reserved: false,
|
|
1814
|
+
description: "User-facing latency-sensitive jobs."
|
|
1815
|
+
}),
|
|
1816
|
+
batch: Object.freeze({
|
|
1817
|
+
queue: "jobs-batch",
|
|
1818
|
+
concurrency: 5,
|
|
1819
|
+
reserved: false,
|
|
1820
|
+
description: "Default pool for background jobs."
|
|
1821
|
+
})
|
|
1822
|
+
});
|
|
1823
|
+
var RESERVED_POOL_NAMES = new Set(
|
|
1824
|
+
Object.entries(FRAMEWORK_POOLS).filter(([, def]) => def.reserved).map(([name]) => name)
|
|
1825
|
+
);
|
|
1826
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1827
|
+
function loadPoolConfig(configPath) {
|
|
1828
|
+
const resolved = resolve(configPath ?? `${process.cwd()}/codegen.config.yaml`);
|
|
1829
|
+
const cached = cache.get(resolved);
|
|
1830
|
+
if (cached) return cached;
|
|
1831
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1832
|
+
for (const [name, def] of Object.entries(FRAMEWORK_POOLS)) {
|
|
1833
|
+
merged.set(name, { ...def });
|
|
1834
|
+
}
|
|
1835
|
+
if (!existsSync(resolved)) {
|
|
1836
|
+
cache.set(resolved, merged);
|
|
1837
|
+
return merged;
|
|
1838
|
+
}
|
|
1839
|
+
let raw;
|
|
1840
|
+
try {
|
|
1841
|
+
raw = parseYaml(readFileSync(resolved, "utf8"));
|
|
1842
|
+
} catch (err) {
|
|
1843
|
+
throw new Error(
|
|
1844
|
+
`pool-config.loader: failed to parse YAML at ${resolved}: ${err.message}`
|
|
1845
|
+
);
|
|
1846
|
+
}
|
|
1847
|
+
const userPools = extractUserPools(raw);
|
|
1848
|
+
for (const [name, userDef] of Object.entries(userPools)) {
|
|
1849
|
+
const existing = merged.get(name);
|
|
1850
|
+
if (existing) {
|
|
1851
|
+
const next = {
|
|
1852
|
+
queue: existing.queue,
|
|
1853
|
+
concurrency: typeof userDef.concurrency === "number" ? userDef.concurrency : existing.concurrency,
|
|
1854
|
+
reserved: existing.reserved,
|
|
1855
|
+
description: userDef.description ?? existing.description
|
|
1856
|
+
};
|
|
1857
|
+
merged.set(name, next);
|
|
1858
|
+
continue;
|
|
1859
|
+
}
|
|
1860
|
+
if (typeof userDef.queue !== "string" || userDef.queue.length === 0) {
|
|
1861
|
+
throw new Error(
|
|
1862
|
+
`pool-config.loader: pool '${name}' must declare a non-empty 'queue'.`
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
if (typeof userDef.concurrency !== "number" || userDef.concurrency <= 0) {
|
|
1866
|
+
throw new Error(
|
|
1867
|
+
`pool-config.loader: pool '${name}' must declare a positive 'concurrency'.`
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
if (userDef.reserved === true) {
|
|
1871
|
+
throw new Error(
|
|
1872
|
+
`pool-config.loader: user-defined pool '${name}' cannot set 'reserved: true' \u2014 reserved is framework-only.`
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1875
|
+
merged.set(name, {
|
|
1876
|
+
queue: userDef.queue,
|
|
1877
|
+
concurrency: userDef.concurrency,
|
|
1878
|
+
reserved: false,
|
|
1879
|
+
description: userDef.description
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
cache.set(resolved, merged);
|
|
1883
|
+
return merged;
|
|
1884
|
+
}
|
|
1885
|
+
function allNonReservedPoolNames(config) {
|
|
1886
|
+
const out = [];
|
|
1887
|
+
for (const [name, def] of config) {
|
|
1888
|
+
if (!def.reserved) out.push(name);
|
|
1889
|
+
}
|
|
1890
|
+
return out;
|
|
1891
|
+
}
|
|
1892
|
+
function allPoolNames(config) {
|
|
1893
|
+
return [...config.keys()];
|
|
1894
|
+
}
|
|
1895
|
+
function extractUserPools(raw) {
|
|
1896
|
+
if (!raw || typeof raw !== "object") return {};
|
|
1897
|
+
const jobs2 = raw.jobs;
|
|
1898
|
+
if (!jobs2 || typeof jobs2 !== "object") return {};
|
|
1899
|
+
const pools = jobs2.pools;
|
|
1900
|
+
if (!pools || typeof pools !== "object") return {};
|
|
1901
|
+
const out = {};
|
|
1902
|
+
for (const [name, def] of Object.entries(pools)) {
|
|
1903
|
+
if (!def || typeof def !== "object") continue;
|
|
1904
|
+
out[name] = def;
|
|
1905
|
+
}
|
|
1906
|
+
return out;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
// runtime/subsystems/jobs/bullmq.config.ts
|
|
1910
|
+
var BULLMQ_CONNECTION = /* @__PURE__ */ Symbol("BULLMQ_CONNECTION");
|
|
1911
|
+
var BULLMQ_RESOLVED_CONFIG = /* @__PURE__ */ Symbol("BULLMQ_RESOLVED_CONFIG");
|
|
1912
|
+
var DEFAULT_REDIS_URL = "redis://localhost:6379";
|
|
1913
|
+
var DEFAULT_BULL_BOARD_MOUNT = "/admin/queues";
|
|
1914
|
+
function resolveBullMqConfig(ext) {
|
|
1915
|
+
const url = ext?.redis_url ?? process.env.REDIS_URL ?? DEFAULT_REDIS_URL;
|
|
1916
|
+
const resolved = {
|
|
1917
|
+
connection: { url },
|
|
1918
|
+
queuePrefix: ext?.queue_prefix
|
|
1919
|
+
};
|
|
1920
|
+
if (ext?.bull_board?.enabled) {
|
|
1921
|
+
resolved.bullBoard = {
|
|
1922
|
+
enabled: true,
|
|
1923
|
+
mountPath: ext.bull_board.mount_path ?? DEFAULT_BULL_BOARD_MOUNT
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
return resolved;
|
|
1927
|
+
}
|
|
1928
|
+
function resolvePoolQueueName(pool, config, poolConfig = loadPoolConfig()) {
|
|
1929
|
+
const alias = poolConfig.get(pool)?.queue ?? pool;
|
|
1930
|
+
const prefix = config?.queuePrefix;
|
|
1931
|
+
return prefix ? `${prefix}:${alias}` : alias;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// runtime/subsystems/jobs/jobs-domain.module.ts
|
|
1935
|
+
var JobsDomainModule = class {
|
|
1936
|
+
static forRoot(opts) {
|
|
1937
|
+
const multiTenant = opts.multiTenant ?? false;
|
|
1938
|
+
const providers = [
|
|
1939
|
+
// JOB-8 — boolean provider consumed by the four service-layer backends.
|
|
1940
|
+
// Always provided (even when `multiTenant === false`) so `@Inject`
|
|
1941
|
+
// always resolves; backends short-circuit the enforcement path when
|
|
1942
|
+
// the value is `false`. See `jobs-domain.tokens.ts` for the claim-loop
|
|
1943
|
+
// cross-tenant-by-design decision.
|
|
1944
|
+
{ provide: JOBS_MULTI_TENANT, useValue: multiTenant }
|
|
1945
|
+
];
|
|
1946
|
+
if (opts.backend === "memory") {
|
|
1947
|
+
const store = new MemoryJobStore();
|
|
1948
|
+
providers.push({ provide: MemoryJobStore, useValue: store });
|
|
1949
|
+
providers.push(MemoryJobStepService);
|
|
1950
|
+
providers.push({ provide: JOB_STEP_SERVICE, useExisting: MemoryJobStepService });
|
|
1951
|
+
providers.push(MemoryJobOrchestrator);
|
|
1952
|
+
providers.push({ provide: JOB_ORCHESTRATOR, useExisting: MemoryJobOrchestrator });
|
|
1953
|
+
providers.push(MemoryJobRunService);
|
|
1954
|
+
providers.push({ provide: JOB_RUN_SERVICE, useExisting: MemoryJobRunService });
|
|
1955
|
+
} else if (opts.backend === "bullmq") {
|
|
1956
|
+
const resolved = resolveBullMqConfig(opts.extensions?.bullmq);
|
|
1957
|
+
providers.push({ provide: BULLMQ_CONNECTION, useValue: resolved.connection });
|
|
1958
|
+
providers.push({ provide: BULLMQ_RESOLVED_CONFIG, useValue: resolved });
|
|
1959
|
+
providers.push({
|
|
1960
|
+
provide: JOB_ORCHESTRATOR,
|
|
1961
|
+
useFactory: async (...args) => {
|
|
1962
|
+
const specifier = "./job-orchestrator.bullmq-backend";
|
|
1963
|
+
const mod = await import(specifier);
|
|
1964
|
+
return new mod.BullMQJobOrchestrator(...args);
|
|
1965
|
+
},
|
|
1966
|
+
// The bullmq orchestrator constructor mirrors DrizzleJobOrchestrator's
|
|
1967
|
+
// injection list: DRIZZLE + JOBS_MULTI_TENANT + the resolved BullMQ
|
|
1968
|
+
// tokens. Importing token references would force a static dep on the
|
|
1969
|
+
// tokens file in this module's import graph; using the existing
|
|
1970
|
+
// symbols already in scope is sufficient.
|
|
1971
|
+
inject: [DRIZZLE, JOBS_MULTI_TENANT, BULLMQ_CONNECTION, BULLMQ_RESOLVED_CONFIG]
|
|
1972
|
+
});
|
|
2198
1973
|
providers.push({ provide: JOB_RUN_SERVICE, useClass: DrizzleJobRunService });
|
|
2199
1974
|
providers.push({ provide: JOB_STEP_SERVICE, useClass: DrizzleJobStepService });
|
|
2200
1975
|
} else {
|
|
@@ -2224,8 +1999,8 @@ JobsDomainModule = __decorateClass([
|
|
|
2224
1999
|
], JobsDomainModule);
|
|
2225
2000
|
|
|
2226
2001
|
// runtime/subsystems/jobs/job-worker.ts
|
|
2227
|
-
import { Inject as
|
|
2228
|
-
import { and as and4, asc as asc2, desc as desc3, eq as
|
|
2002
|
+
import { Inject as Inject6, Injectable as Injectable7, Logger as Logger3 } from "@nestjs/common";
|
|
2003
|
+
import { and as and4, asc as asc2, desc as desc3, eq as eq4, inArray as inArray3, lt as lt2, lte, sql as sql4 } from "drizzle-orm";
|
|
2229
2004
|
var JOB_WORKER_OPTIONS = /* @__PURE__ */ Symbol("JOB_WORKER_OPTIONS");
|
|
2230
2005
|
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
2231
2006
|
var DEFAULT_STALE_SWEEPER_INTERVAL_MS = 6e4;
|
|
@@ -2288,7 +2063,7 @@ var JobWorker = class {
|
|
|
2288
2063
|
stepService;
|
|
2289
2064
|
options;
|
|
2290
2065
|
moduleRef;
|
|
2291
|
-
logger = new
|
|
2066
|
+
logger = new Logger3(JobWorker.name);
|
|
2292
2067
|
shuttingDown = false;
|
|
2293
2068
|
inFlight = /* @__PURE__ */ new Set();
|
|
2294
2069
|
pollTimer = null;
|
|
@@ -2329,7 +2104,7 @@ var JobWorker = class {
|
|
|
2329
2104
|
await this.drainInFlight();
|
|
2330
2105
|
try {
|
|
2331
2106
|
await this.db.update(jobRuns).set({ status: "pending", claimedAt: null, startedAt: null }).where(
|
|
2332
|
-
and4(
|
|
2107
|
+
and4(eq4(jobRuns.status, "running"), eq4(jobRuns.pool, this.options.pool))
|
|
2333
2108
|
);
|
|
2334
2109
|
} catch (err) {
|
|
2335
2110
|
this.logger.error(`shutdown reset failed: ${err.message}`);
|
|
@@ -2379,8 +2154,8 @@ var JobWorker = class {
|
|
|
2379
2154
|
return this.db.transaction(async (tx) => {
|
|
2380
2155
|
const candidates = await tx.select({ id: jobRuns.id }).from(jobRuns).where(
|
|
2381
2156
|
and4(
|
|
2382
|
-
|
|
2383
|
-
|
|
2157
|
+
eq4(jobRuns.status, "pending"),
|
|
2158
|
+
eq4(jobRuns.pool, pool),
|
|
2384
2159
|
lte(jobRuns.runAt, /* @__PURE__ */ new Date())
|
|
2385
2160
|
)
|
|
2386
2161
|
).orderBy(desc3(jobRuns.priority), asc2(jobRuns.runAt)).limit(1).for("update", { skipLocked: true });
|
|
@@ -2391,7 +2166,7 @@ var JobWorker = class {
|
|
|
2391
2166
|
claimedAt: /* @__PURE__ */ new Date(),
|
|
2392
2167
|
startedAt: /* @__PURE__ */ new Date(),
|
|
2393
2168
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2394
|
-
}).where(
|
|
2169
|
+
}).where(eq4(jobRuns.id, candidate.id)).returning();
|
|
2395
2170
|
return claimed ?? null;
|
|
2396
2171
|
});
|
|
2397
2172
|
}
|
|
@@ -2409,7 +2184,7 @@ var JobWorker = class {
|
|
|
2409
2184
|
await this.db.transaction(async (tx) => {
|
|
2410
2185
|
const threshold = new Date(Date.now() - this.staleThresholdMs);
|
|
2411
2186
|
const stale = await tx.select({ id: jobRuns.id }).from(jobRuns).where(
|
|
2412
|
-
and4(
|
|
2187
|
+
and4(eq4(jobRuns.status, "running"), lt2(jobRuns.claimedAt, threshold))
|
|
2413
2188
|
).for("update", { skipLocked: true });
|
|
2414
2189
|
if (stale.length === 0) return;
|
|
2415
2190
|
const ids = stale.map((r) => r.id);
|
|
@@ -2442,8 +2217,8 @@ var JobWorker = class {
|
|
|
2442
2217
|
if (claimed.concurrencyKey) {
|
|
2443
2218
|
const inflight = await this.db.select({ id: jobRuns.id }).from(jobRuns).where(
|
|
2444
2219
|
and4(
|
|
2445
|
-
|
|
2446
|
-
|
|
2220
|
+
eq4(jobRuns.concurrencyKey, claimed.concurrencyKey),
|
|
2221
|
+
eq4(jobRuns.status, "running")
|
|
2447
2222
|
)
|
|
2448
2223
|
);
|
|
2449
2224
|
const other = inflight.find((r) => r.id !== claimed.id);
|
|
@@ -2453,7 +2228,7 @@ var JobWorker = class {
|
|
|
2453
2228
|
claimedAt: null,
|
|
2454
2229
|
startedAt: null,
|
|
2455
2230
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2456
|
-
}).where(
|
|
2231
|
+
}).where(eq4(jobRuns.id, claimed.id));
|
|
2457
2232
|
return;
|
|
2458
2233
|
}
|
|
2459
2234
|
}
|
|
@@ -2468,7 +2243,7 @@ var JobWorker = class {
|
|
|
2468
2243
|
run: claimed,
|
|
2469
2244
|
step: this.makeStepFn(claimed),
|
|
2470
2245
|
spawnChild: this.makeSpawnFn(claimed),
|
|
2471
|
-
logger: new
|
|
2246
|
+
logger: new Logger3(`JobRun:${claimed.id}`)
|
|
2472
2247
|
};
|
|
2473
2248
|
const attemptsBefore = claimed.attempts ?? 0;
|
|
2474
2249
|
try {
|
|
@@ -2479,7 +2254,7 @@ var JobWorker = class {
|
|
|
2479
2254
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
2480
2255
|
updatedAt: /* @__PURE__ */ new Date(),
|
|
2481
2256
|
attempts: attemptsBefore + 1
|
|
2482
|
-
}).where(
|
|
2257
|
+
}).where(eq4(jobRuns.id, claimed.id));
|
|
2483
2258
|
} catch (err) {
|
|
2484
2259
|
const policy = meta.retry;
|
|
2485
2260
|
const decision = classifyError2(err, policy, attemptsBefore);
|
|
@@ -2494,7 +2269,7 @@ var JobWorker = class {
|
|
|
2494
2269
|
claimedAt: null,
|
|
2495
2270
|
error: serialiseError2(err, nextAttempts, true),
|
|
2496
2271
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2497
|
-
}).where(
|
|
2272
|
+
}).where(eq4(jobRuns.id, claimed.id));
|
|
2498
2273
|
} else {
|
|
2499
2274
|
await this.markFailed(claimed, err, nextAttempts);
|
|
2500
2275
|
}
|
|
@@ -2507,7 +2282,7 @@ var JobWorker = class {
|
|
|
2507
2282
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
2508
2283
|
error: serialiseError2(err, finalAttempts, false),
|
|
2509
2284
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2510
|
-
}).where(
|
|
2285
|
+
}).where(eq4(jobRuns.id, claimed.id));
|
|
2511
2286
|
if (claimed.parentClosePolicy === "terminate") {
|
|
2512
2287
|
try {
|
|
2513
2288
|
await this.orchestrator.cancel(claimed.id, {
|
|
@@ -2610,215 +2385,14 @@ var JobWorker = class {
|
|
|
2610
2385
|
// ============================================================================
|
|
2611
2386
|
};
|
|
2612
2387
|
JobWorker = __decorateClass([
|
|
2613
|
-
|
|
2614
|
-
__decorateParam(0,
|
|
2615
|
-
__decorateParam(1,
|
|
2616
|
-
__decorateParam(2,
|
|
2617
|
-
__decorateParam(3,
|
|
2618
|
-
__decorateParam(4,
|
|
2388
|
+
Injectable7(),
|
|
2389
|
+
__decorateParam(0, Inject6(DRIZZLE)),
|
|
2390
|
+
__decorateParam(1, Inject6(JOB_ORCHESTRATOR)),
|
|
2391
|
+
__decorateParam(2, Inject6(JOB_RUN_SERVICE)),
|
|
2392
|
+
__decorateParam(3, Inject6(JOB_STEP_SERVICE)),
|
|
2393
|
+
__decorateParam(4, Inject6(JOB_WORKER_OPTIONS))
|
|
2619
2394
|
], JobWorker);
|
|
2620
2395
|
|
|
2621
|
-
// runtime/subsystems/jobs/job-worker.bullmq-backend.ts
|
|
2622
|
-
import { Logger as Logger5 } from "@nestjs/common";
|
|
2623
|
-
import { eq as eq6 } from "drizzle-orm";
|
|
2624
|
-
function serialiseError3(err, attempt, retryable) {
|
|
2625
|
-
const e = err;
|
|
2626
|
-
return {
|
|
2627
|
-
message: e?.message ?? String(err),
|
|
2628
|
-
stack: e?.stack,
|
|
2629
|
-
retryable,
|
|
2630
|
-
attempt
|
|
2631
|
-
};
|
|
2632
|
-
}
|
|
2633
|
-
var BullMQJobWorker = class _BullMQJobWorker {
|
|
2634
|
-
constructor(db, orchestrator, stepService, options, moduleRef) {
|
|
2635
|
-
this.db = db;
|
|
2636
|
-
this.orchestrator = orchestrator;
|
|
2637
|
-
this.stepService = stepService;
|
|
2638
|
-
this.options = options;
|
|
2639
|
-
this.moduleRef = moduleRef;
|
|
2640
|
-
}
|
|
2641
|
-
db;
|
|
2642
|
-
orchestrator;
|
|
2643
|
-
stepService;
|
|
2644
|
-
options;
|
|
2645
|
-
moduleRef;
|
|
2646
|
-
logger = new Logger5(_BullMQJobWorker.name);
|
|
2647
|
-
worker = null;
|
|
2648
|
-
async onModuleInit() {
|
|
2649
|
-
let WorkerCtor;
|
|
2650
|
-
try {
|
|
2651
|
-
const mod = await import("bullmq");
|
|
2652
|
-
WorkerCtor = mod.Worker;
|
|
2653
|
-
} catch {
|
|
2654
|
-
throw new Error(
|
|
2655
|
-
'BullMQ backend requires the "bullmq" package. Install it with: npm install bullmq'
|
|
2656
|
-
);
|
|
2657
|
-
}
|
|
2658
|
-
this.worker = new WorkerCtor(
|
|
2659
|
-
this.options.queueName,
|
|
2660
|
-
(job) => this.process(job),
|
|
2661
|
-
{
|
|
2662
|
-
connection: this.options.connection,
|
|
2663
|
-
concurrency: this.options.concurrency
|
|
2664
|
-
}
|
|
2665
|
-
);
|
|
2666
|
-
this.worker.on("failed", (job, err) => {
|
|
2667
|
-
if (!job) return;
|
|
2668
|
-
const attemptsMade = job.attemptsMade;
|
|
2669
|
-
const maxAttempts = job.opts.attempts ?? 1;
|
|
2670
|
-
if (attemptsMade >= maxAttempts) {
|
|
2671
|
-
void this.markFailed(job.data.runId, err, attemptsMade);
|
|
2672
|
-
}
|
|
2673
|
-
});
|
|
2674
|
-
this.logger.log(
|
|
2675
|
-
`BullMQ worker started: pool='${this.options.pool}' queue='${this.options.queueName}' concurrency=${this.options.concurrency}`
|
|
2676
|
-
);
|
|
2677
|
-
}
|
|
2678
|
-
async onModuleDestroy() {
|
|
2679
|
-
if (this.worker) {
|
|
2680
|
-
await this.worker.close();
|
|
2681
|
-
this.worker = null;
|
|
2682
|
-
}
|
|
2683
|
-
}
|
|
2684
|
-
/**
|
|
2685
|
-
* Process one BullMQ job. Returns the handler output (stored by BullMQ as
|
|
2686
|
-
* the job return value AND written to `job_run.output`). Throws on handler
|
|
2687
|
-
* failure so BullMQ applies the retry policy.
|
|
2688
|
-
*/
|
|
2689
|
-
async process(job) {
|
|
2690
|
-
const { runId } = job.data;
|
|
2691
|
-
const [row] = await this.db.select().from(jobRuns).where(eq6(jobRuns.id, runId)).limit(1);
|
|
2692
|
-
if (!row) {
|
|
2693
|
-
this.logger.warn(`process: job_run ${runId} not found; skipping`);
|
|
2694
|
-
return {};
|
|
2695
|
-
}
|
|
2696
|
-
const run = row;
|
|
2697
|
-
if (run.status === "canceled") {
|
|
2698
|
-
return {};
|
|
2699
|
-
}
|
|
2700
|
-
const registryEntry = JOB_HANDLER_REGISTRY.get(run.jobType);
|
|
2701
|
-
if (!registryEntry) {
|
|
2702
|
-
throw new Error(
|
|
2703
|
-
`No handler registered for jobType='${run.jobType}' (run ${run.id})`
|
|
2704
|
-
);
|
|
2705
|
-
}
|
|
2706
|
-
await this.db.update(jobRuns).set({
|
|
2707
|
-
status: "running",
|
|
2708
|
-
claimedAt: /* @__PURE__ */ new Date(),
|
|
2709
|
-
startedAt: /* @__PURE__ */ new Date(),
|
|
2710
|
-
attempts: job.attemptsMade + 1,
|
|
2711
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
2712
|
-
}).where(eq6(jobRuns.id, run.id));
|
|
2713
|
-
const HandlerClass = registryEntry.handlerClass;
|
|
2714
|
-
const handler = this.moduleRef.get(
|
|
2715
|
-
HandlerClass,
|
|
2716
|
-
{ strict: false }
|
|
2717
|
-
);
|
|
2718
|
-
const ctx = {
|
|
2719
|
-
input: run.input,
|
|
2720
|
-
run,
|
|
2721
|
-
step: this.makeStepFn(run),
|
|
2722
|
-
spawnChild: this.makeSpawnFn(run),
|
|
2723
|
-
logger: new Logger5(`JobRun:${run.id}`)
|
|
2724
|
-
};
|
|
2725
|
-
const output = await handler.run(ctx);
|
|
2726
|
-
await this.db.update(jobRuns).set({
|
|
2727
|
-
status: "completed",
|
|
2728
|
-
output: output ?? {},
|
|
2729
|
-
finishedAt: /* @__PURE__ */ new Date(),
|
|
2730
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
2731
|
-
}).where(eq6(jobRuns.id, run.id));
|
|
2732
|
-
return output ?? {};
|
|
2733
|
-
}
|
|
2734
|
-
async markFailed(runId, err, finalAttempts) {
|
|
2735
|
-
const [row] = await this.db.select().from(jobRuns).where(eq6(jobRuns.id, runId)).limit(1);
|
|
2736
|
-
if (!row) return;
|
|
2737
|
-
const run = row;
|
|
2738
|
-
await this.db.update(jobRuns).set({
|
|
2739
|
-
status: "failed",
|
|
2740
|
-
attempts: finalAttempts,
|
|
2741
|
-
finishedAt: /* @__PURE__ */ new Date(),
|
|
2742
|
-
error: serialiseError3(err, finalAttempts, false),
|
|
2743
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
2744
|
-
}).where(eq6(jobRuns.id, runId));
|
|
2745
|
-
if (run.parentClosePolicy === "terminate") {
|
|
2746
|
-
try {
|
|
2747
|
-
await this.orchestrator.cancel(run.id, {
|
|
2748
|
-
cascade: true,
|
|
2749
|
-
reason: "parent-failed",
|
|
2750
|
-
tenantId: run.tenantId
|
|
2751
|
-
});
|
|
2752
|
-
} catch (cascadeErr) {
|
|
2753
|
-
this.logger.warn(
|
|
2754
|
-
`cascade on failed run ${run.id}: ${cascadeErr.message}`
|
|
2755
|
-
);
|
|
2756
|
-
}
|
|
2757
|
-
}
|
|
2758
|
-
}
|
|
2759
|
-
// ── ctx.step / ctx.spawnChild (mirror JobWorker) ──────────────────────────
|
|
2760
|
-
makeStepFn(run) {
|
|
2761
|
-
return async (stepId, fn, _opts) => {
|
|
2762
|
-
void _opts;
|
|
2763
|
-
const existing = await this.stepService.findStep(run.id, stepId);
|
|
2764
|
-
if (existing?.status === "completed") {
|
|
2765
|
-
return existing.output;
|
|
2766
|
-
}
|
|
2767
|
-
const nextAttempts = (existing?.attempts ?? 0) + 1;
|
|
2768
|
-
const seq = nextAttempts;
|
|
2769
|
-
await this.stepService.recordStep({
|
|
2770
|
-
jobRunId: run.id,
|
|
2771
|
-
stepId,
|
|
2772
|
-
kind: "task",
|
|
2773
|
-
seq,
|
|
2774
|
-
status: "running",
|
|
2775
|
-
startedAt: /* @__PURE__ */ new Date(),
|
|
2776
|
-
attempts: nextAttempts
|
|
2777
|
-
});
|
|
2778
|
-
try {
|
|
2779
|
-
const output = await fn();
|
|
2780
|
-
await this.stepService.recordStep({
|
|
2781
|
-
jobRunId: run.id,
|
|
2782
|
-
stepId,
|
|
2783
|
-
kind: "task",
|
|
2784
|
-
seq,
|
|
2785
|
-
status: "completed",
|
|
2786
|
-
output,
|
|
2787
|
-
finishedAt: /* @__PURE__ */ new Date(),
|
|
2788
|
-
attempts: nextAttempts
|
|
2789
|
-
});
|
|
2790
|
-
return output;
|
|
2791
|
-
} catch (err) {
|
|
2792
|
-
await this.stepService.recordStep({
|
|
2793
|
-
jobRunId: run.id,
|
|
2794
|
-
stepId,
|
|
2795
|
-
kind: "task",
|
|
2796
|
-
seq,
|
|
2797
|
-
status: "failed",
|
|
2798
|
-
error: serialiseError3(err, nextAttempts, false),
|
|
2799
|
-
finishedAt: /* @__PURE__ */ new Date(),
|
|
2800
|
-
attempts: nextAttempts
|
|
2801
|
-
});
|
|
2802
|
-
throw err;
|
|
2803
|
-
}
|
|
2804
|
-
};
|
|
2805
|
-
}
|
|
2806
|
-
makeSpawnFn(run) {
|
|
2807
|
-
return async (type, input, opts) => {
|
|
2808
|
-
return this.orchestrator.start(type, input, {
|
|
2809
|
-
parentRunId: run.id,
|
|
2810
|
-
parentClosePolicy: opts?.closePolicy,
|
|
2811
|
-
runAt: opts?.runAt,
|
|
2812
|
-
priority: opts?.priority,
|
|
2813
|
-
tags: opts?.tags,
|
|
2814
|
-
triggerSource: "parent",
|
|
2815
|
-
triggerRef: run.id,
|
|
2816
|
-
tenantId: run.tenantId
|
|
2817
|
-
});
|
|
2818
|
-
};
|
|
2819
|
-
}
|
|
2820
|
-
};
|
|
2821
|
-
|
|
2822
2396
|
// runtime/subsystems/jobs/job-worker.module.ts
|
|
2823
2397
|
var DEFAULT_SHUTDOWN_TIMEOUT_MS2 = 3e4;
|
|
2824
2398
|
var JOB_WORKER_MODULE_OPTIONS = /* @__PURE__ */ Symbol("JOB_WORKER_MODULE_OPTIONS");
|
|
@@ -2841,7 +2415,7 @@ var JobWorkerOrchestrator = class {
|
|
|
2841
2415
|
moduleRef;
|
|
2842
2416
|
bullConnection;
|
|
2843
2417
|
bullConfig;
|
|
2844
|
-
logger = new
|
|
2418
|
+
logger = new Logger4(JobWorkerOrchestrator.name);
|
|
2845
2419
|
workers = [];
|
|
2846
2420
|
// ============================================================================
|
|
2847
2421
|
// Lifecycle
|
|
@@ -2871,7 +2445,7 @@ var JobWorkerOrchestrator = class {
|
|
|
2871
2445
|
concurrency: def.concurrency,
|
|
2872
2446
|
shutdownTimeoutMs: this.options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS2
|
|
2873
2447
|
};
|
|
2874
|
-
const worker = this.options.workerFactory ? this.options.workerFactory(workerOptions) : backend === "bullmq" ? this.spawnBullMQWorker(poolName, def.queue, def.concurrency, poolConfig) : this.spawnWorker(workerOptions);
|
|
2448
|
+
const worker = this.options.workerFactory ? this.options.workerFactory(workerOptions) : backend === "bullmq" ? await this.spawnBullMQWorker(poolName, def.queue, def.concurrency, poolConfig) : this.spawnWorker(workerOptions);
|
|
2875
2449
|
await worker.onModuleInit();
|
|
2876
2450
|
this.workers.push(worker);
|
|
2877
2451
|
this.logger.log(
|
|
@@ -2971,7 +2545,15 @@ var JobWorkerOrchestrator = class {
|
|
|
2971
2545
|
* orchestrator's `dispatch` via `resolvePoolQueueName(pool, …)` so producer
|
|
2972
2546
|
* and consumer agree.
|
|
2973
2547
|
*/
|
|
2974
|
-
|
|
2548
|
+
/**
|
|
2549
|
+
* #6 — async + dynamic-import. The `job-worker.bullmq-backend.ts` file is
|
|
2550
|
+
* filtered out of the vendor set for drizzle/memory installs (no `bullmq`
|
|
2551
|
+
* peer dep needed). The non-literal import specifier makes TS treat the
|
|
2552
|
+
* module as `any` so the consumer's tsc never tries to resolve an absent
|
|
2553
|
+
* file. This method is only entered when `backend === 'bullmq'` — at which
|
|
2554
|
+
* point the file IS vendored.
|
|
2555
|
+
*/
|
|
2556
|
+
async spawnBullMQWorker(pool, _queueAlias, concurrency, poolConfig) {
|
|
2975
2557
|
if (!this.db) {
|
|
2976
2558
|
throw new Error(
|
|
2977
2559
|
`JobWorkerModule: BullMQ worker spawning requires the Drizzle client (no DRIZZLE provider available) \u2014 job_run remains the source of truth.`
|
|
@@ -2988,7 +2570,9 @@ var JobWorkerOrchestrator = class {
|
|
|
2988
2570
|
);
|
|
2989
2571
|
}
|
|
2990
2572
|
const queueName = resolvePoolQueueName(pool, this.bullConfig, poolConfig);
|
|
2991
|
-
|
|
2573
|
+
const specifier = "./job-worker.bullmq-backend";
|
|
2574
|
+
const mod = await import(specifier);
|
|
2575
|
+
return new mod.BullMQJobWorker(
|
|
2992
2576
|
this.db,
|
|
2993
2577
|
this.orchestrator,
|
|
2994
2578
|
this.stepService,
|
|
@@ -3003,17 +2587,17 @@ var JobWorkerOrchestrator = class {
|
|
|
3003
2587
|
}
|
|
3004
2588
|
};
|
|
3005
2589
|
JobWorkerOrchestrator = __decorateClass([
|
|
3006
|
-
|
|
3007
|
-
__decorateParam(0,
|
|
3008
|
-
__decorateParam(1,
|
|
3009
|
-
__decorateParam(2,
|
|
3010
|
-
__decorateParam(3,
|
|
3011
|
-
__decorateParam(4,
|
|
3012
|
-
__decorateParam(4,
|
|
3013
|
-
__decorateParam(6,
|
|
3014
|
-
__decorateParam(6,
|
|
3015
|
-
__decorateParam(7,
|
|
3016
|
-
__decorateParam(7,
|
|
2590
|
+
Injectable8(),
|
|
2591
|
+
__decorateParam(0, Inject7(JOB_ORCHESTRATOR)),
|
|
2592
|
+
__decorateParam(1, Inject7(JOB_RUN_SERVICE)),
|
|
2593
|
+
__decorateParam(2, Inject7(JOB_STEP_SERVICE)),
|
|
2594
|
+
__decorateParam(3, Inject7(JOB_WORKER_MODULE_OPTIONS)),
|
|
2595
|
+
__decorateParam(4, Optional2()),
|
|
2596
|
+
__decorateParam(4, Inject7(DRIZZLE)),
|
|
2597
|
+
__decorateParam(6, Optional2()),
|
|
2598
|
+
__decorateParam(6, Inject7(BULLMQ_CONNECTION)),
|
|
2599
|
+
__decorateParam(7, Optional2()),
|
|
2600
|
+
__decorateParam(7, Inject7(BULLMQ_RESOLVED_CONFIG))
|
|
3017
2601
|
], JobWorkerOrchestrator);
|
|
3018
2602
|
var JobWorkerModule = class {
|
|
3019
2603
|
static forRoot(opts) {
|