Skip to content

Commit ba94489

Browse files
MariefayTrigger.dev RepoOps
authored andcommitted
feat(webapp): route the legacy v1 event store to v2
Runs on the legacy `clickhouse` event store now write and read `clickhouse_v2`, and new runs are stored as `clickhouse_v2`. Self-hosters on the legacy store stop seeing events recorded before the upgrade. Set `EVENT_REPOSITORY_V1_REROUTE_ENABLED=0` to keep the legacy store. Mono-RevId: caf2efe61a9990d53f411ea869a58d3db3abfd35
1 parent 4ae1b8b commit ba94489

15 files changed

Lines changed: 473 additions & 39 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: breaking
4+
---
5+
6+
Self-hosted instances on the legacy `clickhouse` event store now use `clickhouse_v2`, so run logs and traces recorded before the upgrade no longer show. Set `EVENT_REPOSITORY_DEFAULT_STORE=clickhouse_v2`, or set `EVENT_REPOSITORY_V1_REROUTE_ENABLED=0` to stay on the legacy store.

‎apps/webapp/app/env.server.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2632,6 +2632,8 @@ const EnvironmentSchema = z
26322632
EVENT_REPOSITORY_DEFAULT_STORE: z
26332633
.enum(["postgres", "clickhouse", "clickhouse_v2"])
26342634
.default("postgres"),
2635+
// Route the legacy "clickhouse" store (task_events_v1) to clickhouse_v2. Set to 0 to keep v1.
2636+
EVENT_REPOSITORY_V1_REROUTE_ENABLED: BoolEnvWithDefault(true),
26352637
EVENT_REPOSITORY_DEBUG_LOGS_DISABLED: BoolEnv.default(false),
26362638
EVENT_REPOSITORY_POSTGRES_WRITES_DISABLED: BoolEnv.default(false),
26372639
EVENTS_CLICKHOUSE_MAX_TRACE_SUMMARY_VIEW_COUNT: z.coerce.number().int().default(25_000),

‎apps/webapp/app/presenters/v3/RunPresenter.server.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,11 @@ export class RunPresenter {
210210
// Read before the first chunk so the first tail covers the background load.
211211
const firstChunkReadAt = Date.now();
212212

213-
const firstChunkPromise = canLiveTail(run.taskEventStore, env.TRACE_VIEW_EMERGENCY_SPAN_CAP)
213+
const firstChunkPromise = canLiveTail(
214+
run.taskEventStore,
215+
env.TRACE_VIEW_EMERGENCY_SPAN_CAP,
216+
env.EVENT_REPOSITORY_V1_REROUTE_ENABLED
217+
)
214218
? repository.getTraceChunk(
215219
getTaskEventStoreTableForRun(run),
216220
environment.id,

‎apps/webapp/app/presenters/v3/liveTailGate.ts‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1+
import { effectiveEventStore } from "~/v3/eventRepository/effectiveEventStore";
2+
13
// Progressive loading only runs with the live tail (without it every stream ping would
24
// reset and re-download the tree), and only the v2 store has the write time it pages by.
35
// The emergency span cap turns both off, so new loads take the summary path that honours it.
4-
export function canLiveTail(taskEventStore: string, emergencySpanCap: number | undefined): boolean {
5-
return taskEventStore === "clickhouse_v2" && emergencySpanCap === undefined;
6+
export function canLiveTail(
7+
taskEventStore: string,
8+
emergencySpanCap: number | undefined,
9+
v1RerouteEnabled: boolean
10+
): boolean {
11+
return (
12+
effectiveEventStore(taskEventStore, v1RerouteEnabled) === "clickhouse_v2" &&
13+
emergencySpanCap === undefined
14+
);
615
}
716

817
export function hasWriteTimes(events: { insertedAt?: string }[]): boolean {

‎apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { clampToEmergencySpanCap } from "~/v3/eventRepository/emergencySpanCap.s
1010
import { singleton } from "~/utils/singleton";
1111
import type { OrganizationDataStoresRegistry } from "~/services/dataStores/organizationDataStoresRegistry.server";
1212
import { type IEventRepository } from "~/v3/eventRepository/eventRepository.types";
13+
import { effectiveEventStore } from "~/v3/eventRepository/effectiveEventStore";
1314

1415
// Declared before the singletons below, which initialize clients at module load.
1516
const LOGS_LIST_REQUEST_TIMEOUT_MARGIN_SECONDS = 5;
@@ -696,7 +697,10 @@ export class ClickhouseFactory {
696697
/** Event repositories keyed by hostname hash (stateful, must be reused). */
697698
private readonly _eventRepositoryCache = new Map<string, ClickhouseEventRepository>();
698699

699-
constructor(private readonly _registry: OrganizationDataStoresRegistry) {}
700+
constructor(
701+
private readonly _registry: OrganizationDataStoresRegistry,
702+
private readonly _v1RerouteEnabled: boolean = env.EVENT_REPOSITORY_V1_REROUTE_ENABLED
703+
) {}
700704

701705
async isReady(): Promise<boolean> {
702706
if (!this._registry.isLoaded) {
@@ -774,9 +778,11 @@ export class ClickhouseFactory {
774778
}
775779

776780
getEventRepositoryForOrganizationSync(
777-
store: string,
781+
requestedStore: string,
778782
organizationId: string
779783
): { key: string; repository: IEventRepository } {
784+
// Normalized before the cache key, so "clickhouse" and "clickhouse_v2" share one repository
785+
const store = effectiveEventStore(requestedStore, this._v1RerouteEnabled);
780786
const dataStore = this._registry.get(organizationId, "CLICKHOUSE");
781787

782788
if (!dataStore) {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* The store a run's events are actually written to and read from. With the reroute on, the legacy
3+
* "clickhouse" store (task_events_v1) resolves to "clickhouse_v2"; every other value is unchanged.
4+
*/
5+
export function effectiveEventStore(store: string, v1RerouteEnabled: boolean): string {
6+
if (v1RerouteEnabled && store === "clickhouse") {
7+
return "clickhouse_v2";
8+
}
9+
10+
return store;
11+
}

‎apps/webapp/app/v3/eventRepository/index.server.ts‎

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { prisma } from "~/db.server";
55
import { runStore } from "../runStore.server";
66
import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server";
77
import { logger } from "~/services/logger.server";
8-
import { FEATURE_FLAG } from "../featureFlags";
98
import { flag } from "../featureFlags.server";
109
import { getTaskEventStore } from "../taskEventStore.server";
10+
import { resolveEventStore } from "./resolveEventStore.server";
1111
import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server";
1212

1313
export const EVENT_STORE_TYPES = {
@@ -55,13 +55,13 @@ export async function getConfiguredEventRepository(
5555
throw new Error("Organization not found when configuring event repository");
5656
}
5757

58-
// resolveTaskEventRepositoryFlag checks:
59-
// 1. organization.featureFlags (highest priority)
60-
// 2. global feature flags (via flags() function)
61-
// 3. env.EVENT_REPOSITORY_DEFAULT_STORE (fallback)
62-
const taskEventStore = await resolveTaskEventRepositoryFlag(
63-
(organization.featureFlags as Record<string, unknown> | null) ?? undefined
64-
);
58+
// Org override, then the global flag, then EVENT_REPOSITORY_DEFAULT_STORE
59+
const taskEventStore = await resolveEventStore({
60+
featureFlags: (organization.featureFlags as Record<string, unknown> | null) ?? undefined,
61+
flag,
62+
defaultStore: env.EVENT_REPOSITORY_DEFAULT_STORE,
63+
v1RerouteEnabled: env.EVENT_REPOSITORY_V1_REROUTE_ENABLED,
64+
});
6565

6666
if (taskEventStore === EVENT_STORE_TYPES.CLICKHOUSE_V2) {
6767
const { repository: resolvedRepository } =
@@ -83,7 +83,14 @@ export async function getEventRepository(
8383
featureFlags: Record<string, unknown> | undefined,
8484
parentStore: string | undefined
8585
): Promise<{ repository: IEventRepository; store: string }> {
86-
const taskEventStore = parentStore ?? (await resolveTaskEventRepositoryFlag(featureFlags));
86+
// New runs store the effective value, so the legacy v1 store stops spreading through old trees
87+
const taskEventStore = await resolveEventStore({
88+
parentStore,
89+
featureFlags,
90+
flag,
91+
defaultStore: env.EVENT_REPOSITORY_DEFAULT_STORE,
92+
v1RerouteEnabled: env.EVENT_REPOSITORY_V1_REROUTE_ENABLED,
93+
});
8794

8895
// Non-ClickHouse stores (e.g. the "taskEvent" DB default for Postgres-backed
8996
// runs, or the legacy "postgres" value) resolve to the Prisma event repo.
@@ -110,26 +117,6 @@ export async function getEventRepository(
110117
}
111118
}
112119

113-
async function resolveTaskEventRepositoryFlag(
114-
featureFlags: Record<string, unknown> | undefined
115-
): Promise<"clickhouse" | "clickhouse_v2" | "postgres"> {
116-
const flagResult = await flag({
117-
key: FEATURE_FLAG.taskEventRepository,
118-
defaultValue: env.EVENT_REPOSITORY_DEFAULT_STORE,
119-
overrides: featureFlags,
120-
});
121-
122-
if (flagResult === "clickhouse_v2") {
123-
return "clickhouse_v2";
124-
}
125-
126-
if (flagResult === "clickhouse") {
127-
return "clickhouse";
128-
}
129-
130-
return flagResult;
131-
}
132-
133120
export async function recordRunDebugLog(
134121
runId: string,
135122
message: string,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { FEATURE_FLAG } from "../featureFlags";
2+
import type { makeFlag } from "../featureFlags.server";
3+
import { effectiveEventStore } from "./effectiveEventStore";
4+
5+
type ResolveEventStoreOptions = {
6+
/** Set for child runs, which inherit their parent's store. */
7+
parentStore?: string;
8+
/** The organization's flag overrides. */
9+
featureFlags: Record<string, unknown> | undefined;
10+
flag: ReturnType<typeof makeFlag>;
11+
defaultStore: "postgres" | "clickhouse" | "clickhouse_v2";
12+
v1RerouteEnabled: boolean;
13+
};
14+
15+
/** The effective event store for a new run, or for an org's configured store when there's no parent. */
16+
export async function resolveEventStore({
17+
parentStore,
18+
featureFlags,
19+
flag,
20+
defaultStore,
21+
v1RerouteEnabled,
22+
}: ResolveEventStoreOptions): Promise<string> {
23+
const store =
24+
parentStore ??
25+
(await flag({
26+
key: FEATURE_FLAG.taskEventRepository,
27+
defaultValue: defaultStore,
28+
overrides: featureFlags,
29+
}));
30+
31+
return effectiveEventStore(store, v1RerouteEnabled);
32+
}

‎apps/webapp/app/v3/otlpExporter.server.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ type OTLPExporterConfig = {
5757
meter?: Meter;
5858
};
5959

60-
class OTLPExporter {
60+
export class OTLPExporter {
6161
private _tracer: Tracer;
6262
private readonly _clickhouseFactory: ClickhouseFactory;
6363
private readonly _verbose: boolean;

‎apps/webapp/test/clickhouseFactory.test.ts‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { postgresTest } from "@internal/testcontainers";
66
import { OrganizationDataStoresRegistry } from "~/services/dataStores/organizationDataStoresRegistry.server";
77
import { ClickhouseConnectionSchema } from "~/services/clickhouse/clickhouseSecretSchemas.server";
88
import { ClickhouseFactory } from "~/services/clickhouse/clickhouseFactory.server";
9+
import type { ClickhouseEventRepository } from "~/v3/eventRepository/clickhouseEventRepository.server";
910

1011
vi.setConfig({ testTimeout: 60_000 });
1112

@@ -124,4 +125,48 @@ describe("ClickHouse Factory", () => {
124125
expect(after).toBe(defaultClient);
125126
}
126127
);
128+
postgresTest(
129+
"routes the v1 store to the shared v2 repository when the reroute is on",
130+
async ({ prisma }) => {
131+
const registry = new OrganizationDataStoresRegistry(prisma, prisma);
132+
133+
await registry.addDataStore({
134+
key: "reroute-store",
135+
kind: "CLICKHOUSE",
136+
organizationIds: ["org-reroute"],
137+
config: ClickhouseConnectionSchema.parse({ url: TEST_URL }),
138+
});
139+
140+
await registry.loadFromDatabase();
141+
142+
const factory = new ClickhouseFactory(registry, true);
143+
const v1 = factory.getEventRepositoryForOrganizationSync("clickhouse", "org-reroute");
144+
const v2 = factory.getEventRepositoryForOrganizationSync("clickhouse_v2", "org-reroute");
145+
146+
expect(v1.key).toBe(v2.key);
147+
expect(v1.repository).toBe(v2.repository);
148+
expect((v1.repository as ClickhouseEventRepository).version).toBe("v2");
149+
}
150+
);
151+
152+
postgresTest("keeps the v1 repository when the reroute is off", async ({ prisma }) => {
153+
const registry = new OrganizationDataStoresRegistry(prisma, prisma);
154+
155+
await registry.addDataStore({
156+
key: "no-reroute-store",
157+
kind: "CLICKHOUSE",
158+
organizationIds: ["org-no-reroute"],
159+
config: ClickhouseConnectionSchema.parse({ url: TEST_URL }),
160+
});
161+
162+
await registry.loadFromDatabase();
163+
164+
const factory = new ClickhouseFactory(registry, false);
165+
const v1 = factory.getEventRepositoryForOrganizationSync("clickhouse", "org-no-reroute");
166+
const v2 = factory.getEventRepositoryForOrganizationSync("clickhouse_v2", "org-no-reroute");
167+
168+
expect(v1.key).not.toBe(v2.key);
169+
expect((v1.repository as ClickhouseEventRepository).version).toBe("v1");
170+
expect((v2.repository as ClickhouseEventRepository).version).toBe("v2");
171+
});
127172
});

0 commit comments

Comments
 (0)