Files
triggerdotdev--trigger.dev/apps/webapp/test/runsReplicationService.part2.test.ts
T
2026-07-13 13:32:57 +08:00

389 lines
13 KiB
TypeScript

import { ClickHouse } from "@internal/clickhouse";
import { replicationContainerTest } from "@internal/testcontainers";
import { Logger } from "@trigger.dev/core/logger";
import { setTimeout } from "node:timers/promises";
import { z } from "zod";
import { RunsReplicationService } from "~/services/runsReplicationService.server";
import { TestReplicationClickhouseFactory } from "./utils/testReplicationClickhouseFactory";
vi.setConfig({ testTimeout: 60_000 });
describe("RunsReplicationService (part 2/7)", () => {
replicationContainerTest(
"should handover leadership to a second service, and the second service should be able to extend the leader lock",
async ({ clickhouseContainer, redisOptions, postgresContainer, prisma }) => {
await prisma.$executeRawUnsafe(`ALTER TABLE public."TaskRun" REPLICA IDENTITY FULL;`);
const clickhouse = new ClickHouse({
url: clickhouseContainer.getConnectionUrl(),
name: "runs-replication-shutdown-handover",
logLevel: "warn",
});
// Service A
const runsReplicationServiceA = new RunsReplicationService({
clickhouseFactory: new TestReplicationClickhouseFactory(clickhouse),
pgConnectionUrl: postgresContainer.getConnectionUri(),
serviceName: "runs-replication-shutdown-handover",
slotName: "task_runs_to_clickhouse_v1",
publicationName: "task_runs_to_clickhouse_v1_publication",
redisOptions,
maxFlushConcurrency: 1,
flushIntervalMs: 100,
flushBatchSize: 1,
leaderLockTimeoutMs: 5000,
leaderLockExtendIntervalMs: 1000,
leaderLockAcquireAdditionalTimeMs: 10_000,
ackIntervalSeconds: 5,
logger: new Logger("runs-replication-shutdown-handover-a", "warn"),
});
await runsReplicationServiceA.start();
// Service A
const runsReplicationServiceB = new RunsReplicationService({
clickhouseFactory: new TestReplicationClickhouseFactory(clickhouse),
pgConnectionUrl: postgresContainer.getConnectionUri(),
serviceName: "runs-replication-shutdown-handover",
slotName: "task_runs_to_clickhouse_v1",
publicationName: "task_runs_to_clickhouse_v1_publication",
redisOptions,
maxFlushConcurrency: 1,
flushIntervalMs: 100,
flushBatchSize: 1,
leaderLockTimeoutMs: 5000,
leaderLockExtendIntervalMs: 1000,
leaderLockAcquireAdditionalTimeMs: 10_000,
ackIntervalSeconds: 5,
logger: new Logger("runs-replication-shutdown-handover-b", "warn"),
});
// Now we need to initiate starting the second service, and after 6 seconds, we need to shutdown the first service
await Promise.all([
setTimeout(6000).then(() => runsReplicationServiceA.stop()),
runsReplicationServiceB.start(),
]);
const organization = await prisma.organization.create({
data: {
title: "test",
slug: "test",
},
});
const project = await prisma.project.create({
data: {
name: "test",
slug: "test",
organizationId: organization.id,
externalRef: "test",
},
});
const runtimeEnvironment = await prisma.runtimeEnvironment.create({
data: {
slug: "test",
type: "DEVELOPMENT",
projectId: project.id,
organizationId: organization.id,
apiKey: "test",
pkApiKey: "test",
shortcode: "test",
},
});
// Now we insert a row into the table
const taskRun = await prisma.taskRun.create({
data: {
friendlyId: "run_1234",
taskIdentifier: "my-task",
payload: JSON.stringify({ foo: "bar" }),
traceId: "1234",
spanId: "1234",
queue: "test",
runtimeEnvironmentId: runtimeEnvironment.id,
projectId: project.id,
organizationId: organization.id,
environmentType: "DEVELOPMENT",
engine: "V2",
},
});
// Check that the row was replicated to clickhouse
const queryRuns = clickhouse.reader.query({
name: "runs-replication",
query: "SELECT * FROM trigger_dev.task_runs_v2",
schema: z.any(),
});
const result = await vi.waitFor(
async () => {
const [queryError, rows] = await queryRuns({});
expect(queryError).toBeNull();
expect(rows?.length).toBe(1);
return rows;
},
{ timeout: 30_000, interval: 250 }
);
expect(result?.[0]).toEqual(
expect.objectContaining({
run_id: taskRun.id,
friendly_id: taskRun.friendlyId,
task_identifier: taskRun.taskIdentifier,
environment_id: runtimeEnvironment.id,
project_id: project.id,
organization_id: organization.id,
environment_type: "DEVELOPMENT",
engine: "V2",
})
);
await runsReplicationServiceB.stop();
}
);
replicationContainerTest(
"should replicate all 1,000 TaskRuns inserted in bulk to ClickHouse",
async ({ clickhouseContainer, redisOptions, postgresContainer, prisma }) => {
await prisma.$executeRawUnsafe(`ALTER TABLE public."TaskRun" REPLICA IDENTITY FULL;`);
const clickhouse = new ClickHouse({
url: clickhouseContainer.getConnectionUrl(),
name: "runs-replication-stress-bulk-insert",
logLevel: "warn",
});
const runsReplicationService = new RunsReplicationService({
clickhouseFactory: new TestReplicationClickhouseFactory(clickhouse),
pgConnectionUrl: postgresContainer.getConnectionUri(),
serviceName: "runs-replication-stress-bulk-insert",
slotName: "task_runs_to_clickhouse_v1",
publicationName: "task_runs_to_clickhouse_v1_publication",
redisOptions,
maxFlushConcurrency: 10,
flushIntervalMs: 100,
flushBatchSize: 50,
leaderLockTimeoutMs: 5000,
leaderLockExtendIntervalMs: 1000,
ackIntervalSeconds: 5,
logLevel: "warn",
});
await runsReplicationService.start();
const organization = await prisma.organization.create({
data: {
title: "test-stress-bulk-insert",
slug: "test-stress-bulk-insert",
},
});
const project = await prisma.project.create({
data: {
name: "test-stress-bulk-insert",
slug: "test-stress-bulk-insert",
organizationId: organization.id,
externalRef: "test-stress-bulk-insert",
},
});
const runtimeEnvironment = await prisma.runtimeEnvironment.create({
data: {
slug: "test-stress-bulk-insert",
type: "DEVELOPMENT",
projectId: project.id,
organizationId: organization.id,
apiKey: "test-stress-bulk-insert",
pkApiKey: "test-stress-bulk-insert",
shortcode: "test-stress-bulk-insert",
},
});
// Prepare 1,000 unique TaskRuns
const now = Date.now();
const runsData = Array.from({ length: 1000 }, (_, i) => ({
friendlyId: `run_bulk_${now}_${i}`,
taskIdentifier: `my-task-bulk`,
payload: JSON.stringify({ bulk: i }),
payloadType: "application/json",
traceId: `bulk-${i}`,
spanId: `bulk-${i}`,
queue: "test-stress-bulk-insert",
runtimeEnvironmentId: runtimeEnvironment.id,
projectId: project.id,
organizationId: organization.id,
environmentType: "DEVELOPMENT" as const,
engine: "V2" as const,
status: "PENDING" as const,
attemptNumber: 1,
createdAt: new Date(now + i),
updatedAt: new Date(now + i),
}));
// Bulk insert
const created = await prisma.taskRun.createMany({ data: runsData });
expect(created.count).toBe(1000);
// Query ClickHouse for all runs using FINAL
const queryRuns = clickhouse.reader.query({
name: "runs-replication-stress-bulk-insert",
query: `SELECT run_id, friendly_id, trace_id, task_identifier FROM trigger_dev.task_runs_v2 FINAL`,
schema: z.any(),
});
const result = await vi.waitFor(
async () => {
const [queryError, rows] = await queryRuns({});
expect(queryError).toBeNull();
expect(rows?.length).toBe(1000);
return rows;
},
{ timeout: 30_000, interval: 250 }
);
// Check a few random runs for correctness
for (let i = 0; i < 10; i++) {
const idx = Math.floor(Math.random() * 1000);
const expected = runsData[idx];
const found = result?.find((r: any) => r.friendly_id === expected.friendlyId);
expect(found).toBeDefined();
expect(found).toEqual(
expect.objectContaining({
friendly_id: expected.friendlyId,
trace_id: expected.traceId,
task_identifier: expected.taskIdentifier,
})
);
}
await runsReplicationService.stop();
}
);
replicationContainerTest(
"should replicate all 1,000 TaskRuns inserted in bulk to ClickHouse with updates",
async ({ clickhouseContainer, redisOptions, postgresContainer, prisma }) => {
await prisma.$executeRawUnsafe(`ALTER TABLE public."TaskRun" REPLICA IDENTITY FULL;`);
const clickhouse = new ClickHouse({
url: clickhouseContainer.getConnectionUrl(),
name: "runs-replication-stress-bulk-insert",
logLevel: "warn",
});
const runsReplicationService = new RunsReplicationService({
clickhouseFactory: new TestReplicationClickhouseFactory(clickhouse),
pgConnectionUrl: postgresContainer.getConnectionUri(),
serviceName: "runs-replication-stress-bulk-insert",
slotName: "task_runs_to_clickhouse_v1",
publicationName: "task_runs_to_clickhouse_v1_publication",
redisOptions,
maxFlushConcurrency: 10,
flushIntervalMs: 100,
flushBatchSize: 50,
leaderLockTimeoutMs: 5000,
leaderLockExtendIntervalMs: 1000,
ackIntervalSeconds: 5,
logLevel: "warn",
});
await runsReplicationService.start();
const organization = await prisma.organization.create({
data: {
title: "test-stress-bulk-insert",
slug: "test-stress-bulk-insert",
},
});
const project = await prisma.project.create({
data: {
name: "test-stress-bulk-insert",
slug: "test-stress-bulk-insert",
organizationId: organization.id,
externalRef: "test-stress-bulk-insert",
},
});
const runtimeEnvironment = await prisma.runtimeEnvironment.create({
data: {
slug: "test-stress-bulk-insert",
type: "DEVELOPMENT",
projectId: project.id,
organizationId: organization.id,
apiKey: "test-stress-bulk-insert",
pkApiKey: "test-stress-bulk-insert",
shortcode: "test-stress-bulk-insert",
},
});
// Prepare 1,000 unique TaskRuns
const now = Date.now();
const runsData = Array.from({ length: 1000 }, (_, i) => ({
friendlyId: `run_bulk_${now}_${i}`,
taskIdentifier: `my-task-bulk`,
payload: JSON.stringify({ bulk: i }),
payloadType: "application/json",
traceId: `bulk-${i}`,
spanId: `bulk-${i}`,
queue: "test-stress-bulk-insert",
runtimeEnvironmentId: runtimeEnvironment.id,
projectId: project.id,
organizationId: organization.id,
environmentType: "DEVELOPMENT" as const,
engine: "V2" as const,
status: "PENDING" as const,
attemptNumber: 1,
createdAt: new Date(now + i),
updatedAt: new Date(now + i),
}));
// Bulk insert
const created = await prisma.taskRun.createMany({ data: runsData });
expect(created.count).toBe(1000);
// Update all the runs
await prisma.taskRun.updateMany({
data: { status: "COMPLETED_SUCCESSFULLY" },
});
// Query ClickHouse for all runs using FINAL
const queryRuns = clickhouse.reader.query({
name: "runs-replication-stress-bulk-insert",
query: `SELECT * FROM trigger_dev.task_runs_v2 FINAL`,
schema: z.any(),
});
await vi.waitFor(
async () => {
const [queryError, result] = await queryRuns({});
expect(queryError).toBeNull();
expect(result?.length).toBe(1000);
// Check a few random runs for correctness
for (let i = 0; i < 10; i++) {
const idx = Math.floor(Math.random() * 1000);
const expected = runsData[idx];
const found = result?.find((r: any) => r.friendly_id === expected.friendlyId);
expect(found).toBeDefined();
expect(found).toEqual(
expect.objectContaining({
friendly_id: expected.friendlyId,
trace_id: expected.traceId,
task_identifier: expected.taskIdentifier,
status: "COMPLETED_SUCCESSFULLY",
})
);
}
},
{ timeout: 30_000, interval: 250 }
);
await runsReplicationService.stop();
}
);
});