@redflow/client 0.0.2 → 0.0.4
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/INTERNALS.md +238 -0
- package/README.md +34 -3
- package/package.json +1 -1
- package/src/client.ts +23 -20
- package/src/types.ts +7 -1
- package/src/worker.ts +102 -21
- package/tests/bugfixes.test.ts +11 -11
- package/tests/fixtures/worker-crash.ts +1 -0
- package/tests/fixtures/worker-recover.ts +1 -0
- package/tests/redflow.e2e.test.ts +182 -72
|
@@ -52,7 +52,7 @@ test("manual run: succeeds and records steps", async () => {
|
|
|
52
52
|
},
|
|
53
53
|
);
|
|
54
54
|
|
|
55
|
-
const worker = await startWorker({
|
|
55
|
+
const worker = await startWorker({ app: "test-app",
|
|
56
56
|
redis,
|
|
57
57
|
prefix,
|
|
58
58
|
queues: ["q1"],
|
|
@@ -82,7 +82,7 @@ test("runAt: scheduled -> promoted -> executed", async () => {
|
|
|
82
82
|
return { ok: true };
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
const worker = await startWorker({
|
|
85
|
+
const worker = await startWorker({ app: "test-app",
|
|
86
86
|
redis,
|
|
87
87
|
prefix,
|
|
88
88
|
queues: ["q2"],
|
|
@@ -120,7 +120,7 @@ test("runAt: cleared after retry exhaustion final failure", async () => {
|
|
|
120
120
|
},
|
|
121
121
|
);
|
|
122
122
|
|
|
123
|
-
const worker = await startWorker({
|
|
123
|
+
const worker = await startWorker({ app: "test-app",
|
|
124
124
|
redis,
|
|
125
125
|
prefix,
|
|
126
126
|
queues: ["q2_retry_fail"],
|
|
@@ -153,7 +153,7 @@ test("NonRetriableError: skips retries and fails immediately on attempt 1", asyn
|
|
|
153
153
|
},
|
|
154
154
|
);
|
|
155
155
|
|
|
156
|
-
const worker = await startWorker({
|
|
156
|
+
const worker = await startWorker({ app: "test-app",
|
|
157
157
|
redis,
|
|
158
158
|
prefix,
|
|
159
159
|
queues: ["q_non_retriable"],
|
|
@@ -196,7 +196,7 @@ test("onFailure: called after retry exhaustion with correct context", async () =
|
|
|
196
196
|
},
|
|
197
197
|
);
|
|
198
198
|
|
|
199
|
-
const worker = await startWorker({
|
|
199
|
+
const worker = await startWorker({ app: "test-app",
|
|
200
200
|
redis,
|
|
201
201
|
prefix,
|
|
202
202
|
queues: ["q_on_failure_1"],
|
|
@@ -239,7 +239,7 @@ test("onFailure: called immediately with NonRetriableError", async () => {
|
|
|
239
239
|
},
|
|
240
240
|
);
|
|
241
241
|
|
|
242
|
-
const worker = await startWorker({
|
|
242
|
+
const worker = await startWorker({ app: "test-app",
|
|
243
243
|
redis,
|
|
244
244
|
prefix,
|
|
245
245
|
queues: ["q_on_failure_2"],
|
|
@@ -283,7 +283,7 @@ test("onFailure: NOT called on cancellation", async () => {
|
|
|
283
283
|
},
|
|
284
284
|
);
|
|
285
285
|
|
|
286
|
-
const worker = await startWorker({
|
|
286
|
+
const worker = await startWorker({ app: "test-app",
|
|
287
287
|
redis,
|
|
288
288
|
prefix,
|
|
289
289
|
queues: ["q_on_failure_3"],
|
|
@@ -333,7 +333,7 @@ test("cron: creates runs and executes workflow", async () => {
|
|
|
333
333
|
},
|
|
334
334
|
);
|
|
335
335
|
|
|
336
|
-
const worker = await startWorker({
|
|
336
|
+
const worker = await startWorker({ app: "test-app",
|
|
337
337
|
redis,
|
|
338
338
|
prefix,
|
|
339
339
|
queues: ["q4"],
|
|
@@ -352,6 +352,75 @@ test("cron: creates runs and executes workflow", async () => {
|
|
|
352
352
|
redis.close();
|
|
353
353
|
});
|
|
354
354
|
|
|
355
|
+
test("cron: skips when workflow already running", async () => {
|
|
356
|
+
const prefix = testPrefix();
|
|
357
|
+
const redis = new RedisClient(redisServer.url);
|
|
358
|
+
const client = createClient({ redis, prefix });
|
|
359
|
+
setDefaultClient(client);
|
|
360
|
+
|
|
361
|
+
const manualCountKey = `${prefix}:t:cronSkipManualCount`;
|
|
362
|
+
const cronCountKey = `${prefix}:t:cronSkipCronCount`;
|
|
363
|
+
|
|
364
|
+
defineWorkflow(
|
|
365
|
+
"cron-skip-running-wf",
|
|
366
|
+
{
|
|
367
|
+
queue: "q4_skip",
|
|
368
|
+
cron: [{ expression: "*/5 * * * * *", input: { source: "cron" } }],
|
|
369
|
+
},
|
|
370
|
+
async ({ input, step }) => {
|
|
371
|
+
await step.run({ name: "record-source" }, async () => {
|
|
372
|
+
const source = (input as { source?: string } | null | undefined)?.source;
|
|
373
|
+
if (source === "cron") {
|
|
374
|
+
await redis.incr(cronCountKey);
|
|
375
|
+
} else {
|
|
376
|
+
await redis.incr(manualCountKey);
|
|
377
|
+
}
|
|
378
|
+
return true;
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
await step.run({ name: "hold" }, async () => {
|
|
382
|
+
await new Promise((resolve) => setTimeout(resolve, 6500));
|
|
383
|
+
return true;
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
return { ok: true };
|
|
387
|
+
},
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const worker = await startWorker({ app: "test-app",
|
|
391
|
+
redis,
|
|
392
|
+
prefix,
|
|
393
|
+
queues: ["q4_skip"],
|
|
394
|
+
concurrency: 2,
|
|
395
|
+
runtime: { leaseMs: 500 },
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
const manualHandle = await client.emitWorkflow("cron-skip-running-wf", { source: "manual" });
|
|
400
|
+
|
|
401
|
+
await waitFor(
|
|
402
|
+
async () => Number((await redis.get(manualCountKey)) ?? "0") >= 1,
|
|
403
|
+
{ timeoutMs: 5000, label: "manual run started" },
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// Wait long enough for one cron tick while manual run is still executing.
|
|
407
|
+
await new Promise((resolve) => setTimeout(resolve, 5500));
|
|
408
|
+
const cronWhileManualRunning = Number((await redis.get(cronCountKey)) ?? "0");
|
|
409
|
+
expect(cronWhileManualRunning).toBe(0);
|
|
410
|
+
|
|
411
|
+
await manualHandle.result({ timeoutMs: 15_000 });
|
|
412
|
+
|
|
413
|
+
// Cron should resume once the running instance is gone.
|
|
414
|
+
await waitFor(
|
|
415
|
+
async () => Number((await redis.get(cronCountKey)) ?? "0") >= 1,
|
|
416
|
+
{ timeoutMs: 10_000, label: "cron resumed after manual run" },
|
|
417
|
+
);
|
|
418
|
+
} finally {
|
|
419
|
+
await worker.stop();
|
|
420
|
+
redis.close();
|
|
421
|
+
}
|
|
422
|
+
}, { timeout: 30_000 });
|
|
423
|
+
|
|
355
424
|
test("step.runWorkflow: auto idempotency and override are forwarded to child runs", async () => {
|
|
356
425
|
const prefix = testPrefix();
|
|
357
426
|
const redis = new RedisClient(redisServer.url);
|
|
@@ -388,7 +457,7 @@ test("step.runWorkflow: auto idempotency and override are forwarded to child run
|
|
|
388
457
|
return { auto, custom };
|
|
389
458
|
});
|
|
390
459
|
|
|
391
|
-
const worker = await startWorker({
|
|
460
|
+
const worker = await startWorker({ app: "test-app",
|
|
392
461
|
redis,
|
|
393
462
|
prefix,
|
|
394
463
|
queues: ["q_rw_parent", "q_rw_child"],
|
|
@@ -451,7 +520,7 @@ test("step.runWorkflow: child workflow executes once across parent retries", asy
|
|
|
451
520
|
},
|
|
452
521
|
);
|
|
453
522
|
|
|
454
|
-
const worker = await startWorker({
|
|
523
|
+
const worker = await startWorker({ app: "test-app",
|
|
455
524
|
redis,
|
|
456
525
|
prefix,
|
|
457
526
|
queues: ["q_rw_retry_parent", "q_rw_retry_child"],
|
|
@@ -508,7 +577,7 @@ test("step.runWorkflow: same queue avoids self-deadlock with concurrency 1", asy
|
|
|
508
577
|
return await step.runWorkflow({ name: "call-child" }, child, {});
|
|
509
578
|
});
|
|
510
579
|
|
|
511
|
-
const worker = await startWorker({
|
|
580
|
+
const worker = await startWorker({ app: "test-app",
|
|
512
581
|
redis,
|
|
513
582
|
prefix,
|
|
514
583
|
queues: ["q_rw_single"],
|
|
@@ -532,6 +601,47 @@ test("step.runWorkflow: same queue avoids self-deadlock with concurrency 1", asy
|
|
|
532
601
|
redis.close();
|
|
533
602
|
}, { timeout: 20_000 });
|
|
534
603
|
|
|
604
|
+
test("step.emitWorkflow: supports workflow name strings", async () => {
|
|
605
|
+
const prefix = testPrefix();
|
|
606
|
+
const redis = new RedisClient(redisServer.url);
|
|
607
|
+
const client = createClient({ redis, prefix });
|
|
608
|
+
setDefaultClient(client);
|
|
609
|
+
|
|
610
|
+
defineWorkflow("child-emit-by-name", { queue: "q_emit_name_child" }, async ({ input }) => {
|
|
611
|
+
return { ok: true, input };
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
const parent = defineWorkflow("parent-emit-by-name", { queue: "q_emit_name_parent" }, async ({ step }) => {
|
|
615
|
+
const childRunId = await step.emitWorkflow(
|
|
616
|
+
{ name: "emit-child-by-name" },
|
|
617
|
+
"child-emit-by-name",
|
|
618
|
+
{ value: 1 },
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
return { childRunId };
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
const worker = await startWorker({ app: "test-app",
|
|
625
|
+
redis,
|
|
626
|
+
prefix,
|
|
627
|
+
queues: ["q_emit_name_parent", "q_emit_name_child"],
|
|
628
|
+
concurrency: 2,
|
|
629
|
+
runtime: { leaseMs: 500 },
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
try {
|
|
633
|
+
const parentHandle = await parent.run({});
|
|
634
|
+
const parentOut = await parentHandle.result({ timeoutMs: 8000 });
|
|
635
|
+
expect(typeof parentOut.childRunId).toBe("string");
|
|
636
|
+
|
|
637
|
+
const childOut = await client.makeRunHandle(parentOut.childRunId).result({ timeoutMs: 8000 });
|
|
638
|
+
expect(childOut).toEqual({ ok: true, input: { value: 1 } });
|
|
639
|
+
} finally {
|
|
640
|
+
await worker.stop();
|
|
641
|
+
redis.close();
|
|
642
|
+
}
|
|
643
|
+
}, { timeout: 20_000 });
|
|
644
|
+
|
|
535
645
|
|
|
536
646
|
|
|
537
647
|
test("retries: step results are cached across attempts", async () => {
|
|
@@ -565,7 +675,7 @@ test("retries: step results are cached across attempts", async () => {
|
|
|
565
675
|
},
|
|
566
676
|
);
|
|
567
677
|
|
|
568
|
-
const worker = await startWorker({
|
|
678
|
+
const worker = await startWorker({ app: "test-app",
|
|
569
679
|
redis,
|
|
570
680
|
prefix,
|
|
571
681
|
queues: ["q5"],
|
|
@@ -613,7 +723,7 @@ test("retries: run queued before worker start keeps workflow maxAttempts", async
|
|
|
613
723
|
const queuedState = await client.getRun(handle.id);
|
|
614
724
|
expect(queuedState?.maxAttempts).toBe(2);
|
|
615
725
|
|
|
616
|
-
const worker = await startWorker({
|
|
726
|
+
const worker = await startWorker({ app: "test-app",
|
|
617
727
|
redis,
|
|
618
728
|
prefix,
|
|
619
729
|
queues: ["q5_before_sync"],
|
|
@@ -646,7 +756,7 @@ test("step timeout: run fails and error is recorded", async () => {
|
|
|
646
756
|
return { ok: true };
|
|
647
757
|
});
|
|
648
758
|
|
|
649
|
-
const worker = await startWorker({
|
|
759
|
+
const worker = await startWorker({ app: "test-app",
|
|
650
760
|
redis,
|
|
651
761
|
prefix,
|
|
652
762
|
queues: ["q6"],
|
|
@@ -696,7 +806,7 @@ test("cancellation: scheduled/queued/running", async () => {
|
|
|
696
806
|
|
|
697
807
|
// Scheduled cancel.
|
|
698
808
|
{
|
|
699
|
-
const worker = await startWorker({
|
|
809
|
+
const worker = await startWorker({ app: "test-app",
|
|
700
810
|
redis,
|
|
701
811
|
prefix,
|
|
702
812
|
queues: ["q7"],
|
|
@@ -732,7 +842,7 @@ test("cancellation: scheduled/queued/running", async () => {
|
|
|
732
842
|
|
|
733
843
|
// Running cancel.
|
|
734
844
|
{
|
|
735
|
-
const worker = await startWorker({
|
|
845
|
+
const worker = await startWorker({ app: "test-app",
|
|
736
846
|
redis,
|
|
737
847
|
prefix,
|
|
738
848
|
queues: ["q7"],
|
|
@@ -815,7 +925,7 @@ test("cancel race: queued cancel before start does not execute handler", async (
|
|
|
815
925
|
|
|
816
926
|
let worker: Awaited<ReturnType<typeof startWorker>> | null = null;
|
|
817
927
|
try {
|
|
818
|
-
worker = await startWorker({
|
|
928
|
+
worker = await startWorker({ app: "test-app",
|
|
819
929
|
redis,
|
|
820
930
|
prefix,
|
|
821
931
|
queues: [queue],
|
|
@@ -936,7 +1046,7 @@ test("idempotencyKey: same key returns same run id and executes once", async ()
|
|
|
936
1046
|
return { ok: true };
|
|
937
1047
|
});
|
|
938
1048
|
|
|
939
|
-
const worker = await startWorker({
|
|
1049
|
+
const worker = await startWorker({ app: "test-app",
|
|
940
1050
|
redis,
|
|
941
1051
|
prefix,
|
|
942
1052
|
queues: ["q9"],
|
|
@@ -974,7 +1084,7 @@ test("idempotencyKey: delayed TTL refresh cannot fork duplicate runs", async ()
|
|
|
974
1084
|
return { ok: true };
|
|
975
1085
|
});
|
|
976
1086
|
|
|
977
|
-
const worker = await startWorker({
|
|
1087
|
+
const worker = await startWorker({ app: "test-app",
|
|
978
1088
|
redis,
|
|
979
1089
|
prefix,
|
|
980
1090
|
queues: ["q9_race_fix"],
|
|
@@ -1022,7 +1132,7 @@ test("enqueue: producer-side zadd failure does not leave runs orphaned", async (
|
|
|
1022
1132
|
setDefaultClient(client);
|
|
1023
1133
|
|
|
1024
1134
|
const wf = defineWorkflow("enqueue-atomic", {queue: "q_enqueue_atomic" }, async () => ({ ok: true }));
|
|
1025
|
-
await client.syncRegistry(getDefaultRegistry());
|
|
1135
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
|
|
1026
1136
|
|
|
1027
1137
|
const originalZadd = redis.zadd.bind(redis);
|
|
1028
1138
|
let injected = false;
|
|
@@ -1041,7 +1151,7 @@ test("enqueue: producer-side zadd failure does not leave runs orphaned", async (
|
|
|
1041
1151
|
redis.zadd = originalZadd;
|
|
1042
1152
|
}
|
|
1043
1153
|
|
|
1044
|
-
const worker = await startWorker({
|
|
1154
|
+
const worker = await startWorker({ app: "test-app",
|
|
1045
1155
|
redis,
|
|
1046
1156
|
prefix,
|
|
1047
1157
|
queues: ["q_enqueue_atomic"],
|
|
@@ -1066,7 +1176,7 @@ test("idempotencyKey: delimiter-like workflow/key pairs do not collide", async (
|
|
|
1066
1176
|
const wfA = defineWorkflow("idem:a", {queue: "q9a" }, async () => ({ workflow: "idem:a" }));
|
|
1067
1177
|
const wfB = defineWorkflow("idem:a:b", {queue: "q9b" }, async () => ({ workflow: "idem:a:b" }));
|
|
1068
1178
|
|
|
1069
|
-
const worker = await startWorker({
|
|
1179
|
+
const worker = await startWorker({ app: "test-app",
|
|
1070
1180
|
redis,
|
|
1071
1181
|
prefix,
|
|
1072
1182
|
queues: ["q9a", "q9b"],
|
|
@@ -1109,7 +1219,7 @@ test("idempotencyTtl: short TTL expires and allows a new run with same key", asy
|
|
|
1109
1219
|
return { ok: true };
|
|
1110
1220
|
});
|
|
1111
1221
|
|
|
1112
|
-
const worker = await startWorker({
|
|
1222
|
+
const worker = await startWorker({ app: "test-app",
|
|
1113
1223
|
redis,
|
|
1114
1224
|
prefix,
|
|
1115
1225
|
queues: ["q_idem_ttl"],
|
|
@@ -1158,7 +1268,7 @@ test("input validation: invalid input fails once and is not retried", async () =
|
|
|
1158
1268
|
},
|
|
1159
1269
|
);
|
|
1160
1270
|
|
|
1161
|
-
const worker = await startWorker({
|
|
1271
|
+
const worker = await startWorker({ app: "test-app",
|
|
1162
1272
|
redis,
|
|
1163
1273
|
prefix,
|
|
1164
1274
|
queues: ["q11"],
|
|
@@ -1193,7 +1303,7 @@ test("unknown workflow: run fails and is not retried", async () => {
|
|
|
1193
1303
|
const redis = new RedisClient(redisServer.url);
|
|
1194
1304
|
const client = createClient({ redis, prefix });
|
|
1195
1305
|
|
|
1196
|
-
const worker = await startWorker({
|
|
1306
|
+
const worker = await startWorker({ app: "test-app",
|
|
1197
1307
|
redis,
|
|
1198
1308
|
prefix,
|
|
1199
1309
|
queues: ["qx"],
|
|
@@ -1246,7 +1356,7 @@ test("cancel during step: run becomes canceled and step error kind is canceled",
|
|
|
1246
1356
|
return { ok: true };
|
|
1247
1357
|
});
|
|
1248
1358
|
|
|
1249
|
-
const worker = await startWorker({
|
|
1359
|
+
const worker = await startWorker({ app: "test-app",
|
|
1250
1360
|
redis,
|
|
1251
1361
|
prefix,
|
|
1252
1362
|
queues: ["q12"],
|
|
@@ -1304,7 +1414,7 @@ test("terminal run re-queued is ignored (no re-execution)", async () => {
|
|
|
1304
1414
|
return { ok: true };
|
|
1305
1415
|
});
|
|
1306
1416
|
|
|
1307
|
-
const worker = await startWorker({
|
|
1417
|
+
const worker = await startWorker({ app: "test-app",
|
|
1308
1418
|
redis,
|
|
1309
1419
|
prefix,
|
|
1310
1420
|
queues: [queue],
|
|
@@ -1352,7 +1462,7 @@ test("lease+reaper: long running step is not duplicated", async () => {
|
|
|
1352
1462
|
return { ok: true };
|
|
1353
1463
|
});
|
|
1354
1464
|
|
|
1355
|
-
const worker = await startWorker({
|
|
1465
|
+
const worker = await startWorker({ app: "test-app",
|
|
1356
1466
|
redis,
|
|
1357
1467
|
prefix,
|
|
1358
1468
|
queues: [queue],
|
|
@@ -1397,8 +1507,8 @@ test(
|
|
|
1397
1507
|
},
|
|
1398
1508
|
);
|
|
1399
1509
|
|
|
1400
|
-
const w1 = await startWorker({ redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
|
|
1401
|
-
const w2 = await startWorker({ redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
|
|
1510
|
+
const w1 = await startWorker({ app: "test-app", redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
|
|
1511
|
+
const w2 = await startWorker({ app: "test-app", redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
|
|
1402
1512
|
|
|
1403
1513
|
await new Promise((r) => setTimeout(r, 3500));
|
|
1404
1514
|
const count = Number((await redis.get(counterKey)) ?? "0");
|
|
@@ -1464,7 +1574,7 @@ test(
|
|
|
1464
1574
|
},
|
|
1465
1575
|
);
|
|
1466
1576
|
|
|
1467
|
-
const worker = await startWorker({
|
|
1577
|
+
const worker = await startWorker({ app: "test-app",
|
|
1468
1578
|
redis,
|
|
1469
1579
|
prefix,
|
|
1470
1580
|
queues: ["q_parent", "q_child"],
|
|
@@ -1519,7 +1629,7 @@ test(
|
|
|
1519
1629
|
const queuedMembers = await redis.zrange(queuedIndex, 0, -1);
|
|
1520
1630
|
expect(queuedMembers.includes(handle.id)).toBe(true);
|
|
1521
1631
|
|
|
1522
|
-
const worker = await startWorker({
|
|
1632
|
+
const worker = await startWorker({ app: "test-app",
|
|
1523
1633
|
redis,
|
|
1524
1634
|
prefix,
|
|
1525
1635
|
queues: [queue],
|
|
@@ -1565,7 +1675,7 @@ test(
|
|
|
1565
1675
|
cron: [{ expression: "*/1 * * * * *" }],
|
|
1566
1676
|
}, async () => ({ ok: true }));
|
|
1567
1677
|
|
|
1568
|
-
await client.syncRegistry(getDefaultRegistry());
|
|
1678
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
|
|
1569
1679
|
|
|
1570
1680
|
const workflowKey = `${prefix}:workflow:${name}`;
|
|
1571
1681
|
const cronIdsJson = await redis.hget(workflowKey, "cronIdsJson");
|
|
@@ -1583,7 +1693,7 @@ test(
|
|
|
1583
1693
|
queue: "q_update",
|
|
1584
1694
|
}, async () => ({ ok: true }));
|
|
1585
1695
|
|
|
1586
|
-
await client.syncRegistry(getDefaultRegistry());
|
|
1696
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
|
|
1587
1697
|
|
|
1588
1698
|
expect(await redis.hget(`${prefix}:cron:def`, cronId)).toBeNull();
|
|
1589
1699
|
const cronNext2 = await redis.zrange(`${prefix}:cron:next`, 0, -1);
|
|
@@ -1617,7 +1727,7 @@ test(
|
|
|
1617
1727
|
},
|
|
1618
1728
|
);
|
|
1619
1729
|
|
|
1620
|
-
const worker = await startWorker({
|
|
1730
|
+
const worker = await startWorker({ app: "test-app",
|
|
1621
1731
|
redis,
|
|
1622
1732
|
prefix,
|
|
1623
1733
|
queues: [queue],
|
|
@@ -1667,8 +1777,8 @@ test(
|
|
|
1667
1777
|
},
|
|
1668
1778
|
);
|
|
1669
1779
|
|
|
1670
|
-
const w1 = await startWorker({ redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
|
|
1671
|
-
const w2 = await startWorker({ redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
|
|
1780
|
+
const w1 = await startWorker({ app: "test-app", redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
|
|
1781
|
+
const w2 = await startWorker({ app: "test-app", redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
|
|
1672
1782
|
|
|
1673
1783
|
await waitFor(
|
|
1674
1784
|
async () => Number((await redis.get(counterKey)) ?? "0") >= 1,
|
|
@@ -1703,7 +1813,7 @@ test("listRuns: status + workflow filters are applied together", async () => {
|
|
|
1703
1813
|
throw new Error("expected failure");
|
|
1704
1814
|
});
|
|
1705
1815
|
|
|
1706
|
-
const worker = await startWorker({ redis, prefix, queues: [queue], runtime: { leaseMs: 500 } });
|
|
1816
|
+
const worker = await startWorker({ app: "test-app", redis, prefix, queues: [queue], runtime: { leaseMs: 500 } });
|
|
1707
1817
|
|
|
1708
1818
|
const successHandle = await succeeds.run({});
|
|
1709
1819
|
const failHandle = await fails.run({});
|
|
@@ -1754,7 +1864,7 @@ test("cron trigger ids: same custom id in two workflows does not collide", async
|
|
|
1754
1864
|
async () => ({ ok: true }),
|
|
1755
1865
|
);
|
|
1756
1866
|
|
|
1757
|
-
await client.syncRegistry(getDefaultRegistry());
|
|
1867
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
|
|
1758
1868
|
|
|
1759
1869
|
const idsA = JSON.parse((await redis.hget(`${prefix}:workflow:cron-id-a`, "cronIdsJson")) ?? "[]") as string[];
|
|
1760
1870
|
const idsB = JSON.parse((await redis.hget(`${prefix}:workflow:cron-id-b`, "cronIdsJson")) ?? "[]") as string[];
|
|
@@ -1782,7 +1892,7 @@ test("syncRegistry: cron trigger with explicit null input preserves null payload
|
|
|
1782
1892
|
async () => ({ ok: true }),
|
|
1783
1893
|
);
|
|
1784
1894
|
|
|
1785
|
-
await client.syncRegistry(getDefaultRegistry());
|
|
1895
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
|
|
1786
1896
|
|
|
1787
1897
|
const workflowKey = `${prefix}:workflow:${workflowName}`;
|
|
1788
1898
|
const cronIds = JSON.parse((await redis.hget(workflowKey, "cronIdsJson")) ?? "[]") as string[];
|
|
@@ -1813,7 +1923,7 @@ test("syncRegistry: invalid cron expression removes existing next schedule for s
|
|
|
1813
1923
|
async () => ({ ok: true }),
|
|
1814
1924
|
);
|
|
1815
1925
|
|
|
1816
|
-
await client.syncRegistry(getDefaultRegistry());
|
|
1926
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
|
|
1817
1927
|
|
|
1818
1928
|
const cronIds = JSON.parse((await redis.hget(workflowKey, "cronIdsJson")) ?? "[]") as string[];
|
|
1819
1929
|
expect(cronIds.length).toBe(1);
|
|
@@ -1829,7 +1939,7 @@ test("syncRegistry: invalid cron expression removes existing next schedule for s
|
|
|
1829
1939
|
async () => ({ ok: true }),
|
|
1830
1940
|
);
|
|
1831
1941
|
|
|
1832
|
-
await client.syncRegistry(getDefaultRegistry());
|
|
1942
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
|
|
1833
1943
|
|
|
1834
1944
|
const after = await redis.zrange(cronNextKey, 0, -1);
|
|
1835
1945
|
expect(after.includes(cronId)).toBe(false);
|
|
@@ -1850,7 +1960,7 @@ test("syncRegistry: removes stale workflow metadata (cron)", async () => {
|
|
|
1850
1960
|
async () => ({ ok: true }),
|
|
1851
1961
|
);
|
|
1852
1962
|
|
|
1853
|
-
await client.syncRegistry(getDefaultRegistry());
|
|
1963
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
|
|
1854
1964
|
|
|
1855
1965
|
const workflowKey = `${prefix}:workflow:${workflowName}`;
|
|
1856
1966
|
const cronIds = JSON.parse((await redis.hget(workflowKey, "cronIdsJson")) ?? "[]") as string[];
|
|
@@ -1862,7 +1972,7 @@ test("syncRegistry: removes stale workflow metadata (cron)", async () => {
|
|
|
1862
1972
|
// Force stale age beyond grace period and sync an empty registry.
|
|
1863
1973
|
await redis.hset(workflowKey, { updatedAt: "1" });
|
|
1864
1974
|
__unstableResetDefaultRegistryForTests();
|
|
1865
|
-
await client.syncRegistry(getDefaultRegistry());
|
|
1975
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
|
|
1866
1976
|
|
|
1867
1977
|
expect((await redis.smembers(`${prefix}:workflows`)).includes(workflowName)).toBe(false);
|
|
1868
1978
|
expect(await redis.hget(workflowKey, "queue")).toBeNull();
|
|
@@ -1873,37 +1983,37 @@ test("syncRegistry: removes stale workflow metadata (cron)", async () => {
|
|
|
1873
1983
|
redis.close();
|
|
1874
1984
|
});
|
|
1875
1985
|
|
|
1876
|
-
test("syncRegistry: stale cleanup is isolated by
|
|
1986
|
+
test("syncRegistry: stale cleanup is isolated by app", async () => {
|
|
1877
1987
|
const prefix = testPrefix();
|
|
1878
1988
|
const redis = new RedisClient(redisServer.url);
|
|
1879
1989
|
const client = createClient({ redis, prefix });
|
|
1880
1990
|
|
|
1881
1991
|
__unstableResetDefaultRegistryForTests();
|
|
1882
|
-
defineWorkflow("
|
|
1992
|
+
defineWorkflow("app-a-wf", {queue: "qa",
|
|
1883
1993
|
cron: [{ id: "a", expression: "*/30 * * * * *" }],
|
|
1884
1994
|
},
|
|
1885
1995
|
async () => ({ ok: true }),
|
|
1886
1996
|
);
|
|
1887
|
-
await client.syncRegistry(getDefaultRegistry(), {
|
|
1997
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "svc-a" });
|
|
1888
1998
|
|
|
1889
1999
|
__unstableResetDefaultRegistryForTests();
|
|
1890
|
-
defineWorkflow("
|
|
2000
|
+
defineWorkflow("app-b-wf", {queue: "qb",
|
|
1891
2001
|
cron: [{ id: "b", expression: "*/30 * * * * *" }],
|
|
1892
2002
|
},
|
|
1893
2003
|
async () => ({ ok: true }),
|
|
1894
2004
|
);
|
|
1895
|
-
await client.syncRegistry(getDefaultRegistry(), {
|
|
2005
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "svc-b" });
|
|
1896
2006
|
|
|
1897
|
-
await redis.hset(`${prefix}:workflow:
|
|
1898
|
-
await redis.hset(`${prefix}:workflow:
|
|
2007
|
+
await redis.hset(`${prefix}:workflow:app-a-wf`, { updatedAt: "1" });
|
|
2008
|
+
await redis.hset(`${prefix}:workflow:app-b-wf`, { updatedAt: "1" });
|
|
1899
2009
|
|
|
1900
2010
|
__unstableResetDefaultRegistryForTests();
|
|
1901
|
-
await client.syncRegistry(getDefaultRegistry(), {
|
|
2011
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "svc-a" });
|
|
1902
2012
|
|
|
1903
|
-
expect(await redis.hget(`${prefix}:workflow:
|
|
1904
|
-
expect(await redis.hget(`${prefix}:workflow:
|
|
1905
|
-
expect((await redis.smembers(`${prefix}:workflows`)).includes("
|
|
1906
|
-
expect((await redis.smembers(`${prefix}:workflows`)).includes("
|
|
2013
|
+
expect(await redis.hget(`${prefix}:workflow:app-a-wf`, "queue")).toBeNull();
|
|
2014
|
+
expect(await redis.hget(`${prefix}:workflow:app-b-wf`, "queue")).not.toBeNull();
|
|
2015
|
+
expect((await redis.smembers(`${prefix}:workflows`)).includes("app-a-wf")).toBe(false);
|
|
2016
|
+
expect((await redis.smembers(`${prefix}:workflows`)).includes("app-b-wf")).toBe(true);
|
|
1907
2017
|
|
|
1908
2018
|
redis.close();
|
|
1909
2019
|
});
|
|
@@ -1916,7 +2026,7 @@ test("cancelRun: terminal runs are unchanged", async () => {
|
|
|
1916
2026
|
|
|
1917
2027
|
const wf = defineWorkflow("cancel-terminal", {queue: "q_cancel_terminal" }, async () => ({ ok: true }));
|
|
1918
2028
|
|
|
1919
|
-
const worker = await startWorker({
|
|
2029
|
+
const worker = await startWorker({ app: "test-app",
|
|
1920
2030
|
redis,
|
|
1921
2031
|
prefix,
|
|
1922
2032
|
queues: ["q_cancel_terminal"],
|
|
@@ -1951,7 +2061,7 @@ test("output serialization: non-JSON output fails once and is not stuck", async
|
|
|
1951
2061
|
async () => ({ bad: 1n }),
|
|
1952
2062
|
);
|
|
1953
2063
|
|
|
1954
|
-
const worker = await startWorker({
|
|
2064
|
+
const worker = await startWorker({ app: "test-app",
|
|
1955
2065
|
redis,
|
|
1956
2066
|
prefix,
|
|
1957
2067
|
queues: ["q_serial"],
|
|
@@ -2006,7 +2116,7 @@ test("retry step sequence: new steps keep monotonic order after cached steps", a
|
|
|
2006
2116
|
},
|
|
2007
2117
|
);
|
|
2008
2118
|
|
|
2009
|
-
const worker = await startWorker({
|
|
2119
|
+
const worker = await startWorker({ app: "test-app",
|
|
2010
2120
|
redis,
|
|
2011
2121
|
prefix,
|
|
2012
2122
|
queues: ["q_seq"],
|
|
@@ -2037,7 +2147,7 @@ test("listRuns: negative offset is clamped to zero", async () => {
|
|
|
2037
2147
|
setDefaultClient(client);
|
|
2038
2148
|
|
|
2039
2149
|
const wf = defineWorkflow("offset-wf", {queue: "q_offset" }, async () => ({ ok: true }));
|
|
2040
|
-
const worker = await startWorker({
|
|
2150
|
+
const worker = await startWorker({ app: "test-app",
|
|
2041
2151
|
redis,
|
|
2042
2152
|
prefix,
|
|
2043
2153
|
queues: ["q_offset"],
|
|
@@ -2101,7 +2211,7 @@ test("error serialization: non-serializable thrown values do not wedge a run", a
|
|
|
2101
2211
|
},
|
|
2102
2212
|
);
|
|
2103
2213
|
|
|
2104
|
-
const worker = await startWorker({
|
|
2214
|
+
const worker = await startWorker({ app: "test-app",
|
|
2105
2215
|
redis,
|
|
2106
2216
|
prefix,
|
|
2107
2217
|
queues: ["q_bigint"],
|
|
@@ -2144,7 +2254,7 @@ test("cron trigger ids: delimiter-like custom ids do not collide", async () => {
|
|
|
2144
2254
|
async () => ({ ok: true }),
|
|
2145
2255
|
);
|
|
2146
2256
|
|
|
2147
|
-
await client.syncRegistry(getDefaultRegistry());
|
|
2257
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
|
|
2148
2258
|
|
|
2149
2259
|
const idsA = JSON.parse((await redis.hget(`${prefix}:workflow:wf:a`, "cronIdsJson")) ?? "[]") as string[];
|
|
2150
2260
|
const idsB = JSON.parse((await redis.hget(`${prefix}:workflow:wf:a:b`, "cronIdsJson")) ?? "[]") as string[];
|
|
@@ -2178,7 +2288,7 @@ test("idempotencyKey: stale pointer is recovered instead of returning missing ru
|
|
|
2178
2288
|
|
|
2179
2289
|
const wf = defineWorkflow(workflowName, {queue }, async () => ({ ok: true }));
|
|
2180
2290
|
|
|
2181
|
-
const worker = await startWorker({
|
|
2291
|
+
const worker = await startWorker({ app: "test-app",
|
|
2182
2292
|
redis,
|
|
2183
2293
|
prefix,
|
|
2184
2294
|
queues: [queue],
|
|
@@ -2221,7 +2331,7 @@ test("idempotencyKey: partial existing run is repaired and executed", async () =
|
|
|
2221
2331
|
return { ok: true };
|
|
2222
2332
|
});
|
|
2223
2333
|
|
|
2224
|
-
const worker = await startWorker({
|
|
2334
|
+
const worker = await startWorker({ app: "test-app",
|
|
2225
2335
|
redis,
|
|
2226
2336
|
prefix,
|
|
2227
2337
|
queues: [queue],
|
|
@@ -2273,7 +2383,7 @@ test("syncRegistry: duplicate custom cron ids in one workflow are rejected", asy
|
|
|
2273
2383
|
async () => ({ ok: true }),
|
|
2274
2384
|
);
|
|
2275
2385
|
|
|
2276
|
-
await expect(client.syncRegistry(getDefaultRegistry())).rejects.toThrow("Duplicate cron trigger id");
|
|
2386
|
+
await expect(client.syncRegistry(getDefaultRegistry(), { app: "test-app" })).rejects.toThrow("Duplicate cron trigger id");
|
|
2277
2387
|
|
|
2278
2388
|
redis.close();
|
|
2279
2389
|
});
|
|
@@ -2290,7 +2400,7 @@ test("scheduled promoter: stale scheduled entry does not resurrect terminal run"
|
|
|
2290
2400
|
return { ok: true };
|
|
2291
2401
|
});
|
|
2292
2402
|
|
|
2293
|
-
const worker = await startWorker({
|
|
2403
|
+
const worker = await startWorker({ app: "test-app",
|
|
2294
2404
|
redis,
|
|
2295
2405
|
prefix,
|
|
2296
2406
|
queues: [queue],
|
|
@@ -2357,7 +2467,7 @@ test(
|
|
|
2357
2467
|
|
|
2358
2468
|
let worker: Awaited<ReturnType<typeof startWorker>> | null = null;
|
|
2359
2469
|
try {
|
|
2360
|
-
worker = await startWorker({
|
|
2470
|
+
worker = await startWorker({ app: "test-app",
|
|
2361
2471
|
redis,
|
|
2362
2472
|
prefix,
|
|
2363
2473
|
queues: [queue],
|
|
@@ -2408,7 +2518,7 @@ test(
|
|
|
2408
2518
|
|
|
2409
2519
|
let worker: Awaited<ReturnType<typeof startWorker>> | null = null;
|
|
2410
2520
|
try {
|
|
2411
|
-
worker = await startWorker({
|
|
2521
|
+
worker = await startWorker({ app: "test-app",
|
|
2412
2522
|
redis,
|
|
2413
2523
|
prefix,
|
|
2414
2524
|
queues: [queue],
|
|
@@ -2451,7 +2561,7 @@ test("workflow names ending with ':runs' do not collide with workflow run indexe
|
|
|
2451
2561
|
|
|
2452
2562
|
expect(keys.workflow(prefix, "keyspace-base:runs")).not.toBe(keys.workflowRuns(prefix, "keyspace-base"));
|
|
2453
2563
|
|
|
2454
|
-
const worker = await startWorker({
|
|
2564
|
+
const worker = await startWorker({ app: "test-app",
|
|
2455
2565
|
redis,
|
|
2456
2566
|
prefix,
|
|
2457
2567
|
queues: [queue],
|
|
@@ -2496,7 +2606,7 @@ test("cancelRun race: near-finish cancellation settles as canceled", async () =>
|
|
|
2496
2606
|
return await originalFinalizeRun.call(this, runId, args);
|
|
2497
2607
|
}) as RedflowClient["finalizeRun"];
|
|
2498
2608
|
|
|
2499
|
-
const worker = await startWorker({
|
|
2609
|
+
const worker = await startWorker({ app: "test-app",
|
|
2500
2610
|
redis,
|
|
2501
2611
|
prefix,
|
|
2502
2612
|
queues: [queue],
|
|
@@ -2559,7 +2669,7 @@ test("cancelRun race: cancellation requested during finalize wins over success",
|
|
|
2559
2669
|
return await originalFinalizeRun.call(this, runId, args);
|
|
2560
2670
|
}) as RedflowClient["finalizeRun"];
|
|
2561
2671
|
|
|
2562
|
-
const worker = await startWorker({
|
|
2672
|
+
const worker = await startWorker({ app: "test-app",
|
|
2563
2673
|
redis,
|
|
2564
2674
|
prefix,
|
|
2565
2675
|
queues: [queue],
|
|
@@ -2609,7 +2719,7 @@ test(
|
|
|
2609
2719
|
await new Promise<never>(() => {});
|
|
2610
2720
|
});
|
|
2611
2721
|
|
|
2612
|
-
const worker = await startWorker({
|
|
2722
|
+
const worker = await startWorker({ app: "test-app",
|
|
2613
2723
|
redis,
|
|
2614
2724
|
prefix,
|
|
2615
2725
|
queues: [queue],
|