@cosmicdrift/kumiko-framework 0.182.1 → 0.183.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.183.0",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -182,7 +182,7 @@
|
|
|
182
182
|
"./package.json": "./package.json"
|
|
183
183
|
},
|
|
184
184
|
"dependencies": {
|
|
185
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
185
|
+
"@cosmicdrift/kumiko-types": "0.183.0",
|
|
186
186
|
"bullmq": "^5.76.7",
|
|
187
187
|
"bun-types": "^1.3.13",
|
|
188
188
|
"hono": "^4.12.27",
|
|
@@ -198,7 +198,7 @@
|
|
|
198
198
|
"zod": "^4.4.3"
|
|
199
199
|
},
|
|
200
200
|
"devDependencies": {
|
|
201
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
201
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.183.0",
|
|
202
202
|
"bun-types": "^1.3.13",
|
|
203
203
|
"pino-pretty": "^13.1.3"
|
|
204
204
|
},
|
|
@@ -582,6 +582,9 @@ describe("concurrency: replace", () => {
|
|
|
582
582
|
const queue = new Queue(`${queueNamePrefix}-worker`, {
|
|
583
583
|
connection: { host: testRedis.redis.options.host, port: testRedis.redis.options.port },
|
|
584
584
|
});
|
|
585
|
+
// A post-close 'error' here is otherwise unhandled and bun:test
|
|
586
|
+
// attributes it to whichever test runs next (fw#1805).
|
|
587
|
+
queue.on("error", () => {});
|
|
585
588
|
try {
|
|
586
589
|
const waiting = (await queue.getWaiting()).filter(
|
|
587
590
|
(job) => job.name === "test:job:replace-job",
|
package/src/jobs/job-runner.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type TenantId,
|
|
14
14
|
} from "../engine/types";
|
|
15
15
|
import { createFileContext } from "../files/file-handle";
|
|
16
|
+
import { createFallbackLogger } from "../logging";
|
|
16
17
|
import type { Logger } from "../logging/types";
|
|
17
18
|
import {
|
|
18
19
|
emitJobQueueDepth,
|
|
@@ -197,6 +198,7 @@ export function createJobRunner(options: JobRunnerOptions): JobRunner {
|
|
|
197
198
|
// Use the context's tracer when present (observability-provider injected at
|
|
198
199
|
// boot); otherwise noop so dispatch/handleJob stay zero-cost without config.
|
|
199
200
|
const tracer: Tracer = context.tracer ?? getFallbackTracer();
|
|
201
|
+
const errorLogger = createFallbackLogger("job-runner", context.log);
|
|
200
202
|
|
|
201
203
|
const allJobs = registry.getAllJobs();
|
|
202
204
|
|
|
@@ -218,6 +220,13 @@ export function createJobRunner(options: JobRunnerOptions): JobRunner {
|
|
|
218
220
|
let sequentialLock: DistributedLock | null = null;
|
|
219
221
|
if (hasSequential) {
|
|
220
222
|
lockRedis = new Redis(redisOpts);
|
|
223
|
+
// Without a listener, a post-close 'error' (e.g. a teardown-race
|
|
224
|
+
// "Connection is closed") is unhandled and crashes the process — in
|
|
225
|
+
// bun:test it gets attributed to whichever test happens to run next
|
|
226
|
+
// (fw#1805).
|
|
227
|
+
lockRedis.on("error", (err) =>
|
|
228
|
+
errorLogger.error("lock redis connection error", { error: err.message }),
|
|
229
|
+
);
|
|
221
230
|
const lockScope = consumerLane ?? "enqueue";
|
|
222
231
|
sequentialLock = createDistributedLock(lockRedis, `${RedisKeys.lock}seq:${lockScope}:`);
|
|
223
232
|
}
|
|
@@ -239,6 +248,13 @@ export function createJobRunner(options: JobRunnerOptions): JobRunner {
|
|
|
239
248
|
api: new Queue(queueNameFor(queueNamePrefix, "api"), { connection: redisOpts }),
|
|
240
249
|
worker: new Queue(queueNameFor(queueNamePrefix, "worker"), { connection: redisOpts }),
|
|
241
250
|
};
|
|
251
|
+
// Same unhandled-'error'-crash hazard as lockRedis above, just via
|
|
252
|
+
// BullMQ's internal ioredis client (fw#1805).
|
|
253
|
+
for (const queue of Object.values(queues)) {
|
|
254
|
+
queue.on("error", (err) =>
|
|
255
|
+
errorLogger.error("queue redis connection error", { error: err.message }),
|
|
256
|
+
);
|
|
257
|
+
}
|
|
242
258
|
let worker: Worker | null = null;
|
|
243
259
|
let queueDepthTimer: ReturnType<typeof setInterval> | null = null;
|
|
244
260
|
// Forward reference to the runner's own API, exposed on the job-handler ctx
|
|
@@ -488,6 +504,17 @@ export function createJobRunner(options: JobRunnerOptions): JobRunner {
|
|
|
488
504
|
connection: redisOpts,
|
|
489
505
|
concurrency: 5,
|
|
490
506
|
});
|
|
507
|
+
worker.on("error", (err) =>
|
|
508
|
+
errorLogger.error("worker redis connection error", { error: err.message }),
|
|
509
|
+
);
|
|
510
|
+
// A caller that calls stop() right after start() otherwise races the
|
|
511
|
+
// still-settling blocking connection: it rejects in-flight commands
|
|
512
|
+
// via ioredis's flushQueue() during close(), which isn't a listenable
|
|
513
|
+
// 'error' event — the only fix is to not return until both of the
|
|
514
|
+
// worker's connections (main + blocking) are ready (fw#1805). This
|
|
515
|
+
// mirrors the wait BullMQ already does internally for
|
|
516
|
+
// upsertJobScheduler()/add() below when the lane has a cron/boot job.
|
|
517
|
+
await worker.waitUntilReady();
|
|
491
518
|
|
|
492
519
|
// Only schedule cron + boot for jobs that belong to this lane. Jobs
|
|
493
520
|
// assigned to the other lane get their cron/boot wiring from the
|