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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,5 @@ dist
# Vite logs files
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

launch.json
5 changes: 5 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ cp .env.example .env
- **`CLICKHOUSE_DATABASE`** - ClickHouse database name
- Default: `default`

- **`CLICKHOUSE_REQUEST_TIMEOUT_MS`** - Timeout in milliseconds for ClickHouse HTTP requests
- Default: `30000` (30 seconds)
- Guards against indefinitely hung queries — particularly important when running under Bun, which does not send TCP keepalive probes via `http.Agent`, meaning a silently dead connection (e.g. NAT table expiry in Kubernetes) would otherwise hang forever
- Increase if your ClickHouse queries legitimately take longer than 30 seconds

### RPC Configuration

- **`NODE_URL`** - EVM RPC node URL (required)
Expand Down
16 changes: 16 additions & 0 deletions lib/clickhouse.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type ClickHouseClient, createClient } from '@clickhouse/client';
import { CLICKHOUSE_REQUEST_TIMEOUT_MS } from './config';
import { createLogger } from './logger';
import { trackClickHouseOperation } from './prometheus';

Expand Down Expand Up @@ -31,6 +32,7 @@ function getClient(): ClickHouseClient {
username: process.env.CLICKHOUSE_USERNAME || 'default',
password: process.env.CLICKHOUSE_PASSWORD || '',
database: process.env.CLICKHOUSE_DATABASE,
request_timeout: CLICKHOUSE_REQUEST_TIMEOUT_MS,
});
}
return _client;
Expand All @@ -47,6 +49,7 @@ function getInsertClient(): ClickHouseClient {
username: process.env.CLICKHOUSE_USERNAME || 'default',
password: process.env.CLICKHOUSE_PASSWORD || '',
database: getInsertDatabase(),
request_timeout: CLICKHOUSE_REQUEST_TIMEOUT_MS,
});
}
return _insertClient;
Expand All @@ -64,6 +67,7 @@ function getSetupClient(): ClickHouseClient {
username: process.env.CLICKHOUSE_USERNAME || 'default',
password: process.env.CLICKHOUSE_PASSWORD || '',
database: getInsertDatabase(),
request_timeout: CLICKHOUSE_REQUEST_TIMEOUT_MS,
});
}
return _setupClient;
Expand Down Expand Up @@ -149,13 +153,23 @@ export async function query<T = any>(
// Track total operation time
const startTime = performance.now();

const controller = new AbortController();
const timeoutHandle = setTimeout(() => {
controller.abort(
new Error(
`ClickHouse query timed out after ${CLICKHOUSE_REQUEST_TIMEOUT_MS}ms`,
),
);
}, CLICKHOUSE_REQUEST_TIMEOUT_MS);

try {
// Track query execution time
const queryStartTime = performance.now();
const resultSet = await client.query({
query,
query_params,
format: 'JSONEachRow',
abort_signal: controller.signal,
});
trackClickHouseOperation('read', 'success', startTime);
const queryEndTime = performance.now();
Expand All @@ -182,6 +196,7 @@ export async function query<T = any>(
totalTimeMs,
});

clearTimeout(timeoutHandle);
return {
data,
metrics: {
Expand All @@ -191,6 +206,7 @@ export async function query<T = any>(
},
};
} catch (error: unknown) {
clearTimeout(timeoutHandle);
trackClickHouseOperation('read', 'error', startTime);
const url = process.env.CLICKHOUSE_URL || 'http://localhost:8123';
const urlObj = new URL(url);
Expand Down
13 changes: 13 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export const DEFAULT_CONFIG = {

// Auto-restart
AUTO_RESTART_DELAY: 10,

// ClickHouse request timeout
CLICKHOUSE_REQUEST_TIMEOUT_MS: 30_000,
} as const;

// ============================================================================
Expand Down Expand Up @@ -112,6 +115,16 @@ export const CLICKHOUSE_PASSWORD =
process.env.CLICKHOUSE_PASSWORD || DEFAULT_CONFIG.CLICKHOUSE_PASSWORD;
export const CLICKHOUSE_DATABASE = process.env.CLICKHOUSE_DATABASE;

/**
* Timeout in milliseconds for ClickHouse HTTP requests
* Default: 30000ms (30 seconds)
*/
export const CLICKHOUSE_REQUEST_TIMEOUT_MS = parseInt(
process.env.CLICKHOUSE_REQUEST_TIMEOUT_MS ||
String(DEFAULT_CONFIG.CLICKHOUSE_REQUEST_TIMEOUT_MS),
10,
);

/**
* Database name for insert operations (INSERT, DDL)
* Optional override - falls back to CLICKHOUSE_DATABASE if not set
Expand Down
5 changes: 4 additions & 1 deletion lib/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ function transportJSON(logObj: Record<string, unknown>): void {
logger: meta.name,
msg,
};
if (payload !== undefined && (payload === null || typeof payload !== 'object')) {
if (
payload !== undefined &&
(payload === null || typeof payload !== 'object')
) {
flat.data = payload;
}
process.stdout.write(JSON.stringify(flat) + '\n');
Expand Down
7 changes: 4 additions & 3 deletions services/polymarket/fetch-gamma.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ const mockFetch = mock(() =>
);
globalThis.fetch = mockFetch as unknown as typeof fetch;

const conditionId = (i: number) =>
`0x${i.toString(16).padStart(64, '0')}`;
const conditionId = (i: number) => `0x${i.toString(16).padStart(64, '0')}`;

const marketStub = (id: number) => ({
id: String(id),
Expand All @@ -31,7 +30,9 @@ describe('fetchGammaApi', () => {
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({ markets: [marketStub(1), marketStub(2)] }),
Promise.resolve({
markets: [marketStub(1), marketStub(2)],
}),
}),
);
const result = await fetchGammaApi(
Expand Down
Loading
Loading