Skip to content

Commit 3167cdc

Browse files
committed
fix(webapp): dedupe bulk replay triggers
1 parent bfa902b commit 3167cdc

2 files changed

Lines changed: 78 additions & 1 deletion

File tree

apps/webapp/app/v3/services/replayTaskRun.server.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ export class ReplayTaskRunService extends BaseService {
6161
const payloadType = payloadPacket.dataType;
6262
const metadata = overrideOptions.metadata ?? (await this.getExistingMetadata(existingTaskRun));
6363
const tags = overrideOptions.tags ?? existingTaskRun.runTags;
64+
const idempotencyKey = overrideOptions.bulkActionId
65+
? `bulk-replay:${overrideOptions.bulkActionId}:${existingTaskRun.id}`
66+
: overrideOptions.idempotencyKey;
6467
// Only use the region from the existing run if V2 engine and neither environment is dev
6568
const ignoreRegion =
6669
existingTaskRun.engine === "V1" ||
@@ -100,7 +103,7 @@ export class ReplayTaskRunService extends BaseService {
100103
? new Date(Date.now() + overrideOptions.delaySeconds * 1000)
101104
: undefined,
102105
ttl: overrideOptions.ttlSeconds,
103-
idempotencyKey: overrideOptions.idempotencyKey,
106+
idempotencyKey,
104107
idempotencyKeyTTL: overrideOptions.idempotencyKeyTTLSeconds
105108
? `${overrideOptions.idempotencyKeyTTLSeconds}s`
106109
: undefined,
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
vi.mock("~/db.server", () => ({ prisma: {}, $replica: {} }));
4+
5+
vi.mock("~/models/runtimeEnvironment.server", () => ({
6+
findEnvironmentById: vi.fn(async () => ({
7+
id: "env_1",
8+
type: "PRODUCTION",
9+
archivedAt: null,
10+
})),
11+
}));
12+
13+
const triggerCall = vi.fn(async () => ({
14+
run: { id: "run_new", friendlyId: "run_new_friendly" },
15+
isCached: false,
16+
}));
17+
18+
vi.mock("~/v3/services/triggerTask.server", () => ({
19+
TriggerTaskService: class {
20+
call = triggerCall;
21+
},
22+
OutOfEntitlementError: class OutOfEntitlementError extends Error {},
23+
}));
24+
25+
import { ReplayTaskRunService } from "~/v3/services/replayTaskRun.server";
26+
27+
const SOURCE_RUN = {
28+
id: "run_source_internal",
29+
friendlyId: "run_source_friendly",
30+
taskIdentifier: "hello-world",
31+
runtimeEnvironmentId: "env_1",
32+
payload: JSON.stringify({ message: "hi" }),
33+
payloadType: "application/json",
34+
seedMetadata: null,
35+
seedMetadataType: "application/json",
36+
runTags: [],
37+
queue: "task/hello-world",
38+
workerQueue: "worker-queue-1",
39+
concurrencyKey: null,
40+
machinePreset: "small-1x",
41+
isTest: false,
42+
engine: "V2",
43+
region: null,
44+
traceId: "trace_1",
45+
spanId: "span_1",
46+
realtimeStreamsVersion: "v1",
47+
} as any;
48+
49+
function makeFakePrisma() {
50+
return {
51+
runtimeEnvironment: {
52+
findFirstOrThrow: vi.fn(async () => ({ id: "env_1", type: "PRODUCTION" })),
53+
},
54+
taskQueue: {
55+
findFirst: vi.fn(async () => null),
56+
},
57+
} as any;
58+
}
59+
60+
describe("ReplayTaskRunService idempotency (TRI-10467 fix #3)", () => {
61+
beforeEach(() => {
62+
triggerCall.mockClear();
63+
});
64+
65+
it("sets a stable idempotency key for bulk replay source runs", async () => {
66+
const service = new ReplayTaskRunService(makeFakePrisma());
67+
68+
await service.call(SOURCE_RUN, { bulkActionId: "bulk_1", triggerSource: "dashboard" });
69+
70+
expect(triggerCall.mock.calls[0][2].options.idempotencyKey).toBe(
71+
"bulk-replay:bulk_1:run_source_internal"
72+
);
73+
});
74+
});

0 commit comments

Comments
 (0)