From 3cbace8eb87d84394fa1df41935c47746febe632 Mon Sep 17 00:00:00 2001 From: Spencer Torres Date: Wed, 13 Aug 2025 23:48:44 -0400 Subject: [PATCH 1/5] add queryTimeout to API --- packages/api/src/models/team.ts | 2 ++ packages/api/src/routers/api/team.ts | 13 +++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/api/src/models/team.ts b/packages/api/src/models/team.ts index d2e164fb9..41a25a2bd 100644 --- a/packages/api/src/models/team.ts +++ b/packages/api/src/models/team.ts @@ -6,6 +6,7 @@ type ObjectId = mongoose.Types.ObjectId; export type TeamCHSettings = { metadataMaxRowsToRead?: number; searchRowLimit?: number; + queryTimeout?: number; fieldMetadataDisabled?: boolean; }; @@ -45,6 +46,7 @@ export default mongoose.model( // CH Client Settings metadataMaxRowsToRead: Number, searchRowLimit: Number, + queryTimeout: Number, fieldMetadataDisabled: Boolean, }, { diff --git a/packages/api/src/routers/api/team.ts b/packages/api/src/routers/api/team.ts index f6b1e14c7..fc7819c83 100644 --- a/packages/api/src/routers/api/team.ts +++ b/packages/api/src/routers/api/team.ts @@ -94,6 +94,7 @@ router.patch( body: z.object({ fieldMetadataDisabled: z.boolean().optional(), searchRowLimit: z.number().optional(), + queryTimeout: z.number().optional(), metadataMaxRowsToRead: z.number().optional(), }), }), @@ -104,11 +105,16 @@ router.patch( throw new Error(`User ${req.user?._id} not associated with a team`); } - const { fieldMetadataDisabled, metadataMaxRowsToRead, searchRowLimit } = - req.body; + const { + fieldMetadataDisabled, + metadataMaxRowsToRead, + searchRowLimit, + queryTimeout, + } = req.body; const settings = { ...(searchRowLimit !== undefined && { searchRowLimit }), + ...(queryTimeout !== undefined && { queryTimeout }), ...(fieldMetadataDisabled !== undefined && { fieldMetadataDisabled }), ...(metadataMaxRowsToRead !== undefined && { metadataMaxRowsToRead }), }; @@ -123,6 +129,9 @@ router.patch( ...(searchRowLimit !== undefined && { searchRowLimit: team?.searchRowLimit, }), + ...(queryTimeout !== undefined && { + queryTimeout: team?.queryTimeout, + }), ...(fieldMetadataDisabled !== undefined && { fieldMetadataDisabled: team?.fieldMetadataDisabled, }), From 674f996cf58dd455cca707c8288021d15225e6cb Mon Sep 17 00:00:00 2001 From: Spencer Torres Date: Thu, 14 Aug 2025 00:16:07 -0400 Subject: [PATCH 2/5] add queryTimeout to team settings page --- packages/app/src/TeamPage.tsx | 39 +++++++++++++++++++++++------------ packages/app/src/defaults.ts | 1 + 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/packages/app/src/TeamPage.tsx b/packages/app/src/TeamPage.tsx index cedb300f9..a2cd786a8 100644 --- a/packages/app/src/TeamPage.tsx +++ b/packages/app/src/TeamPage.tsx @@ -43,7 +43,7 @@ import { IS_LOCAL_MODE } from '@/config'; import { PageHeader } from './components/PageHeader'; import api from './api'; import { useConnections } from './connection'; -import { DEFAULT_SEARCH_ROW_LIMIT } from './defaults'; +import { DEFAULT_QUERY_TIMEOUT, DEFAULT_SEARCH_ROW_LIMIT } from './defaults'; import { withAppNav } from './layout'; import { useSources } from './source'; import { useConfirm } from './useConfirm'; @@ -1069,6 +1069,7 @@ type ClickhouseSettingType = 'number' | 'boolean'; interface ClickhouseSettingFormProps { settingKey: | 'searchRowLimit' + | 'queryTimeout' | 'metadataMaxRowsToRead' | 'fieldMetadataDisabled'; label: string; @@ -1078,7 +1079,7 @@ interface ClickhouseSettingFormProps { placeholder?: string; min?: number; max?: number; - displayValue?: (value: any) => string; + displayValue?: (value: any, defaultValue?: any) => string; options?: string[]; // For boolean settings displayed as select } @@ -1237,7 +1238,7 @@ function ClickhouseSettingForm({ {displayValue - ? displayValue(currentValue) + ? displayValue(currentValue, defaultValue) : currentValue?.toString() || 'Not set'} {hasAdminAccess && ( @@ -1257,6 +1258,14 @@ function ClickhouseSettingForm({ } function TeamQueryConfigSection() { + const displayValueWithUnit = + (unit: string) => (value: any, defaultValue?: any) => + value === undefined || value === defaultValue + ? `${defaultValue.toLocaleString()} ${unit} (System Default)` + : value === 0 + ? 'Unlimited' + : `${value.toLocaleString()} ${unit}`; + return ( @@ -1271,10 +1280,20 @@ function TeamQueryConfigSection() { tooltip="The number of rows per query for the Search page or search dashboard tiles" type="number" defaultValue={DEFAULT_SEARCH_ROW_LIMIT} - placeholder={`Enter value (default: ${DEFAULT_SEARCH_ROW_LIMIT})`} + placeholder={`default = ${DEFAULT_SEARCH_ROW_LIMIT}, 0 = unlimited`} min={1} max={100000} - displayValue={value => value ?? 'System Default'} + displayValue={displayValueWithUnit('rows')} + /> + - value == null - ? `System Default (${DEFAULT_METADATA_MAX_ROWS_TO_READ.toLocaleString()})` - : value === 0 - ? 'Unlimited' - : value.toLocaleString() - } + displayValue={displayValueWithUnit('rows')} /> Date: Thu, 14 Aug 2025 21:40:29 -0400 Subject: [PATCH 3/5] apply queryTimeout to clickhouse client --- packages/app/src/BenchmarkPage.tsx | 8 ++--- packages/app/src/clickhouse.ts | 25 +++++++++++++- .../app/src/components/KubeComponents.tsx | 4 +-- packages/app/src/hooks/useChartConfig.tsx | 4 +-- packages/app/src/hooks/useExplainQuery.tsx | 4 +-- .../app/src/hooks/useOffsetPaginatedQuery.tsx | 33 +++++++++++++------ packages/app/src/sessions.ts | 4 +-- packages/common-utils/src/clickhouse.ts | 20 +++++++++-- 8 files changed, 76 insertions(+), 26 deletions(-) diff --git a/packages/app/src/BenchmarkPage.tsx b/packages/app/src/BenchmarkPage.tsx index 91b7440fe..f28dc50d2 100644 --- a/packages/app/src/BenchmarkPage.tsx +++ b/packages/app/src/BenchmarkPage.tsx @@ -21,7 +21,7 @@ import { } from '@mantine/core'; import { useQuery, UseQueryOptions } from '@tanstack/react-query'; -import { getClickhouseClient } from '@/clickhouse'; +import { useClickhouseClient } from '@/clickhouse'; import { ConnectionSelectControlled } from './components/ConnectionSelect'; import DBTableChart from './components/DBTableChart'; @@ -38,7 +38,7 @@ function useBenchmarkQueryIds({ iterations?: number; }) { const enabled = queries.length > 0 && connections.length > 0; - const clickhouseClient = getClickhouseClient(); + const clickhouseClient = useClickhouseClient(); return useQuery({ enabled, @@ -95,7 +95,7 @@ function useEstimates( }, options: Omit, 'queryKey' | 'queryFn'> = {}, ) { - const clickhouseClient = getClickhouseClient(); + const clickhouseClient = useClickhouseClient(); return useQuery({ queryKey: ['estimate', queries, connections], queryFn: async () => { @@ -125,7 +125,7 @@ function useIndexes( }, options: Omit, 'queryKey' | 'queryFn'> = {}, ) { - const clickhouseClient = getClickhouseClient(); + const clickhouseClient = useClickhouseClient(); return useQuery({ queryKey: ['indexes', queries, connections], queryFn: async () => { diff --git a/packages/app/src/clickhouse.ts b/packages/app/src/clickhouse.ts index 7e3c6e9db..1d9074b7c 100644 --- a/packages/app/src/clickhouse.ts +++ b/packages/app/src/clickhouse.ts @@ -9,6 +9,7 @@ import type { ResponseJSON } from '@clickhouse/client'; import { chSql, ClickhouseClient, + ClickhouseClientOptions, ColumnMeta, } from '@hyperdx/common-utils/dist/clickhouse'; import { useQuery, UseQueryOptions } from '@tanstack/react-query'; @@ -16,28 +17,50 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query'; import { IS_LOCAL_MODE } from '@/config'; import { getLocalConnections } from '@/connection'; +import api from './api'; +import { DEFAULT_QUERY_TIMEOUT } from './defaults'; + const PROXY_CLICKHOUSE_HOST = '/api/clickhouse-proxy'; -export const getClickhouseClient = () => { +export const getClickhouseClient = ( + options: ClickhouseClientOptions = {}, +): ClickhouseClient => { if (IS_LOCAL_MODE) { const localConnections = getLocalConnections(); if (localConnections.length === 0) { console.warn('No local connection found'); return new ClickhouseClient({ host: '', + ...options, }); } return new ClickhouseClient({ host: localConnections[0].host, username: localConnections[0].username, password: localConnections[0].password, + ...options, }); } return new ClickhouseClient({ host: PROXY_CLICKHOUSE_HOST, + ...options, }); }; +export const useClickhouseClient = ( + options: ClickhouseClientOptions = {}, +): ClickhouseClient => { + const { data: me } = api.useMe(); + const teamQueryTimeout = me?.team?.queryTimeout; + if (teamQueryTimeout !== undefined) { + options.queryTimeout = teamQueryTimeout; + } else { + options.queryTimeout = DEFAULT_QUERY_TIMEOUT; + } + + return getClickhouseClient(options); +}; + export function useDatabasesDirect( { connectionId }: { connectionId: string }, options?: Omit, 'queryKey'>, diff --git a/packages/app/src/components/KubeComponents.tsx b/packages/app/src/components/KubeComponents.tsx index 4cc6c3a28..aeef9c253 100644 --- a/packages/app/src/components/KubeComponents.tsx +++ b/packages/app/src/components/KubeComponents.tsx @@ -13,7 +13,7 @@ import { import { Anchor, Badge, Group, Text, Timeline } from '@mantine/core'; import { useQuery, UseQueryOptions } from '@tanstack/react-query'; -import { getClickhouseClient } from '@/clickhouse'; +import { useClickhouseClient } from '@/clickhouse'; import { getMetadata } from '@/metadata'; import { getDisplayedTimestampValueExpression, getEventBody } from '@/source'; @@ -56,7 +56,7 @@ export const useV2LogBatch = ( }, options?: Omit, 'queryKey' | 'queryFn'>, ) => { - const clickhouseClient = getClickhouseClient(); + const clickhouseClient = useClickhouseClient(); return useQuery, Error>({ queryKey: [ 'v2LogBatch', diff --git a/packages/app/src/hooks/useChartConfig.tsx b/packages/app/src/hooks/useChartConfig.tsx index 9982fa1a8..72cc9e7be 100644 --- a/packages/app/src/hooks/useChartConfig.tsx +++ b/packages/app/src/hooks/useChartConfig.tsx @@ -14,7 +14,7 @@ import { format } from '@hyperdx/common-utils/dist/sqlFormatter'; import { ChartConfigWithOptDateRange } from '@hyperdx/common-utils/dist/types'; import { useQuery, UseQueryOptions } from '@tanstack/react-query'; -import { getClickhouseClient } from '@/clickhouse'; +import { useClickhouseClient } from '@/clickhouse'; import { IS_MTVIEWS_ENABLED } from '@/config'; import { buildMTViewSelectQuery } from '@/hdxMTViews'; import { getMetadata } from '@/metadata'; @@ -29,7 +29,7 @@ export function useQueriedChartConfig( options?: Partial>> & AdditionalUseQueriedChartConfigOptions, ) { - const clickhouseClient = getClickhouseClient(); + const clickhouseClient = useClickhouseClient(); const query = useQuery, ClickHouseQueryError | Error>({ queryKey: [config], queryFn: async ({ signal }) => { diff --git a/packages/app/src/hooks/useExplainQuery.tsx b/packages/app/src/hooks/useExplainQuery.tsx index 54f6bd12d..9d3d475c3 100644 --- a/packages/app/src/hooks/useExplainQuery.tsx +++ b/packages/app/src/hooks/useExplainQuery.tsx @@ -2,7 +2,7 @@ import { renderChartConfig } from '@hyperdx/common-utils/dist/renderChartConfig' import { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types'; import { useQuery, UseQueryOptions } from '@tanstack/react-query'; -import { getClickhouseClient } from '@/clickhouse'; +import { useClickhouseClient } from '@/clickhouse'; import { getMetadata } from '@/metadata'; export function useExplainQuery( @@ -13,7 +13,7 @@ export function useExplainQuery( ..._config, with: undefined, }; - const clickhouseClient = getClickhouseClient(); + const clickhouseClient = useClickhouseClient(); const { data, isLoading, error } = useQuery({ queryKey: ['explain', config], queryFn: async ({ signal }) => { diff --git a/packages/app/src/hooks/useOffsetPaginatedQuery.tsx b/packages/app/src/hooks/useOffsetPaginatedQuery.tsx index 8f390ad06..b0454c664 100644 --- a/packages/app/src/hooks/useOffsetPaginatedQuery.tsx +++ b/packages/app/src/hooks/useOffsetPaginatedQuery.tsx @@ -15,12 +15,22 @@ import { useQueryClient, } from '@tanstack/react-query'; +import api from '@/api'; import { getClickhouseClient } from '@/clickhouse'; import { getMetadata } from '@/metadata'; import { omit } from '@/utils'; -function queryKeyFn(prefix: string, config: ChartConfigWithDateRange) { - return [prefix, config] as const; +type TQueryKey = readonly [ + string, + ChartConfigWithDateRange, + number | undefined, +]; +function queryKeyFn( + prefix: string, + config: ChartConfigWithDateRange, + queryTimeout?: number, +): TQueryKey { + return [prefix, config, queryTimeout]; } type TPageParam = number; @@ -34,11 +44,12 @@ type TData = { pageParams: TPageParam[]; }; -const queryFn: QueryFunction< - TQueryFnData, - readonly [string, ChartConfigWithDateRange], - number -> = async ({ queryKey, pageParam, signal, meta }) => { +const queryFn: QueryFunction = async ({ + queryKey, + pageParam, + signal, + meta, +}) => { if (meta == null) { throw new Error('Query missing client meta'); } @@ -60,7 +71,8 @@ const queryFn: QueryFunction< getMetadata(), ); - const clickhouseClient = getClickhouseClient(); + const queryTimeout = queryKey[2]; + const clickhouseClient = getClickhouseClient({ queryTimeout }); const resultSet = await clickhouseClient.query<'JSONCompactEachRowWithNamesAndTypes'>({ query: query.sql, @@ -247,7 +259,8 @@ export default function useOffsetPaginatedQuery( queryKeyPrefix?: string; } = {}, ) { - const key = queryKeyFn(queryKeyPrefix, config); + const { data: meData } = api.useMe(); + const key = queryKeyFn(queryKeyPrefix, config, meData?.team?.queryTimeout); const queryClient = useQueryClient(); const matchedQueries = queryClient.getQueriesData({ queryKey: [queryKeyPrefix, omit(config, ['dateRange'])], @@ -268,7 +281,7 @@ export default function useOffsetPaginatedQuery( TQueryFnData, Error | ClickHouseQueryError, TData, - Readonly<[string, typeof config]>, + TQueryKey, TPageParam >({ queryKey: key, diff --git a/packages/app/src/sessions.ts b/packages/app/src/sessions.ts index 8fcd589aa..90d8233bd 100644 --- a/packages/app/src/sessions.ts +++ b/packages/app/src/sessions.ts @@ -15,7 +15,7 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query'; import { getMetadata } from '@/metadata'; import { usePrevious } from '@/utils'; -import { getClickhouseClient } from './clickhouse'; +import { getClickhouseClient, useClickhouseClient } from './clickhouse'; import { IS_LOCAL_MODE } from './config'; import { getLocalConnections } from './connection'; import { useSource } from './source'; @@ -54,7 +54,7 @@ export function useSessions( ) { const FIXED_SDK_ATTRIBUTES = ['teamId', 'teamName', 'userEmail', 'userName']; const SESSIONS_CTE_NAME = 'sessions'; - const clickhouseClient = getClickhouseClient(); + const clickhouseClient = useClickhouseClient(); return useQuery, Error>({ queryKey: [ 'sessions', diff --git a/packages/common-utils/src/clickhouse.ts b/packages/common-utils/src/clickhouse.ts index f1decf475..655c318fb 100644 --- a/packages/common-utils/src/clickhouse.ts +++ b/packages/common-utils/src/clickhouse.ts @@ -385,15 +385,17 @@ interface QueryInputs { } export type ClickhouseClientOptions = { - host: string; + host?: string; username?: string; password?: string; + queryTimeout?: number; }; export class ClickhouseClient { private readonly host: string; private readonly username?: string; private readonly password?: string; + private readonly queryTimeout?: number; /* * Some clickhouse db's (the demo instance for example) make the * max_rows_to_read setting readonly and the query will fail if you try to @@ -401,10 +403,16 @@ export class ClickhouseClient { */ private maxRowReadOnly: boolean; - constructor({ host, username, password }: ClickhouseClientOptions) { - this.host = host; + constructor({ + host, + username, + password, + queryTimeout, + }: ClickhouseClientOptions) { + this.host = host!; this.username = username; this.password = password; + this.queryTimeout = queryTimeout; this.maxRowReadOnly = false; } @@ -484,6 +492,12 @@ export class ClickhouseClient { if (clickhouse_settings?.max_rows_to_read && this.maxRowReadOnly) { delete clickhouse_settings['max_rows_to_read']; } + if ( + clickhouse_settings?.max_execution_time === undefined && + (this.queryTimeout || 0) > 0 + ) { + clickhouse_settings.max_execution_time = this.queryTimeout; + } if (isBrowser) { // TODO: check if we can use the client-web directly From a8881a41a157e33aaf3d28dc93f22a0d0e0f20cc Mon Sep 17 00:00:00 2001 From: Spencer Torres Date: Thu, 14 Aug 2025 21:43:28 -0400 Subject: [PATCH 4/5] changeset --- .changeset/afraid-rocks-smile.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/afraid-rocks-smile.md diff --git a/.changeset/afraid-rocks-smile.md b/.changeset/afraid-rocks-smile.md new file mode 100644 index 000000000..acd3f0658 --- /dev/null +++ b/.changeset/afraid-rocks-smile.md @@ -0,0 +1,7 @@ +--- +"@hyperdx/common-utils": minor +"@hyperdx/api": minor +"@hyperdx/app": minor +--- + +added team level queryTimeout to ClickHouse client From b3ba05bdec92243b03d6f2d63ace0e3b7141ce86 Mon Sep 17 00:00:00 2001 From: Spencer Torres Date: Wed, 27 Aug 2025 21:06:04 -0400 Subject: [PATCH 5/5] add metadata query timeout --- packages/app/src/metadata.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/app/src/metadata.ts b/packages/app/src/metadata.ts index ecb493719..6fe3f45a2 100644 --- a/packages/app/src/metadata.ts +++ b/packages/app/src/metadata.ts @@ -2,5 +2,8 @@ import { getMetadata as _getMetadata } from '@hyperdx/common-utils/dist/metadata import { getClickhouseClient } from '@/clickhouse'; +import { DEFAULT_QUERY_TIMEOUT } from './defaults'; + // TODO: Get rid of this function and convert to singleton -export const getMetadata = () => _getMetadata(getClickhouseClient()); +export const getMetadata = () => + _getMetadata(getClickhouseClient({ queryTimeout: DEFAULT_QUERY_TIMEOUT }));