@cosmicdrift/kumiko-framework 0.182.1 → 0.183.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.182.1",
3
+ "version": "0.183.1",
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.182.1",
185
+ "@cosmicdrift/kumiko-types": "0.183.1",
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.182.1",
201
+ "@cosmicdrift/kumiko-dispatcher-live": "0.183.1",
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",
@@ -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