@redflow/client 0.0.3 → 0.0.5
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 +24 -3
- package/package.json +1 -1
- package/src/client.ts +36 -32
- package/src/types.ts +9 -1
- package/src/worker.ts +88 -16
- 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 +142 -73
|
@@ -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"],
|
|
@@ -552,7 +621,7 @@ test("step.emitWorkflow: supports workflow name strings", async () => {
|
|
|
552
621
|
return { childRunId };
|
|
553
622
|
});
|
|
554
623
|
|
|
555
|
-
const worker = await startWorker({
|
|
624
|
+
const worker = await startWorker({ app: "test-app",
|
|
556
625
|
redis,
|
|
557
626
|
prefix,
|
|
558
627
|
queues: ["q_emit_name_parent", "q_emit_name_child"],
|
|
@@ -606,7 +675,7 @@ test("retries: step results are cached across attempts", async () => {
|
|
|
606
675
|
},
|
|
607
676
|
);
|
|
608
677
|
|
|
609
|
-
const worker = await startWorker({
|
|
678
|
+
const worker = await startWorker({ app: "test-app",
|
|
610
679
|
redis,
|
|
611
680
|
prefix,
|
|
612
681
|
queues: ["q5"],
|
|
@@ -654,7 +723,7 @@ test("retries: run queued before worker start keeps workflow maxAttempts", async
|
|
|
654
723
|
const queuedState = await client.getRun(handle.id);
|
|
655
724
|
expect(queuedState?.maxAttempts).toBe(2);
|
|
656
725
|
|
|
657
|
-
const worker = await startWorker({
|
|
726
|
+
const worker = await startWorker({ app: "test-app",
|
|
658
727
|
redis,
|
|
659
728
|
prefix,
|
|
660
729
|
queues: ["q5_before_sync"],
|
|
@@ -687,7 +756,7 @@ test("step timeout: run fails and error is recorded", async () => {
|
|
|
687
756
|
return { ok: true };
|
|
688
757
|
});
|
|
689
758
|
|
|
690
|
-
const worker = await startWorker({
|
|
759
|
+
const worker = await startWorker({ app: "test-app",
|
|
691
760
|
redis,
|
|
692
761
|
prefix,
|
|
693
762
|
queues: ["q6"],
|
|
@@ -737,7 +806,7 @@ test("cancellation: scheduled/queued/running", async () => {
|
|
|
737
806
|
|
|
738
807
|
// Scheduled cancel.
|
|
739
808
|
{
|
|
740
|
-
const worker = await startWorker({
|
|
809
|
+
const worker = await startWorker({ app: "test-app",
|
|
741
810
|
redis,
|
|
742
811
|
prefix,
|
|
743
812
|
queues: ["q7"],
|
|
@@ -773,7 +842,7 @@ test("cancellation: scheduled/queued/running", async () => {
|
|
|
773
842
|
|
|
774
843
|
// Running cancel.
|
|
775
844
|
{
|
|
776
|
-
const worker = await startWorker({
|
|
845
|
+
const worker = await startWorker({ app: "test-app",
|
|
777
846
|
redis,
|
|
778
847
|
prefix,
|
|
779
848
|
queues: ["q7"],
|
|
@@ -856,7 +925,7 @@ test("cancel race: queued cancel before start does not execute handler", async (
|
|
|
856
925
|
|
|
857
926
|
let worker: Awaited<ReturnType<typeof startWorker>> | null = null;
|
|
858
927
|
try {
|
|
859
|
-
worker = await startWorker({
|
|
928
|
+
worker = await startWorker({ app: "test-app",
|
|
860
929
|
redis,
|
|
861
930
|
prefix,
|
|
862
931
|
queues: [queue],
|
|
@@ -977,7 +1046,7 @@ test("idempotencyKey: same key returns same run id and executes once", async ()
|
|
|
977
1046
|
return { ok: true };
|
|
978
1047
|
});
|
|
979
1048
|
|
|
980
|
-
const worker = await startWorker({
|
|
1049
|
+
const worker = await startWorker({ app: "test-app",
|
|
981
1050
|
redis,
|
|
982
1051
|
prefix,
|
|
983
1052
|
queues: ["q9"],
|
|
@@ -1015,7 +1084,7 @@ test("idempotencyKey: delayed TTL refresh cannot fork duplicate runs", async ()
|
|
|
1015
1084
|
return { ok: true };
|
|
1016
1085
|
});
|
|
1017
1086
|
|
|
1018
|
-
const worker = await startWorker({
|
|
1087
|
+
const worker = await startWorker({ app: "test-app",
|
|
1019
1088
|
redis,
|
|
1020
1089
|
prefix,
|
|
1021
1090
|
queues: ["q9_race_fix"],
|
|
@@ -1063,7 +1132,7 @@ test("enqueue: producer-side zadd failure does not leave runs orphaned", async (
|
|
|
1063
1132
|
setDefaultClient(client);
|
|
1064
1133
|
|
|
1065
1134
|
const wf = defineWorkflow("enqueue-atomic", {queue: "q_enqueue_atomic" }, async () => ({ ok: true }));
|
|
1066
|
-
await client.syncRegistry(getDefaultRegistry());
|
|
1135
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
|
|
1067
1136
|
|
|
1068
1137
|
const originalZadd = redis.zadd.bind(redis);
|
|
1069
1138
|
let injected = false;
|
|
@@ -1082,7 +1151,7 @@ test("enqueue: producer-side zadd failure does not leave runs orphaned", async (
|
|
|
1082
1151
|
redis.zadd = originalZadd;
|
|
1083
1152
|
}
|
|
1084
1153
|
|
|
1085
|
-
const worker = await startWorker({
|
|
1154
|
+
const worker = await startWorker({ app: "test-app",
|
|
1086
1155
|
redis,
|
|
1087
1156
|
prefix,
|
|
1088
1157
|
queues: ["q_enqueue_atomic"],
|
|
@@ -1107,7 +1176,7 @@ test("idempotencyKey: delimiter-like workflow/key pairs do not collide", async (
|
|
|
1107
1176
|
const wfA = defineWorkflow("idem:a", {queue: "q9a" }, async () => ({ workflow: "idem:a" }));
|
|
1108
1177
|
const wfB = defineWorkflow("idem:a:b", {queue: "q9b" }, async () => ({ workflow: "idem:a:b" }));
|
|
1109
1178
|
|
|
1110
|
-
const worker = await startWorker({
|
|
1179
|
+
const worker = await startWorker({ app: "test-app",
|
|
1111
1180
|
redis,
|
|
1112
1181
|
prefix,
|
|
1113
1182
|
queues: ["q9a", "q9b"],
|
|
@@ -1150,7 +1219,7 @@ test("idempotencyTtl: short TTL expires and allows a new run with same key", asy
|
|
|
1150
1219
|
return { ok: true };
|
|
1151
1220
|
});
|
|
1152
1221
|
|
|
1153
|
-
const worker = await startWorker({
|
|
1222
|
+
const worker = await startWorker({ app: "test-app",
|
|
1154
1223
|
redis,
|
|
1155
1224
|
prefix,
|
|
1156
1225
|
queues: ["q_idem_ttl"],
|
|
@@ -1199,7 +1268,7 @@ test("input validation: invalid input fails once and is not retried", async () =
|
|
|
1199
1268
|
},
|
|
1200
1269
|
);
|
|
1201
1270
|
|
|
1202
|
-
const worker = await startWorker({
|
|
1271
|
+
const worker = await startWorker({ app: "test-app",
|
|
1203
1272
|
redis,
|
|
1204
1273
|
prefix,
|
|
1205
1274
|
queues: ["q11"],
|
|
@@ -1234,7 +1303,7 @@ test("unknown workflow: run fails and is not retried", async () => {
|
|
|
1234
1303
|
const redis = new RedisClient(redisServer.url);
|
|
1235
1304
|
const client = createClient({ redis, prefix });
|
|
1236
1305
|
|
|
1237
|
-
const worker = await startWorker({
|
|
1306
|
+
const worker = await startWorker({ app: "test-app",
|
|
1238
1307
|
redis,
|
|
1239
1308
|
prefix,
|
|
1240
1309
|
queues: ["qx"],
|
|
@@ -1287,7 +1356,7 @@ test("cancel during step: run becomes canceled and step error kind is canceled",
|
|
|
1287
1356
|
return { ok: true };
|
|
1288
1357
|
});
|
|
1289
1358
|
|
|
1290
|
-
const worker = await startWorker({
|
|
1359
|
+
const worker = await startWorker({ app: "test-app",
|
|
1291
1360
|
redis,
|
|
1292
1361
|
prefix,
|
|
1293
1362
|
queues: ["q12"],
|
|
@@ -1345,7 +1414,7 @@ test("terminal run re-queued is ignored (no re-execution)", async () => {
|
|
|
1345
1414
|
return { ok: true };
|
|
1346
1415
|
});
|
|
1347
1416
|
|
|
1348
|
-
const worker = await startWorker({
|
|
1417
|
+
const worker = await startWorker({ app: "test-app",
|
|
1349
1418
|
redis,
|
|
1350
1419
|
prefix,
|
|
1351
1420
|
queues: [queue],
|
|
@@ -1393,7 +1462,7 @@ test("lease+reaper: long running step is not duplicated", async () => {
|
|
|
1393
1462
|
return { ok: true };
|
|
1394
1463
|
});
|
|
1395
1464
|
|
|
1396
|
-
const worker = await startWorker({
|
|
1465
|
+
const worker = await startWorker({ app: "test-app",
|
|
1397
1466
|
redis,
|
|
1398
1467
|
prefix,
|
|
1399
1468
|
queues: [queue],
|
|
@@ -1438,8 +1507,8 @@ test(
|
|
|
1438
1507
|
},
|
|
1439
1508
|
);
|
|
1440
1509
|
|
|
1441
|
-
const w1 = await startWorker({ redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
|
|
1442
|
-
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 } });
|
|
1443
1512
|
|
|
1444
1513
|
await new Promise((r) => setTimeout(r, 3500));
|
|
1445
1514
|
const count = Number((await redis.get(counterKey)) ?? "0");
|
|
@@ -1505,7 +1574,7 @@ test(
|
|
|
1505
1574
|
},
|
|
1506
1575
|
);
|
|
1507
1576
|
|
|
1508
|
-
const worker = await startWorker({
|
|
1577
|
+
const worker = await startWorker({ app: "test-app",
|
|
1509
1578
|
redis,
|
|
1510
1579
|
prefix,
|
|
1511
1580
|
queues: ["q_parent", "q_child"],
|
|
@@ -1560,7 +1629,7 @@ test(
|
|
|
1560
1629
|
const queuedMembers = await redis.zrange(queuedIndex, 0, -1);
|
|
1561
1630
|
expect(queuedMembers.includes(handle.id)).toBe(true);
|
|
1562
1631
|
|
|
1563
|
-
const worker = await startWorker({
|
|
1632
|
+
const worker = await startWorker({ app: "test-app",
|
|
1564
1633
|
redis,
|
|
1565
1634
|
prefix,
|
|
1566
1635
|
queues: [queue],
|
|
@@ -1606,7 +1675,7 @@ test(
|
|
|
1606
1675
|
cron: [{ expression: "*/1 * * * * *" }],
|
|
1607
1676
|
}, async () => ({ ok: true }));
|
|
1608
1677
|
|
|
1609
|
-
await client.syncRegistry(getDefaultRegistry());
|
|
1678
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
|
|
1610
1679
|
|
|
1611
1680
|
const workflowKey = `${prefix}:workflow:${name}`;
|
|
1612
1681
|
const cronIdsJson = await redis.hget(workflowKey, "cronIdsJson");
|
|
@@ -1624,7 +1693,7 @@ test(
|
|
|
1624
1693
|
queue: "q_update",
|
|
1625
1694
|
}, async () => ({ ok: true }));
|
|
1626
1695
|
|
|
1627
|
-
await client.syncRegistry(getDefaultRegistry());
|
|
1696
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
|
|
1628
1697
|
|
|
1629
1698
|
expect(await redis.hget(`${prefix}:cron:def`, cronId)).toBeNull();
|
|
1630
1699
|
const cronNext2 = await redis.zrange(`${prefix}:cron:next`, 0, -1);
|
|
@@ -1658,7 +1727,7 @@ test(
|
|
|
1658
1727
|
},
|
|
1659
1728
|
);
|
|
1660
1729
|
|
|
1661
|
-
const worker = await startWorker({
|
|
1730
|
+
const worker = await startWorker({ app: "test-app",
|
|
1662
1731
|
redis,
|
|
1663
1732
|
prefix,
|
|
1664
1733
|
queues: [queue],
|
|
@@ -1708,8 +1777,8 @@ test(
|
|
|
1708
1777
|
},
|
|
1709
1778
|
);
|
|
1710
1779
|
|
|
1711
|
-
const w1 = await startWorker({ redis, prefix, queues: [queue], concurrency: 1, runtime: { leaseMs: 500 } });
|
|
1712
|
-
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 } });
|
|
1713
1782
|
|
|
1714
1783
|
await waitFor(
|
|
1715
1784
|
async () => Number((await redis.get(counterKey)) ?? "0") >= 1,
|
|
@@ -1744,7 +1813,7 @@ test("listRuns: status + workflow filters are applied together", async () => {
|
|
|
1744
1813
|
throw new Error("expected failure");
|
|
1745
1814
|
});
|
|
1746
1815
|
|
|
1747
|
-
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 } });
|
|
1748
1817
|
|
|
1749
1818
|
const successHandle = await succeeds.run({});
|
|
1750
1819
|
const failHandle = await fails.run({});
|
|
@@ -1795,7 +1864,7 @@ test("cron trigger ids: same custom id in two workflows does not collide", async
|
|
|
1795
1864
|
async () => ({ ok: true }),
|
|
1796
1865
|
);
|
|
1797
1866
|
|
|
1798
|
-
await client.syncRegistry(getDefaultRegistry());
|
|
1867
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
|
|
1799
1868
|
|
|
1800
1869
|
const idsA = JSON.parse((await redis.hget(`${prefix}:workflow:cron-id-a`, "cronIdsJson")) ?? "[]") as string[];
|
|
1801
1870
|
const idsB = JSON.parse((await redis.hget(`${prefix}:workflow:cron-id-b`, "cronIdsJson")) ?? "[]") as string[];
|
|
@@ -1823,7 +1892,7 @@ test("syncRegistry: cron trigger with explicit null input preserves null payload
|
|
|
1823
1892
|
async () => ({ ok: true }),
|
|
1824
1893
|
);
|
|
1825
1894
|
|
|
1826
|
-
await client.syncRegistry(getDefaultRegistry());
|
|
1895
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
|
|
1827
1896
|
|
|
1828
1897
|
const workflowKey = `${prefix}:workflow:${workflowName}`;
|
|
1829
1898
|
const cronIds = JSON.parse((await redis.hget(workflowKey, "cronIdsJson")) ?? "[]") as string[];
|
|
@@ -1854,7 +1923,7 @@ test("syncRegistry: invalid cron expression removes existing next schedule for s
|
|
|
1854
1923
|
async () => ({ ok: true }),
|
|
1855
1924
|
);
|
|
1856
1925
|
|
|
1857
|
-
await client.syncRegistry(getDefaultRegistry());
|
|
1926
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
|
|
1858
1927
|
|
|
1859
1928
|
const cronIds = JSON.parse((await redis.hget(workflowKey, "cronIdsJson")) ?? "[]") as string[];
|
|
1860
1929
|
expect(cronIds.length).toBe(1);
|
|
@@ -1870,7 +1939,7 @@ test("syncRegistry: invalid cron expression removes existing next schedule for s
|
|
|
1870
1939
|
async () => ({ ok: true }),
|
|
1871
1940
|
);
|
|
1872
1941
|
|
|
1873
|
-
await client.syncRegistry(getDefaultRegistry());
|
|
1942
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
|
|
1874
1943
|
|
|
1875
1944
|
const after = await redis.zrange(cronNextKey, 0, -1);
|
|
1876
1945
|
expect(after.includes(cronId)).toBe(false);
|
|
@@ -1891,7 +1960,7 @@ test("syncRegistry: removes stale workflow metadata (cron)", async () => {
|
|
|
1891
1960
|
async () => ({ ok: true }),
|
|
1892
1961
|
);
|
|
1893
1962
|
|
|
1894
|
-
await client.syncRegistry(getDefaultRegistry());
|
|
1963
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
|
|
1895
1964
|
|
|
1896
1965
|
const workflowKey = `${prefix}:workflow:${workflowName}`;
|
|
1897
1966
|
const cronIds = JSON.parse((await redis.hget(workflowKey, "cronIdsJson")) ?? "[]") as string[];
|
|
@@ -1903,7 +1972,7 @@ test("syncRegistry: removes stale workflow metadata (cron)", async () => {
|
|
|
1903
1972
|
// Force stale age beyond grace period and sync an empty registry.
|
|
1904
1973
|
await redis.hset(workflowKey, { updatedAt: "1" });
|
|
1905
1974
|
__unstableResetDefaultRegistryForTests();
|
|
1906
|
-
await client.syncRegistry(getDefaultRegistry());
|
|
1975
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
|
|
1907
1976
|
|
|
1908
1977
|
expect((await redis.smembers(`${prefix}:workflows`)).includes(workflowName)).toBe(false);
|
|
1909
1978
|
expect(await redis.hget(workflowKey, "queue")).toBeNull();
|
|
@@ -1914,37 +1983,37 @@ test("syncRegistry: removes stale workflow metadata (cron)", async () => {
|
|
|
1914
1983
|
redis.close();
|
|
1915
1984
|
});
|
|
1916
1985
|
|
|
1917
|
-
test("syncRegistry: stale cleanup is isolated by
|
|
1986
|
+
test("syncRegistry: stale cleanup is isolated by app", async () => {
|
|
1918
1987
|
const prefix = testPrefix();
|
|
1919
1988
|
const redis = new RedisClient(redisServer.url);
|
|
1920
1989
|
const client = createClient({ redis, prefix });
|
|
1921
1990
|
|
|
1922
1991
|
__unstableResetDefaultRegistryForTests();
|
|
1923
|
-
defineWorkflow("
|
|
1992
|
+
defineWorkflow("app-a-wf", {queue: "qa",
|
|
1924
1993
|
cron: [{ id: "a", expression: "*/30 * * * * *" }],
|
|
1925
1994
|
},
|
|
1926
1995
|
async () => ({ ok: true }),
|
|
1927
1996
|
);
|
|
1928
|
-
await client.syncRegistry(getDefaultRegistry(), {
|
|
1997
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "svc-a" });
|
|
1929
1998
|
|
|
1930
1999
|
__unstableResetDefaultRegistryForTests();
|
|
1931
|
-
defineWorkflow("
|
|
2000
|
+
defineWorkflow("app-b-wf", {queue: "qb",
|
|
1932
2001
|
cron: [{ id: "b", expression: "*/30 * * * * *" }],
|
|
1933
2002
|
},
|
|
1934
2003
|
async () => ({ ok: true }),
|
|
1935
2004
|
);
|
|
1936
|
-
await client.syncRegistry(getDefaultRegistry(), {
|
|
2005
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "svc-b" });
|
|
1937
2006
|
|
|
1938
|
-
await redis.hset(`${prefix}:workflow:
|
|
1939
|
-
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" });
|
|
1940
2009
|
|
|
1941
2010
|
__unstableResetDefaultRegistryForTests();
|
|
1942
|
-
await client.syncRegistry(getDefaultRegistry(), {
|
|
2011
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "svc-a" });
|
|
1943
2012
|
|
|
1944
|
-
expect(await redis.hget(`${prefix}:workflow:
|
|
1945
|
-
expect(await redis.hget(`${prefix}:workflow:
|
|
1946
|
-
expect((await redis.smembers(`${prefix}:workflows`)).includes("
|
|
1947
|
-
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);
|
|
1948
2017
|
|
|
1949
2018
|
redis.close();
|
|
1950
2019
|
});
|
|
@@ -1957,7 +2026,7 @@ test("cancelRun: terminal runs are unchanged", async () => {
|
|
|
1957
2026
|
|
|
1958
2027
|
const wf = defineWorkflow("cancel-terminal", {queue: "q_cancel_terminal" }, async () => ({ ok: true }));
|
|
1959
2028
|
|
|
1960
|
-
const worker = await startWorker({
|
|
2029
|
+
const worker = await startWorker({ app: "test-app",
|
|
1961
2030
|
redis,
|
|
1962
2031
|
prefix,
|
|
1963
2032
|
queues: ["q_cancel_terminal"],
|
|
@@ -1992,7 +2061,7 @@ test("output serialization: non-JSON output fails once and is not stuck", async
|
|
|
1992
2061
|
async () => ({ bad: 1n }),
|
|
1993
2062
|
);
|
|
1994
2063
|
|
|
1995
|
-
const worker = await startWorker({
|
|
2064
|
+
const worker = await startWorker({ app: "test-app",
|
|
1996
2065
|
redis,
|
|
1997
2066
|
prefix,
|
|
1998
2067
|
queues: ["q_serial"],
|
|
@@ -2047,7 +2116,7 @@ test("retry step sequence: new steps keep monotonic order after cached steps", a
|
|
|
2047
2116
|
},
|
|
2048
2117
|
);
|
|
2049
2118
|
|
|
2050
|
-
const worker = await startWorker({
|
|
2119
|
+
const worker = await startWorker({ app: "test-app",
|
|
2051
2120
|
redis,
|
|
2052
2121
|
prefix,
|
|
2053
2122
|
queues: ["q_seq"],
|
|
@@ -2078,7 +2147,7 @@ test("listRuns: negative offset is clamped to zero", async () => {
|
|
|
2078
2147
|
setDefaultClient(client);
|
|
2079
2148
|
|
|
2080
2149
|
const wf = defineWorkflow("offset-wf", {queue: "q_offset" }, async () => ({ ok: true }));
|
|
2081
|
-
const worker = await startWorker({
|
|
2150
|
+
const worker = await startWorker({ app: "test-app",
|
|
2082
2151
|
redis,
|
|
2083
2152
|
prefix,
|
|
2084
2153
|
queues: ["q_offset"],
|
|
@@ -2142,7 +2211,7 @@ test("error serialization: non-serializable thrown values do not wedge a run", a
|
|
|
2142
2211
|
},
|
|
2143
2212
|
);
|
|
2144
2213
|
|
|
2145
|
-
const worker = await startWorker({
|
|
2214
|
+
const worker = await startWorker({ app: "test-app",
|
|
2146
2215
|
redis,
|
|
2147
2216
|
prefix,
|
|
2148
2217
|
queues: ["q_bigint"],
|
|
@@ -2185,7 +2254,7 @@ test("cron trigger ids: delimiter-like custom ids do not collide", async () => {
|
|
|
2185
2254
|
async () => ({ ok: true }),
|
|
2186
2255
|
);
|
|
2187
2256
|
|
|
2188
|
-
await client.syncRegistry(getDefaultRegistry());
|
|
2257
|
+
await client.syncRegistry(getDefaultRegistry(), { app: "test-app" });
|
|
2189
2258
|
|
|
2190
2259
|
const idsA = JSON.parse((await redis.hget(`${prefix}:workflow:wf:a`, "cronIdsJson")) ?? "[]") as string[];
|
|
2191
2260
|
const idsB = JSON.parse((await redis.hget(`${prefix}:workflow:wf:a:b`, "cronIdsJson")) ?? "[]") as string[];
|
|
@@ -2219,7 +2288,7 @@ test("idempotencyKey: stale pointer is recovered instead of returning missing ru
|
|
|
2219
2288
|
|
|
2220
2289
|
const wf = defineWorkflow(workflowName, {queue }, async () => ({ ok: true }));
|
|
2221
2290
|
|
|
2222
|
-
const worker = await startWorker({
|
|
2291
|
+
const worker = await startWorker({ app: "test-app",
|
|
2223
2292
|
redis,
|
|
2224
2293
|
prefix,
|
|
2225
2294
|
queues: [queue],
|
|
@@ -2262,7 +2331,7 @@ test("idempotencyKey: partial existing run is repaired and executed", async () =
|
|
|
2262
2331
|
return { ok: true };
|
|
2263
2332
|
});
|
|
2264
2333
|
|
|
2265
|
-
const worker = await startWorker({
|
|
2334
|
+
const worker = await startWorker({ app: "test-app",
|
|
2266
2335
|
redis,
|
|
2267
2336
|
prefix,
|
|
2268
2337
|
queues: [queue],
|
|
@@ -2314,7 +2383,7 @@ test("syncRegistry: duplicate custom cron ids in one workflow are rejected", asy
|
|
|
2314
2383
|
async () => ({ ok: true }),
|
|
2315
2384
|
);
|
|
2316
2385
|
|
|
2317
|
-
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");
|
|
2318
2387
|
|
|
2319
2388
|
redis.close();
|
|
2320
2389
|
});
|
|
@@ -2331,7 +2400,7 @@ test("scheduled promoter: stale scheduled entry does not resurrect terminal run"
|
|
|
2331
2400
|
return { ok: true };
|
|
2332
2401
|
});
|
|
2333
2402
|
|
|
2334
|
-
const worker = await startWorker({
|
|
2403
|
+
const worker = await startWorker({ app: "test-app",
|
|
2335
2404
|
redis,
|
|
2336
2405
|
prefix,
|
|
2337
2406
|
queues: [queue],
|
|
@@ -2398,7 +2467,7 @@ test(
|
|
|
2398
2467
|
|
|
2399
2468
|
let worker: Awaited<ReturnType<typeof startWorker>> | null = null;
|
|
2400
2469
|
try {
|
|
2401
|
-
worker = await startWorker({
|
|
2470
|
+
worker = await startWorker({ app: "test-app",
|
|
2402
2471
|
redis,
|
|
2403
2472
|
prefix,
|
|
2404
2473
|
queues: [queue],
|
|
@@ -2449,7 +2518,7 @@ test(
|
|
|
2449
2518
|
|
|
2450
2519
|
let worker: Awaited<ReturnType<typeof startWorker>> | null = null;
|
|
2451
2520
|
try {
|
|
2452
|
-
worker = await startWorker({
|
|
2521
|
+
worker = await startWorker({ app: "test-app",
|
|
2453
2522
|
redis,
|
|
2454
2523
|
prefix,
|
|
2455
2524
|
queues: [queue],
|
|
@@ -2492,7 +2561,7 @@ test("workflow names ending with ':runs' do not collide with workflow run indexe
|
|
|
2492
2561
|
|
|
2493
2562
|
expect(keys.workflow(prefix, "keyspace-base:runs")).not.toBe(keys.workflowRuns(prefix, "keyspace-base"));
|
|
2494
2563
|
|
|
2495
|
-
const worker = await startWorker({
|
|
2564
|
+
const worker = await startWorker({ app: "test-app",
|
|
2496
2565
|
redis,
|
|
2497
2566
|
prefix,
|
|
2498
2567
|
queues: [queue],
|
|
@@ -2537,7 +2606,7 @@ test("cancelRun race: near-finish cancellation settles as canceled", async () =>
|
|
|
2537
2606
|
return await originalFinalizeRun.call(this, runId, args);
|
|
2538
2607
|
}) as RedflowClient["finalizeRun"];
|
|
2539
2608
|
|
|
2540
|
-
const worker = await startWorker({
|
|
2609
|
+
const worker = await startWorker({ app: "test-app",
|
|
2541
2610
|
redis,
|
|
2542
2611
|
prefix,
|
|
2543
2612
|
queues: [queue],
|
|
@@ -2600,7 +2669,7 @@ test("cancelRun race: cancellation requested during finalize wins over success",
|
|
|
2600
2669
|
return await originalFinalizeRun.call(this, runId, args);
|
|
2601
2670
|
}) as RedflowClient["finalizeRun"];
|
|
2602
2671
|
|
|
2603
|
-
const worker = await startWorker({
|
|
2672
|
+
const worker = await startWorker({ app: "test-app",
|
|
2604
2673
|
redis,
|
|
2605
2674
|
prefix,
|
|
2606
2675
|
queues: [queue],
|
|
@@ -2650,7 +2719,7 @@ test(
|
|
|
2650
2719
|
await new Promise<never>(() => {});
|
|
2651
2720
|
});
|
|
2652
2721
|
|
|
2653
|
-
const worker = await startWorker({
|
|
2722
|
+
const worker = await startWorker({ app: "test-app",
|
|
2654
2723
|
redis,
|
|
2655
2724
|
prefix,
|
|
2656
2725
|
queues: [queue],
|