@cosmicdrift/kumiko-server-runtime 0.305.0 → 0.307.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-server-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.307.0",
|
|
4
4
|
"description": "Production server-boot runtime for Kumiko apps: connections, schema-drift-gate, seeds, lifecycle, graceful shutdown. Symmetric to kumiko-dev-server's runDevApp, without dev/scaffold/codegen tooling.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -80,9 +80,9 @@
|
|
|
80
80
|
}
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
84
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
85
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
83
|
+
"@cosmicdrift/kumiko-bundled-features": "0.307.0",
|
|
84
|
+
"@cosmicdrift/kumiko-framework": "0.307.0",
|
|
85
|
+
"@cosmicdrift/kumiko-headless": "0.307.0",
|
|
86
86
|
"temporal-polyfill": "^0.3.2"
|
|
87
87
|
},
|
|
88
88
|
"publishConfig": {
|
|
@@ -71,6 +71,7 @@ describe("startDevJobRunners", () => {
|
|
|
71
71
|
db,
|
|
72
72
|
context: {},
|
|
73
73
|
redisUrl: testRedis.redisUrl,
|
|
74
|
+
queueNamePrefix: `test-lane-default-${crypto.randomUUID()}`,
|
|
74
75
|
// Construction-only — this job never calls ctx.write/ctx.queryAs, so a
|
|
75
76
|
// dispatcher built against a bare `{}` context is enough to satisfy
|
|
76
77
|
// the (now required) opts.dispatcher without wiring a full stack.
|
|
@@ -152,11 +153,12 @@ describe("startDevJobRunners attaches the dispatcher (kumiko-framework#2553)", (
|
|
|
152
153
|
});
|
|
153
154
|
|
|
154
155
|
test("a dev-run job's ctx.write and ctx.queryAs both succeed", async () => {
|
|
156
|
+
const attachQueueNamePrefix = `test-attach-dispatcher-${crypto.randomUUID()}`;
|
|
155
157
|
const stack = await setupTestStack({
|
|
156
158
|
features: [attachDispatcherFeature, createJobsFeature()],
|
|
157
159
|
// Enqueuer-only — startDevJobRunners below is the sole consumer,
|
|
158
160
|
// mirroring create-kumiko-server.ts.
|
|
159
|
-
jobs: {},
|
|
161
|
+
jobs: { queueNamePrefix: attachQueueNamePrefix },
|
|
160
162
|
});
|
|
161
163
|
await unsafeCreateEntityTable(stack.db, attachNoteEntity);
|
|
162
164
|
|
|
@@ -167,6 +169,7 @@ describe("startDevJobRunners attaches the dispatcher (kumiko-framework#2553)", (
|
|
|
167
169
|
context: stack.context,
|
|
168
170
|
redisUrl: stack.redis.redisUrl,
|
|
169
171
|
dispatcher: stack.dispatcher,
|
|
172
|
+
queueNamePrefix: attachQueueNamePrefix,
|
|
170
173
|
});
|
|
171
174
|
|
|
172
175
|
try {
|
|
@@ -190,3 +193,70 @@ describe("startDevJobRunners attaches the dispatcher (kumiko-framework#2553)", (
|
|
|
190
193
|
}
|
|
191
194
|
}, 15000);
|
|
192
195
|
});
|
|
196
|
+
|
|
197
|
+
// Regression guard for the shared-Redis job-queue-collision bug: two boots
|
|
198
|
+
// (e.g. two parallel dev/e2e servers) that don't set distinct
|
|
199
|
+
// queueNamePrefix values consume from the same BullMQ queue names and steal
|
|
200
|
+
// each other's jobs ("Unknown job: ..."). Both registries below register the
|
|
201
|
+
// SAME feature/job name on purpose — if the queues were shared, runner B
|
|
202
|
+
// would recognize and execute jobs meant for runner A (proving the
|
|
203
|
+
// collision), not just fail to run them. Distinct prefixes must keep every
|
|
204
|
+
// dispatched job on runner A's side.
|
|
205
|
+
describe("startDevJobRunners isolates queues by queueNamePrefix", () => {
|
|
206
|
+
test("two runners with distinct prefixes on the same Redis don't cross-consume", async () => {
|
|
207
|
+
const runsA: string[] = [];
|
|
208
|
+
const runsB: string[] = [];
|
|
209
|
+
|
|
210
|
+
const isoFeatureA = defineFeature("iso", (r) => {
|
|
211
|
+
r.job("mark", { trigger: { manual: true } }, async (payload) => {
|
|
212
|
+
runsA.push((payload as { note: string }).note);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
const isoFeatureB = defineFeature("iso", (r) => {
|
|
216
|
+
r.job("mark", { trigger: { manual: true } }, async (payload) => {
|
|
217
|
+
runsB.push((payload as { note: string }).note);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const registryA = createRegistry([isoFeatureA, createJobsFeature()]);
|
|
222
|
+
const registryB = createRegistry([isoFeatureB, createJobsFeature()]);
|
|
223
|
+
|
|
224
|
+
const runnerA = await startDevJobRunners({
|
|
225
|
+
registry: registryA,
|
|
226
|
+
db,
|
|
227
|
+
context: {},
|
|
228
|
+
redisUrl: testRedis.redisUrl,
|
|
229
|
+
queueNamePrefix: `test-iso-a-${crypto.randomUUID()}`,
|
|
230
|
+
dispatcher: createDispatcher(registryA, {}),
|
|
231
|
+
});
|
|
232
|
+
const runnerB = await startDevJobRunners({
|
|
233
|
+
registry: registryB,
|
|
234
|
+
db,
|
|
235
|
+
context: {},
|
|
236
|
+
redisUrl: testRedis.redisUrl,
|
|
237
|
+
queueNamePrefix: `test-iso-b-${crypto.randomUUID()}`,
|
|
238
|
+
dispatcher: createDispatcher(registryB, {}),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const dispatchA = runnerA.runners[0];
|
|
243
|
+
if (dispatchA === undefined) throw new Error("expected a runner for registry A");
|
|
244
|
+
|
|
245
|
+
const notes = Array.from({ length: 10 }, (_, i) => `from-a-${i}`);
|
|
246
|
+
for (const note of notes) {
|
|
247
|
+
await dispatchA.dispatch("iso:job:mark", { note });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// If the queues were shared, some of these 10 jobs would land on
|
|
251
|
+
// runner B instead — runsA would never reach length 10 and this
|
|
252
|
+
// would time out rather than pass.
|
|
253
|
+
await waitFor(() => {
|
|
254
|
+
expect(runsA).toHaveLength(10);
|
|
255
|
+
});
|
|
256
|
+
expect(runsB).toEqual([]);
|
|
257
|
+
} finally {
|
|
258
|
+
await runnerA.stop();
|
|
259
|
+
await runnerB.stop();
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -20,6 +20,10 @@ export async function startDevJobRunners(opts: {
|
|
|
20
20
|
readonly context: AppContext;
|
|
21
21
|
readonly redisUrl: string;
|
|
22
22
|
readonly dispatcher: Dispatcher;
|
|
23
|
+
// Redis is shared across boots and apps (unlike the ephemeral per-boot
|
|
24
|
+
// test DB); no silent default here so every caller is forced to pick a
|
|
25
|
+
// prefix that isolates its own queues.
|
|
26
|
+
readonly queueNamePrefix: string;
|
|
23
27
|
}): Promise<{ readonly runners: readonly JobRunner[]; readonly stop: () => Promise<void> }> {
|
|
24
28
|
const jobs = [...opts.registry.getAllJobs().values()];
|
|
25
29
|
if (opts.registry.getFeature("jobs") === undefined || jobs.length === 0) {
|
|
@@ -38,6 +42,7 @@ export async function startDevJobRunners(opts: {
|
|
|
38
42
|
registry: opts.registry,
|
|
39
43
|
context: { ...opts.context, db: opts.db },
|
|
40
44
|
redisUrl: opts.redisUrl,
|
|
45
|
+
queueNamePrefix: opts.queueNamePrefix,
|
|
41
46
|
consumerLane: lane,
|
|
42
47
|
...logger,
|
|
43
48
|
});
|