@cosmicdrift/kumiko-framework 0.324.0 → 0.325.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.
|
|
3
|
+
"version": "0.325.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>",
|
|
@@ -202,8 +202,8 @@
|
|
|
202
202
|
"./package.json": "./package.json"
|
|
203
203
|
},
|
|
204
204
|
"dependencies": {
|
|
205
|
-
"@cosmicdrift/kumiko-http": "0.
|
|
206
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
205
|
+
"@cosmicdrift/kumiko-http": "0.325.1",
|
|
206
|
+
"@cosmicdrift/kumiko-types": "0.325.1",
|
|
207
207
|
"bullmq": "^5.76.7",
|
|
208
208
|
"bun-types": "^1.3.13",
|
|
209
209
|
"hono": "^4.13.1",
|
|
@@ -219,7 +219,7 @@
|
|
|
219
219
|
"zod": "^4.4.3"
|
|
220
220
|
},
|
|
221
221
|
"devDependencies": {
|
|
222
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
222
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.325.1",
|
|
223
223
|
"bun-types": "^1.3.13",
|
|
224
224
|
"pino-pretty": "^13.1.3"
|
|
225
225
|
},
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.325.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "JobContext exposes ctx.attempt and ctx.finalAttempt",
|
|
6
|
+
"detail": "Job handlers can read the 1-based run number and whether the current run is\nthe last one BullMQ will attempt, so a handler can defer writing a\nbusiness-facing failure state until the final attempt instead of writing\nit on every retry.\n\nMigration note: hand-built `JobContext` objects (test doubles, boot\nseeders) must add `attempt` and `finalAttempt` (e.g. `attempt: 1,\nfinalAttempt: true`)."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.324.0",
|
|
4
10
|
"type": "breaking",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { defineFeature } from "../../engine";
|
|
4
|
+
import { setupTestStack, type TestStack, TestUsers } from "../../stack";
|
|
5
|
+
|
|
6
|
+
const retryRuns: [number, boolean][] = [];
|
|
7
|
+
const singleRuns: [number, boolean][] = [];
|
|
8
|
+
|
|
9
|
+
const attemptFixtureFeature = defineFeature("attemptfixture", (r) => {
|
|
10
|
+
r.writeHandler(
|
|
11
|
+
"trigger-retry",
|
|
12
|
+
z.object({}),
|
|
13
|
+
async () => ({ isSuccess: true as const, data: {} }),
|
|
14
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
15
|
+
);
|
|
16
|
+
r.job(
|
|
17
|
+
"retry",
|
|
18
|
+
{ trigger: { on: "attemptfixture:write:trigger-retry" }, retries: 2 },
|
|
19
|
+
async (_payload, ctx) => {
|
|
20
|
+
retryRuns.push([ctx.attempt, ctx.finalAttempt]);
|
|
21
|
+
if (ctx.attempt < 3) throw new Error(`fails on attempt ${ctx.attempt}`);
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
r.writeHandler(
|
|
26
|
+
"trigger-single",
|
|
27
|
+
z.object({}),
|
|
28
|
+
async () => ({ isSuccess: true as const, data: {} }),
|
|
29
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
30
|
+
);
|
|
31
|
+
r.job(
|
|
32
|
+
"single",
|
|
33
|
+
{ trigger: { on: "attemptfixture:write:trigger-single" } },
|
|
34
|
+
async (_payload, ctx) => {
|
|
35
|
+
singleRuns.push([ctx.attempt, ctx.finalAttempt]);
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
let stack: TestStack;
|
|
41
|
+
|
|
42
|
+
beforeAll(async () => {
|
|
43
|
+
stack = await setupTestStack({
|
|
44
|
+
features: [attemptFixtureFeature],
|
|
45
|
+
jobs: { consumerLane: "worker" },
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterAll(async () => {
|
|
50
|
+
await stack.cleanup();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
retryRuns.length = 0;
|
|
55
|
+
singleRuns.length = 0;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("JobContext.attempt / finalAttempt", () => {
|
|
59
|
+
test("increments per retry and marks the last configured attempt final", async () => {
|
|
60
|
+
await stack.http.writeOk("attemptfixture:write:trigger-retry", {}, TestUsers.admin);
|
|
61
|
+
|
|
62
|
+
await stack.drainJobs();
|
|
63
|
+
expect(retryRuns).toEqual([
|
|
64
|
+
[1, false],
|
|
65
|
+
[2, false],
|
|
66
|
+
[3, true],
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("a job without retries starts at attempt 1 and is already final", async () => {
|
|
71
|
+
await stack.http.writeOk("attemptfixture:write:trigger-single", {}, TestUsers.admin);
|
|
72
|
+
|
|
73
|
+
await stack.drainJobs();
|
|
74
|
+
expect(singleRuns).toEqual([[1, true]]);
|
|
75
|
+
});
|
|
76
|
+
});
|
package/src/jobs/job-runner.ts
CHANGED
|
@@ -745,10 +745,11 @@ export function createJobRunner(options: JobRunnerOptions): JobRunner {
|
|
|
745
745
|
// retry number, so audit queries distinguish fresh from retry runs
|
|
746
746
|
// without peeking at BullMQ internals.
|
|
747
747
|
const rawData = bullJob.data as DbRow;
|
|
748
|
+
const attempt = bullJob.attemptsMade + 1;
|
|
748
749
|
const meta: JobMeta = {
|
|
749
750
|
triggeredById: rawData["_triggeredById"] as string | undefined, // @cast-boundary dynamic-key
|
|
750
751
|
payload: rawData["_payload"] as string | undefined, // @cast-boundary dynamic-key
|
|
751
|
-
attempt
|
|
752
|
+
attempt,
|
|
752
753
|
};
|
|
753
754
|
|
|
754
755
|
// Build handler payload (without internal meta fields)
|
|
@@ -773,7 +774,7 @@ export function createJobRunner(options: JobRunnerOptions): JobRunner {
|
|
|
773
774
|
: null;
|
|
774
775
|
// BullMQ stops retrying once attemptsMade reaches the configured attempts
|
|
775
776
|
// (`retries + 1`), so this is the attempt whose failure is final.
|
|
776
|
-
const finalAttempt =
|
|
777
|
+
const finalAttempt = attempt >= (jobDef.retries ?? 0) + 1;
|
|
777
778
|
const outcomeMeta = (messageKey: string | null): JobOutcomeMeta => ({
|
|
778
779
|
tenantId,
|
|
779
780
|
finalAttempt,
|
|
@@ -898,6 +899,8 @@ export function createJobRunner(options: JobRunnerOptions): JobRunner {
|
|
|
898
899
|
...(selfRunner !== undefined && { jobRunner: selfRunner }),
|
|
899
900
|
systemUser: jobSystemUser,
|
|
900
901
|
triggeredBy: triggeredById !== null ? { id: triggeredById, tenantId } : null,
|
|
902
|
+
attempt,
|
|
903
|
+
finalAttempt,
|
|
901
904
|
log: createJobLogger(logs),
|
|
902
905
|
...(triggerName !== undefined && { triggerName }),
|
|
903
906
|
...(triggerEvent !== undefined && { triggerEvent }),
|
|
@@ -1003,7 +1006,7 @@ export function createJobRunner(options: JobRunnerOptions): JobRunner {
|
|
|
1003
1006
|
attributes: {
|
|
1004
1007
|
"job.name": jobName,
|
|
1005
1008
|
"job.id": jobId,
|
|
1006
|
-
"job.attempt":
|
|
1009
|
+
"job.attempt": attempt,
|
|
1007
1010
|
"kumiko.tenant_id": tenantId,
|
|
1008
1011
|
// Lane-routing attributes (Welle 2.6). `run_in` is the job's
|
|
1009
1012
|
// declared lane (explicit or default-"worker"); `consumer_lane`
|