@cogenta/core 0.1.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/dist/cache/entry.d.ts +32 -0
- package/dist/cache/entry.d.ts.map +1 -0
- package/dist/cache/entry.js +72 -0
- package/dist/cache/entry.js.map +1 -0
- package/dist/cache/file.d.ts +16 -0
- package/dist/cache/file.d.ts.map +1 -0
- package/dist/cache/file.js +191 -0
- package/dist/cache/file.js.map +1 -0
- package/dist/cache/index.d.ts +20 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +25 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/memory.d.ts +11 -0
- package/dist/cache/memory.d.ts.map +1 -0
- package/dist/cache/memory.js +89 -0
- package/dist/cache/memory.js.map +1 -0
- package/dist/cache/redis.d.ts +54 -0
- package/dist/cache/redis.d.ts.map +1 -0
- package/dist/cache/redis.js +167 -0
- package/dist/cache/redis.js.map +1 -0
- package/dist/cache/types.d.ts +37 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cache/types.js +2 -0
- package/dist/cache/types.js.map +1 -0
- package/dist/config/define-config.d.ts +19 -0
- package/dist/config/define-config.d.ts.map +1 -0
- package/dist/config/define-config.js +20 -0
- package/dist/config/define-config.js.map +1 -0
- package/dist/config/env.d.ts +30 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +139 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +5 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/load-config.d.ts +30 -0
- package/dist/config/load-config.d.ts.map +1 -0
- package/dist/config/load-config.js +83 -0
- package/dist/config/load-config.js.map +1 -0
- package/dist/config/resolve-config.d.ts +14 -0
- package/dist/config/resolve-config.d.ts.map +1 -0
- package/dist/config/resolve-config.js +146 -0
- package/dist/config/resolve-config.js.map +1 -0
- package/dist/config/schema.d.ts +68 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +75 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/types.d.ts +126 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +6 -0
- package/dist/config/types.js.map +1 -0
- package/dist/db/dialect.d.ts +64 -0
- package/dist/db/dialect.d.ts.map +1 -0
- package/dist/db/dialect.js +147 -0
- package/dist/db/dialect.js.map +1 -0
- package/dist/db/drizzle.d.ts +35 -0
- package/dist/db/drizzle.d.ts.map +1 -0
- package/dist/db/drizzle.js +128 -0
- package/dist/db/drizzle.js.map +1 -0
- package/dist/db/index.d.ts +28 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +30 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/mysql.d.ts +39 -0
- package/dist/db/mysql.d.ts.map +1 -0
- package/dist/db/mysql.js +224 -0
- package/dist/db/mysql.js.map +1 -0
- package/dist/db/postgres.d.ts +47 -0
- package/dist/db/postgres.d.ts.map +1 -0
- package/dist/db/postgres.js +174 -0
- package/dist/db/postgres.js.map +1 -0
- package/dist/db/sqlite.d.ts +41 -0
- package/dist/db/sqlite.d.ts.map +1 -0
- package/dist/db/sqlite.js +213 -0
- package/dist/db/sqlite.js.map +1 -0
- package/dist/db/types.d.ts +71 -0
- package/dist/db/types.d.ts.map +1 -0
- package/dist/db/types.js +2 -0
- package/dist/db/types.js.map +1 -0
- package/dist/drivers/index.d.ts +5 -0
- package/dist/drivers/index.d.ts.map +1 -0
- package/dist/drivers/index.js +3 -0
- package/dist/drivers/index.js.map +1 -0
- package/dist/drivers/registry.d.ts +27 -0
- package/dist/drivers/registry.d.ts.map +1 -0
- package/dist/drivers/registry.js +162 -0
- package/dist/drivers/registry.js.map +1 -0
- package/dist/drivers/types.d.ts +59 -0
- package/dist/drivers/types.d.ts.map +1 -0
- package/dist/drivers/types.js +2 -0
- package/dist/drivers/types.js.map +1 -0
- package/dist/errors/codes.d.ts +8 -0
- package/dist/errors/codes.d.ts.map +1 -0
- package/dist/errors/codes.js +201 -0
- package/dist/errors/codes.js.map +1 -0
- package/dist/errors/cogenta-error.d.ts +38 -0
- package/dist/errors/cogenta-error.d.ts.map +1 -0
- package/dist/errors/cogenta-error.js +31 -0
- package/dist/errors/cogenta-error.js.map +1 -0
- package/dist/errors/index.d.ts +5 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +3 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/id.d.ts +5 -0
- package/dist/id.d.ts.map +1 -0
- package/dist/id.js +82 -0
- package/dist/id.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/logger/index.d.ts +5 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +4 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/logger.d.ts +10 -0
- package/dist/logger/logger.d.ts.map +1 -0
- package/dist/logger/logger.js +108 -0
- package/dist/logger/logger.js.map +1 -0
- package/dist/logger/redact.d.ts +12 -0
- package/dist/logger/redact.d.ts.map +1 -0
- package/dist/logger/redact.js +96 -0
- package/dist/logger/redact.js.map +1 -0
- package/dist/logger/types.d.ts +32 -0
- package/dist/logger/types.d.ts.map +1 -0
- package/dist/logger/types.js +3 -0
- package/dist/logger/types.js.map +1 -0
- package/dist/media/format-sniff.d.ts +22 -0
- package/dist/media/format-sniff.d.ts.map +1 -0
- package/dist/media/format-sniff.js +77 -0
- package/dist/media/format-sniff.js.map +1 -0
- package/dist/media/index.d.ts +5 -0
- package/dist/media/index.d.ts.map +1 -0
- package/dist/media/index.js +4 -0
- package/dist/media/index.js.map +1 -0
- package/dist/media/store.d.ts +13 -0
- package/dist/media/store.d.ts.map +1 -0
- package/dist/media/store.js +211 -0
- package/dist/media/store.js.map +1 -0
- package/dist/media/types.d.ts +70 -0
- package/dist/media/types.d.ts.map +1 -0
- package/dist/media/types.js +2 -0
- package/dist/media/types.js.map +1 -0
- package/dist/migrations/index.d.ts +4 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +2 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/migrations/migrator.d.ts +18 -0
- package/dist/migrations/migrator.d.ts.map +1 -0
- package/dist/migrations/migrator.js +247 -0
- package/dist/migrations/migrator.js.map +1 -0
- package/dist/migrations/types.d.ts +63 -0
- package/dist/migrations/types.d.ts.map +1 -0
- package/dist/migrations/types.js +2 -0
- package/dist/migrations/types.js.map +1 -0
- package/dist/queue/bullmq.d.ts +119 -0
- package/dist/queue/bullmq.d.ts.map +1 -0
- package/dist/queue/bullmq.js +391 -0
- package/dist/queue/bullmq.js.map +1 -0
- package/dist/queue/database.d.ts +17 -0
- package/dist/queue/database.d.ts.map +1 -0
- package/dist/queue/database.js +242 -0
- package/dist/queue/database.js.map +1 -0
- package/dist/queue/index.d.ts +7 -0
- package/dist/queue/index.d.ts.map +1 -0
- package/dist/queue/index.js +4 -0
- package/dist/queue/index.js.map +1 -0
- package/dist/queue/types.d.ts +61 -0
- package/dist/queue/types.d.ts.map +1 -0
- package/dist/queue/types.js +2 -0
- package/dist/queue/types.js.map +1 -0
- package/dist/storage/index.d.ts +14 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +17 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/key.d.ts +12 -0
- package/dist/storage/key.d.ts.map +1 -0
- package/dist/storage/key.js +43 -0
- package/dist/storage/key.js.map +1 -0
- package/dist/storage/local.d.ts +23 -0
- package/dist/storage/local.d.ts.map +1 -0
- package/dist/storage/local.js +204 -0
- package/dist/storage/local.js.map +1 -0
- package/dist/storage/s3.d.ts +54 -0
- package/dist/storage/s3.d.ts.map +1 -0
- package/dist/storage/s3.js +240 -0
- package/dist/storage/s3.js.map +1 -0
- package/dist/storage/types.d.ts +45 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +2 -0
- package/dist/storage/types.js.map +1 -0
- package/package.json +82 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { CogentaError } from '../errors/index.js';
|
|
3
|
+
import { createLogger } from '../logger/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Loads bullmq if the host application installed it. Absent is a normal
|
|
6
|
+
* outcome, not an error: it is what makes a site without Redis fall back to the
|
|
7
|
+
* database queue instead of failing to boot.
|
|
8
|
+
*/
|
|
9
|
+
export async function loadBullmqModule() {
|
|
10
|
+
try {
|
|
11
|
+
return (await import('bullmq'));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const DEFAULT_PREFIX = 'cogenta:queue';
|
|
18
|
+
const DEFAULT_LEASE_MS = 5 * 60 * 1000;
|
|
19
|
+
const DEFAULT_BATCH_SIZE = 10;
|
|
20
|
+
const DEFAULT_BACKOFF_MS = 1000;
|
|
21
|
+
const DEFAULT_STALLED_INTERVAL_MS = 30_000;
|
|
22
|
+
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
23
|
+
const PROBE_TIMEOUT_MS = 3000;
|
|
24
|
+
/** The name of the queue that exists only to hold a connection for `ping`. */
|
|
25
|
+
const CONTROL_QUEUE = '__cogenta_control';
|
|
26
|
+
/**
|
|
27
|
+
* Cogenta says "higher priority runs first"; bullmq says "lower number runs
|
|
28
|
+
* first", and reserves 0 for "no priority at all". Mapping onto a mid-range
|
|
29
|
+
* origin keeps both signs of a Cogenta priority inside bullmq's 1..2097151
|
|
30
|
+
* window, and keeps every job prioritised so ordering never depends on which
|
|
31
|
+
* of the two internal lists a job landed in.
|
|
32
|
+
*/
|
|
33
|
+
const PRIORITY_ORIGIN = 1_048_576;
|
|
34
|
+
const MAX_BULLMQ_PRIORITY = 2_097_151;
|
|
35
|
+
function toBullmqPriority(priority) {
|
|
36
|
+
return Math.min(Math.max(PRIORITY_ORIGIN - Math.trunc(priority), 1), MAX_BULLMQ_PRIORITY);
|
|
37
|
+
}
|
|
38
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
39
|
+
/**
|
|
40
|
+
* A Cogenta job id carries the job name, because bullmq shards by queue and a
|
|
41
|
+
* bare id says nothing about which queue to look in. The alternative — a second
|
|
42
|
+
* index in Redis — is state that can drift from the jobs it describes.
|
|
43
|
+
*/
|
|
44
|
+
function encodeJobId(name, jobId) {
|
|
45
|
+
return `${name}:${jobId}`;
|
|
46
|
+
}
|
|
47
|
+
function decodeJobId(id) {
|
|
48
|
+
const at = id.lastIndexOf(':');
|
|
49
|
+
if (at <= 0)
|
|
50
|
+
return null;
|
|
51
|
+
const jobId = id.slice(at + 1);
|
|
52
|
+
if (!UUID.test(jobId))
|
|
53
|
+
return null;
|
|
54
|
+
return { name: id.slice(0, at), jobId };
|
|
55
|
+
}
|
|
56
|
+
function toJobStatus(state) {
|
|
57
|
+
switch (state) {
|
|
58
|
+
case 'completed':
|
|
59
|
+
return 'completed';
|
|
60
|
+
case 'failed':
|
|
61
|
+
return 'failed';
|
|
62
|
+
case 'active':
|
|
63
|
+
return 'running';
|
|
64
|
+
// waiting, waiting-children, delayed, prioritized: all "not started yet".
|
|
65
|
+
default:
|
|
66
|
+
return 'pending';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The optimal job queue: Redis, driven through bullmq.
|
|
71
|
+
*
|
|
72
|
+
* Jobs are fetched by hand rather than by a bullmq `Worker` loop, because
|
|
73
|
+
* `QueueDriver.tick()` is the contract both drivers answer to — a site on shared
|
|
74
|
+
* hosting drives it from cron, and the same call has to mean the same thing
|
|
75
|
+
* here. bullmq documents this as the manual-fetching pattern, and it keeps the
|
|
76
|
+
* atomic claim (the L0 acceptance criterion) inside Redis where it belongs.
|
|
77
|
+
*/
|
|
78
|
+
export function createBullmqQueue(options) {
|
|
79
|
+
const { module, url } = options;
|
|
80
|
+
const now = options.now ?? Date.now;
|
|
81
|
+
const prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
82
|
+
const leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;
|
|
83
|
+
const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
84
|
+
const backoffMs = options.backoffMs ?? DEFAULT_BACKOFF_MS;
|
|
85
|
+
const stalledInterval = options.stalledIntervalMs ?? DEFAULT_STALLED_INTERVAL_MS;
|
|
86
|
+
const logger = (options.logger ?? createLogger()).child({ component: 'queue' });
|
|
87
|
+
const connection = { url };
|
|
88
|
+
const handlers = new Map();
|
|
89
|
+
const queues = new Map();
|
|
90
|
+
const workers = new Map();
|
|
91
|
+
/** Cancelled jobs, by id. bullmq has no cancelled state, so we keep our own. */
|
|
92
|
+
const tombstoneKey = `${prefix}:cancelled`;
|
|
93
|
+
function queueFor(name) {
|
|
94
|
+
const existing = queues.get(name);
|
|
95
|
+
if (existing !== undefined)
|
|
96
|
+
return existing;
|
|
97
|
+
const created = new module.Queue(name, { connection, prefix });
|
|
98
|
+
queues.set(name, created);
|
|
99
|
+
return created;
|
|
100
|
+
}
|
|
101
|
+
async function redis() {
|
|
102
|
+
return queueFor(CONTROL_QUEUE).getBackend().client;
|
|
103
|
+
}
|
|
104
|
+
async function workerFor(name) {
|
|
105
|
+
const existing = workers.get(name);
|
|
106
|
+
if (existing !== undefined)
|
|
107
|
+
return existing;
|
|
108
|
+
const created = new module.Worker(name, null, {
|
|
109
|
+
connection,
|
|
110
|
+
prefix,
|
|
111
|
+
autorun: false,
|
|
112
|
+
lockDuration: leaseMs,
|
|
113
|
+
stalledInterval,
|
|
114
|
+
// Nothing renews a lock here: `tick` holds it for the duration of one
|
|
115
|
+
// handler and then reports an outcome. A lock that outlives the process
|
|
116
|
+
// is exactly what the stalled checker is for.
|
|
117
|
+
skipLockRenewal: true,
|
|
118
|
+
});
|
|
119
|
+
workers.set(name, created);
|
|
120
|
+
// Manual fetching does not start the stalled checker, and without it a job
|
|
121
|
+
// whose worker was killed stays "active" forever — no other worker ever
|
|
122
|
+
// gets it back.
|
|
123
|
+
await created.startStalledCheckTimer();
|
|
124
|
+
return created;
|
|
125
|
+
}
|
|
126
|
+
async function runOne(job, token) {
|
|
127
|
+
const handler = handlers.get(job.name);
|
|
128
|
+
const id = encodeJobId(job.name, job.id ?? '');
|
|
129
|
+
const maxAttempts = job.opts.attempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
130
|
+
if (handler === undefined) {
|
|
131
|
+
// Unreachable in practice: a worker only exists for a registered name.
|
|
132
|
+
// Releasing the job is still better than holding a lock we cannot honour.
|
|
133
|
+
await job.moveToFailed(new Error(`No handler for jobs named "${job.name}".`), token, false);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const decoded = {
|
|
137
|
+
id,
|
|
138
|
+
name: job.name,
|
|
139
|
+
payload: job.data.payload,
|
|
140
|
+
// bullmq counts attempts that finished; this one is in flight.
|
|
141
|
+
attempt: job.attemptsMade + 1,
|
|
142
|
+
maxAttempts,
|
|
143
|
+
};
|
|
144
|
+
try {
|
|
145
|
+
await handler(decoded);
|
|
146
|
+
await job.moveToCompleted(null, token, false);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
150
|
+
await job.moveToFailed(failure, token, false);
|
|
151
|
+
const exhausted = decoded.attempt >= maxAttempts;
|
|
152
|
+
logger.warn(exhausted ? 'job failed for good' : 'job failed, will retry', {
|
|
153
|
+
id,
|
|
154
|
+
name: job.name,
|
|
155
|
+
attempt: decoded.attempt,
|
|
156
|
+
maxAttempts,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
enqueue: async (enqueueOptions) => {
|
|
162
|
+
if (enqueueOptions.name.length === 0) {
|
|
163
|
+
throw new CogentaError({
|
|
164
|
+
code: 'QUEUE_FAILED',
|
|
165
|
+
message: 'A job must have a name.',
|
|
166
|
+
hint: 'The name is what routes the job to a handler registered with process().',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const jobId = randomUUID();
|
|
170
|
+
const delay = Math.max(0, (enqueueOptions.runAt ?? now()) - now());
|
|
171
|
+
await queueFor(enqueueOptions.name).add(enqueueOptions.name, { payload: enqueueOptions.payload ?? null }, {
|
|
172
|
+
jobId,
|
|
173
|
+
priority: toBullmqPriority(enqueueOptions.priority ?? 0),
|
|
174
|
+
attempts: enqueueOptions.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
|
|
175
|
+
backoff: { type: 'exponential', delay: backoffMs },
|
|
176
|
+
...(delay > 0 ? { delay } : {}),
|
|
177
|
+
});
|
|
178
|
+
return encodeJobId(enqueueOptions.name, jobId);
|
|
179
|
+
},
|
|
180
|
+
process: (name, handler) => {
|
|
181
|
+
if (handlers.has(name)) {
|
|
182
|
+
throw new CogentaError({
|
|
183
|
+
code: 'QUEUE_FAILED',
|
|
184
|
+
message: `A handler is already registered for jobs named "${name}".`,
|
|
185
|
+
hint: 'One handler per job name. Register a different name, or compose inside the existing handler.',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
handlers.set(name, handler);
|
|
189
|
+
},
|
|
190
|
+
tick: async () => {
|
|
191
|
+
let handled = 0;
|
|
192
|
+
// Only names this process registered are fetched, so two workers with
|
|
193
|
+
// different handlers each take their own work instead of locking jobs
|
|
194
|
+
// they would have to put back.
|
|
195
|
+
for (const name of handlers.keys()) {
|
|
196
|
+
const worker = await workerFor(name);
|
|
197
|
+
while (handled < batchSize) {
|
|
198
|
+
// A fresh token per job: it is the lock's owner, and it has to be
|
|
199
|
+
// presented again to report the outcome.
|
|
200
|
+
const token = randomUUID();
|
|
201
|
+
const job = await worker.getNextJob(token, { block: false });
|
|
202
|
+
if (job === undefined || job === null)
|
|
203
|
+
break;
|
|
204
|
+
handled += 1;
|
|
205
|
+
await runOne(job, token);
|
|
206
|
+
}
|
|
207
|
+
if (handled >= batchSize)
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
return handled;
|
|
211
|
+
},
|
|
212
|
+
cancel: async (id) => {
|
|
213
|
+
const parsed = decodeJobId(id);
|
|
214
|
+
if (parsed === null)
|
|
215
|
+
return;
|
|
216
|
+
const job = await queueFor(parsed.name).getJob(parsed.jobId);
|
|
217
|
+
if (job === undefined)
|
|
218
|
+
return;
|
|
219
|
+
const state = toJobStatus(await job.getState());
|
|
220
|
+
// A running job is left alone: cancelling it here would only lose track of
|
|
221
|
+
// work that is still happening in another process.
|
|
222
|
+
if (state !== 'pending')
|
|
223
|
+
return;
|
|
224
|
+
const tombstone = {
|
|
225
|
+
id,
|
|
226
|
+
name: job.name,
|
|
227
|
+
status: 'cancelled',
|
|
228
|
+
attempt: job.attemptsMade,
|
|
229
|
+
maxAttempts: job.opts.attempts ?? DEFAULT_MAX_ATTEMPTS,
|
|
230
|
+
runAt: job.timestamp + job.delay,
|
|
231
|
+
lastError: undefined,
|
|
232
|
+
};
|
|
233
|
+
// Written before the removal: a crash between the two leaves a job that
|
|
234
|
+
// reads as cancelled and is gone, never one that reads as alive and is not.
|
|
235
|
+
const client = await redis();
|
|
236
|
+
await client.hset(tombstoneKey, id, JSON.stringify(tombstone));
|
|
237
|
+
await job.remove();
|
|
238
|
+
},
|
|
239
|
+
status: async (id) => {
|
|
240
|
+
const parsed = decodeJobId(id);
|
|
241
|
+
if (parsed === null)
|
|
242
|
+
return null;
|
|
243
|
+
const client = await redis();
|
|
244
|
+
const tombstone = await client.hget(tombstoneKey, id);
|
|
245
|
+
if (tombstone !== null)
|
|
246
|
+
return JSON.parse(tombstone);
|
|
247
|
+
const job = await queueFor(parsed.name).getJob(parsed.jobId);
|
|
248
|
+
if (job === undefined)
|
|
249
|
+
return null;
|
|
250
|
+
const failedReason = job.failedReason;
|
|
251
|
+
return {
|
|
252
|
+
id,
|
|
253
|
+
name: job.name,
|
|
254
|
+
status: toJobStatus(await job.getState()),
|
|
255
|
+
attempt: job.attemptsMade,
|
|
256
|
+
maxAttempts: job.opts.attempts ?? DEFAULT_MAX_ATTEMPTS,
|
|
257
|
+
runAt: job.timestamp + job.delay,
|
|
258
|
+
lastError: failedReason === undefined || failedReason === '' ? undefined : failedReason,
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
close: async () => {
|
|
262
|
+
handlers.clear();
|
|
263
|
+
// Workers first: each holds a blocking connection, and closing the queues
|
|
264
|
+
// underneath it turns a clean shutdown into a pile of connection errors.
|
|
265
|
+
for (const worker of workers.values())
|
|
266
|
+
await worker.close();
|
|
267
|
+
workers.clear();
|
|
268
|
+
for (const queue of queues.values())
|
|
269
|
+
await queue.close();
|
|
270
|
+
queues.clear();
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/** Redis URLs carry a password. Nothing derived from one reaches an operator. */
|
|
275
|
+
function withoutUrl(message) {
|
|
276
|
+
return message.replace(/rediss?:\/\/\S+/gi, '<redis url>');
|
|
277
|
+
}
|
|
278
|
+
async function withDeadline(work, ms) {
|
|
279
|
+
let timer;
|
|
280
|
+
try {
|
|
281
|
+
return await Promise.race([
|
|
282
|
+
work,
|
|
283
|
+
new Promise((_, reject) => {
|
|
284
|
+
timer = setTimeout(() => reject(new Error('Redis did not answer in time.')), ms);
|
|
285
|
+
}),
|
|
286
|
+
]);
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
if (timer !== undefined)
|
|
290
|
+
clearTimeout(timer);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
export function bullmqQueueDriver(options = {}) {
|
|
294
|
+
const prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
295
|
+
let instance;
|
|
296
|
+
let probe;
|
|
297
|
+
async function loadOrThrow() {
|
|
298
|
+
const module = await loadBullmqModule();
|
|
299
|
+
if (module === null) {
|
|
300
|
+
throw new CogentaError({
|
|
301
|
+
code: 'DRIVER_INIT_FAILED',
|
|
302
|
+
message: 'The Redis queue driver needs the "bullmq" package.',
|
|
303
|
+
hint: 'Run `pnpm add bullmq`, or leave queue.driver unset to use the database queue.',
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return module;
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
name: 'bullmq',
|
|
310
|
+
tier: 'optimal',
|
|
311
|
+
// Does Redis actually answer? Not "is a URL configured?" — the difference is
|
|
312
|
+
// what turns a graceful fallback into a startup crash.
|
|
313
|
+
available: async (config) => {
|
|
314
|
+
if (config.url === undefined)
|
|
315
|
+
return false;
|
|
316
|
+
const module = await loadBullmqModule();
|
|
317
|
+
if (module === null)
|
|
318
|
+
return false;
|
|
319
|
+
// `retryStrategy` returning null tells ioredis to give up instead of
|
|
320
|
+
// reconnecting forever, which is what makes an absent Redis answer "no"
|
|
321
|
+
// rather than hang. The working connection keeps the default.
|
|
322
|
+
const attempt = new module.Queue(CONTROL_QUEUE, {
|
|
323
|
+
connection: { url: config.url, retryStrategy: () => null },
|
|
324
|
+
prefix,
|
|
325
|
+
});
|
|
326
|
+
try {
|
|
327
|
+
const client = await withDeadline(attempt.getBackend().client, PROBE_TIMEOUT_MS);
|
|
328
|
+
await withDeadline(client.ping(), PROBE_TIMEOUT_MS);
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
await attempt.close().catch(() => undefined);
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
init: async (config) => {
|
|
339
|
+
const module = await loadOrThrow();
|
|
340
|
+
const { url } = config;
|
|
341
|
+
if (url === undefined) {
|
|
342
|
+
throw new CogentaError({
|
|
343
|
+
code: 'DRIVER_INIT_FAILED',
|
|
344
|
+
message: 'The Redis queue driver needs a connection URL.',
|
|
345
|
+
hint: 'Set queue.url (or REDIS_URL), or leave queue.driver unset to use the database queue.',
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
probe ??= new module.Queue(CONTROL_QUEUE, { connection: { url }, prefix });
|
|
349
|
+
instance ??= createBullmqQueue({ ...options, module, url });
|
|
350
|
+
return instance;
|
|
351
|
+
},
|
|
352
|
+
dispose: async () => {
|
|
353
|
+
try {
|
|
354
|
+
await instance?.close();
|
|
355
|
+
await probe?.close();
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
// Shutdown must not fail because the connection already dropped.
|
|
359
|
+
}
|
|
360
|
+
instance = undefined;
|
|
361
|
+
probe = undefined;
|
|
362
|
+
},
|
|
363
|
+
health: async () => {
|
|
364
|
+
if (probe === undefined) {
|
|
365
|
+
return { status: 'down', driver: 'bullmq', tier: 'optimal', message: 'Not connected.' };
|
|
366
|
+
}
|
|
367
|
+
const startedAt = Date.now();
|
|
368
|
+
try {
|
|
369
|
+
const client = await withDeadline(probe.getBackend().client, PROBE_TIMEOUT_MS);
|
|
370
|
+
await withDeadline(client.ping(), PROBE_TIMEOUT_MS);
|
|
371
|
+
return {
|
|
372
|
+
status: 'ok',
|
|
373
|
+
driver: 'bullmq',
|
|
374
|
+
tier: 'optimal',
|
|
375
|
+
latencyMs: Date.now() - startedAt,
|
|
376
|
+
// The URL is not reported: it routinely carries a password.
|
|
377
|
+
message: 'Connected.',
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
return {
|
|
382
|
+
status: 'down',
|
|
383
|
+
driver: 'bullmq',
|
|
384
|
+
tier: 'optimal',
|
|
385
|
+
message: withoutUrl(error instanceof Error ? error.message : 'Redis did not answer.'),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
//# sourceMappingURL=bullmq.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bullmq.js","sourceRoot":"","sources":["../../src/queue/bullmq.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type DatabaseHandle } from '../db/index.js';
|
|
2
|
+
import { type Logger } from '../logger/index.js';
|
|
3
|
+
import type { QueueDriver, QueueDriverOptions } from './types.js';
|
|
4
|
+
export interface DatabaseQueueOptions extends QueueDriverOptions {
|
|
5
|
+
readonly db: DatabaseHandle;
|
|
6
|
+
readonly logger?: Logger;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The degraded job queue: a SQL table, drained by whoever calls `tick()` — a
|
|
10
|
+
* system cron on shared hosting, a loop in a long-lived process elsewhere.
|
|
11
|
+
*
|
|
12
|
+
* Its whole reason to exist is rule R1: a Cogenta install must work with no
|
|
13
|
+
* Redis and no persistent worker. That makes correctness under concurrency
|
|
14
|
+
* non-negotiable, because this is the driver most sites will actually run.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createDatabaseQueue(options: DatabaseQueueOptions): QueueDriver;
|
|
17
|
+
//# sourceMappingURL=database.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/queue/database.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,cAAc,EAMpB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAC9D,OAAO,KAAK,EAMV,WAAW,EACX,kBAAkB,EACnB,MAAM,YAAY,CAAA;AAOnB,MAAM,WAAW,oBAAqB,SAAQ,kBAAkB;IAC9D,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAA;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAkCD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,WAAW,CAiP9E"}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { identifier, limit, sql, unsafeRaw, } from '../db/index.js';
|
|
4
|
+
import { CogentaError } from '../errors/index.js';
|
|
5
|
+
import { createLogger } from '../logger/index.js';
|
|
6
|
+
const TABLE = 'cogenta_jobs';
|
|
7
|
+
const DEFAULT_LEASE_MS = 5 * 60 * 1000;
|
|
8
|
+
const DEFAULT_BATCH_SIZE = 10;
|
|
9
|
+
const MAX_BACKOFF_MS = 60_000;
|
|
10
|
+
/** Exponential, capped. A failing job must not retry in a tight loop. */
|
|
11
|
+
function backoffMs(attempt) {
|
|
12
|
+
return Math.min(2 ** attempt * 1000, MAX_BACKOFF_MS);
|
|
13
|
+
}
|
|
14
|
+
const CLAIM_ATTEMPTS = 5;
|
|
15
|
+
/**
|
|
16
|
+
* Deadlocks and serialisation failures are normal under contention, not bugs.
|
|
17
|
+
*
|
|
18
|
+
* InnoDB detects a cycle between two claim transactions and kills one of them;
|
|
19
|
+
* Postgres reports a serialisation failure for the same situation. Both
|
|
20
|
+
* databases document the remedy as "retry the transaction", so that is what the
|
|
21
|
+
* claim does — silently, because a worker losing a race is not an incident.
|
|
22
|
+
*/
|
|
23
|
+
function isRetryableConflict(error) {
|
|
24
|
+
const code = error.cause?.code ?? '';
|
|
25
|
+
if (code === 'ER_LOCK_DEADLOCK' || code === '40001' || code === '40P01')
|
|
26
|
+
return true;
|
|
27
|
+
return /deadlock|could not serialize/i.test(error instanceof Error ? error.message : String(error));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The degraded job queue: a SQL table, drained by whoever calls `tick()` — a
|
|
31
|
+
* system cron on shared hosting, a loop in a long-lived process elsewhere.
|
|
32
|
+
*
|
|
33
|
+
* Its whole reason to exist is rule R1: a Cogenta install must work with no
|
|
34
|
+
* Redis and no persistent worker. That makes correctness under concurrency
|
|
35
|
+
* non-negotiable, because this is the driver most sites will actually run.
|
|
36
|
+
*/
|
|
37
|
+
export function createDatabaseQueue(options) {
|
|
38
|
+
const { db } = options;
|
|
39
|
+
const now = options.now ?? Date.now;
|
|
40
|
+
const leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;
|
|
41
|
+
const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
42
|
+
const logger = (options.logger ?? createLogger()).child({ component: 'queue' });
|
|
43
|
+
const worker = `${process.pid}-${randomUUID().slice(0, 8)}`;
|
|
44
|
+
const handlers = new Map();
|
|
45
|
+
const table = identifier(TABLE, db.dialect);
|
|
46
|
+
let ready = false;
|
|
47
|
+
// Postgres, MySQL 8 and MariaDB 10.6+ can skip rows another transaction has
|
|
48
|
+
// locked. SQLite cannot, and does not need to: BEGIN IMMEDIATE serialises
|
|
49
|
+
// writers, so only one claim runs at a time.
|
|
50
|
+
const skipLocked = db.dialect === 'sqlite' ? unsafeRaw('') : unsafeRaw('for update skip locked');
|
|
51
|
+
async function ensureTable() {
|
|
52
|
+
if (ready)
|
|
53
|
+
return;
|
|
54
|
+
await db.query(sql `
|
|
55
|
+
create table if not exists ${table} (
|
|
56
|
+
id varchar(64) not null primary key,
|
|
57
|
+
name varchar(255) not null,
|
|
58
|
+
payload text,
|
|
59
|
+
status varchar(16) not null,
|
|
60
|
+
attempt integer not null,
|
|
61
|
+
max_attempts integer not null,
|
|
62
|
+
priority integer not null,
|
|
63
|
+
run_at bigint not null,
|
|
64
|
+
locked_until bigint,
|
|
65
|
+
locked_by varchar(255),
|
|
66
|
+
last_error text
|
|
67
|
+
)`);
|
|
68
|
+
// The index the claim query lives on. Without it, every tick scans the
|
|
69
|
+
// whole table, which is fine at ten jobs and fatal at a hundred thousand.
|
|
70
|
+
await db
|
|
71
|
+
.query(sql `create index ${identifier('cogenta_jobs_claim', db.dialect)}
|
|
72
|
+
on ${table} (status, run_at)`)
|
|
73
|
+
.catch(() => undefined); // already there
|
|
74
|
+
ready = true;
|
|
75
|
+
}
|
|
76
|
+
async function claim(names) {
|
|
77
|
+
for (let attempt = 1;; attempt += 1) {
|
|
78
|
+
try {
|
|
79
|
+
return await claimOnce(names);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (attempt >= CLAIM_ATTEMPTS || !isRetryableConflict(error))
|
|
83
|
+
throw error;
|
|
84
|
+
logger.debug('claim lost a race, retrying', { attempt });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function claimOnce(names) {
|
|
89
|
+
if (names.length === 0)
|
|
90
|
+
return [];
|
|
91
|
+
const at = now();
|
|
92
|
+
const nameList = names.map((name) => sql `${name}`);
|
|
93
|
+
const inList = nameList.reduce((left, right) => sql `${left}, ${right}`);
|
|
94
|
+
return db.transaction(async (tx) => {
|
|
95
|
+
const found = await tx.query(sql `
|
|
96
|
+
select id, name, payload, attempt, max_attempts
|
|
97
|
+
from ${table}
|
|
98
|
+
where status = ${'pending'}
|
|
99
|
+
and run_at <= ${at}
|
|
100
|
+
and name in (${inList})
|
|
101
|
+
order by priority desc, run_at asc
|
|
102
|
+
limit ${limit(batchSize)}
|
|
103
|
+
${skipLocked}`);
|
|
104
|
+
const claimed = [];
|
|
105
|
+
for (const row of found.rows) {
|
|
106
|
+
const attempt = Number(row.attempt) + 1;
|
|
107
|
+
const updated = await tx.query(sql `
|
|
108
|
+
update ${table}
|
|
109
|
+
set status = ${'running'},
|
|
110
|
+
attempt = ${attempt},
|
|
111
|
+
locked_by = ${worker},
|
|
112
|
+
locked_until = ${at + leaseMs}
|
|
113
|
+
where id = ${row.id} and status = ${'pending'}`);
|
|
114
|
+
// On SQLite the write lock makes this always true. Elsewhere it is the
|
|
115
|
+
// second gate: if another worker got there first, we skip the job
|
|
116
|
+
// rather than run it twice.
|
|
117
|
+
if (updated.rowsAffected === 0)
|
|
118
|
+
continue;
|
|
119
|
+
claimed.push({
|
|
120
|
+
id: row.id,
|
|
121
|
+
name: row.name,
|
|
122
|
+
payload: row.payload === null ? null : JSON.parse(row.payload),
|
|
123
|
+
attempt,
|
|
124
|
+
maxAttempts: Number(row.max_attempts),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return claimed;
|
|
128
|
+
}, { immediate: true });
|
|
129
|
+
}
|
|
130
|
+
/** Returns an expired lease to the pool: the worker holding it is gone. */
|
|
131
|
+
async function reclaimExpired() {
|
|
132
|
+
await db.query(sql `
|
|
133
|
+
update ${table}
|
|
134
|
+
set status = ${'pending'}, locked_by = ${null}, locked_until = ${null}
|
|
135
|
+
where status = ${'running'} and locked_until is not null and locked_until <= ${now()}`);
|
|
136
|
+
}
|
|
137
|
+
async function succeed(job) {
|
|
138
|
+
await db.query(sql `
|
|
139
|
+
update ${table}
|
|
140
|
+
set status = ${'completed'}, locked_by = ${null}, locked_until = ${null}
|
|
141
|
+
where id = ${job.id}`);
|
|
142
|
+
}
|
|
143
|
+
async function fail(job, error) {
|
|
144
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
145
|
+
const exhausted = job.attempt >= job.maxAttempts;
|
|
146
|
+
await db.query(sql `
|
|
147
|
+
update ${table}
|
|
148
|
+
set status = ${exhausted ? 'failed' : 'pending'},
|
|
149
|
+
run_at = ${exhausted ? now() : now() + backoffMs(job.attempt)},
|
|
150
|
+
locked_by = ${null},
|
|
151
|
+
locked_until = ${null},
|
|
152
|
+
last_error = ${message}
|
|
153
|
+
where id = ${job.id}`);
|
|
154
|
+
logger.warn(exhausted ? 'job failed for good' : 'job failed, will retry', {
|
|
155
|
+
id: job.id,
|
|
156
|
+
name: job.name,
|
|
157
|
+
attempt: job.attempt,
|
|
158
|
+
maxAttempts: job.maxAttempts,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
enqueue: async (enqueueOptions) => {
|
|
163
|
+
await ensureTable();
|
|
164
|
+
if (enqueueOptions.name.length === 0) {
|
|
165
|
+
throw new CogentaError({
|
|
166
|
+
code: 'QUEUE_FAILED',
|
|
167
|
+
message: 'A job must have a name.',
|
|
168
|
+
hint: 'The name is what routes the job to a handler registered with process().',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const id = randomUUID();
|
|
172
|
+
await db.query(sql `
|
|
173
|
+
insert into ${table} (id, name, payload, status, attempt, max_attempts, priority, run_at)
|
|
174
|
+
values (${id}, ${enqueueOptions.name},
|
|
175
|
+
${JSON.stringify(enqueueOptions.payload ?? null)},
|
|
176
|
+
${'pending'}, ${0}, ${enqueueOptions.maxAttempts ?? 3},
|
|
177
|
+
${enqueueOptions.priority ?? 0}, ${enqueueOptions.runAt ?? now()})`);
|
|
178
|
+
return id;
|
|
179
|
+
},
|
|
180
|
+
process: (name, handler) => {
|
|
181
|
+
if (handlers.has(name)) {
|
|
182
|
+
throw new CogentaError({
|
|
183
|
+
code: 'QUEUE_FAILED',
|
|
184
|
+
message: `A handler is already registered for jobs named "${name}".`,
|
|
185
|
+
hint: 'One handler per job name. Register a different name, or compose inside the existing handler.',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
handlers.set(name, handler);
|
|
189
|
+
},
|
|
190
|
+
tick: async () => {
|
|
191
|
+
await ensureTable();
|
|
192
|
+
await reclaimExpired();
|
|
193
|
+
// Only jobs this process can actually handle are claimed. Two workers with
|
|
194
|
+
// different handlers each take their own work instead of locking jobs they
|
|
195
|
+
// would have to put back.
|
|
196
|
+
const claimed = await claim([...handlers.keys()]);
|
|
197
|
+
for (const job of claimed) {
|
|
198
|
+
const handler = handlers.get(job.name);
|
|
199
|
+
if (handler === undefined)
|
|
200
|
+
continue;
|
|
201
|
+
try {
|
|
202
|
+
await handler(job);
|
|
203
|
+
await succeed(job);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
await fail(job, error);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return claimed.length;
|
|
210
|
+
},
|
|
211
|
+
cancel: async (id) => {
|
|
212
|
+
await ensureTable();
|
|
213
|
+
// A running job is left alone: cancelling it here would only lose track of
|
|
214
|
+
// work that is still happening in another process.
|
|
215
|
+
await db.query(sql `
|
|
216
|
+
update ${table} set status = ${'cancelled'}
|
|
217
|
+
where id = ${id} and status = ${'pending'}`);
|
|
218
|
+
},
|
|
219
|
+
status: async (id) => {
|
|
220
|
+
await ensureTable();
|
|
221
|
+
const result = await db.query(sql `
|
|
222
|
+
select id, name, status, attempt, max_attempts, run_at, last_error
|
|
223
|
+
from ${table} where id = ${id}`);
|
|
224
|
+
const row = result.rows[0];
|
|
225
|
+
if (row === undefined)
|
|
226
|
+
return null;
|
|
227
|
+
return {
|
|
228
|
+
id: row.id,
|
|
229
|
+
name: row.name,
|
|
230
|
+
status: row.status,
|
|
231
|
+
attempt: Number(row.attempt),
|
|
232
|
+
maxAttempts: Number(row.max_attempts),
|
|
233
|
+
runAt: Number(row.run_at),
|
|
234
|
+
lastError: row.last_error ?? undefined,
|
|
235
|
+
};
|
|
236
|
+
},
|
|
237
|
+
close: async () => {
|
|
238
|
+
handlers.clear();
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=database.js.map
|