Added UI support for waitTillSegmentsLoad (#15110)

This relies on the work done in #14322 and #15076. It allows the user to set waitTillSegmentsLoad in the query context (if they want, else it defaults to true) and shows the results in the UI :
This commit is contained in:
Sébastien 2023-10-11 06:48:42 -04:00 committed by GitHub
parent 5f86072456
commit dba0246aca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 141 additions and 4 deletions

View File

@ -268,6 +268,7 @@ describe('Execution', () => {
"maxNumTasks": 2, "maxNumTasks": 2,
}, },
"result": undefined, "result": undefined,
"segmentStatus": undefined,
"sqlQuery": "REPLACE INTO \\"kttm_simple\\" OVERWRITE ALL "sqlQuery": "REPLACE INTO \\"kttm_simple\\" OVERWRITE ALL
SELECT SELECT
TIME_PARSE(\\"timestamp\\") AS \\"__time\\", TIME_PARSE(\\"timestamp\\") AS \\"__time\\",
@ -643,6 +644,7 @@ describe('Execution', () => {
"sqlQuery": undefined, "sqlQuery": undefined,
"sqlQueryId": undefined, "sqlQueryId": undefined,
}, },
"segmentStatus": undefined,
"sqlQuery": undefined, "sqlQuery": undefined,
"stages": undefined, "stages": undefined,
"startTime": 2023-07-05T21:33:19.147Z, "startTime": 2023-07-05T21:33:19.147Z,
@ -679,6 +681,7 @@ describe('Execution', () => {
"nativeQuery": undefined, "nativeQuery": undefined,
"queryContext": undefined, "queryContext": undefined,
"result": undefined, "result": undefined,
"segmentStatus": undefined,
"sqlQuery": undefined, "sqlQuery": undefined,
"stages": undefined, "stages": undefined,
"startTime": 2023-07-05T21:40:39.986Z, "startTime": 2023-07-05T21:40:39.986Z,

View File

@ -164,6 +164,18 @@ function formatPendingMessage(
} }
} }
interface SegmentStatus {
duration: number;
onDemandSegments: number;
pendingSegments: number;
precachedSegments: number;
startTime: Date;
state: 'INIT' | 'WAITING' | 'SUCCESS';
totalSegments: number;
unknownSegments: number;
usedSegments: number;
}
export interface ExecutionValue { export interface ExecutionValue {
engine: DruidEngine; engine: DruidEngine;
id: string; id: string;
@ -182,6 +194,7 @@ export interface ExecutionValue {
warnings?: ExecutionError[]; warnings?: ExecutionError[];
capacityInfo?: CapacityInfo; capacityInfo?: CapacityInfo;
_payload?: MsqTaskPayloadResponse; _payload?: MsqTaskPayloadResponse;
segmentStatus?: SegmentStatus;
} }
export class Execution { export class Execution {
@ -292,6 +305,11 @@ export class Execution {
const startTime = new Date(deepGet(taskReport, 'multiStageQuery.payload.status.startTime')); const startTime = new Date(deepGet(taskReport, 'multiStageQuery.payload.status.startTime'));
const durationMs = deepGet(taskReport, 'multiStageQuery.payload.status.durationMs'); const durationMs = deepGet(taskReport, 'multiStageQuery.payload.status.durationMs');
const segmentLoaderStatus = deepGet(
taskReport,
'multiStageQuery.payload.status.segmentLoadWaiterStatus',
);
let result: QueryResult | undefined; let result: QueryResult | undefined;
const resultsPayload: { const resultsPayload: {
signature: { name: string; type: string }[]; signature: { name: string; type: string }[];
@ -313,6 +331,7 @@ export class Execution {
engine: 'sql-msq-task', engine: 'sql-msq-task',
id, id,
status: Execution.normalizeTaskStatus(status), status: Execution.normalizeTaskStatus(status),
segmentStatus: segmentLoaderStatus,
startTime: isNaN(startTime.getTime()) ? undefined : startTime, startTime: isNaN(startTime.getTime()) ? undefined : startTime,
duration: typeof durationMs === 'number' ? durationMs : undefined, duration: typeof durationMs === 'number' ? durationMs : undefined,
usageInfo: getUsageInfoFromStatusPayload( usageInfo: getUsageInfoFromStatusPayload(
@ -369,6 +388,7 @@ export class Execution {
public readonly error?: ExecutionError; public readonly error?: ExecutionError;
public readonly warnings?: ExecutionError[]; public readonly warnings?: ExecutionError[];
public readonly capacityInfo?: CapacityInfo; public readonly capacityInfo?: CapacityInfo;
public readonly segmentStatus?: SegmentStatus;
public readonly _payload?: { payload: any; task: string }; public readonly _payload?: { payload: any; task: string };
@ -390,6 +410,7 @@ export class Execution {
this.error = value.error; this.error = value.error;
this.warnings = nonEmptyArray(value.warnings) ? value.warnings : undefined; this.warnings = nonEmptyArray(value.warnings) ? value.warnings : undefined;
this.capacityInfo = value.capacityInfo; this.capacityInfo = value.capacityInfo;
this.segmentStatus = value.segmentStatus;
this._payload = value._payload; this._payload = value._payload;
} }
@ -412,6 +433,7 @@ export class Execution {
error: this.error, error: this.error,
warnings: this.warnings, warnings: this.warnings,
capacityInfo: this.capacityInfo, capacityInfo: this.capacityInfo,
segmentStatus: this.segmentStatus,
_payload: this._payload, _payload: this._payload,
}; };
@ -526,6 +548,34 @@ export class Execution {
return status !== 'SUCCESS' && status !== 'FAILED'; return status !== 'SUCCESS' && status !== 'FAILED';
} }
public getSegmentStatusDescription() {
const { segmentStatus } = this;
let label = '';
switch (segmentStatus?.state) {
case 'INIT':
label = 'Waiting for segments loading to start...';
break;
case 'WAITING':
label = 'Waiting for segments loading to complete...';
break;
case 'SUCCESS':
label = 'Segments loaded successfully in ' + segmentStatus.duration + 'ms.';
break;
default:
break;
}
return {
label,
...segmentStatus,
};
}
public isFullyComplete(): boolean { public isFullyComplete(): boolean {
if (this.isWaitingForQuery()) return false; if (this.isWaitingForQuery()) return false;

View File

@ -162,6 +162,22 @@ export function changeFinalizeAggregations(
: deepDelete(context, 'finalizeAggregations'); : deepDelete(context, 'finalizeAggregations');
} }
// waitTillSegmentsLoad
export function getWaitTillSegmentsLoad(context: QueryContext): boolean | undefined {
const { waitTillSegmentsLoad } = context;
return typeof waitTillSegmentsLoad === 'boolean' ? waitTillSegmentsLoad : undefined;
}
export function changeWaitTillSegmentsLoad(
context: QueryContext,
waitTillSegmentsLoad: boolean | undefined,
): QueryContext {
return typeof waitTillSegmentsLoad === 'boolean'
? deepSet(context, 'waitTillSegmentsLoad', waitTillSegmentsLoad)
: deepDelete(context, 'waitTillSegmentsLoad');
}
// groupByEnableMultiValueUnnesting // groupByEnableMultiValueUnnesting
export function getGroupByEnableMultiValueUnnesting(context: QueryContext): boolean | undefined { export function getGroupByEnableMultiValueUnnesting(context: QueryContext): boolean | undefined {

View File

@ -423,6 +423,7 @@ describe('WorkbenchQuery', () => {
finalizeAggregations: false, finalizeAggregations: false,
groupByEnableMultiValueUnnesting: false, groupByEnableMultiValueUnnesting: false,
useCache: false, useCache: false,
waitTillSegmentsLoad: true,
}, },
header: true, header: true,
query: 'INSERT INTO wiki2 SELECT * FROM wikipedia', query: 'INSERT INTO wiki2 SELECT * FROM wikipedia',

View File

@ -552,6 +552,7 @@ export class WorkbenchQuery {
apiQuery.context.executionMode ??= 'async'; apiQuery.context.executionMode ??= 'async';
apiQuery.context.finalizeAggregations ??= !ingestQuery; apiQuery.context.finalizeAggregations ??= !ingestQuery;
apiQuery.context.groupByEnableMultiValueUnnesting ??= !ingestQuery; apiQuery.context.groupByEnableMultiValueUnnesting ??= !ingestQuery;
apiQuery.context.waitTillSegmentsLoad ??= true;
} }
if (Array.isArray(queryParameters) && queryParameters.length) { if (Array.isArray(queryParameters) && queryParameters.length) {

View File

@ -57,7 +57,13 @@ export interface SubmitTaskQueryOptions {
export async function submitTaskQuery( export async function submitTaskQuery(
options: SubmitTaskQueryOptions, options: SubmitTaskQueryOptions,
): Promise<Execution | IntermediateQueryState<Execution>> { ): Promise<Execution | IntermediateQueryState<Execution>> {
const { query, context, prefixLines, cancelToken, preserveOnTermination, onSubmitted } = options; const { query, prefixLines, cancelToken, preserveOnTermination, onSubmitted } = options;
// setting waitTillSegmentsLoad to true by default
const context = {
waitTillSegmentsLoad: true,
...(options.context || {}),
};
let sqlQuery: string; let sqlQuery: string;
let jsonQuery: Record<string, any>; let jsonQuery: Record<string, any>;
@ -261,6 +267,11 @@ export async function updateExecutionWithDatasourceLoadedIfNeeded(
return execution; return execution;
} }
// This means we don't have to perform the SQL query to check if the segments are loaded
if (execution.queryContext?.waitTillSegmentsLoad === true) {
return execution.markDestinationDatasourceLoaded();
}
const endTime = execution.getEndTime(); const endTime = execution.getEndTime();
if ( if (
!endTime || // If endTime is not set (this is not expected to happen) then just bow out !endTime || // If endTime is not set (this is not expected to happen) then just bow out

View File

@ -22,6 +22,7 @@ exports[`ExecutionDetailsPane matches snapshot no init tab 1`] = `
"id": "native", "id": "native",
"label": "Native query", "label": "Native query",
}, },
false,
undefined, undefined,
undefined, undefined,
Object { Object {
@ -286,6 +287,7 @@ PARTITIONED BY DAY",
"maxParseExceptions": 2, "maxParseExceptions": 2,
}, },
"result": undefined, "result": undefined,
"segmentStatus": undefined,
"sqlQuery": "REPLACE INTO \\"kttm-blank-lines\\" OVERWRITE ALL "sqlQuery": "REPLACE INTO \\"kttm-blank-lines\\" OVERWRITE ALL
SELECT SELECT
TIME_PARSE(\\"timestamp\\") AS \\"__time\\", TIME_PARSE(\\"timestamp\\") AS \\"__time\\",
@ -909,6 +911,7 @@ PARTITIONED BY DAY",
"maxParseExceptions": 2, "maxParseExceptions": 2,
}, },
"result": undefined, "result": undefined,
"segmentStatus": undefined,
"sqlQuery": "REPLACE INTO \\"kttm-blank-lines\\" OVERWRITE ALL "sqlQuery": "REPLACE INTO \\"kttm-blank-lines\\" OVERWRITE ALL
SELECT SELECT
TIME_PARSE(\\"timestamp\\") AS \\"__time\\", TIME_PARSE(\\"timestamp\\") AS \\"__time\\",
@ -1319,6 +1322,7 @@ exports[`ExecutionDetailsPane matches snapshot with init tab 1`] = `
"id": "native", "id": "native",
"label": "Native query", "label": "Native query",
}, },
false,
undefined, undefined,
undefined, undefined,
Object { Object {
@ -1576,6 +1580,7 @@ PARTITIONED BY DAY",
"maxParseExceptions": 2, "maxParseExceptions": 2,
}, },
"result": undefined, "result": undefined,
"segmentStatus": undefined,
"sqlQuery": "REPLACE INTO \\"kttm-blank-lines\\" OVERWRITE ALL "sqlQuery": "REPLACE INTO \\"kttm-blank-lines\\" OVERWRITE ALL
SELECT SELECT
TIME_PARSE(\\"timestamp\\") AS \\"__time\\", TIME_PARSE(\\"timestamp\\") AS \\"__time\\",

View File

@ -23,7 +23,7 @@ import React, { useState } from 'react';
import { FancyTabPane } from '../../../components'; import { FancyTabPane } from '../../../components';
import type { Execution } from '../../../druid-models'; import type { Execution } from '../../../druid-models';
import { pluralIfNeeded } from '../../../utils'; import { formatDuration, formatDurationWithMs, pluralIfNeeded } from '../../../utils';
import { DestinationPagesPane } from '../destination-pages-pane/destination-pages-pane'; import { DestinationPagesPane } from '../destination-pages-pane/destination-pages-pane';
import { ExecutionErrorPane } from '../execution-error-pane/execution-error-pane'; import { ExecutionErrorPane } from '../execution-error-pane/execution-error-pane';
import { ExecutionStagesPane } from '../execution-stages-pane/execution-stages-pane'; import { ExecutionStagesPane } from '../execution-stages-pane/execution-stages-pane';
@ -40,7 +40,8 @@ export type ExecutionDetailsTab =
| 'result' | 'result'
| 'pages' | 'pages'
| 'error' | 'error'
| 'warnings'; | 'warnings'
| 'segmentStatus';
interface ExecutionDetailsPaneProps { interface ExecutionDetailsPaneProps {
execution: Execution; execution: Execution;
@ -53,6 +54,7 @@ export const ExecutionDetailsPane = React.memo(function ExecutionDetailsPane(
) { ) {
const { execution, initTab, goToTask } = props; const { execution, initTab, goToTask } = props;
const [activeTab, setActiveTab] = useState<ExecutionDetailsTab>(initTab || 'general'); const [activeTab, setActiveTab] = useState<ExecutionDetailsTab>(initTab || 'general');
const segmentStatusDescription = execution.getSegmentStatusDescription();
function renderContent() { function renderContent() {
switch (activeTab) { switch (activeTab) {
@ -120,6 +122,25 @@ export const ExecutionDetailsPane = React.memo(function ExecutionDetailsPane(
case 'warnings': case 'warnings':
return <ExecutionWarningsPane execution={execution} />; return <ExecutionWarningsPane execution={execution} />;
case 'segmentStatus':
return (
<>
<p>
Duration:{' '}
{segmentStatusDescription.duration
? formatDurationWithMs(segmentStatusDescription.duration)
: '-'}
{execution.duration
? ` (query duration was ${formatDuration(execution.duration)})`
: ''}
</p>
<p>Total segments: {segmentStatusDescription.totalSegments ?? '-'}</p>
<p>Used segments: {segmentStatusDescription.usedSegments ?? '-'}</p>
<p>Precached segments: {segmentStatusDescription.precachedSegments ?? '-'}</p>
<p>On demand segments: {segmentStatusDescription.onDemandSegments ?? '-'}</p>
</>
);
default: default:
return; return;
} }
@ -146,6 +167,11 @@ export const ExecutionDetailsPane = React.memo(function ExecutionDetailsPane(
label: 'Native query', label: 'Native query',
icon: IconNames.COG, icon: IconNames.COG,
}, },
Boolean(execution.segmentStatus) && {
id: 'segmentStatus',
label: 'Segments',
icon: IconNames.HEAT_GRID,
},
execution.result && { execution.result && {
id: 'result', id: 'result',
label: 'Results', label: 'Results',

View File

@ -20,5 +20,8 @@ exports[`ExecutionProgressBarPane matches snapshot 1`] = `
className="overall" className="overall"
intent="primary" intent="primary"
/> />
<Unknown>
</Unknown>
</div> </div>
`; `;

View File

@ -50,6 +50,9 @@ export const ExecutionProgressBarPane = React.memo(function ExecutionProgressBar
const idx = stages ? stages.currentStageIndex() : -1; const idx = stages ? stages.currentStageIndex() : -1;
const waitingForSegments = stages && !execution.isWaitingForQuery(); const waitingForSegments = stages && !execution.isWaitingForQuery();
const segmentStatusDescription = execution?.getSegmentStatusDescription();
return ( return (
<div className="execution-progress-bar-pane"> <div className="execution-progress-bar-pane">
<Label> <Label>
@ -78,6 +81,7 @@ export const ExecutionProgressBarPane = React.memo(function ExecutionProgressBar
intent={stages ? Intent.PRIMARY : undefined} intent={stages ? Intent.PRIMARY : undefined}
value={stages && execution.isWaitingForQuery() ? stages.overallProgress() : undefined} value={stages && execution.isWaitingForQuery() ? stages.overallProgress() : undefined}
/> />
{segmentStatusDescription && <Label>{segmentStatusDescription.label}</Label>}
{stages && idx >= 0 && ( {stages && idx >= 0 && (
<> <>
<Label>{`Current stage (${idx + 1} of ${stages.stageCount()})`}</Label> <Label>{`Current stage (${idx + 1} of ${stages.stageCount()})`}</Label>

View File

@ -9,6 +9,7 @@ exports[`IngestSuccessPane matches snapshot 1`] = `
</p> </p>
<p> <p>
Insert query took 0:00:23. Insert query took 0:00:23.
<span <span
className="action" className="action"
onClick={[Function]} onClick={[Function]}

View File

@ -44,7 +44,9 @@ export const IngestSuccessPane = React.memo(function IngestSuccessPane(
const warnings = execution.stages?.getWarningCount() || 0; const warnings = execution.stages?.getWarningCount() || 0;
const duration = execution.duration; const { duration } = execution;
const segmentStatusDescription = execution.getSegmentStatusDescription();
return ( return (
<div className="ingest-success-pane"> <div className="ingest-success-pane">
<p> <p>
@ -63,10 +65,12 @@ export const IngestSuccessPane = React.memo(function IngestSuccessPane(
</p> </p>
<p> <p>
{duration ? `Insert query took ${formatDuration(duration)}. ` : `Insert query completed. `} {duration ? `Insert query took ${formatDuration(duration)}. ` : `Insert query completed. `}
{segmentStatusDescription ? segmentStatusDescription.label + ' ' : ''}
<span className="action" onClick={() => onDetails(execution.id)}> <span className="action" onClick={() => onDetails(execution.id)}>
Show details Show details
</span> </span>
</p> </p>
{onQueryTab && ( {onQueryTab && (
<p> <p>
Open new tab with:{' '} Open new tab with:{' '}

View File

@ -45,6 +45,7 @@ import {
changeUseApproximateCountDistinct, changeUseApproximateCountDistinct,
changeUseApproximateTopN, changeUseApproximateTopN,
changeUseCache, changeUseCache,
changeWaitTillSegmentsLoad,
getDurableShuffleStorage, getDurableShuffleStorage,
getFinalizeAggregations, getFinalizeAggregations,
getGroupByEnableMultiValueUnnesting, getGroupByEnableMultiValueUnnesting,
@ -53,6 +54,7 @@ import {
getUseApproximateCountDistinct, getUseApproximateCountDistinct,
getUseApproximateTopN, getUseApproximateTopN,
getUseCache, getUseCache,
getWaitTillSegmentsLoad,
summarizeIndexSpec, summarizeIndexSpec,
} from '../../../druid-models'; } from '../../../druid-models';
import { deepGet, deepSet, pluralIfNeeded, tickIcon } from '../../../utils'; import { deepGet, deepSet, pluralIfNeeded, tickIcon } from '../../../utils';
@ -110,6 +112,7 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
const maxParseExceptions = getMaxParseExceptions(queryContext); const maxParseExceptions = getMaxParseExceptions(queryContext);
const finalizeAggregations = getFinalizeAggregations(queryContext); const finalizeAggregations = getFinalizeAggregations(queryContext);
const waitTillSegmentsLoad = getWaitTillSegmentsLoad(queryContext);
const groupByEnableMultiValueUnnesting = getGroupByEnableMultiValueUnnesting(queryContext); const groupByEnableMultiValueUnnesting = getGroupByEnableMultiValueUnnesting(queryContext);
const sqlJoinAlgorithm = queryContext.sqlJoinAlgorithm ?? 'broadcast'; const sqlJoinAlgorithm = queryContext.sqlJoinAlgorithm ?? 'broadcast';
const selectDestination = queryContext.selectDestination ?? 'taskReport'; const selectDestination = queryContext.selectDestination ?? 'taskReport';
@ -311,6 +314,15 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
changeQueryContext(changeFinalizeAggregations(queryContext, v)) changeQueryContext(changeFinalizeAggregations(queryContext, v))
} }
/> />
<MenuTristate
icon={IconNames.STOPWATCH}
text="Wait until segments have loaded"
value={waitTillSegmentsLoad}
undefinedEffectiveValue /* ={true} */
onValueChange={v =>
changeQueryContext(changeWaitTillSegmentsLoad(queryContext, v))
}
/>
<MenuTristate <MenuTristate
icon={IconNames.FORK} icon={IconNames.FORK}
text="Enable GroupBy multi-value unnesting" text="Enable GroupBy multi-value unnesting"