Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .server-changes/clickhouse-read-replica.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: improvement
---

Optionally route ClickHouse read traffic to a read replica while writes stay on the primary. Set `CLICKHOUSE_READER_URL` to move all reads, or target the busiest paths with `RUNS_LIST_CLICKHOUSE_URL` (runs list) and `EVENTS_READER_CLICKHOUSE_URL` (traces, spans, logs). All optional; unset keeps current behavior.
Comment thread
ericallam marked this conversation as resolved.
31 changes: 26 additions & 5 deletions apps/webapp/app/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1718,6 +1718,11 @@ const EnvironmentSchema = z

// Clickhouse
CLICKHOUSE_URL: z.string(),
// Optional read replica endpoint. Read-only clients (logs, query, admin, runsList,
// engine, realtime) default to this when their own URL is unset; writes always stay on
// CLICKHOUSE_URL. Events reads opt in separately via EVENTS_READER_CLICKHOUSE_URL (no
// fallback here). Must share storage with the CLICKHOUSE_URL warehouse.
CLICKHOUSE_READER_URL: z.string().optional(),
CLICKHOUSE_KEEP_ALIVE_ENABLED: z.string().default("1"),
CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS: z.coerce.number().int().optional(),
CLICKHOUSE_MAX_OPEN_CONNECTIONS: z.coerce.number().int().default(10),
Expand Down Expand Up @@ -1780,13 +1785,13 @@ const EnvironmentSchema = z
LOGS_CLICKHOUSE_URL: z
.string()
.optional()
.transform((v) => v ?? process.env.CLICKHOUSE_URL),
.transform((v) => v ?? process.env.CLICKHOUSE_READER_URL ?? process.env.CLICKHOUSE_URL),

// Query page ClickHouse limits (for TSQL queries)
QUERY_CLICKHOUSE_URL: z
.string()
.optional()
.transform((v) => v ?? process.env.CLICKHOUSE_URL),
.transform((v) => v ?? process.env.CLICKHOUSE_READER_URL ?? process.env.CLICKHOUSE_URL),
QUERY_CLICKHOUSE_MAX_EXECUTION_TIME: z.coerce.number().int().default(10),
QUERY_CLICKHOUSE_MAX_MEMORY_USAGE: z.coerce.number().int().default(1_073_741_824), // 1GB in bytes
QUERY_CLICKHOUSE_MAX_AST_ELEMENTS: z.coerce.number().int().default(4_000_000),
Expand All @@ -1805,12 +1810,14 @@ const EnvironmentSchema = z
ADMIN_CLICKHOUSE_URL: z
.string()
.optional()
.transform((v) => v ?? process.env.CLICKHOUSE_URL),
.transform((v) => v ?? process.env.CLICKHOUSE_READER_URL ?? process.env.CLICKHOUSE_URL),

EVENTS_CLICKHOUSE_URL: z
.string()
.optional()
.transform((v) => v ?? process.env.CLICKHOUSE_URL),
// Events read replica (traces/spans/logs). No CLICKHOUSE_READER_URL fallback by design: this write-capable client opts in explicitly.
EVENTS_READER_CLICKHOUSE_URL: z.string().optional(),
EVENTS_CLICKHOUSE_KEEP_ALIVE_ENABLED: z.string().default("1"),
EVENTS_CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS: z.coerce.number().int().optional(),
EVENTS_CLICKHOUSE_MAX_OPEN_CONNECTIONS: z.coerce.number().int().default(10),
Expand All @@ -1823,7 +1830,7 @@ const EnvironmentSchema = z
RUN_ENGINE_CLICKHOUSE_URL: z
.string()
.optional()
.transform((v) => v ?? process.env.CLICKHOUSE_URL),
.transform((v) => v ?? process.env.CLICKHOUSE_READER_URL ?? process.env.CLICKHOUSE_URL),
RUN_ENGINE_CLICKHOUSE_KEEP_ALIVE_ENABLED: z.string().default("1"),
RUN_ENGINE_CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS: z.coerce.number().int().optional(),
RUN_ENGINE_CLICKHOUSE_MAX_OPEN_CONNECTIONS: z.coerce.number().int().default(5),
Expand All @@ -1835,7 +1842,7 @@ const EnvironmentSchema = z
REALTIME_BACKEND_NATIVE_CLICKHOUSE_URL: z
.string()
.optional()
.transform((v) => v ?? process.env.CLICKHOUSE_URL),
.transform((v) => v ?? process.env.CLICKHOUSE_READER_URL ?? process.env.CLICKHOUSE_URL),
REALTIME_BACKEND_NATIVE_CLICKHOUSE_KEEP_ALIVE_ENABLED: z.string().default("1"),
REALTIME_BACKEND_NATIVE_CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS: z.coerce
.number()
Expand All @@ -1846,6 +1853,20 @@ const EnvironmentSchema = z
.enum(["log", "error", "warn", "info", "debug"])
.default("info"),
REALTIME_BACKEND_NATIVE_CLICKHOUSE_COMPRESSION_REQUEST: z.string().default("1"),
// Dedicated ClickHouse pool for the runs list (dashboard + API). Lets us point
// the highest-traffic read path at a read replica without moving ingest/replication
// writes off CLICKHOUSE_URL. Falls back to CLICKHOUSE_URL when unset.
RUNS_LIST_CLICKHOUSE_URL: z
.string()
.optional()
.transform((v) => v ?? process.env.CLICKHOUSE_READER_URL ?? process.env.CLICKHOUSE_URL),
RUNS_LIST_CLICKHOUSE_KEEP_ALIVE_ENABLED: z.string().default("1"),
RUNS_LIST_CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS: z.coerce.number().int().optional(),
RUNS_LIST_CLICKHOUSE_MAX_OPEN_CONNECTIONS: z.coerce.number().int().default(10),
RUNS_LIST_CLICKHOUSE_LOG_LEVEL: z
.enum(["log", "error", "warn", "info", "debug"])
.default("info"),
RUNS_LIST_CLICKHOUSE_COMPRESSION_REQUEST: z.string().default("1"),
EVENTS_CLICKHOUSE_BATCH_SIZE: z.coerce.number().int().default(1000),
EVENTS_CLICKHOUSE_FLUSH_INTERVAL_MS: z.coerce.number().int().default(1000),
METRICS_CLICKHOUSE_BATCH_SIZE: z.coerce.number().int().default(10000),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ export class ApiRunListPresenter extends BasePresenter {

const clickhouse = await clickhouseFactory.getClickhouseForOrganization(
organizationId,
"standard"
"runsList"
);
const presenter = new NextRunListPresenter(this._replica, clickhouse, this.readThroughDeps);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ async function getRunsListFromTableState({

const clickhouse = await clickhouseFactory.getClickhouseForOrganization(
project.organizationId,
"standard"
"runsList"
);
const runsListPresenter = new NextRunListPresenter($replica, clickhouse);
const currentPageResult = await runsListPresenter.call(project.organizationId, environment.id, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {

const clickhouse = await clickhouseFactory.getClickhouseForOrganization(
project.organizationId,
"standard"
"runsList"
Comment thread
nicktrn marked this conversation as resolved.
);
const presenter = new NextRunListPresenter($replica, clickhouse);
const list = presenter.call(project.organizationId, environment.id, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) {

const clickhouse = await clickhouseFactory.getClickhouseForOrganization(
project.organizationId,
"standard"
"runsList"
);
const runsRepository = new RunsRepository({ clickhouse, prisma: $replica });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) {

const clickhouse = await clickhouseFactory.getClickhouseForOrganization(
project.organizationId,
"standard"
"runsList"
);
const runsRepository = new RunsRepository({ clickhouse, prisma: $replica });

Expand Down
66 changes: 60 additions & 6 deletions apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,36 @@ function initializeRealtimeClickhouseClient(): ClickHouse {
});
}

/** Runs list reads — dashboard + API (`RUNS_LIST_CLICKHOUSE_URL`);
* falls back to the default client if unset. */
const defaultRunsListClickhouseClient = singleton(
"runsListClickhouseClient",
initializeRunsListClickhouseClient
);

function initializeRunsListClickhouseClient(): ClickHouse {
if (!env.RUNS_LIST_CLICKHOUSE_URL) {
return defaultClickhouseClient;
}

const url = new URL(env.RUNS_LIST_CLICKHOUSE_URL);
url.searchParams.delete("secure");

return new ClickHouse({
url: url.toString(),
name: "runs-list-clickhouse",
keepAlive: {
enabled: env.RUNS_LIST_CLICKHOUSE_KEEP_ALIVE_ENABLED === "1",
idleSocketTtl: env.RUNS_LIST_CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS,
},
logLevel: env.RUNS_LIST_CLICKHOUSE_LOG_LEVEL,
compression: {
request: env.RUNS_LIST_CLICKHOUSE_COMPRESSION_REQUEST === "1",
},
maxOpenConnections: env.RUNS_LIST_CLICKHOUSE_MAX_OPEN_CONNECTIONS,
});
}

/** Task events (`EVENTS_CLICKHOUSE_URL`); not exported — accessed via factory. */
const defaultEventsClickhouseClient = singleton(
"eventsClickhouseClient",
Expand All @@ -262,12 +292,10 @@ function initializeEventsClickhouseClient(): ClickHouse {
throw new Error("EVENTS_CLICKHOUSE_URL is not set");
}

const url = new URL(env.EVENTS_CLICKHOUSE_URL);
url.searchParams.delete("secure");
const writerUrl = new URL(env.EVENTS_CLICKHOUSE_URL);
writerUrl.searchParams.delete("secure");

return new ClickHouse({
url: url.toString(),
name: "task-events",
const commonConfig = {
keepAlive: {
enabled: env.EVENTS_CLICKHOUSE_KEEP_ALIVE_ENABLED === "1",
idleSocketTtl: env.EVENTS_CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS,
Expand All @@ -277,6 +305,28 @@ function initializeEventsClickhouseClient(): ClickHouse {
request: env.EVENTS_CLICKHOUSE_COMPRESSION_REQUEST === "1",
},
maxOpenConnections: env.EVENTS_CLICKHOUSE_MAX_OPEN_CONNECTIONS,
};

// Mixed read+write client: split reads to its own EVENTS_READER_CLICKHOUSE_URL (not the global reader) so inserts can never hit the replica.
if (env.EVENTS_READER_CLICKHOUSE_URL) {
const readerUrl = new URL(env.EVENTS_READER_CLICKHOUSE_URL);
readerUrl.searchParams.delete("secure");

if (readerUrl.toString() !== writerUrl.toString()) {
return new ClickHouse({
...commonConfig,
writerName: "task-events-writer",
writerUrl: writerUrl.toString(),
readerName: "task-events-reader",
readerUrl: readerUrl.toString(),
});
}
}

return new ClickHouse({
...commonConfig,
name: "task-events",
url: writerUrl.toString(),
});
}

Expand All @@ -298,7 +348,8 @@ export type ClientType =
| "query"
| "admin"
| "engine"
| "realtime";
| "realtime"
| "runsList";

function buildOrgClickhouseClient(url: string, clientType: ClientType): ClickHouse {
const parsed = new URL(url);
Expand Down Expand Up @@ -388,6 +439,7 @@ function buildOrgClickhouseClient(url: string, clientType: ClientType): ClickHou
case "standard":
case "query":
case "admin":
case "runsList":
return new ClickHouse({
url: parsed.toString(),
name,
Expand Down Expand Up @@ -455,6 +507,8 @@ export class ClickhouseFactory {
return defaultRunEngineClickhouseClient;
case "realtime":
return defaultRealtimeClickhouseClient;
case "runsList":
return defaultRunsListClickhouseClient;
}
}

Expand Down