perf(runs-replication): Improve the CPU efficiency and throughput of the runs replication to clickhouse (#2866)

## Summary

Optimizes the runs replication service for better CPU efficiency and
throughput when inserting task runs into ClickHouse.

### Key Changes

- **Switch to compact array format** - Uses
`JSONCompactEachRowWithNames` instead of `JSONEachRow` for ClickHouse
inserts, reducing JSON serialization overhead
- **Type-safe tuple arrays** - Introduces `TaskRunInsertArray` and
`PayloadInsertArray` tuple types with compile-time column order
validation
- **Pre-sorted batch inserts** - Sorts inserts by primary key before
flushing for better ClickHouse insert performance
- **Programmatic index generation** - `TASK_RUN_INDEX` and
`PAYLOAD_INDEX` are generated from column arrays to prevent manual
synchronization errors

### Files Changed

- `runsReplicationService.server.ts` - Core optimization to use compact
array inserts
- `@internal/clickhouse` - Added `insertCompactRaw` method and tuple
types
- `taskRuns.ts` - Column definitions, index constants, and insert
functions

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eric Allam
2026-01-13 12:18:30 +00:00
committed by GitHub
parent c8686b5f1c
commit bb253400a2
15 changed files with 1630 additions and 493 deletions
@@ -1,4 +1,5 @@
import type { ClickHouse, RawTaskRunPayloadV1, TaskRunV2 } from "@internal/clickhouse";
import type { ClickHouse, TaskRunInsertArray, PayloadInsertArray } from "@internal/clickhouse";
import { getTaskRunField, getPayloadField } from "@internal/clickhouse";
import { type RedisOptions } from "@internal/redis";
import {
LogicalReplicationClient,
@@ -81,7 +82,7 @@ type TaskRunInsert = {
export type RunsReplicationServiceEvents = {
message: [{ lsn: string; message: PgoutputMessage; service: RunsReplicationService }];
batchFlushed: [
{ flushId: string; taskRunInserts: TaskRunV2[]; payloadInserts: RawTaskRunPayloadV1[] }
{ flushId: string; taskRunInserts: TaskRunInsertArray[]; payloadInserts: PayloadInsertArray[] }
];
};
@@ -171,12 +172,9 @@ export class RunsReplicationService {
description: "Insert retry attempts",
});
this._eventsProcessedCounter = this._meter.createCounter(
"runs_replication.events_processed",
{
description: "Replication events processed (inserts, updates, deletes)",
}
);
this._eventsProcessedCounter = this._meter.createCounter("runs_replication.events_processed", {
description: "Replication events processed (inserts, updates, deletes)",
});
this._flushDurationHistogram = this._meter.createHistogram(
"runs_replication.flush_duration_ms",
@@ -216,34 +214,18 @@ export class RunsReplicationService {
flushInterval: options.flushIntervalMs ?? 100,
maxConcurrency: options.maxFlushConcurrency ?? 100,
callback: this.#flushBatch.bind(this),
// we can do some pre-merging to reduce the amount of data we need to send to clickhouse
mergeBatch: (existingBatch: TaskRunInsert[], newBatch: TaskRunInsert[]) => {
const merged = new Map<string, TaskRunInsert>();
for (const item of existingBatch) {
const key = `${item.event}_${item.run.id}`;
merged.set(key, item);
// Key-based deduplication to reduce duplicates sent to ClickHouse
getKey: (item) => {
if (!item?.run?.id) {
this.logger.warn("Skipping replication event with null run", { event: item });
return null;
}
for (const item of newBatch) {
if (!item?.run?.id) {
this.logger.warn("Skipping replication event with null run", { event: item });
continue;
}
const key = `${item.event}_${item.run.id}`;
const existingItem = merged.get(key);
// Keep the run with the higher version (latest)
// and take the last occurrence for that version.
// Items originating from the same DB transaction have the same version.
if (!existingItem || item._version >= existingItem._version) {
merged.set(key, item);
}
}
return Array.from(merged.values());
return `${item.event}_${item.run.id}`;
},
// Keep the run with the higher version (latest)
// and take the last occurrence for that version.
// Items originating from the same DB transaction have the same version.
shouldReplace: (existing, incoming) => incoming._version >= existing._version,
logger: new Logger("ConcurrentFlushScheduler", options.logLevel ?? "info"),
tracer: options.tracer,
});
@@ -506,7 +488,7 @@ export class RunsReplicationService {
this._eventsProcessedCounter.add(1, { event_type: event.tag });
}
this.logger.info("handle_transaction", {
this.logger.debug("handle_transaction", {
transaction: {
xid: transaction.xid,
commitLsn: transaction.commitLsn,
@@ -578,32 +560,46 @@ export class RunsReplicationService {
const taskRunInserts = preparedInserts
.map(({ taskRunInsert }) => taskRunInsert)
.filter(Boolean)
.filter((x): x is TaskRunInsertArray => Boolean(x))
// batch inserts in clickhouse are more performant if the items
// are pre-sorted by the primary key
.sort((a, b) => {
if (a.organization_id !== b.organization_id) {
return a.organization_id < b.organization_id ? -1 : 1;
const aOrgId = getTaskRunField(a, "organization_id");
const bOrgId = getTaskRunField(b, "organization_id");
if (aOrgId !== bOrgId) {
return aOrgId < bOrgId ? -1 : 1;
}
if (a.project_id !== b.project_id) {
return a.project_id < b.project_id ? -1 : 1;
const aProjId = getTaskRunField(a, "project_id");
const bProjId = getTaskRunField(b, "project_id");
if (aProjId !== bProjId) {
return aProjId < bProjId ? -1 : 1;
}
if (a.environment_id !== b.environment_id) {
return a.environment_id < b.environment_id ? -1 : 1;
const aEnvId = getTaskRunField(a, "environment_id");
const bEnvId = getTaskRunField(b, "environment_id");
if (aEnvId !== bEnvId) {
return aEnvId < bEnvId ? -1 : 1;
}
if (a.created_at !== b.created_at) {
return a.created_at - b.created_at;
const aCreatedAt = getTaskRunField(a, "created_at");
const bCreatedAt = getTaskRunField(b, "created_at");
if (aCreatedAt !== bCreatedAt) {
return aCreatedAt - bCreatedAt;
}
return a.run_id < b.run_id ? -1 : 1;
const aRunId = getTaskRunField(a, "run_id");
const bRunId = getTaskRunField(b, "run_id");
if (aRunId === bRunId) return 0;
return aRunId < bRunId ? -1 : 1;
});
const payloadInserts = preparedInserts
.map(({ payloadInsert }) => payloadInsert)
.filter(Boolean)
.filter((x): x is PayloadInsertArray => Boolean(x))
// batch inserts in clickhouse are more performant if the items
// are pre-sorted by the primary key
.sort((a, b) => {
return a.run_id < b.run_id ? -1 : 1;
const aRunId = getPayloadField(a, "run_id");
const bRunId = getPayloadField(b, "run_id");
if (aRunId === bRunId) return 0;
return aRunId < bRunId ? -1 : 1;
});
span.setAttribute("task_run_inserts", taskRunInserts.length);
@@ -633,7 +629,6 @@ export class RunsReplicationService {
this.logger.error("Error inserting task run inserts", {
error: taskRunError,
flushId,
runIds: taskRunInserts.map((r) => r.run_id),
});
recordSpanError(span, taskRunError);
}
@@ -642,7 +637,6 @@ export class RunsReplicationService {
this.logger.error("Error inserting payload inserts", {
error: payloadError,
flushId,
runIds: payloadInserts.map((r) => r.run_id),
});
recordSpanError(span, payloadError);
}
@@ -760,26 +754,24 @@ export class RunsReplicationService {
#getClickhouseInsertSettings() {
if (this._insertStrategy === "insert") {
return {};
} else if (this._insertStrategy === "insert_async") {
return {
async_insert: 1 as const,
async_insert_max_data_size: "1000000",
async_insert_busy_timeout_ms: 1000,
wait_for_async_insert: this.options.waitForAsyncInsert ? (1 as const) : (0 as const),
};
}
return {
async_insert: 1 as const,
async_insert_max_data_size: "1000000",
async_insert_busy_timeout_ms: 1000,
wait_for_async_insert: this.options.waitForAsyncInsert ? (1 as const) : (0 as const),
};
}
async #insertTaskRunInserts(taskRunInserts: TaskRunV2[], attempt: number) {
async #insertTaskRunInserts(taskRunInserts: TaskRunInsertArray[], attempt: number) {
return await startSpan(this._tracer, "insertTaskRunsInserts", async (span) => {
const [insertError, insertResult] = await this.options.clickhouse.taskRuns.insert(
taskRunInserts,
{
const [insertError, insertResult] =
await this.options.clickhouse.taskRuns.insertCompactArrays(taskRunInserts, {
params: {
clickhouse_settings: this.#getClickhouseInsertSettings(),
},
}
);
});
if (insertError) {
this.logger.error("Error inserting task run inserts attempt", {
@@ -795,16 +787,14 @@ export class RunsReplicationService {
});
}
async #insertPayloadInserts(payloadInserts: RawTaskRunPayloadV1[], attempt: number) {
async #insertPayloadInserts(payloadInserts: PayloadInsertArray[], attempt: number) {
return await startSpan(this._tracer, "insertPayloadInserts", async (span) => {
const [insertError, insertResult] = await this.options.clickhouse.taskRuns.insertPayloads(
payloadInserts,
{
const [insertError, insertResult] =
await this.options.clickhouse.taskRuns.insertPayloadsCompactArrays(payloadInserts, {
params: {
clickhouse_settings: this.#getClickhouseInsertSettings(),
},
}
);
});
if (insertError) {
this.logger.error("Error inserting payload inserts attempt", {
@@ -822,25 +812,15 @@ export class RunsReplicationService {
async #prepareRunInserts(
batchedRun: TaskRunInsert
): Promise<{ taskRunInsert?: TaskRunV2; payloadInsert?: RawTaskRunPayloadV1 }> {
): Promise<{ taskRunInsert?: TaskRunInsertArray; payloadInsert?: PayloadInsertArray }> {
this.logger.debug("Preparing run", {
batchedRun,
});
const { run, _version, event } = batchedRun;
if (!run.environmentType) {
return {
taskRunInsert: undefined,
payloadInsert: undefined,
};
}
if (!run.organizationId) {
return {
taskRunInsert: undefined,
payloadInsert: undefined,
};
if (!run.environmentType || !run.organizationId) {
return {};
}
if (event === "update" || event === "delete" || this._disablePayloadInsert) {
@@ -852,10 +832,7 @@ export class RunsReplicationService {
_version
);
return {
taskRunInsert,
payloadInsert: undefined,
};
return { taskRunInsert };
}
const [taskRunInsert, payloadInsert] = await Promise.all([
@@ -863,10 +840,7 @@ export class RunsReplicationService {
this.#preparePayloadInsert(run, _version),
]);
return {
taskRunInsert,
payloadInsert,
};
return { taskRunInsert, payloadInsert };
}
async #prepareTaskRunInsert(
@@ -875,66 +849,68 @@ export class RunsReplicationService {
environmentType: string,
event: "insert" | "update" | "delete",
_version: bigint
): Promise<TaskRunV2> {
): Promise<TaskRunInsertArray> {
const output = await this.#prepareJson(run.output, run.outputType);
return {
environment_id: run.runtimeEnvironmentId,
organization_id: organizationId,
project_id: run.projectId,
run_id: run.id,
updated_at: run.updatedAt.getTime(),
created_at: run.createdAt.getTime(),
status: run.status,
environment_type: environmentType,
friendly_id: run.friendlyId,
engine: run.engine,
task_identifier: run.taskIdentifier,
queue: run.queue,
span_id: run.spanId,
trace_id: run.traceId,
error: { data: run.error },
attempt: run.attemptNumber ?? 1,
schedule_id: run.scheduleId ?? "",
batch_id: run.batchId ?? "",
completed_at: run.completedAt?.getTime(),
started_at: run.startedAt?.getTime(),
executed_at: run.executedAt?.getTime(),
delay_until: run.delayUntil?.getTime(),
queued_at: run.queuedAt?.getTime(),
expired_at: run.expiredAt?.getTime(),
usage_duration_ms: run.usageDurationMs,
cost_in_cents: run.costInCents,
base_cost_in_cents: run.baseCostInCents,
tags: run.runTags ?? [],
task_version: run.taskVersion ?? "",
sdk_version: run.sdkVersion ?? "",
cli_version: run.cliVersion ?? "",
machine_preset: run.machinePreset ?? "",
root_run_id: run.rootTaskRunId ?? "",
parent_run_id: run.parentTaskRunId ?? "",
depth: run.depth,
is_test: run.isTest,
idempotency_key: run.idempotencyKey ?? "",
expiration_ttl: run.ttl ?? "",
output,
concurrency_key: run.concurrencyKey ?? "",
bulk_action_group_ids: run.bulkActionGroupIds ?? [],
worker_queue: run.masterQueue,
max_duration_in_seconds: run.maxDurationInSeconds ?? undefined,
_version: _version.toString(),
_is_deleted: event === "delete" ? 1 : 0,
};
// Return array matching TASK_RUN_COLUMNS order
return [
run.runtimeEnvironmentId, // environment_id
organizationId, // organization_id
run.projectId, // project_id
run.id, // run_id
run.updatedAt.getTime(), // updated_at
run.createdAt.getTime(), // created_at
run.status, // status
environmentType, // environment_type
run.friendlyId, // friendly_id
run.attemptNumber ?? 1, // attempt
run.engine, // engine
run.taskIdentifier, // task_identifier
run.queue, // queue
run.scheduleId ?? "", // schedule_id
run.batchId ?? "", // batch_id
run.completedAt?.getTime() ?? null, // completed_at
run.startedAt?.getTime() ?? null, // started_at
run.executedAt?.getTime() ?? null, // executed_at
run.delayUntil?.getTime() ?? null, // delay_until
run.queuedAt?.getTime() ?? null, // queued_at
run.expiredAt?.getTime() ?? null, // expired_at
run.usageDurationMs ?? 0, // usage_duration_ms
run.costInCents ?? 0, // cost_in_cents
run.baseCostInCents ?? 0, // base_cost_in_cents
output, // output
{ data: run.error }, // error
run.runTags ?? [], // tags
run.taskVersion ?? "", // task_version
run.sdkVersion ?? "", // sdk_version
run.cliVersion ?? "", // cli_version
run.machinePreset ?? "", // machine_preset
run.rootTaskRunId ?? "", // root_run_id
run.parentTaskRunId ?? "", // parent_run_id
run.depth ?? 0, // depth
run.spanId, // span_id
run.traceId, // trace_id
run.idempotencyKey ?? "", // idempotency_key
run.ttl ?? "", // expiration_ttl
run.isTest ?? false, // is_test
_version.toString(), // _version
event === "delete" ? 1 : 0, // _is_deleted
run.concurrencyKey ?? "", // concurrency_key
run.bulkActionGroupIds ?? [], // bulk_action_group_ids
run.masterQueue ?? "", // worker_queue
run.maxDurationInSeconds ?? null, // max_duration_in_seconds
];
}
async #preparePayloadInsert(run: TaskRun, _version: bigint): Promise<RawTaskRunPayloadV1> {
async #preparePayloadInsert(run: TaskRun, _version: bigint): Promise<PayloadInsertArray> {
const payload = await this.#prepareJson(run.payload, run.payloadType);
return {
run_id: run.id,
created_at: run.createdAt.getTime(),
payload,
};
// Return array matching PAYLOAD_COLUMNS order
return [
run.id, // run_id
run.createdAt.getTime(), // created_at
payload, // payload
];
}
async #prepareJson(
@@ -982,13 +958,16 @@ export type ConcurrentFlushSchedulerConfig<T> = {
flushInterval: number;
maxConcurrency?: number;
callback: (flushId: string, batch: T[]) => Promise<void>;
mergeBatch?: (existingBatch: T[], newBatch: T[]) => T[];
/** Key-based deduplication. Return null to skip the item. */
getKey: (item: T) => string | null;
/** Determine if incoming item should replace existing. */
shouldReplace: (existing: T, incoming: T) => boolean;
tracer?: Tracer;
logger?: Logger;
};
export class ConcurrentFlushScheduler<T> {
private currentBatch: T[];
private batch = new Map<string, T>();
private readonly BATCH_SIZE: number;
private readonly flushInterval: number;
private readonly MAX_CONCURRENCY: number;
@@ -1003,7 +982,6 @@ export class ConcurrentFlushScheduler<T> {
this.logger = config.logger ?? new Logger("ConcurrentFlushScheduler", "info");
this._tracer = config.tracer ?? trace.getTracer("concurrent-flush-scheduler");
this.currentBatch = [];
this.BATCH_SIZE = config.batchSize;
this.flushInterval = config.flushInterval;
this.MAX_CONCURRENCY = config.maxConcurrency || 1;
@@ -1013,9 +991,17 @@ export class ConcurrentFlushScheduler<T> {
}
addToBatch(items: T[]): void {
this.currentBatch = this.config.mergeBatch
? this.config.mergeBatch(this.currentBatch, items)
: this.currentBatch.concat(items);
for (const item of items) {
const key = this.config.getKey(item);
if (key === null) {
continue;
}
const existing = this.batch.get(key);
if (!existing || this.config.shouldReplace(existing, item)) {
this.batch.set(key, item);
}
}
this.#flushNextBatchIfNeeded();
}
@@ -1039,11 +1025,16 @@ export class ConcurrentFlushScheduler<T> {
this.#flushNextBatchIfNeeded();
}
#getBatchSize(): number {
return this.batch.size;
}
#flushNextBatchIfNeeded(): void {
if (this.currentBatch.length >= this.BATCH_SIZE || this._isShutDown) {
const currentSize = this.#getBatchSize();
if (currentSize >= this.BATCH_SIZE || this._isShutDown) {
this.logger.debug("Batch size threshold reached, initiating flush", {
batchSize: this.BATCH_SIZE,
currentSize: this.currentBatch.length,
currentSize,
isShutDown: this._isShutDown,
});
@@ -1068,19 +1059,20 @@ export class ConcurrentFlushScheduler<T> {
}
async #checkAndFlush(): Promise<void> {
if (this.currentBatch.length > 0) {
const currentSize = this.#getBatchSize();
if (currentSize > 0) {
this.logger.debug("Periodic flush check triggered", {
currentBatchSize: this.currentBatch.length,
currentBatchSize: currentSize,
});
await this.#flushNextBatch();
}
}
async #flushNextBatch(): Promise<void> {
if (this.currentBatch.length === 0) return;
if (this.batch.size === 0) return;
const batch = this.currentBatch;
this.currentBatch = [];
const batch = Array.from(this.batch.values());
this.batch.clear();
const callback = this.config.callback;
@@ -0,0 +1,161 @@
import { ConcurrentFlushScheduler } from "~/services/runsReplicationService.server";
vi.setConfig({ testTimeout: 10_000 });
type TestItem = {
id: string;
event: "insert" | "update";
version: number;
};
describe("ConcurrentFlushScheduler", () => {
it("should deduplicate items by key, keeping the latest version", async () => {
const flushedBatches: TestItem[][] = [];
const scheduler = new ConcurrentFlushScheduler<TestItem>({
batchSize: 100,
flushInterval: 50,
maxConcurrency: 1,
callback: async (_flushId, batch) => {
flushedBatches.push([...batch]);
},
getKey: (item) => `${item.event}_${item.id}`,
shouldReplace: (existing, incoming) => incoming.version >= existing.version,
});
scheduler.start();
// Add items with duplicate keys but different versions
scheduler.addToBatch([
{ id: "run_1", event: "insert", version: 1 },
{ id: "run_1", event: "update", version: 2 },
{ id: "run_2", event: "insert", version: 1 },
]);
// Add more items - should merge with existing
scheduler.addToBatch([
{ id: "run_1", event: "insert", version: 3 }, // Higher version, should replace
{ id: "run_1", event: "update", version: 1 }, // Lower version, should NOT replace
{ id: "run_2", event: "update", version: 4 },
]);
// Wait for flush
await new Promise((resolve) => setTimeout(resolve, 100));
scheduler.shutdown();
// Should have flushed once with deduplicated items
expect(flushedBatches.length).toBeGreaterThanOrEqual(1);
const allFlushed = flushedBatches.flat();
// Find items by their key
const insertRun1 = allFlushed.find((i) => i.id === "run_1" && i.event === "insert");
const updateRun1 = allFlushed.find((i) => i.id === "run_1" && i.event === "update");
const insertRun2 = allFlushed.find((i) => i.id === "run_2" && i.event === "insert");
const updateRun2 = allFlushed.find((i) => i.id === "run_2" && i.event === "update");
// Verify correct versions were kept
expect(insertRun1?.version).toBe(3); // Latest version for insert_run_1
expect(updateRun1?.version).toBe(2); // Original update_run_1 (v1 didn't replace v2)
expect(insertRun2?.version).toBe(1); // Only version for insert_run_2
expect(updateRun2?.version).toBe(4); // Only version for update_run_2
});
it("should skip items where getKey returns null", async () => {
const flushedBatches: TestItem[][] = [];
const scheduler = new ConcurrentFlushScheduler<TestItem>({
batchSize: 100,
flushInterval: 50,
maxConcurrency: 1,
callback: async (_flushId, batch) => {
flushedBatches.push([...batch]);
},
getKey: (item) => {
if (!item.id) {
return null;
}
return `${item.event}_${item.id}`;
},
shouldReplace: (existing, incoming) => incoming.version >= existing.version,
});
scheduler.start();
scheduler.addToBatch([
{ id: "run_1", event: "insert", version: 1 },
{ id: "", event: "insert", version: 2 }, // Should be skipped (null key)
{ id: "run_2", event: "insert", version: 1 },
]);
await new Promise((resolve) => setTimeout(resolve, 100));
scheduler.shutdown();
const allFlushed = flushedBatches.flat();
expect(allFlushed).toHaveLength(2);
expect(allFlushed.map((i) => i.id).sort()).toEqual(["run_1", "run_2"]);
});
it("should flush when batch size threshold is reached", async () => {
const flushedBatches: TestItem[][] = [];
const scheduler = new ConcurrentFlushScheduler<TestItem>({
batchSize: 3,
flushInterval: 10000, // Long interval so timer doesn't trigger
maxConcurrency: 1,
callback: async (_flushId, batch) => {
flushedBatches.push([...batch]);
},
getKey: (item) => `${item.event}_${item.id}`,
shouldReplace: (existing, incoming) => incoming.version >= existing.version,
});
scheduler.start();
// Add 3 unique items - should trigger flush
scheduler.addToBatch([
{ id: "run_1", event: "insert", version: 1 },
{ id: "run_2", event: "insert", version: 1 },
{ id: "run_3", event: "insert", version: 1 },
]);
await new Promise((resolve) => setTimeout(resolve, 50));
expect(flushedBatches.length).toBe(1);
expect(flushedBatches[0]).toHaveLength(3);
scheduler.shutdown();
});
it("should respect shouldReplace returning false", async () => {
const flushedBatches: TestItem[][] = [];
const scheduler = new ConcurrentFlushScheduler<TestItem>({
batchSize: 100,
flushInterval: 50,
maxConcurrency: 1,
callback: async (_flushId, batch) => {
flushedBatches.push([...batch]);
},
getKey: (item) => `${item.event}_${item.id}`,
// Never replace - first item wins
shouldReplace: () => false,
});
scheduler.start();
scheduler.addToBatch([{ id: "run_1", event: "insert", version: 10 }]);
scheduler.addToBatch([{ id: "run_1", event: "insert", version: 999 }]);
await new Promise((resolve) => setTimeout(resolve, 100));
scheduler.shutdown();
const allFlushed = flushedBatches.flat();
const insertRun1 = allFlushed.find((i) => i.id === "run_1" && i.event === "insert");
expect(insertRun1?.version).toBe(10); // First one wins
});
});
@@ -1,6 +1,5 @@
import { ClickHouse } from "@internal/clickhouse";
import { containerTest } from "@internal/testcontainers";
import { Logger } from "@trigger.dev/core/logger";
import { setTimeout } from "node:timers/promises";
import { z } from "zod";
import { TaskRunStatus } from "~/database-types";
@@ -22,6 +21,7 @@ describe("RunsReplicationService (part 1/2)", () => {
compression: {
request: true,
},
logLevel: "warn",
});
const { tracer, exporter } = createInMemoryTracing();
@@ -40,6 +40,7 @@ describe("RunsReplicationService (part 1/2)", () => {
leaderLockExtendIntervalMs: 1000,
ackIntervalSeconds: 5,
tracer,
logLevel: "warn",
});
await runsReplicationService.start();
@@ -135,6 +136,7 @@ describe("RunsReplicationService (part 1/2)", () => {
compression: {
request: true,
},
logLevel: "warn",
});
const { tracer, exporter } = createInMemoryTracing();
@@ -153,6 +155,7 @@ describe("RunsReplicationService (part 1/2)", () => {
leaderLockExtendIntervalMs: 1000,
ackIntervalSeconds: 5,
tracer,
logLevel: "warn",
});
await runsReplicationService.start();
@@ -271,6 +274,7 @@ describe("RunsReplicationService (part 1/2)", () => {
const clickhouse = new ClickHouse({
url: clickhouseContainer.getConnectionUrl(),
name: "runs-replication",
logLevel: "warn",
});
const { tracer, exporter } = createInMemoryTracing();
@@ -289,6 +293,7 @@ describe("RunsReplicationService (part 1/2)", () => {
leaderLockExtendIntervalMs: 1000,
ackIntervalSeconds: 5,
tracer,
logLevel: "warn",
});
await runsReplicationService.start();
@@ -341,6 +346,7 @@ describe("RunsReplicationService (part 1/2)", () => {
const clickhouse = new ClickHouse({
url: clickhouseContainer.getConnectionUrl(),
name: "runs-replication-batching",
logLevel: "warn",
});
const runsReplicationService = new RunsReplicationService({
@@ -356,6 +362,7 @@ describe("RunsReplicationService (part 1/2)", () => {
leaderLockTimeoutMs: 5000,
leaderLockExtendIntervalMs: 1000,
ackIntervalSeconds: 5,
logLevel: "warn",
});
await runsReplicationService.start();
@@ -443,6 +450,7 @@ describe("RunsReplicationService (part 1/2)", () => {
const clickhouse = new ClickHouse({
url: clickhouseContainer.getConnectionUrl(),
name: "runs-replication-payload",
logLevel: "warn",
});
const runsReplicationService = new RunsReplicationService({
@@ -458,6 +466,7 @@ describe("RunsReplicationService (part 1/2)", () => {
leaderLockTimeoutMs: 5000,
leaderLockExtendIntervalMs: 1000,
ackIntervalSeconds: 5,
logLevel: "warn",
});
await runsReplicationService.start();
@@ -542,6 +551,7 @@ describe("RunsReplicationService (part 1/2)", () => {
const clickhouse = new ClickHouse({
url: clickhouseContainer.getConnectionUrl(),
name: "runs-replication-payload",
logLevel: "warn",
});
const runsReplicationService = new RunsReplicationService({
@@ -557,6 +567,7 @@ describe("RunsReplicationService (part 1/2)", () => {
leaderLockTimeoutMs: 5000,
leaderLockExtendIntervalMs: 1000,
ackIntervalSeconds: 5,
logLevel: "warn",
});
await runsReplicationService.start();
@@ -646,6 +657,7 @@ describe("RunsReplicationService (part 1/2)", () => {
const clickhouse = new ClickHouse({
url: clickhouseContainer.getConnectionUrl(),
name: "runs-replication-update",
logLevel: "warn",
});
const runsReplicationService = new RunsReplicationService({
@@ -661,6 +673,7 @@ describe("RunsReplicationService (part 1/2)", () => {
leaderLockTimeoutMs: 5000,
leaderLockExtendIntervalMs: 1000,
ackIntervalSeconds: 5,
logLevel: "warn",
});
await runsReplicationService.start();
@@ -751,6 +764,7 @@ describe("RunsReplicationService (part 1/2)", () => {
const clickhouse = new ClickHouse({
url: clickhouseContainer.getConnectionUrl(),
name: "runs-replication-delete",
logLevel: "warn",
});
const runsReplicationService = new RunsReplicationService({
@@ -766,6 +780,7 @@ describe("RunsReplicationService (part 1/2)", () => {
leaderLockTimeoutMs: 5000,
leaderLockExtendIntervalMs: 1000,
ackIntervalSeconds: 5,
logLevel: "warn",
});
await runsReplicationService.start();
@@ -849,6 +864,7 @@ describe("RunsReplicationService (part 1/2)", () => {
const clickhouse = new ClickHouse({
url: clickhouseContainer.getConnectionUrl(),
name: "runs-replication-shutdown-handover",
logLevel: "warn",
});
// Service A
@@ -865,6 +881,7 @@ describe("RunsReplicationService (part 1/2)", () => {
leaderLockTimeoutMs: 5000,
leaderLockExtendIntervalMs: 1000,
ackIntervalSeconds: 5,
logLevel: "warn",
});
await runsReplicationServiceA.start();
@@ -968,6 +985,7 @@ describe("RunsReplicationService (part 1/2)", () => {
leaderLockTimeoutMs: 5000,
leaderLockExtendIntervalMs: 1000,
ackIntervalSeconds: 5,
logLevel: "warn",
});
await runsReplicationServiceB.start();
@@ -997,6 +1015,7 @@ describe("RunsReplicationService (part 1/2)", () => {
const clickhouse = new ClickHouse({
url: clickhouseContainer.getConnectionUrl(),
name: "runs-replication-shutdown-after-processed",
logLevel: "warn",
});
// Service A
@@ -1013,6 +1032,7 @@ describe("RunsReplicationService (part 1/2)", () => {
leaderLockTimeoutMs: 5000,
leaderLockExtendIntervalMs: 1000,
ackIntervalSeconds: 5,
logLevel: "warn",
});
await runsReplicationServiceA.start();
@@ -1114,6 +1134,7 @@ describe("RunsReplicationService (part 1/2)", () => {
leaderLockTimeoutMs: 5000,
leaderLockExtendIntervalMs: 1000,
ackIntervalSeconds: 5,
logLevel: "warn",
});
await runsReplicationServiceB.start();
@@ -1137,6 +1158,7 @@ describe("RunsReplicationService (part 1/2)", () => {
const clickhouse = new ClickHouse({
url: clickhouseContainer.getConnectionUrl(),
name: "runs-replication-metrics",
logLevel: "warn",
});
const { tracer } = createInMemoryTracing();
@@ -1157,6 +1179,7 @@ describe("RunsReplicationService (part 1/2)", () => {
ackIntervalSeconds: 5,
tracer,
meter: metricsHelper.meter,
logLevel: "warn",
});
await runsReplicationService.start();
@@ -1,4 +1,4 @@
import { ClickHouse } from "@internal/clickhouse";
import { ClickHouse, getTaskRunField, getPayloadField } from "@internal/clickhouse";
import { containerTest } from "@internal/testcontainers";
import { Logger } from "@trigger.dev/core/logger";
import { readFile } from "node:fs/promises";
@@ -18,6 +18,7 @@ describe("RunsReplicationService (part 2/2)", () => {
const clickhouse = new ClickHouse({
url: clickhouseContainer.getConnectionUrl(),
name: "runs-replication-shutdown-handover",
logLevel: "warn",
});
// Service A
@@ -35,7 +36,7 @@ describe("RunsReplicationService (part 2/2)", () => {
leaderLockExtendIntervalMs: 1000,
leaderLockAcquireAdditionalTimeMs: 10_000,
ackIntervalSeconds: 5,
logger: new Logger("runs-replication-shutdown-handover-a", "debug"),
logger: new Logger("runs-replication-shutdown-handover-a", "warn"),
});
await runsReplicationServiceA.start();
@@ -55,7 +56,7 @@ describe("RunsReplicationService (part 2/2)", () => {
leaderLockExtendIntervalMs: 1000,
leaderLockAcquireAdditionalTimeMs: 10_000,
ackIntervalSeconds: 5,
logger: new Logger("runs-replication-shutdown-handover-b", "debug"),
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
@@ -147,6 +148,7 @@ describe("RunsReplicationService (part 2/2)", () => {
const clickhouse = new ClickHouse({
url: clickhouseContainer.getConnectionUrl(),
name: "runs-replication-stress-bulk-insert",
logLevel: "warn",
});
const runsReplicationService = new RunsReplicationService({
@@ -162,7 +164,7 @@ describe("RunsReplicationService (part 2/2)", () => {
leaderLockTimeoutMs: 5000,
leaderLockExtendIntervalMs: 1000,
ackIntervalSeconds: 5,
logger: new Logger("runs-replication-stress-bulk-insert", "info"),
logLevel: "warn",
});
await runsReplicationService.start();
@@ -261,6 +263,7 @@ describe("RunsReplicationService (part 2/2)", () => {
const clickhouse = new ClickHouse({
url: clickhouseContainer.getConnectionUrl(),
name: "runs-replication-stress-bulk-insert",
logLevel: "warn",
});
const runsReplicationService = new RunsReplicationService({
@@ -276,7 +279,7 @@ describe("RunsReplicationService (part 2/2)", () => {
leaderLockTimeoutMs: 5000,
leaderLockExtendIntervalMs: 1000,
ackIntervalSeconds: 5,
logger: new Logger("runs-replication-stress-bulk-insert", "info"),
logLevel: "warn",
});
await runsReplicationService.start();
@@ -381,6 +384,7 @@ describe("RunsReplicationService (part 2/2)", () => {
const clickhouse = new ClickHouse({
url: clickhouseContainer.getConnectionUrl(),
name: "runs-replication-multi-event-tx",
logLevel: "warn",
});
const runsReplicationService = new RunsReplicationService({
@@ -396,6 +400,7 @@ describe("RunsReplicationService (part 2/2)", () => {
leaderLockTimeoutMs: 5000,
leaderLockExtendIntervalMs: 1000,
ackIntervalSeconds: 5,
logLevel: "warn",
});
await runsReplicationService.start();
@@ -513,6 +518,7 @@ describe("RunsReplicationService (part 2/2)", () => {
const clickhouse = new ClickHouse({
url: clickhouseContainer.getConnectionUrl(),
name: "runs-replication-long-tx",
logLevel: "warn",
});
const runsReplicationService = new RunsReplicationService({
@@ -528,7 +534,7 @@ describe("RunsReplicationService (part 2/2)", () => {
leaderLockTimeoutMs: 5000,
leaderLockExtendIntervalMs: 1000,
ackIntervalSeconds: 5,
logger: new Logger("runs-replication-long-tx", "info"),
logLevel: "warn",
});
await runsReplicationService.start();
@@ -619,6 +625,7 @@ describe("RunsReplicationService (part 2/2)", () => {
const clickhouse = new ClickHouse({
url: clickhouseContainer.getConnectionUrl(),
name: "runs-replication-stress-bulk-insert",
logLevel: "warn",
});
const runsReplicationService = new RunsReplicationService({
@@ -634,7 +641,7 @@ describe("RunsReplicationService (part 2/2)", () => {
leaderLockTimeoutMs: 5000,
leaderLockExtendIntervalMs: 1000,
ackIntervalSeconds: 5,
logger: new Logger("runs-replication-stress-bulk-insert", "info"),
logLevel: "warn",
});
await runsReplicationService.start();
@@ -787,6 +794,7 @@ describe("RunsReplicationService (part 2/2)", () => {
const clickhouse = new ClickHouse({
url: clickhouseContainer.getConnectionUrl(),
name: "runs-replication-merge-batch",
logLevel: "warn",
});
const runsReplicationService = new RunsReplicationService({
@@ -802,7 +810,7 @@ describe("RunsReplicationService (part 2/2)", () => {
leaderLockTimeoutMs: 5000,
leaderLockExtendIntervalMs: 1000,
ackIntervalSeconds: 5,
logger: new Logger("runs-replication-merge-batch", "info"),
logLevel: "warn",
});
// Listen to batchFlushed events to verify merging
@@ -889,17 +897,14 @@ describe("RunsReplicationService (part 2/2)", () => {
await setTimeout(1000);
expect(batchFlushedEvents?.[0].taskRunInserts).toHaveLength(2);
expect(batchFlushedEvents?.[0].taskRunInserts[0]).toEqual(
expect.objectContaining({
run_id: run.id,
status: "PENDING_VERSION",
})
// Use getTaskRunField for type-safe array access
expect(getTaskRunField(batchFlushedEvents![0].taskRunInserts[0], "run_id")).toEqual(run.id);
expect(getTaskRunField(batchFlushedEvents![0].taskRunInserts[0], "status")).toEqual(
"PENDING_VERSION"
);
expect(batchFlushedEvents?.[0].taskRunInserts[1]).toEqual(
expect.objectContaining({
run_id: run.id,
status: "COMPLETED_SUCCESSFULLY",
})
expect(getTaskRunField(batchFlushedEvents![0].taskRunInserts[1], "run_id")).toEqual(run.id);
expect(getTaskRunField(batchFlushedEvents![0].taskRunInserts[1], "status")).toEqual(
"COMPLETED_SUCCESSFULLY"
);
await runsReplicationService.stop();
@@ -914,6 +919,7 @@ describe("RunsReplicationService (part 2/2)", () => {
const clickhouse = new ClickHouse({
url: clickhouseContainer.getConnectionUrl(),
name: "runs-replication-sorting",
logLevel: "warn",
});
const runsReplicationService = new RunsReplicationService({
@@ -929,7 +935,7 @@ describe("RunsReplicationService (part 2/2)", () => {
leaderLockTimeoutMs: 5000,
leaderLockExtendIntervalMs: 1000,
ackIntervalSeconds: 5,
logger: new Logger("runs-replication-sorting", "info"),
logLevel: "warn",
});
// Listen to batchFlushed events to verify sorting
@@ -1066,22 +1072,22 @@ describe("RunsReplicationService (part 2/2)", () => {
// Verify sorting order: organization_id, project_id, environment_id, created_at, run_id
for (let i = 1; i < batchFlushedEvents[0]?.taskRunInserts.length; i++) {
const prev = batchFlushedEvents[0]?.taskRunInserts[i - 1];
const curr = batchFlushedEvents[0]?.taskRunInserts[i];
const prev = batchFlushedEvents[0]!.taskRunInserts[i - 1];
const curr = batchFlushedEvents[0]!.taskRunInserts[i];
const prevKey = [
prev.organization_id,
prev.project_id,
prev.environment_id,
prev.created_at,
prev.run_id,
getTaskRunField(prev, "organization_id"),
getTaskRunField(prev, "project_id"),
getTaskRunField(prev, "environment_id"),
getTaskRunField(prev, "created_at"),
getTaskRunField(prev, "run_id"),
];
const currKey = [
curr.organization_id,
curr.project_id,
curr.environment_id,
curr.created_at,
curr.run_id,
getTaskRunField(curr, "organization_id"),
getTaskRunField(curr, "project_id"),
getTaskRunField(curr, "environment_id"),
getTaskRunField(curr, "created_at"),
getTaskRunField(curr, "run_id"),
];
const keysAreEqual = prevKey.every((val, idx) => val === currKey[idx]);
@@ -1109,12 +1115,316 @@ describe("RunsReplicationService (part 2/2)", () => {
// Verify payloadInserts are also sorted by run_id
for (let i = 1; i < batchFlushedEvents[0]?.payloadInserts.length; i++) {
const prev = batchFlushedEvents[0]?.payloadInserts[i - 1];
const curr = batchFlushedEvents[0]?.payloadInserts[i];
expect(prev.run_id <= curr.run_id).toBeTruthy();
const prev = batchFlushedEvents[0]!.payloadInserts[i - 1];
const curr = batchFlushedEvents[0]!.payloadInserts[i];
expect(getPayloadField(prev, "run_id") <= getPayloadField(curr, "run_id")).toBeTruthy();
}
await runsReplicationService.stop();
}
);
containerTest(
"should exhaustively replicate all TaskRun columns 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-exhaustive",
logLevel: "warn",
});
const runsReplicationService = new RunsReplicationService({
clickhouse,
pgConnectionUrl: postgresContainer.getConnectionUri(),
serviceName: "runs-replication-exhaustive",
slotName: "task_runs_to_clickhouse_v1",
publicationName: "task_runs_to_clickhouse_v1_publication",
redisOptions,
maxFlushConcurrency: 1,
flushIntervalMs: 100,
flushBatchSize: 1,
leaderLockTimeoutMs: 5000,
leaderLockExtendIntervalMs: 1000,
ackIntervalSeconds: 5,
logLevel: "warn",
});
await runsReplicationService.start();
const organization = await prisma.organization.create({
data: {
title: "test-exhaustive",
slug: "test-exhaustive",
},
});
const project = await prisma.project.create({
data: {
name: "test-exhaustive",
slug: "test-exhaustive",
organizationId: organization.id,
externalRef: "test-exhaustive",
},
});
const runtimeEnvironment = await prisma.runtimeEnvironment.create({
data: {
slug: "test-exhaustive",
type: "PRODUCTION",
projectId: project.id,
organizationId: organization.id,
apiKey: "test-exhaustive",
pkApiKey: "test-exhaustive",
shortcode: "test-exhaustive",
},
});
// Create a batch for the batchId field
const batch = await prisma.batchTaskRun.create({
data: {
friendlyId: "batch_exhaustive",
runtimeEnvironmentId: runtimeEnvironment.id,
status: "PENDING",
},
});
// Create a root run for the rootTaskRunId field
const rootRun = await prisma.taskRun.create({
data: {
friendlyId: "run_root_exhaustive",
taskIdentifier: "root-task",
payload: JSON.stringify({ root: true }),
traceId: "root-trace-id",
spanId: "root-span-id",
queue: "root-queue",
runtimeEnvironmentId: runtimeEnvironment.id,
projectId: project.id,
organizationId: organization.id,
environmentType: "PRODUCTION",
engine: "V2",
},
});
// Create a parent run for the parentTaskRunId field
const parentRun = await prisma.taskRun.create({
data: {
friendlyId: "run_parent_exhaustive",
taskIdentifier: "parent-task",
payload: JSON.stringify({ parent: true }),
traceId: "parent-trace-id",
spanId: "parent-span-id",
queue: "parent-queue",
runtimeEnvironmentId: runtimeEnvironment.id,
projectId: project.id,
organizationId: organization.id,
environmentType: "PRODUCTION",
engine: "V2",
rootTaskRunId: rootRun.id,
depth: 1,
},
});
// Set up all the dates we'll use
const now = new Date();
const createdAt = new Date(now.getTime() - 10000);
const updatedAt = new Date(now.getTime() - 5000);
const startedAt = new Date(now.getTime() - 8000);
const executedAt = new Date(now.getTime() - 7500);
const completedAt = new Date(now.getTime() - 6000);
const delayUntil = new Date(now.getTime() - 9000);
const queuedAt = new Date(now.getTime() - 9500);
const expiredAt = null; // Not expired
// Create the main task run with ALL fields populated
const taskRun = await prisma.taskRun.create({
data: {
// Core identifiers
friendlyId: "run_exhaustive_test",
taskIdentifier: "exhaustive-task",
// Environment/project/org
runtimeEnvironmentId: runtimeEnvironment.id,
projectId: project.id,
organizationId: organization.id,
environmentType: "PRODUCTION",
// Engine and execution
engine: "V2",
status: "COMPLETED_SUCCESSFULLY",
attemptNumber: 3,
queue: "exhaustive-queue",
workerQueue: "exhaustive-worker-queue",
// Relationships
// Note: scheduleId is not set to test empty string handling
batchId: batch.id,
rootTaskRunId: rootRun.id,
parentTaskRunId: parentRun.id,
depth: 2,
// Timestamps
createdAt,
updatedAt,
startedAt,
executedAt,
completedAt,
delayUntil,
queuedAt,
expiredAt,
// Payload and output
payload: JSON.stringify({ input: "test-payload" }),
payloadType: "application/json",
output: JSON.stringify({ result: "test-output" }),
outputType: "application/json",
error: { message: "test error", name: "TestError" },
// Tracing
traceId: "exhaustive-trace-id-12345",
spanId: "exhaustive-span-id-67890",
// Versioning
taskVersion: "1.2.3",
sdkVersion: "3.0.0",
cliVersion: "2.5.1",
// Execution settings
machinePreset: "large-1x",
idempotencyKey: "exhaustive-idempotency-key",
ttl: "1h",
isTest: true,
concurrencyKey: "exhaustive-concurrency-key",
maxDurationInSeconds: 3600,
// Tags and bulk actions
runTags: ["tag1", "tag2", "exhaustive-tag"],
bulkActionGroupIds: ["bulk-group-1", "bulk-group-2"],
// Usage metrics
usageDurationMs: 12345,
costInCents: 50,
baseCostInCents: 25,
},
});
// Wait for replication
await setTimeout(1500);
// Query ClickHouse directly to get all columns
const queryRuns = clickhouse.reader.query({
name: "exhaustive-replication-test",
query: "SELECT * FROM trigger_dev.task_runs_v2 FINAL WHERE run_id = {run_id:String}",
schema: z.any(),
params: z.object({ run_id: z.string() }),
});
const [queryError, result] = await queryRuns({ run_id: taskRun.id });
expect(queryError).toBeNull();
expect(result).toHaveLength(1);
const clickhouseRun = result![0];
// Exhaustively verify each column
// Core identifiers
expect(clickhouseRun.run_id).toBe(taskRun.id);
expect(clickhouseRun.friendly_id).toBe("run_exhaustive_test");
expect(clickhouseRun.task_identifier).toBe("exhaustive-task");
// Environment/project/org
expect(clickhouseRun.environment_id).toBe(runtimeEnvironment.id);
expect(clickhouseRun.project_id).toBe(project.id);
expect(clickhouseRun.organization_id).toBe(organization.id);
expect(clickhouseRun.environment_type).toBe("PRODUCTION");
// Engine and execution
expect(clickhouseRun.engine).toBe("V2");
expect(clickhouseRun.status).toBe("COMPLETED_SUCCESSFULLY");
expect(clickhouseRun.attempt).toBe(3);
expect(clickhouseRun.queue).toBe("exhaustive-queue");
expect(clickhouseRun.worker_queue).toBe("exhaustive-worker-queue");
// Relationships
expect(clickhouseRun.schedule_id).toBe(""); // Empty when not set
expect(clickhouseRun.batch_id).toBe(batch.id);
expect(clickhouseRun.root_run_id).toBe(rootRun.id);
expect(clickhouseRun.parent_run_id).toBe(parentRun.id);
expect(clickhouseRun.depth).toBe(2);
// Timestamps (ClickHouse returns DateTime64 as strings in UTC without 'Z' suffix)
// Helper to parse ClickHouse timestamp strings to milliseconds
function parseClickhouseTimestamp(ts: string | null): number | null {
if (ts === null || ts === "1970-01-01 00:00:00.000") return null;
return new Date(ts + "Z").getTime();
}
expect(parseClickhouseTimestamp(clickhouseRun.created_at)).toBe(createdAt.getTime());
expect(parseClickhouseTimestamp(clickhouseRun.updated_at)).toBe(updatedAt.getTime());
expect(parseClickhouseTimestamp(clickhouseRun.started_at)).toBe(startedAt.getTime());
expect(parseClickhouseTimestamp(clickhouseRun.executed_at)).toBe(executedAt.getTime());
expect(parseClickhouseTimestamp(clickhouseRun.completed_at)).toBe(completedAt.getTime());
expect(parseClickhouseTimestamp(clickhouseRun.delay_until)).toBe(delayUntil.getTime());
expect(parseClickhouseTimestamp(clickhouseRun.queued_at)).toBe(queuedAt.getTime());
expect(parseClickhouseTimestamp(clickhouseRun.expired_at)).toBeNull();
// Output (parsed JSON)
expect(clickhouseRun.output).toEqual({ data: { result: "test-output" } });
// Error
expect(clickhouseRun.error).toEqual({
data: { message: "test error", name: "TestError" },
});
// Tracing
expect(clickhouseRun.trace_id).toBe("exhaustive-trace-id-12345");
expect(clickhouseRun.span_id).toBe("exhaustive-span-id-67890");
// Versioning
expect(clickhouseRun.task_version).toBe("1.2.3");
expect(clickhouseRun.sdk_version).toBe("3.0.0");
expect(clickhouseRun.cli_version).toBe("2.5.1");
// Execution settings
expect(clickhouseRun.machine_preset).toBe("large-1x");
expect(clickhouseRun.idempotency_key).toBe("exhaustive-idempotency-key");
expect(clickhouseRun.expiration_ttl).toBe("1h");
expect(clickhouseRun.is_test).toBe(1); // ClickHouse returns booleans as integers
expect(clickhouseRun.concurrency_key).toBe("exhaustive-concurrency-key");
expect(clickhouseRun.max_duration_in_seconds).toBe(3600);
// Tags and bulk actions
expect(clickhouseRun.tags).toEqual(["tag1", "tag2", "exhaustive-tag"]);
expect(clickhouseRun.bulk_action_group_ids).toEqual(["bulk-group-1", "bulk-group-2"]);
// Usage metrics
expect(clickhouseRun.usage_duration_ms).toBe(12345);
expect(clickhouseRun.cost_in_cents).toBe(50);
expect(clickhouseRun.base_cost_in_cents).toBe(25);
// Internal ClickHouse columns
expect(clickhouseRun._is_deleted).toBe(0);
expect(clickhouseRun._version).toBeDefined();
expect(typeof clickhouseRun._version).toBe("number"); // ClickHouse returns UInt64 as number
// Also verify the payload was inserted into the payloads table
const queryPayloads = clickhouse.reader.query({
name: "exhaustive-payload-test",
query: "SELECT * FROM trigger_dev.raw_task_runs_payload_v1 WHERE run_id = {run_id:String}",
schema: z.any(),
params: z.object({ run_id: z.string() }),
});
const [payloadError, payloadResult] = await queryPayloads({ run_id: taskRun.id });
expect(payloadError).toBeNull();
expect(payloadResult).toHaveLength(1);
expect(payloadResult![0].run_id).toBe(taskRun.id);
expect(parseClickhouseTimestamp(payloadResult![0].created_at)).toBe(createdAt.getTime());
expect(payloadResult![0].payload).toEqual({ data: { input: "test-payload" } });
await runsReplicationService.stop();
}
);
});
+179 -12
View File
@@ -6,9 +6,11 @@ import {
createClient,
type ResultSet,
type Row,
type BaseQueryParams,
type InsertResult,
} from "@clickhouse/client";
import { recordSpanError, Span, startSpan, trace, Tracer } from "@internal/tracing";
import { flattenAttributes, tryCatch } from "@trigger.dev/core/v3";
import { flattenAttributes, tryCatch, type Result } from "@trigger.dev/core/v3";
import { z } from "zod";
import { InsertError, QueryError } from "./errors.js";
import type {
@@ -645,6 +647,76 @@ export class ClickhouseClient implements ClickhouseReader, ClickhouseWriter {
};
}
public insertCompact<TRecord extends Record<string, any>>(req: {
name: string;
table: string;
columns: readonly string[];
toArray: (record: TRecord) => any[];
settings?: ClickHouseSettings;
}): ClickhouseInsertFunction<TRecord> {
return async (events, options) => {
const queryId = randomUUID();
return await startSpan(this.tracer, "insert", async (span) => {
const eventsArray = Array.isArray(events) ? events : [events];
this.logger.debug("Inserting into clickhouse (compact)", {
clientName: this.name,
name: req.name,
table: req.table,
events: eventsArray.length,
settings: req.settings,
attributes: options?.attributes,
options,
queryId,
});
span.setAttributes({
"clickhouse.clientName": this.name,
"clickhouse.tableName": req.table,
"clickhouse.operationName": req.name,
"clickhouse.queryId": queryId,
"clickhouse.format": "JSONCompactEachRowWithNames",
...flattenAttributes(req.settings, "clickhouse.settings"),
...flattenAttributes(options?.attributes),
});
// Build compact format: [columns, ...rows]
const compactData: any[] = [Array.from(req.columns)];
for (let i = 0; i < eventsArray.length; i++) {
compactData.push(req.toArray(eventsArray[i]));
}
const [clickhouseError, result] = await tryCatch(
this.client.insert({
table: req.table,
format: "JSONCompactEachRowWithNames",
values: compactData,
query_id: queryId,
...options?.params,
clickhouse_settings: {
...req.settings,
...options?.params?.clickhouse_settings,
},
})
);
if (clickhouseError) {
this.logger.error("Error inserting into clickhouse", {
name: req.name,
error: clickhouseError,
table: req.table,
});
recordClickhouseError(span, clickhouseError);
return [new InsertError(clickhouseError.message), null];
}
return [null, result];
});
};
}
public insertUnsafe<TRecord extends Record<string, any>>(req: {
name: string;
table: string;
@@ -654,11 +726,13 @@ export class ClickhouseClient implements ClickhouseReader, ClickhouseWriter {
const queryId = randomUUID();
return await startSpan(this.tracer, "insert", async (span) => {
const eventsArray = Array.isArray(events) ? events : [events];
this.logger.debug("Inserting into clickhouse", {
clientName: this.name,
name: req.name,
table: req.table,
events: Array.isArray(events) ? events.length : 1,
events: eventsArray.length,
settings: req.settings,
attributes: options?.attributes,
options,
@@ -678,7 +752,7 @@ export class ClickhouseClient implements ClickhouseReader, ClickhouseWriter {
this.client.insert({
table: req.table,
format: "JSONEachRow",
values: Array.isArray(events) ? events : [events],
values: eventsArray,
query_id: queryId,
...options?.params,
clickhouse_settings: {
@@ -725,26 +799,119 @@ export class ClickhouseClient implements ClickhouseReader, ClickhouseWriter {
});
};
}
public insertCompactRaw(req: {
name: string;
table: string;
columns: readonly string[];
settings?: ClickHouseSettings;
}): (
events: readonly any[][] | any[],
options?: {
attributes?: Record<string, string | number | boolean>;
params?: BaseQueryParams;
}
) => Promise<Result<InsertResult, InsertError>> {
return async (events, options) => {
const queryId = randomUUID();
return await startSpan(this.tracer, "insert", async (span) => {
// Check if events is a single row (array) or multiple rows (array of arrays)
// If first element is not an array, treat as single row
const isSingleRow = events.length > 0 && !Array.isArray(events[0]);
const eventsArray: readonly any[][] = isSingleRow
? [events as any[]]
: (events as readonly any[][]);
this.logger.debug("Inserting into clickhouse (compact raw)", {
clientName: this.name,
name: req.name,
table: req.table,
events: eventsArray.length,
settings: req.settings,
attributes: options?.attributes,
options,
queryId,
});
span.setAttributes({
"clickhouse.clientName": this.name,
"clickhouse.tableName": req.table,
"clickhouse.operationName": req.name,
"clickhouse.queryId": queryId,
"clickhouse.format": "JSONCompactEachRowWithNames",
...flattenAttributes(req.settings, "clickhouse.settings"),
...flattenAttributes(options?.attributes),
});
// Build compact format: [columns, ...rows]
// Data is already in array format, no conversion needed
const compactData: any[] = [Array.from(req.columns), ...eventsArray];
const [clickhouseError, result] = await tryCatch(
this.client.insert({
table: req.table,
format: "JSONCompactEachRowWithNames",
values: compactData,
query_id: queryId,
...options?.params,
clickhouse_settings: {
...req.settings,
...options?.params?.clickhouse_settings,
},
})
);
if (clickhouseError) {
this.logger.error("Error inserting into clickhouse", {
name: req.name,
error: clickhouseError,
table: req.table,
});
recordClickhouseError(span, clickhouseError);
return [new InsertError(clickhouseError.message), null];
}
this.logger.debug("Inserted into clickhouse", {
clientName: this.name,
name: req.name,
table: req.table,
result,
queryId,
});
span.setAttributes({
"clickhouse.query_id": result.query_id,
"clickhouse.executed": result.executed,
"clickhouse.summary.read_rows": result.summary?.read_rows,
"clickhouse.summary.read_bytes": result.summary?.read_bytes,
"clickhouse.summary.written_rows": result.summary?.written_rows,
"clickhouse.summary.written_bytes": result.summary?.written_bytes,
"clickhouse.summary.total_rows_to_read": result.summary?.total_rows_to_read,
"clickhouse.summary.result_rows": result.summary?.result_rows,
"clickhouse.summary.result_bytes": result.summary?.result_bytes,
"clickhouse.summary.elapsed_ns": result.summary?.elapsed_ns,
});
return [null, result];
});
};
}
}
function recordClickhouseError(span: Span, error: Error) {
function recordClickhouseError(span: Span, error: Error): void {
if (error instanceof ClickHouseError) {
span.setAttributes({
"clickhouse.error.code": error.code,
"clickhouse.error.message": error.message,
"clickhouse.error.type": error.type,
});
recordSpanError(span, error);
} else {
recordSpanError(span, error);
}
recordSpanError(span, error);
}
function convertLogLevelToClickhouseLogLevel(logLevel?: LogLevel) {
if (!logLevel) {
return ClickHouseLogLevel.INFO;
}
function convertLogLevelToClickhouseLogLevel(logLevel?: LogLevel): ClickHouseLogLevel {
switch (logLevel) {
case "debug":
return ClickHouseLogLevel.DEBUG;
@@ -159,4 +159,61 @@ export class NoopClient implements ClickhouseReader, ClickhouseWriter {
];
};
}
public insertCompact<TRecord extends Record<string, any>>(req: {
name: string;
table: string;
columns: readonly string[];
toArray: (record: TRecord) => any[];
settings?: ClickHouseSettings;
}): (events: TRecord | TRecord[]) => Promise<Result<InsertResult, InsertError>> {
return async (events: TRecord | TRecord[]) => {
return [
null,
{
executed: true,
query_id: "noop",
summary: {
read_rows: "0",
read_bytes: "0",
written_rows: "0",
written_bytes: "0",
total_rows_to_read: "0",
result_rows: "0",
result_bytes: "0",
elapsed_ns: "0",
},
response_headers: {},
},
];
};
}
public insertCompactRaw(req: {
name: string;
table: string;
columns: readonly string[];
settings?: ClickHouseSettings;
}): (events: readonly any[][] | any[]) => Promise<Result<InsertResult, InsertError>> {
return async (events: readonly any[][] | any[]) => {
return [
null,
{
executed: true,
query_id: "noop",
summary: {
read_rows: "0",
read_bytes: "0",
written_rows: "0",
written_bytes: "0",
total_rows_to_read: "0",
result_rows: "0",
result_bytes: "0",
elapsed_ns: "0",
},
response_headers: {},
},
];
};
}
}
@@ -220,5 +220,26 @@ export interface ClickhouseWriter {
settings?: ClickHouseSettings;
}): ClickhouseInsertFunction<TRecord>;
insertCompact<TRecord extends Record<string, any>>(req: {
name: string;
table: string;
columns: readonly string[];
toArray: (record: TRecord) => any[];
settings?: ClickHouseSettings;
}): ClickhouseInsertFunction<TRecord>;
insertCompactRaw(req: {
name: string;
table: string;
columns: readonly string[];
settings?: ClickHouseSettings;
}): (
events: readonly any[][] | any[],
options?: {
attributes?: Record<string, string | number | boolean>;
params?: BaseQueryParams;
}
) => Promise<Result<InsertResult, InsertError>>;
close(): Promise<void>;
}
+14 -4
View File
@@ -4,8 +4,8 @@ import { ClickhouseClient } from "./client/client.js";
import { ClickhouseReader, ClickhouseWriter } from "./client/types.js";
import { NoopClient } from "./client/noop.js";
import {
insertTaskRuns,
insertRawTaskRunPayloads,
insertTaskRunsCompactArrays,
insertRawTaskRunPayloadsCompactArrays,
getTaskRunsQueryBuilder,
getTaskActivityQueryBuilder,
getCurrentRunningStats,
@@ -34,6 +34,16 @@ export type * from "./taskRuns.js";
export type * from "./taskEvents.js";
export type * from "./client/queryBuilder.js";
// Re-export column constants, indices, and type-safe accessors
export {
TASK_RUN_COLUMNS,
TASK_RUN_INDEX,
PAYLOAD_COLUMNS,
PAYLOAD_INDEX,
getTaskRunField,
getPayloadField,
} from "./taskRuns.js";
// TSQL query execution
export {
executeTSQL,
@@ -182,8 +192,8 @@ export class ClickHouse {
get taskRuns() {
return {
insert: insertTaskRuns(this.writer),
insertPayloads: insertRawTaskRunPayloads(this.writer),
insertCompactArrays: insertTaskRunsCompactArrays(this.writer),
insertPayloadsCompactArrays: insertRawTaskRunPayloadsCompactArrays(this.writer),
queryBuilder: getTaskRunsQueryBuilder(this.reader),
countQueryBuilder: getTaskRunsCountQueryBuilder(this.reader),
tagQueryBuilder: getTaskRunTagsQueryBuilder(this.reader),
+229 -198
View File
@@ -1,7 +1,13 @@
import { clickhouseTest } from "@internal/testcontainers";
import { z } from "zod";
import { ClickhouseClient } from "./client/client.js";
import { getTaskRunsQueryBuilder, insertRawTaskRunPayloads, insertTaskRuns } from "./taskRuns.js";
import {
getTaskRunsQueryBuilder,
insertRawTaskRunPayloadsCompactArrays,
insertTaskRunsCompactArrays,
type TaskRunInsertArray,
type PayloadInsertArray,
} from "./taskRuns.js";
describe("Task Runs V2", () => {
clickhouseTest("should be able to insert task runs", async ({ clickhouseContainer }) => {
@@ -11,61 +17,64 @@ describe("Task Runs V2", () => {
logLevel: "debug",
});
const insert = insertTaskRuns(client, {
const insert = insertTaskRunsCompactArrays(client, {
async_insert: 0, // turn off async insert for this test
});
const insertPayloads = insertRawTaskRunPayloads(client, {
const insertPayloads = insertRawTaskRunPayloadsCompactArrays(client, {
async_insert: 0, // turn off async insert for this test
});
const [insertError, insertResult] = await insert([
{
environment_id: "env_1234",
environment_type: "DEVELOPMENT",
organization_id: "org_1234",
project_id: "project_1234",
run_id: "run_1234",
friendly_id: "friendly_1234",
attempt: 1,
engine: "V2",
status: "PENDING",
task_identifier: "my-task",
queue: "my-queue",
schedule_id: "schedule_1234",
batch_id: "batch_1234",
created_at: Date.now(),
updated_at: Date.now(),
completed_at: undefined,
tags: ["tag1", "tag2"],
output: {
key: "value",
},
error: {
type: "BUILT_IN_ERROR",
name: "Error",
message: "error",
stackTrace: "stack trace",
},
usage_duration_ms: 1000,
cost_in_cents: 100,
task_version: "1.0.0",
sdk_version: "1.0.0",
cli_version: "1.0.0",
machine_preset: "small-1x",
is_test: true,
span_id: "span_1234",
trace_id: "trace_1234",
idempotency_key: "idempotency_key_1234",
expiration_ttl: "1h",
root_run_id: "root_run_1234",
parent_run_id: "parent_run_1234",
depth: 1,
concurrency_key: "concurrency_key_1234",
bulk_action_group_ids: ["bulk_action_group_id_1234", "bulk_action_group_id_1235"],
_version: "1",
},
]);
const now = Date.now();
const taskRunData: TaskRunInsertArray = [
"env_1234", // environment_id
"org_1234", // organization_id
"project_1234", // project_id
"run_1234", // run_id
now, // updated_at
now, // created_at
"PENDING", // status
"DEVELOPMENT", // environment_type
"friendly_1234", // friendly_id
1, // attempt
"V2", // engine
"my-task", // task_identifier
"my-queue", // queue
"schedule_1234", // schedule_id
"batch_1234", // batch_id
null, // completed_at
null, // started_at
null, // executed_at
null, // delay_until
null, // queued_at
null, // expired_at
1000, // usage_duration_ms
100, // cost_in_cents
0, // base_cost_in_cents
{ data: { key: "value" } }, // output
{ data: { type: "BUILT_IN_ERROR", name: "Error", message: "error", stackTrace: "stack trace" } }, // error
["tag1", "tag2"], // tags
"1.0.0", // task_version
"1.0.0", // sdk_version
"1.0.0", // cli_version
"small-1x", // machine_preset
"root_run_1234", // root_run_id
"parent_run_1234", // parent_run_id
1, // depth
"span_1234", // span_id
"trace_1234", // trace_id
"idempotency_key_1234", // idempotency_key
"1h", // expiration_ttl
true, // is_test
"1", // _version
0, // _is_deleted
"concurrency_key_1234", // concurrency_key
["bulk_action_group_id_1234", "bulk_action_group_id_1235"], // bulk_action_group_ids
"", // worker_queue
null, // max_duration_in_seconds
];
const [insertError, insertResult] = await insert([taskRunData]);
expect(insertError).toBeNull();
expect(insertResult).toEqual(expect.objectContaining({ executed: true }));
@@ -99,15 +108,13 @@ describe("Task Runs V2", () => {
])
);
const [insertPayloadsError, insertPayloadsResult] = await insertPayloads([
{
run_id: "run_1234",
created_at: Date.now(),
payload: {
key: "value",
},
},
]);
const payloadData: PayloadInsertArray = [
"run_1234", // run_id
Date.now(), // created_at
{ data: { key: "value" } }, // payload
];
const [insertPayloadsError, insertPayloadsResult] = await insertPayloads([payloadData]);
expect(insertPayloadsError).toBeNull();
expect(insertPayloadsResult).toEqual(expect.objectContaining({ executed: true }));
@@ -137,96 +144,110 @@ describe("Task Runs V2", () => {
url: clickhouseContainer.getConnectionUrl(),
});
const insert = insertTaskRuns(client, {
const insert = insertTaskRunsCompactArrays(client, {
async_insert: 0, // turn off async insert for this test
});
const [insertError, insertResult] = await insert([
{
environment_id: "cm9kddfcs01zqdy88ld9mmrli",
organization_id: "cm8zs78wb0002dy616dg75tv3",
project_id: "cm9kddfbz01zpdy88t9dstecu",
run_id: "cma45oli70002qrdy47w0j4n7",
environment_type: "PRODUCTION",
friendly_id: "run_cma45oli70002qrdy47w0j4n7",
attempt: 1,
engine: "V2",
status: "PENDING",
task_identifier: "retry-task",
queue: "task/retry-task",
schedule_id: "",
batch_id: "",
root_run_id: "",
parent_run_id: "",
depth: 0,
span_id: "538677637f937f54",
trace_id: "20a28486b0b9f50c647b35e8863e36a5",
idempotency_key: "",
created_at: new Date("2025-04-30 16:34:04.312").getTime(),
updated_at: new Date("2025-04-30 16:34:04.312").getTime(),
started_at: null,
executed_at: null,
completed_at: null,
delay_until: null,
queued_at: new Date("2025-04-30 16:34:04.311").getTime(),
expired_at: null,
expiration_ttl: "",
usage_duration_ms: 0,
cost_in_cents: 0,
base_cost_in_cents: 0,
output: null,
error: null,
tags: [],
task_version: "",
sdk_version: "",
cli_version: "",
machine_preset: "",
is_test: true,
_version: "1",
},
{
environment_id: "cm9kddfcs01zqdy88ld9mmrli",
organization_id: "cm8zs78wb0002dy616dg75tv3",
project_id: "cm9kddfbz01zpdy88t9dstecu",
run_id: "cma45oli70002qrdy47w0j4n7",
environment_type: "PRODUCTION",
friendly_id: "run_cma45oli70002qrdy47w0j4n7",
attempt: 1,
engine: "V2",
status: "COMPLETED_SUCCESSFULLY",
task_identifier: "retry-task",
queue: "task/retry-task",
schedule_id: "",
batch_id: "",
root_run_id: "",
parent_run_id: "",
depth: 0,
span_id: "538677637f937f54",
trace_id: "20a28486b0b9f50c647b35e8863e36a5",
idempotency_key: "",
created_at: new Date("2025-04-30 16:34:04.312").getTime(),
updated_at: new Date("2025-04-30 16:34:04.312").getTime(),
started_at: null,
executed_at: null,
completed_at: null,
delay_until: null,
queued_at: new Date("2025-04-30 16:34:04.311").getTime(),
expired_at: null,
expiration_ttl: "",
usage_duration_ms: 0,
cost_in_cents: 0,
base_cost_in_cents: 0,
output: null,
error: null,
tags: [],
task_version: "",
sdk_version: "",
cli_version: "",
machine_preset: "",
is_test: true,
_version: "2",
},
]);
const createdAt = new Date("2025-04-30 16:34:04.312").getTime();
const queuedAt = new Date("2025-04-30 16:34:04.311").getTime();
const run1: TaskRunInsertArray = [
"cm9kddfcs01zqdy88ld9mmrli", // environment_id
"cm8zs78wb0002dy616dg75tv3", // organization_id
"cm9kddfbz01zpdy88t9dstecu", // project_id
"cma45oli70002qrdy47w0j4n7", // run_id
createdAt, // updated_at
createdAt, // created_at
"PENDING", // status
"PRODUCTION", // environment_type
"run_cma45oli70002qrdy47w0j4n7", // friendly_id
1, // attempt
"V2", // engine
"retry-task", // task_identifier
"task/retry-task", // queue
"", // schedule_id
"", // batch_id
null, // completed_at
null, // started_at
null, // executed_at
null, // delay_until
queuedAt, // queued_at
null, // expired_at
0, // usage_duration_ms
0, // cost_in_cents
0, // base_cost_in_cents
{ data: null }, // output
{ data: null }, // error
[], // tags
"", // task_version
"", // sdk_version
"", // cli_version
"", // machine_preset
"", // root_run_id
"", // parent_run_id
0, // depth
"538677637f937f54", // span_id
"20a28486b0b9f50c647b35e8863e36a5", // trace_id
"", // idempotency_key
"", // expiration_ttl
true, // is_test
"1", // _version
0, // _is_deleted
"", // concurrency_key
[], // bulk_action_group_ids
"", // worker_queue
null, // max_duration_in_seconds
];
const run2: TaskRunInsertArray = [
"cm9kddfcs01zqdy88ld9mmrli", // environment_id
"cm8zs78wb0002dy616dg75tv3", // organization_id
"cm9kddfbz01zpdy88t9dstecu", // project_id
"cma45oli70002qrdy47w0j4n7", // run_id
createdAt, // updated_at
createdAt, // created_at
"COMPLETED_SUCCESSFULLY", // status
"PRODUCTION", // environment_type
"run_cma45oli70002qrdy47w0j4n7", // friendly_id
1, // attempt
"V2", // engine
"retry-task", // task_identifier
"task/retry-task", // queue
"", // schedule_id
"", // batch_id
null, // completed_at
null, // started_at
null, // executed_at
null, // delay_until
queuedAt, // queued_at
null, // expired_at
0, // usage_duration_ms
0, // cost_in_cents
0, // base_cost_in_cents
{ data: null }, // output
{ data: null }, // error
[], // tags
"", // task_version
"", // sdk_version
"", // cli_version
"", // machine_preset
"", // root_run_id
"", // parent_run_id
0, // depth
"538677637f937f54", // span_id
"20a28486b0b9f50c647b35e8863e36a5", // trace_id
"", // idempotency_key
"", // expiration_ttl
true, // is_test
"2", // _version
0, // _is_deleted
"", // concurrency_key
[], // bulk_action_group_ids
"", // worker_queue
null, // max_duration_in_seconds
];
const [insertError, insertResult] = await insert([run1, run2]);
expect(insertError).toBeNull();
expect(insertResult).toEqual(expect.objectContaining({ executed: true }));
@@ -266,54 +287,62 @@ describe("Task Runs V2", () => {
url: clickhouseContainer.getConnectionUrl(),
});
const insert = insertTaskRuns(client, {
const insert = insertTaskRunsCompactArrays(client, {
async_insert: 0, // turn off async insert for this test
});
const [insertError, insertResult] = await insert([
{
environment_id: "cm9kddfcs01zqdy88ld9mmrli",
organization_id: "cm8zs78wb0002dy616dg75tv3",
project_id: "cm9kddfbz01zpdy88t9dstecu",
run_id: "cma45oli70002qrdy47w0j4n7",
environment_type: "PRODUCTION",
friendly_id: "run_cma45oli70002qrdy47w0j4n7",
attempt: 1,
engine: "V2",
status: "PENDING",
task_identifier: "retry-task",
queue: "task/retry-task",
schedule_id: "",
batch_id: "",
root_run_id: "",
parent_run_id: "",
depth: 0,
span_id: "538677637f937f54",
trace_id: "20a28486b0b9f50c647b35e8863e36a5",
idempotency_key: "",
created_at: new Date("2025-04-30 16:34:04.312").getTime(),
updated_at: new Date("2025-04-30 16:34:04.312").getTime(),
started_at: null,
executed_at: null,
completed_at: null,
delay_until: null,
queued_at: new Date("2025-04-30 16:34:04.311").getTime(),
expired_at: null,
expiration_ttl: "",
usage_duration_ms: 0,
cost_in_cents: 0,
base_cost_in_cents: 0,
output: null,
error: null,
tags: [],
task_version: "",
sdk_version: "",
cli_version: "",
machine_preset: "",
is_test: true,
_version: "1",
},
]);
const createdAt = new Date("2025-04-30 16:34:04.312").getTime();
const queuedAt = new Date("2025-04-30 16:34:04.311").getTime();
const taskRun: TaskRunInsertArray = [
"cm9kddfcs01zqdy88ld9mmrli", // environment_id
"cm8zs78wb0002dy616dg75tv3", // organization_id
"cm9kddfbz01zpdy88t9dstecu", // project_id
"cma45oli70002qrdy47w0j4n7", // run_id
createdAt, // updated_at
createdAt, // created_at
"PENDING", // status
"PRODUCTION", // environment_type
"run_cma45oli70002qrdy47w0j4n7", // friendly_id
1, // attempt
"V2", // engine
"retry-task", // task_identifier
"task/retry-task", // queue
"", // schedule_id
"", // batch_id
null, // completed_at
null, // started_at
null, // executed_at
null, // delay_until
queuedAt, // queued_at
null, // expired_at
0, // usage_duration_ms
0, // cost_in_cents
0, // base_cost_in_cents
{ data: null }, // output
{ data: null }, // error
[], // tags
"", // task_version
"", // sdk_version
"", // cli_version
"", // machine_preset
"", // root_run_id
"", // parent_run_id
0, // depth
"538677637f937f54", // span_id
"20a28486b0b9f50c647b35e8863e36a5", // trace_id
"", // idempotency_key
"", // expiration_ttl
true, // is_test
"1", // _version
0, // _is_deleted
"", // concurrency_key
[], // bulk_action_group_ids
"", // worker_queue
null, // max_duration_in_seconds
];
const [insertError, insertResult] = await insert([taskRun]);
const queryBuilder = getTaskRunsQueryBuilder(client)();
queryBuilder.where("environment_id = {environmentId: String}", {
@@ -360,15 +389,15 @@ describe("Task Runs V2", () => {
url: clickhouseContainer.getConnectionUrl(),
});
const insertPayloads = insertRawTaskRunPayloads(client, {
const insertPayloads = insertRawTaskRunPayloadsCompactArrays(client, {
async_insert: 0, // turn off async insert for this test
});
const [insertPayloadsError, insertPayloadsResult] = await insertPayloads([
const payloadData: PayloadInsertArray = [
"run_1234", // run_id
Date.now(), // created_at
{
run_id: "run_1234",
created_at: Date.now(),
payload: {
data: {
data: {
title: {
id: "123",
@@ -376,8 +405,10 @@ describe("Task Runs V2", () => {
"title.id": 123,
},
},
},
]);
}, // payload
];
const [insertPayloadsError, insertPayloadsResult] = await insertPayloads([payloadData]);
expect(insertPayloadsError).toBeNull();
expect(insertPayloadsResult).toEqual(expect.objectContaining({ executed: true }));
@@ -52,6 +52,144 @@ export const TaskRunV2 = z.object({
export type TaskRunV2 = z.input<typeof TaskRunV2>;
// Column order for compact format - must match ClickHouse table schema
export const TASK_RUN_COLUMNS = [
"environment_id",
"organization_id",
"project_id",
"run_id",
"updated_at",
"created_at",
"status",
"environment_type",
"friendly_id",
"attempt",
"engine",
"task_identifier",
"queue",
"schedule_id",
"batch_id",
"completed_at",
"started_at",
"executed_at",
"delay_until",
"queued_at",
"expired_at",
"usage_duration_ms",
"cost_in_cents",
"base_cost_in_cents",
"output",
"error",
"tags",
"task_version",
"sdk_version",
"cli_version",
"machine_preset",
"root_run_id",
"parent_run_id",
"depth",
"span_id",
"trace_id",
"idempotency_key",
"expiration_ttl",
"is_test",
"_version",
"_is_deleted",
"concurrency_key",
"bulk_action_group_ids",
"worker_queue",
"max_duration_in_seconds",
] as const;
export type TaskRunColumnName = (typeof TASK_RUN_COLUMNS)[number];
// Type-safe column indices generated from TASK_RUN_COLUMNS
// This ensures indices stay in sync with column order automatically
export const TASK_RUN_INDEX = Object.fromEntries(
TASK_RUN_COLUMNS.map((col, idx) => [col, idx])
) as { readonly [K in TaskRunColumnName]: number };
/**
* Type mapping from column name to its type in TaskRunInsertArray.
* This enables type-safe field access without manual casting.
*/
export type TaskRunFieldTypes = {
environment_id: string;
organization_id: string;
project_id: string;
run_id: string;
updated_at: number;
created_at: number;
status: string;
environment_type: string;
friendly_id: string;
attempt: number;
engine: string;
task_identifier: string;
queue: string;
schedule_id: string;
batch_id: string;
completed_at: number | null;
started_at: number | null;
executed_at: number | null;
delay_until: number | null;
queued_at: number | null;
expired_at: number | null;
usage_duration_ms: number;
cost_in_cents: number;
base_cost_in_cents: number;
output: { data: unknown };
error: { data: unknown };
tags: string[];
task_version: string;
sdk_version: string;
cli_version: string;
machine_preset: string;
root_run_id: string;
parent_run_id: string;
depth: number;
span_id: string;
trace_id: string;
idempotency_key: string;
expiration_ttl: string;
is_test: boolean;
_version: string;
_is_deleted: number;
concurrency_key: string;
bulk_action_group_ids: string[];
worker_queue: string;
max_duration_in_seconds: number | null;
};
/**
* Type-safe accessor for TaskRunInsertArray fields.
* Returns the correct type for each field without manual casting.
*
* @example
* const orgId = getTaskRunField(run, "organization_id"); // type: string
* const createdAt = getTaskRunField(run, "created_at"); // type: number
*/
export function getTaskRunField<K extends TaskRunColumnName>(
run: TaskRunInsertArray,
field: K
): TaskRunFieldTypes[K] {
return run[TASK_RUN_INDEX[field]] as TaskRunFieldTypes[K];
}
export function insertTaskRunsCompactArrays(ch: ClickhouseWriter, settings?: ClickHouseSettings) {
return ch.insertCompactRaw({
name: "insertTaskRunsCompactArrays",
table: "trigger_dev.task_runs_v2",
columns: TASK_RUN_COLUMNS,
settings: {
enable_json_type: 1,
type_json_skip_duplicated_paths: 1,
...settings,
},
});
}
// Object-based insert function for tests and non-performance-critical code
export function insertTaskRuns(ch: ClickhouseWriter, settings?: ClickHouseSettings) {
return ch.insert({
name: "insertTaskRuns",
@@ -73,6 +211,114 @@ export const RawTaskRunPayloadV1 = z.object({
export type RawTaskRunPayloadV1 = z.infer<typeof RawTaskRunPayloadV1>;
export const PAYLOAD_COLUMNS = ["run_id", "created_at", "payload"] as const;
export type PayloadColumnName = (typeof PAYLOAD_COLUMNS)[number];
// Type-safe column indices generated from PAYLOAD_COLUMNS
export const PAYLOAD_INDEX = Object.fromEntries(PAYLOAD_COLUMNS.map((col, idx) => [col, idx])) as {
readonly [K in PayloadColumnName]: number;
};
/**
* Type mapping from column name to its type in PayloadInsertArray.
*/
export type PayloadFieldTypes = {
run_id: string;
created_at: number;
payload: { data: unknown };
};
/**
* Type-safe accessor for PayloadInsertArray fields.
* Returns the correct type for each field without manual casting.
*/
export function getPayloadField<K extends PayloadColumnName>(
payload: PayloadInsertArray,
field: K
): PayloadFieldTypes[K] {
return payload[PAYLOAD_INDEX[field]] as PayloadFieldTypes[K];
}
/**
* Type-safe tuple representing a task run insert array.
* Order matches TASK_RUN_COLUMNS exactly.
*/
export type TaskRunInsertArray = [
environment_id: string,
organization_id: string,
project_id: string,
run_id: string,
updated_at: number,
created_at: number,
status: string,
environment_type: string,
friendly_id: string,
attempt: number,
engine: string,
task_identifier: string,
queue: string,
schedule_id: string,
batch_id: string,
completed_at: number | null,
started_at: number | null,
executed_at: number | null,
delay_until: number | null,
queued_at: number | null,
expired_at: number | null,
usage_duration_ms: number,
cost_in_cents: number,
base_cost_in_cents: number,
output: { data: unknown },
error: { data: unknown },
tags: string[],
task_version: string,
sdk_version: string,
cli_version: string,
machine_preset: string,
root_run_id: string,
parent_run_id: string,
depth: number,
span_id: string,
trace_id: string,
idempotency_key: string,
expiration_ttl: string,
is_test: boolean,
_version: string,
_is_deleted: number,
concurrency_key: string,
bulk_action_group_ids: string[],
worker_queue: string,
max_duration_in_seconds: number | null,
];
/**
* Type-safe tuple representing a payload insert array.
* Order matches PAYLOAD_COLUMNS exactly.
*/
export type PayloadInsertArray = [run_id: string, created_at: number, payload: { data: unknown }];
export function insertRawTaskRunPayloadsCompactArrays(
ch: ClickhouseWriter,
settings?: ClickHouseSettings
) {
return ch.insertCompactRaw({
name: "insertRawTaskRunPayloadsCompactArrays",
table: "trigger_dev.raw_task_runs_payload_v1",
columns: PAYLOAD_COLUMNS,
settings: {
async_insert: 1,
wait_for_async_insert: 0,
async_insert_max_data_size: "1000000",
async_insert_busy_timeout_ms: 1000,
enable_json_type: 1,
type_json_skip_duplicated_paths: 1,
...settings,
},
});
}
// Object-based insert function for tests and non-performance-critical code
export function insertRawTaskRunPayloads(ch: ClickhouseWriter, settings?: ClickHouseSettings) {
return ch.insert({
name: "insertRawTaskRunPayloads",
@@ -20,6 +20,18 @@ export type PgoutputMessage =
| MessageType
| MessageUpdate;
export type PgoutputMessageArray =
| MessageBegin
| MessageCommit
| MessageDeleteArray
| MessageInsertArray
| MessageMessage
| MessageOrigin
| MessageRelation
| MessageTruncate
| MessageType
| MessageUpdateArray;
export interface MessageBegin {
tag: "begin";
commitLsn: string | null;
@@ -95,6 +107,26 @@ export interface MessageUpdate {
new: Record<string, any>;
}
// Array variants for zero-copy performance
export interface MessageInsertArray {
tag: "insert";
relation: MessageRelation;
new: any[];
}
export interface MessageUpdateArray {
tag: "update";
relation: MessageRelation;
key: any[] | null;
old: any[] | null;
new: any[];
}
export interface MessageDeleteArray {
tag: "delete";
relation: MessageRelation;
key: any[] | null;
old: any[] | null;
}
class BinaryReader {
private offset = 0;
constructor(private buf: Buffer) {}
@@ -193,6 +225,35 @@ export class PgoutputParser {
}
}
public parseArray(buf: Buffer): PgoutputMessageArray {
const reader = new BinaryReader(buf);
const tag = reader.readUint8();
switch (tag) {
case 0x42:
return this.msgBegin(reader);
case 0x4f:
return this.msgOrigin(reader);
case 0x59:
return this.msgType(reader);
case 0x52:
return this.msgRelation(reader);
case 0x49:
return this.msgInsertArray(reader);
case 0x55:
return this.msgUpdateArray(reader);
case 0x44:
return this.msgDeleteArray(reader);
case 0x54:
return this.msgTruncate(reader);
case 0x4d:
return this.msgMessage(reader);
case 0x43:
return this.msgCommit(reader);
default:
throw Error("unknown pgoutput message");
}
}
private msgBegin(reader: BinaryReader): MessageBegin {
return {
tag: "begin",
@@ -312,6 +373,55 @@ export class PgoutputParser {
}
return { tag: "delete", relation, key, old };
}
// Array variants - skip object creation for performance
private msgInsertArray(reader: BinaryReader): MessageInsertArray {
const relation = this._relationCache.get(reader.readInt32());
if (!relation) throw Error("missing relation");
reader.readUint8(); // consume the 'N' key
return {
tag: "insert",
relation,
new: this.readTupleAsArray(reader, relation),
};
}
private msgUpdateArray(reader: BinaryReader): MessageUpdateArray {
const relation = this._relationCache.get(reader.readInt32());
if (!relation) throw Error("missing relation");
let key: any[] | null = null;
let old: any[] | null = null;
let new_: any[] | null = null;
const subMsgKey = reader.readUint8();
if (subMsgKey === 0x4b) {
key = this.readTupleAsArray(reader, relation);
reader.readUint8();
new_ = this.readTupleAsArray(reader, relation);
} else if (subMsgKey === 0x4f) {
old = this.readTupleAsArray(reader, relation);
reader.readUint8();
new_ = this.readTupleAsArray(reader, relation, old);
} else if (subMsgKey === 0x4e) {
new_ = this.readTupleAsArray(reader, relation);
} else {
throw Error(`unknown submessage key ${String.fromCharCode(subMsgKey)}`);
}
return { tag: "update", relation, key, old, new: new_ };
}
private msgDeleteArray(reader: BinaryReader): MessageDeleteArray {
const relation = this._relationCache.get(reader.readInt32());
if (!relation) throw Error("missing relation");
let key: any[] | null = null;
let old: any[] | null = null;
const subMsgKey = reader.readUint8();
if (subMsgKey === 0x4b) {
key = this.readTupleAsArray(reader, relation);
} else if (subMsgKey === 0x4f) {
old = this.readTupleAsArray(reader, relation);
} else {
throw Error(`unknown submessage key ${String.fromCharCode(subMsgKey)}`);
}
return { tag: "delete", relation, key, old };
}
private readKeyTuple(reader: BinaryReader, relation: MessageRelation): Record<string, any> {
const tuple = this.readTuple(reader, relation);
const key = Object.create(null);
@@ -354,6 +464,40 @@ export class PgoutputParser {
}
return tuple;
}
private readTupleAsArray(
reader: BinaryReader,
{ columns }: MessageRelation,
unchangedToastFallback?: any[] | null
): any[] {
const nfields = reader.readInt16();
const tuple = new Array(nfields);
for (let i = 0; i < nfields; i++) {
const { parser } = columns[i];
const kind = reader.readUint8();
switch (kind) {
case 0x62: // 'b' binary
const bsize = reader.readInt32();
tuple[i] = reader.read(bsize);
break;
case 0x74: // 't' text
const valsize = reader.readInt32();
const valbuf = reader.read(valsize);
const valtext = reader.decodeText(valbuf);
tuple[i] = parser(valtext);
break;
case 0x6e: // 'n' null
tuple[i] = null;
break;
case 0x75: // 'u' unchanged toast datum
tuple[i] = unchangedToastFallback?.[i];
break;
default:
throw Error(`unknown attribute kind ${String.fromCharCode(kind)}`);
}
}
return tuple;
}
private msgTruncate(reader: BinaryReader): MessageTruncate {
const nrels = reader.readInt32();
const flags = reader.readUint8();
@@ -0,0 +1,15 @@
// @ts-ignore
const { default: superjson } = require("superjson");
// @ts-ignore
superjson.registerCustom<Buffer, number[]>(
{
isApplicable: (v: unknown): v is Buffer => typeof Buffer === "function" && Buffer.isBuffer(v),
serialize: (v: Buffer) => [...v],
deserialize: (v: number[]) => Buffer.from(v),
},
"buffer"
);
// @ts-ignore
module.exports.default = superjson;
+14
View File
@@ -0,0 +1,14 @@
// @ts-ignore
import superjson from "superjson";
superjson.registerCustom<Buffer, number[]>(
{
isApplicable: (v): v is Buffer => typeof Buffer === "function" && Buffer.isBuffer(v),
serialize: (v) => [...v],
deserialize: (v) => Buffer.from(v),
},
"buffer"
);
// @ts-ignore
export default superjson;
+9 -35
View File
@@ -13,6 +13,7 @@ import { SemanticInternalAttributes } from "../semanticInternalAttributes.js";
import { TriggerTracer } from "../tracer.js";
import { zodfetch } from "../zodfetch.js";
import { flattenAttributes } from "./flattenAttributes.js";
import superjson from "../imports/superjson.js";
export type IOPacket = {
data?: string | undefined;
@@ -32,9 +33,7 @@ export async function parsePacket(value: IOPacket, options?: ParsePacketOptions)
case "application/json":
return JSON.parse(value.data, makeSafeReviver(options));
case "application/super+json":
const { parse } = await loadSuperJSON();
return parse(value.data);
return superjson.parse(value.data);
case "text/plain":
return value.data;
case "application/store":
@@ -58,11 +57,9 @@ export async function parsePacketAsJson(
case "application/json":
return JSON.parse(value.data, makeSafeReviver(options));
case "application/super+json":
const { parse, serialize } = await loadSuperJSON();
const superJsonResult = superjson.parse(value.data);
const superJsonResult = parse(value.data);
const { json } = serialize(superJsonResult);
const { json } = superjson.serialize(superJsonResult);
return json;
case "text/plain":
@@ -95,8 +92,7 @@ export async function stringifyIO(value: any): Promise<IOPacket> {
}
try {
const { stringify } = await loadSuperJSON();
const data = stringify(value);
const data = superjson.stringify(value);
return { data, dataType: "application/super+json" };
} catch {
@@ -302,14 +298,12 @@ export async function createPacketAttributes(
[dataTypeKey]: packet.dataType,
};
case "application/super+json":
const { parse } = await loadSuperJSON();
if (typeof packet.data === "undefined" || packet.data === null) {
return;
}
try {
const parsed = parse(packet.data) as any;
const parsed = superjson.parse(packet.data) as any;
const jsonified = JSON.parse(JSON.stringify(parsed, makeSafeReplacer()));
const result = {
@@ -358,9 +352,7 @@ export async function createPacketAttributesAsJson(
);
}
case "application/super+json": {
const { deserialize } = await loadSuperJSON();
const deserialized = deserialize(data) as any;
const deserialized = superjson.deserialize(data) as any;
const jsonify = safeJsonParse(JSON.stringify(deserialized, makeSafeReplacer()));
return imposeAttributeLimits(
@@ -390,18 +382,16 @@ export async function prettyPrintPacket(
rawData = safeJsonParse(rawData);
}
const { deserialize } = await loadSuperJSON();
const hasCircularReferences = rawData && rawData.meta && hasCircularReference(rawData.meta);
if (hasCircularReferences) {
return await prettyPrintPacket(deserialize(rawData), "application/json", {
return await prettyPrintPacket(superjson.deserialize(rawData), "application/json", {
...options,
cloneReferences: false,
});
}
return await prettyPrintPacket(deserialize(rawData), "application/json", {
return await prettyPrintPacket(superjson.deserialize(rawData), "application/json", {
...options,
cloneReferences: true,
});
@@ -512,21 +502,6 @@ function getPacketExtension(outputType: string): string {
}
}
async function loadSuperJSON() {
const superjson = await import("superjson");
superjson.registerCustom<Buffer, number[]>(
{
isApplicable: (v): v is Buffer => typeof Buffer === "function" && Buffer.isBuffer(v),
serialize: (v) => [...v],
deserialize: (v) => Buffer.from(v),
},
"buffer"
);
return superjson;
}
function safeJsonParse(value: string): any {
try {
return JSON.parse(value);
@@ -554,7 +529,6 @@ function safeJsonParse(value: string): any {
* @throws {Error} If the newPayload is not valid JSON
*/
export async function replaceSuperJsonPayload(original: string, newPayload: string) {
const superjson = await loadSuperJSON();
const originalObject = superjson.parse(original);
const newPayloadObject = JSON.parse(newPayload);
const { meta } = superjson.serialize(originalObject);
+25 -53
View File
@@ -5628,9 +5628,6 @@ packages:
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
engines: {node: '>=6.0.0'}
'@jridgewell/source-map@0.3.11':
resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==}
'@jridgewell/source-map@0.3.3':
resolution: {integrity: sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==}
@@ -17304,9 +17301,6 @@ packages:
pump@2.0.1:
resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==}
pump@3.0.0:
resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
pump@3.0.2:
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
@@ -18801,11 +18795,6 @@ packages:
uglify-js:
optional: true
terser@5.17.1:
resolution: {integrity: sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw==}
engines: {node: '>=10'}
hasBin: true
terser@5.44.1:
resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==}
engines: {node: '>=10'}
@@ -23937,11 +23926,6 @@ snapshots:
'@jridgewell/set-array@1.2.1': {}
'@jridgewell/source-map@0.3.11':
dependencies:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/source-map@0.3.3':
dependencies:
'@jridgewell/gen-mapping': 0.3.8
@@ -24335,7 +24319,7 @@ snapshots:
json-parse-even-better-errors: 3.0.0
normalize-package-data: 5.0.0
proc-log: 3.0.0
semver: 7.7.2
semver: 7.7.3
transitivePeerDependencies:
- bluebird
@@ -24933,7 +24917,7 @@ snapshots:
'@types/shimmer': 1.2.0
import-in-the-middle: 1.11.0
require-in-the-middle: 7.1.1(supports-color@10.0.0)
semver: 7.7.2
semver: 7.7.3
shimmer: 1.2.1
transitivePeerDependencies:
- supports-color
@@ -24945,7 +24929,7 @@ snapshots:
'@types/shimmer': 1.2.0
import-in-the-middle: 1.11.0
require-in-the-middle: 7.1.1(supports-color@10.0.0)
semver: 7.7.2
semver: 7.7.3
shimmer: 1.2.1
transitivePeerDependencies:
- supports-color
@@ -31078,7 +31062,7 @@ snapshots:
debug: 4.4.1(supports-color@10.0.0)
globby: 11.1.0
is-glob: 4.0.3
semver: 7.7.2
semver: 7.7.3
tsutils: 3.21.0(typescript@5.9.3)
optionalDependencies:
typescript: 5.9.3
@@ -31095,7 +31079,7 @@ snapshots:
'@typescript-eslint/typescript-estree': 5.59.6(typescript@5.9.3)
eslint: 8.31.0
eslint-scope: 5.1.1
semver: 7.7.2
semver: 7.7.3
transitivePeerDependencies:
- supports-color
- typescript
@@ -31584,17 +31568,17 @@ snapshots:
mime-types: 3.0.0
negotiator: 1.0.0
acorn-import-assertions@1.9.0(acorn@8.14.1):
acorn-import-assertions@1.9.0(acorn@8.15.0):
dependencies:
acorn: 8.14.1
acorn: 8.15.0
acorn-import-attributes@1.9.5(acorn@8.12.1):
dependencies:
acorn: 8.12.1
acorn-import-attributes@1.9.5(acorn@8.14.1):
acorn-import-attributes@1.9.5(acorn@8.15.0):
dependencies:
acorn: 8.14.1
acorn: 8.15.0
acorn-import-phases@1.0.4(acorn@8.15.0):
dependencies:
@@ -31604,9 +31588,9 @@ snapshots:
dependencies:
acorn: 8.12.1
acorn-jsx@5.3.2(acorn@8.14.1):
acorn-jsx@5.3.2(acorn@8.15.0):
dependencies:
acorn: 8.14.1
acorn: 8.15.0
acorn-node@1.8.2:
dependencies:
@@ -34194,8 +34178,8 @@ snapshots:
espree@9.6.0:
dependencies:
acorn: 8.14.1
acorn-jsx: 5.3.2(acorn@8.14.1)
acorn: 8.15.0
acorn-jsx: 5.3.2(acorn@8.15.0)
eslint-visitor-keys: 3.4.2
esprima@4.0.1: {}
@@ -34526,7 +34510,7 @@ snapshots:
process-warning: 5.0.0
rfdc: 1.4.1
secure-json-parse: 4.0.0
semver: 7.7.2
semver: 7.7.3
toad-cache: 3.7.0
fastq@1.15.0:
@@ -35019,7 +35003,7 @@ snapshots:
chalk: 4.1.2
debug: 4.4.1(supports-color@10.0.0)
interpret: 3.1.1
semver: 7.7.2
semver: 7.7.3
tslib: 2.8.1
yargs: 17.7.2
transitivePeerDependencies:
@@ -35365,8 +35349,8 @@ snapshots:
import-in-the-middle@1.14.2:
dependencies:
acorn: 8.14.1
acorn-import-attributes: 1.9.5(acorn@8.14.1)
acorn: 8.15.0
acorn-import-attributes: 1.9.5(acorn@8.15.0)
cjs-module-lexer: 1.2.3
module-details-from-path: 1.0.3
@@ -36742,8 +36726,8 @@ snapshots:
micromark-extension-mdxjs@1.0.0:
dependencies:
acorn: 8.14.1
acorn-jsx: 5.3.2(acorn@8.14.1)
acorn: 8.15.0
acorn-jsx: 5.3.2(acorn@8.15.0)
micromark-extension-mdx-expression: 1.0.3
micromark-extension-mdx-jsx: 1.0.3
micromark-extension-mdx-md: 1.0.0
@@ -38433,7 +38417,7 @@ snapshots:
mkdirp-classic: 0.5.3
napi-build-utils: 2.0.0
node-abi: 3.75.0
pump: 3.0.0
pump: 3.0.2
rc: 1.2.8
simple-get: 4.0.1
tar-fs: 2.1.3
@@ -38629,11 +38613,6 @@ snapshots:
end-of-stream: 1.4.4
once: 1.4.0
pump@3.0.0:
dependencies:
end-of-stream: 1.4.4
once: 1.4.0
pump@3.0.2:
dependencies:
end-of-stream: 1.4.4
@@ -40813,22 +40792,15 @@ snapshots:
jest-worker: 27.5.1
schema-utils: 3.3.0
serialize-javascript: 6.0.1
terser: 5.17.1
terser: 5.44.1
webpack: 5.88.2(@swc/core@1.3.101(@swc/helpers@0.5.15))(esbuild@0.19.11)
optionalDependencies:
'@swc/core': 1.3.101(@swc/helpers@0.5.15)
esbuild: 0.19.11
terser@5.17.1:
dependencies:
'@jridgewell/source-map': 0.3.3
acorn: 8.14.1
commander: 2.20.3
source-map-support: 0.5.21
terser@5.44.1:
dependencies:
'@jridgewell/source-map': 0.3.11
'@jridgewell/source-map': 0.3.3
acorn: 8.15.0
commander: 2.20.3
source-map-support: 0.5.21
@@ -41822,7 +41794,7 @@ snapshots:
webpack-bundle-analyzer@4.10.1(bufferutil@4.0.9):
dependencies:
'@discoveryjs/json-ext': 0.5.7
acorn: 8.14.1
acorn: 8.15.0
acorn-walk: 8.3.2
commander: 7.2.0
debounce: 1.2.1
@@ -41881,8 +41853,8 @@ snapshots:
'@webassemblyjs/ast': 1.11.5
'@webassemblyjs/wasm-edit': 1.11.5
'@webassemblyjs/wasm-parser': 1.11.5
acorn: 8.14.1
acorn-import-assertions: 1.9.0(acorn@8.14.1)
acorn: 8.15.0
acorn-import-assertions: 1.9.0(acorn@8.15.0)
browserslist: 4.24.4
chrome-trace-event: 1.0.3
enhanced-resolve: 5.18.3