This is an automated email from the ASF dual-hosted git repository.
vogievetsky pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new 22faad77d2b Web console: Improve explore max time cancelation (#18830)
22faad77d2b is described below
commit 22faad77d2b93fc2ba7473ef56185c0a9931b266
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Thu Dec 11 00:47:05 2025 +0000
Web console: Improve explore max time cancelation (#18830)
* add a timeout
* hook up cancel for table
* remove console.log
---
web-console/src/utils/general.spec.ts | 115 +++++++++++++++++++++
web-console/src/utils/general.tsx | 26 ++++-
.../src/views/explore-view/explore-view.tsx | 1 +
.../grouping-table-module.tsx | 19 ++--
.../explore-view/query-macros/max-data-time.ts | 3 +-
.../views/explore-view/utils/max-time-for-table.ts | 14 ++-
6 files changed, 164 insertions(+), 14 deletions(-)
diff --git a/web-console/src/utils/general.spec.ts
b/web-console/src/utils/general.spec.ts
index 2e863ca7d6f..7f2d5c56643 100644
--- a/web-console/src/utils/general.spec.ts
+++ b/web-console/src/utils/general.spec.ts
@@ -34,6 +34,7 @@ import {
OVERLAY_OPEN_SELECTOR,
parseCsvLine,
swapElements,
+ wait,
} from './general';
describe('general', () => {
@@ -231,4 +232,118 @@ describe('general', () => {
expect(OVERLAY_OPEN_SELECTOR).toEqual('.bp5-portal .bp5-overlay-open');
});
});
+
+ describe('wait', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it('resolves after the specified time', async () => {
+ const promise = wait(100);
+ expect(promise).toBeInstanceOf(Promise);
+
+ jest.advanceTimersByTime(99);
+ await Promise.resolve(); // Let microtasks run
+ expect(promise).not.toBe(await Promise.race([promise,
Promise.resolve('pending')]));
+
+ jest.advanceTimersByTime(1);
+ await expect(promise).resolves.toBeUndefined();
+ });
+
+ it('works without a signal (backward compatibility)', async () => {
+ const promise = wait(50);
+ jest.advanceTimersByTime(50);
+ await expect(promise).resolves.toBeUndefined();
+ });
+
+ it('resolves normally when signal does not abort', async () => {
+ const controller = new AbortController();
+ const promise = wait(100, controller.signal);
+
+ jest.advanceTimersByTime(100);
+ await expect(promise).resolves.toBeUndefined();
+ });
+
+ it('rejects when signal aborts before timeout', async () => {
+ const controller = new AbortController();
+ const promise = wait(100, controller.signal);
+
+ jest.advanceTimersByTime(50);
+ controller.abort();
+
+ await expect(promise).rejects.toThrow('Aborted');
+ });
+
+ it('rejects immediately if signal is already aborted', async () => {
+ const controller = new AbortController();
+ controller.abort();
+
+ const promise = wait(100, controller.signal);
+ await expect(promise).rejects.toThrow('Aborted');
+
+ // Timer should not have been created
+ expect(jest.getTimerCount()).toBe(0);
+ });
+
+ it('cleans up timeout when aborted', async () => {
+ const controller = new AbortController();
+ const promise = wait(100, controller.signal);
+
+ expect(jest.getTimerCount()).toBe(1);
+
+ controller.abort();
+
+ try {
+ await promise;
+ } catch {
+ // Expected
+ }
+
+ // Timer should be cleaned up
+ expect(jest.getTimerCount()).toBe(0);
+ });
+
+ it('cleans up event listener when timeout completes', async () => {
+ const controller = new AbortController();
+ const removeEventListenerSpy = jest.spyOn(controller.signal,
'removeEventListener');
+
+ const promise = wait(100, controller.signal);
+ jest.advanceTimersByTime(100);
+ await promise;
+
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('abort',
expect.any(Function));
+ });
+
+ it('cleans up event listener when aborted', async () => {
+ const controller = new AbortController();
+ const removeEventListenerSpy = jest.spyOn(controller.signal,
'removeEventListener');
+
+ const promise = wait(100, controller.signal);
+ controller.abort();
+
+ try {
+ await promise;
+ } catch {
+ // Expected
+ }
+
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('abort',
expect.any(Function));
+ });
+
+ it('handles multiple waits with same signal', async () => {
+ const controller = new AbortController();
+ const promise1 = wait(100, controller.signal);
+ const promise2 = wait(200, controller.signal);
+
+ jest.advanceTimersByTime(50);
+ controller.abort();
+
+ await expect(promise1).rejects.toThrow('Aborted');
+ await expect(promise2).rejects.toThrow('Aborted');
+ });
+ });
});
diff --git a/web-console/src/utils/general.tsx
b/web-console/src/utils/general.tsx
index f206ed02457..d719df030ce 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -63,9 +63,29 @@ export function arraysEqualByElement<T>(xs: T[], ys: T[]):
boolean {
return xs.length === ys.length && xs.every((x, i) => x === ys[i]);
}
-export function wait(ms: number): Promise<void> {
- return new Promise(resolve => {
- setTimeout(resolve, ms);
+export function wait(ms: number, signal?: AbortSignal): Promise<void> {
+ return new Promise((resolve, reject) => {
+ if (signal?.aborted) {
+ reject(new Error('Aborted'));
+ return;
+ }
+
+ const timeoutId = setTimeout(() => {
+ cleanup();
+ resolve();
+ }, ms);
+
+ const onAbort = () => {
+ cleanup();
+ reject(new Error('Aborted'));
+ };
+
+ const cleanup = () => {
+ clearTimeout(timeoutId);
+ signal?.removeEventListener('abort', onAbort);
+ };
+
+ signal?.addEventListener('abort', onAbort);
});
}
diff --git a/web-console/src/views/explore-view/explore-view.tsx
b/web-console/src/views/explore-view/explore-view.tsx
index 9176421263c..7176c05cf65 100644
--- a/web-console/src/views/explore-view/explore-view.tsx
+++ b/web-console/src/views/explore-view/explore-view.tsx
@@ -229,6 +229,7 @@ export const ExploreView = React.memo(function
ExploreView({ capabilities }: Exp
const { query: rewrittenQuery, maxTime } = await rewriteMaxDataTime(
rewriteAggregate(parsedQuery, querySource.measures),
+ signal,
);
const results = await runSqlQuery(rewrittenQuery, queryTimezone, signal);
diff --git
a/web-console/src/views/explore-view/modules/grouping-table-module/grouping-table-module.tsx
b/web-console/src/views/explore-view/modules/grouping-table-module/grouping-table-module.tsx
index 62e6314b62d..7a0fbf148c9 100644
---
a/web-console/src/views/explore-view/modules/grouping-table-module/grouping-table-module.tsx
+++
b/web-console/src/views/explore-view/modules/grouping-table-module/grouping-table-module.tsx
@@ -19,7 +19,7 @@
import { Button } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import type { Timezone } from 'chronoshift';
-import type { SqlExpression, SqlOrderByDirection, SqlQuery } from
'druid-query-toolkit';
+import type { SqlExpression, SqlOrderByDirection } from 'druid-query-toolkit';
import { C, F } from 'druid-query-toolkit';
import { useMemo } from 'react';
@@ -233,10 +233,10 @@
ModuleRepository.registerModule<GroupingTableParameterValues>({
.changeLimitValue(maxPivotValues);
}, [querySource, where, moduleWhere, parameterValues]);
- const [pivotValueState, queryManager] = useQueryManager({
+ const [pivotValueState, pivotValueQueryManager] = useQueryManager({
query: pivotValueQuery,
- processQuery: async (pivotValueQuery: SqlQuery) => {
- return (await runSqlQuery(pivotValueQuery)).getColumnByName('v') as
string[];
+ processQuery: async (pivotValueQuery, signal) => {
+ return (await runSqlQuery(pivotValueQuery,
signal)).getColumnByName('v') as string[];
},
});
@@ -272,11 +272,12 @@
ModuleRepository.registerModule<GroupingTableParameterValues>({
};
}, [querySource.query, timezone, where, parameterValues,
pivotValueState.data]);
- const [resultState] = useQueryManager({
+ const [resultState, resultQueryManager] = useQueryManager({
query: queryAndMore,
processQuery: async (queryAndMore, signal) => {
const { timezone, globalWhere, queryAndHints } = queryAndMore;
const { query, columnHints } = queryAndHints;
+
let result = await runSqlQuery({ query, timezone }, signal);
if (result.sqlQuery) {
result = result.attachQuery(
@@ -328,7 +329,13 @@
ModuleRepository.registerModule<GroupingTableParameterValues>({
/>
) : undefined}
{resultState.loading && (
- <Loader cancelText="Cancel query" onCancel={() =>
queryManager.cancelCurrent()} />
+ <Loader
+ cancelText="Cancel query"
+ onCancel={() => {
+ pivotValueQueryManager.cancelCurrent();
+ resultQueryManager.cancelCurrent();
+ }}
+ />
)}
</div>
);
diff --git a/web-console/src/views/explore-view/query-macros/max-data-time.ts
b/web-console/src/views/explore-view/query-macros/max-data-time.ts
index 1f0a2627a9b..5628f65575f 100644
--- a/web-console/src/views/explore-view/query-macros/max-data-time.ts
+++ b/web-console/src/views/explore-view/query-macros/max-data-time.ts
@@ -28,6 +28,7 @@ const tablesForWhichWeCouldNotDetermineMaxTime = new
Set<string>();
export async function rewriteMaxDataTime(
query: SqlQuery,
+ signal?: AbortSignal,
): Promise<{ query: SqlQuery; maxTime?: Date }> {
if (!query.containsFunction('MAX_DATA_TIME')) return { query };
@@ -36,7 +37,7 @@ export async function rewriteMaxDataTime(
let maxTime: Date;
try {
- maxTime = await getMaxTimeForTable(tableName);
+ maxTime = await getMaxTimeForTable(tableName, signal);
} catch (error) {
if (!tablesForWhichWeCouldNotDetermineMaxTime.has(tableName)) {
tablesForWhichWeCouldNotDetermineMaxTime.add(tableName);
diff --git a/web-console/src/views/explore-view/utils/max-time-for-table.ts
b/web-console/src/views/explore-view/utils/max-time-for-table.ts
index ab3d57f0bf0..e21fac0d16c 100644
--- a/web-console/src/views/explore-view/utils/max-time-for-table.ts
+++ b/web-console/src/views/explore-view/utils/max-time-for-table.ts
@@ -27,7 +27,7 @@ let lastMaxTimeTable: string | undefined;
let lastMaxTimeValue: Date | undefined;
let lastMaxTimeTimestamp = 0;
-export async function getMaxTimeForTable(tableName: string): Promise<Date> {
+export async function getMaxTimeForTable(tableName: string, signal?:
AbortSignal): Promise<Date> {
// micro-cache get
if (
lastMaxTimeTable === tableName &&
@@ -37,9 +37,15 @@ export async function getMaxTimeForTable(tableName: string):
Promise<Date> {
return lastMaxTimeValue;
}
- const d = await queryDruidSql({
- query: sql`SELECT MAX(__time) AS "maxTime" FROM ${T(tableName)}`,
- });
+ const d = await queryDruidSql(
+ {
+ query: sql`SELECT MAX(__time) AS "maxTime" FROM ${T(tableName)}`,
+ context: {
+ timeout: 2000, // We expect this query to be superfast
+ },
+ },
+ signal,
+ );
const maxTimeRaw = deepGet(d, '0.maxTime');
const maxTime = new Date(maxTimeRaw);
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]