mirror of https://github.com/apache/druid.git
Web console: fix query timer issues (#16235)
* fix timer issues * wording
This commit is contained in:
parent
7759f25095
commit
9658e1ad7f
|
@ -23,24 +23,24 @@ interface EditorState {
|
|||
}
|
||||
|
||||
export class AceEditorStateCache {
|
||||
static states: Record<string, EditorState> = {};
|
||||
static states = new Map<string, EditorState>();
|
||||
|
||||
static saveState(id: string, editor: Ace.Editor): void {
|
||||
const session = editor.getSession();
|
||||
const undoManager: any = session.getUndoManager();
|
||||
AceEditorStateCache.states[id] = {
|
||||
AceEditorStateCache.states.set(id, {
|
||||
undoManager,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
static applyState(id: string, editor: Ace.Editor): void {
|
||||
const state = AceEditorStateCache.states[id];
|
||||
const state = AceEditorStateCache.states.get(id);
|
||||
if (!state) return;
|
||||
const session = editor.getSession();
|
||||
session.setUndoManager(state.undoManager);
|
||||
}
|
||||
|
||||
static deleteState(id: string): void {
|
||||
delete AceEditorStateCache.states[id];
|
||||
AceEditorStateCache.states.delete(id);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,17 +20,17 @@ import type { Execution } from '../druid-models';
|
|||
import type { DruidError, QueryState } from '../utils';
|
||||
|
||||
export class ExecutionStateCache {
|
||||
private static readonly cache: Record<string, QueryState<Execution, DruidError, Execution>> = {};
|
||||
private static readonly cache = new Map<string, QueryState<Execution, DruidError, Execution>>();
|
||||
|
||||
static storeState(id: string, report: QueryState<Execution, DruidError, Execution>): void {
|
||||
ExecutionStateCache.cache[id] = report;
|
||||
ExecutionStateCache.cache.set(id, report);
|
||||
}
|
||||
|
||||
static getState(id: string): QueryState<Execution, DruidError, Execution> | undefined {
|
||||
return ExecutionStateCache.cache[id];
|
||||
return ExecutionStateCache.cache.get(id);
|
||||
}
|
||||
|
||||
static deleteState(id: string): void {
|
||||
delete ExecutionStateCache.cache[id];
|
||||
ExecutionStateCache.cache.delete(id);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,24 +21,25 @@ import type { QueryResult } from '@druid-toolkit/query';
|
|||
export interface WorkbenchRunningPromise {
|
||||
promise: Promise<QueryResult>;
|
||||
prefixLines: number;
|
||||
startTime: Date;
|
||||
}
|
||||
|
||||
export class WorkbenchRunningPromises {
|
||||
private static readonly promises: Record<string, WorkbenchRunningPromise> = {};
|
||||
private static readonly promises = new Map<string, WorkbenchRunningPromise>();
|
||||
|
||||
static isWorkbenchRunningPromise(x: any): x is WorkbenchRunningPromise {
|
||||
return Boolean(x.promise);
|
||||
}
|
||||
|
||||
static storePromise(id: string, promise: WorkbenchRunningPromise): void {
|
||||
WorkbenchRunningPromises.promises[id] = promise;
|
||||
WorkbenchRunningPromises.promises.set(id, promise);
|
||||
}
|
||||
|
||||
static getPromise(id: string): WorkbenchRunningPromise | undefined {
|
||||
return WorkbenchRunningPromises.promises[id];
|
||||
return WorkbenchRunningPromises.promises.get(id);
|
||||
}
|
||||
|
||||
static deletePromise(id: string): void {
|
||||
delete WorkbenchRunningPromises.promises[id];
|
||||
WorkbenchRunningPromises.promises.delete(id);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -267,6 +267,7 @@ export class DruidError extends Error {
|
|||
public startRowColumn?: RowColumn;
|
||||
public endRowColumn?: RowColumn;
|
||||
public suggestion?: QuerySuggestion;
|
||||
public queryDuration?: number;
|
||||
|
||||
// Deprecated
|
||||
public error?: string;
|
||||
|
|
|
@ -36,6 +36,7 @@ import './execution-summary-panel.scss';
|
|||
|
||||
export interface ExecutionSummaryPanelProps {
|
||||
execution: Execution | undefined;
|
||||
queryErrorDuration: number | undefined;
|
||||
onExecutionDetail(): void;
|
||||
onReset?: () => void;
|
||||
}
|
||||
|
@ -43,12 +44,22 @@ export interface ExecutionSummaryPanelProps {
|
|||
export const ExecutionSummaryPanel = React.memo(function ExecutionSummaryPanel(
|
||||
props: ExecutionSummaryPanelProps,
|
||||
) {
|
||||
const { execution, onExecutionDetail, onReset } = props;
|
||||
const { execution, queryErrorDuration, onExecutionDetail, onReset } = props;
|
||||
const [showDestinationPages, setShowDestinationPages] = useState(false);
|
||||
const queryResult = execution?.result;
|
||||
|
||||
const buttons: JSX.Element[] = [];
|
||||
|
||||
if (typeof queryErrorDuration === 'number') {
|
||||
buttons.push(
|
||||
<Button
|
||||
key="timing"
|
||||
minimal
|
||||
text={`Error after ${formatDurationHybrid(queryErrorDuration)}`}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (queryResult) {
|
||||
const wrapQueryLimit = queryResult.getSqlOuterLimit();
|
||||
let resultCount: string;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AnchoredQueryTimer matches snapshot 1`] = `
|
||||
exports[`AnchoredQueryTimer matches snapshot with execution 1`] = `
|
||||
<div
|
||||
class="bp4-button-group execution-timer-panel"
|
||||
>
|
||||
|
@ -57,3 +57,61 @@ exports[`AnchoredQueryTimer matches snapshot 1`] = `
|
|||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AnchoredQueryTimer matches snapshot with startTime 1`] = `
|
||||
<div
|
||||
class="bp4-button-group execution-timer-panel"
|
||||
>
|
||||
<button
|
||||
class="bp4-button bp4-minimal timer"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="bp4-icon bp4-icon-stopwatch"
|
||||
icon="stopwatch"
|
||||
>
|
||||
<svg
|
||||
data-icon="stopwatch"
|
||||
height="16"
|
||||
role="img"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
>
|
||||
<path
|
||||
d="M9 2v1.083A6.002 6.002 0 018 15 6 6 0 017 3.083V2H6a1 1 0 110-2h4a1 1 0 010 2H9zM8 5a4 4 0 104 4H8V5z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="bp4-button-text"
|
||||
>
|
||||
1.00s
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="bp4-button bp4-minimal"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="bp4-icon bp4-icon-cross"
|
||||
icon="cross"
|
||||
>
|
||||
<svg
|
||||
data-icon="cross"
|
||||
height="16"
|
||||
role="img"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
>
|
||||
<path
|
||||
d="M9.41 8l3.29-3.29c.19-.18.3-.43.3-.71a1.003 1.003 0 00-1.71-.71L8 6.59l-3.29-3.3a1.003 1.003 0 00-1.42 1.42L6.59 8 3.3 11.29c-.19.18-.3.43-.3.71a1.003 1.003 0 001.71.71L8 9.41l3.29 3.29c.18.19.43.3.71.3a1.003 1.003 0 00.71-1.71L9.41 8z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -37,13 +37,21 @@ describe('AnchoredQueryTimer', () => {
|
|||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('matches snapshot', () => {
|
||||
it('matches snapshot with execution', () => {
|
||||
const { container } = render(
|
||||
<ExecutionTimerPanel
|
||||
execution={new Execution({ engine: 'sql-msq-task', id: 'xxx', startTime: new Date(start) })}
|
||||
startTime={undefined}
|
||||
onCancel={() => {}}
|
||||
/>,
|
||||
);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot with startTime', () => {
|
||||
const { container } = render(
|
||||
<ExecutionTimerPanel execution={undefined} startTime={new Date(start)} onCancel={() => {}} />,
|
||||
);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -29,15 +29,16 @@ import './execution-timer-panel.scss';
|
|||
|
||||
export interface ExecutionTimerPanelProps {
|
||||
execution: Execution | undefined;
|
||||
startTime: Date | undefined;
|
||||
onCancel(): void;
|
||||
}
|
||||
|
||||
export const ExecutionTimerPanel = React.memo(function ExecutionTimerPanel(
|
||||
props: ExecutionTimerPanelProps,
|
||||
) {
|
||||
const { execution, onCancel } = props;
|
||||
const { execution, startTime, onCancel } = props;
|
||||
const [showCancelConfirm, setShowCancelConfirm] = useState(false);
|
||||
const [mountTime] = useState(Date.now());
|
||||
const [mountTime] = useState(startTime?.valueOf() ?? Date.now());
|
||||
const [currentTime, setCurrentTime] = useState(Date.now());
|
||||
|
||||
useInterval(() => {
|
||||
|
|
|
@ -170,16 +170,16 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
|||
|
||||
const queryInputRef = useRef<FlexibleQueryInput | null>(null);
|
||||
|
||||
const cachedExecutionState = ExecutionStateCache.getState(id);
|
||||
const currentRunningPromise = WorkbenchRunningPromises.getPromise(id);
|
||||
const [executionState, queryManager] = useQueryManager<
|
||||
WorkbenchQuery | WorkbenchRunningPromise | LastExecution,
|
||||
Execution,
|
||||
Execution,
|
||||
DruidError
|
||||
>({
|
||||
initQuery: ExecutionStateCache.getState(id)
|
||||
? undefined
|
||||
: WorkbenchRunningPromises.getPromise(id) || query.getLastExecution(),
|
||||
initState: ExecutionStateCache.getState(id),
|
||||
initQuery: cachedExecutionState ? undefined : currentRunningPromise || query.getLastExecution(),
|
||||
initState: cachedExecutionState,
|
||||
processQuery: async (q, cancelToken) => {
|
||||
if (q instanceof WorkbenchQuery) {
|
||||
ExecutionStateCache.deleteState(id);
|
||||
|
@ -214,6 +214,7 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
|||
|
||||
onQueryChange(props.query.changeLastExecution(undefined));
|
||||
|
||||
const startTime = new Date();
|
||||
let result: QueryResult;
|
||||
try {
|
||||
const resultPromise = queryRunner.runQuery({
|
||||
|
@ -223,13 +224,19 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
|||
nativeQueryCancelFnRef.current = cancelFn;
|
||||
}),
|
||||
});
|
||||
WorkbenchRunningPromises.storePromise(id, { promise: resultPromise, prefixLines });
|
||||
WorkbenchRunningPromises.storePromise(id, {
|
||||
promise: resultPromise,
|
||||
prefixLines,
|
||||
startTime,
|
||||
});
|
||||
|
||||
result = await resultPromise;
|
||||
nativeQueryCancelFnRef.current = undefined;
|
||||
} catch (e) {
|
||||
nativeQueryCancelFnRef.current = undefined;
|
||||
throw new DruidError(e, prefixLines);
|
||||
const druidError = new DruidError(e, prefixLines);
|
||||
druidError.queryDuration = Date.now() - startTime.valueOf();
|
||||
throw druidError;
|
||||
}
|
||||
|
||||
return Execution.fromResult(engine, result);
|
||||
|
@ -240,11 +247,9 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
|||
try {
|
||||
result = await q.promise;
|
||||
} catch (e) {
|
||||
WorkbenchRunningPromises.deletePromise(id);
|
||||
throw new DruidError(e, q.prefixLines);
|
||||
}
|
||||
|
||||
WorkbenchRunningPromises.deletePromise(id);
|
||||
return Execution.fromResult('sql-native', result);
|
||||
} else {
|
||||
switch (q.engine) {
|
||||
|
@ -265,9 +270,9 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!executionState.data) return;
|
||||
if (!executionState.data && !executionState.error) return;
|
||||
WorkbenchRunningPromises.deletePromise(id);
|
||||
ExecutionStateCache.storeState(id, executionState);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [executionState.data, executionState.error]);
|
||||
|
||||
const incrementWorkVersion = useStore(
|
||||
|
@ -397,12 +402,14 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
|
|||
{executionState.isLoading() && (
|
||||
<ExecutionTimerPanel
|
||||
execution={executionState.intermediate}
|
||||
startTime={currentRunningPromise?.startTime}
|
||||
onCancel={() => queryManager.cancelCurrent()}
|
||||
/>
|
||||
)}
|
||||
{(execution || executionState.error) && (
|
||||
<ExecutionSummaryPanel
|
||||
execution={execution}
|
||||
queryErrorDuration={executionState.error?.queryDuration}
|
||||
onExecutionDetail={() => onDetails(statsTaskId!)}
|
||||
onReset={() => {
|
||||
queryManager.reset();
|
||||
|
|
Loading…
Reference in New Issue