mirror of https://github.com/apache/druid.git
Web console: be more robust to aux queries failing and improve kill tasks (#13431)
* be more robust to aux queries failing * feedback fixes * remove empty block * fix spelling * remove killAllDataSources from the console
This commit is contained in:
parent
37b8d4861c
commit
d8f4353c43
|
@ -16,19 +16,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import '../../variables';
|
|
||||||
|
|
||||||
.auto-form {
|
.auto-form {
|
||||||
// Popover in info label
|
|
||||||
label.#{$bp-ns}-label {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.#{$bp-ns}-text-muted {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-input input {
|
.custom-input input {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,20 @@
|
||||||
@import '../../variables';
|
@import '../../variables';
|
||||||
|
|
||||||
.form-group-with-info {
|
.form-group-with-info {
|
||||||
|
label.#{$bp-ns}-label {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.#{$bp-ns}-text-muted {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
// This is only needed because BP4 alerts are too agro in setting CSS on icons
|
||||||
|
.#{$bp-ns}-icon {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.#{$bp-ns}-text-muted .#{$bp-ns}-popover2-target {
|
.#{$bp-ns}-text-muted .#{$bp-ns}-popover2-target {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,10 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.hover-icon {
|
.hover-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: $table-cell-v-padding;
|
top: $table-cell-v-padding;
|
||||||
|
|
|
@ -27,18 +27,23 @@ export interface TableClickableCellProps {
|
||||||
onClick: MouseEventHandler<any>;
|
onClick: MouseEventHandler<any>;
|
||||||
hoverIcon?: IconName;
|
hoverIcon?: IconName;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
disabled?: boolean;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TableClickableCell = React.memo(function TableClickableCell(
|
export const TableClickableCell = React.memo(function TableClickableCell(
|
||||||
props: TableClickableCellProps,
|
props: TableClickableCellProps,
|
||||||
) {
|
) {
|
||||||
const { className, onClick, hoverIcon, title, children } = props;
|
const { className, onClick, hoverIcon, title, disabled, children } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('table-clickable-cell', className)} title={title} onClick={onClick}>
|
<div
|
||||||
|
className={classNames('table-clickable-cell', className, { disabled })}
|
||||||
|
title={title}
|
||||||
|
onClick={disabled ? undefined : onClick}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
{hoverIcon && <Icon className="hover-icon" icon={hoverIcon} />}
|
{hoverIcon && !disabled && <Icon className="hover-icon" icon={hoverIcon} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,29 +17,31 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Switch } from '@blueprintjs/core';
|
import { Switch } from '@blueprintjs/core';
|
||||||
import React, { useState } from 'react';
|
import React, { ReactNode, useState } from 'react';
|
||||||
|
|
||||||
export interface WarningChecklistProps {
|
export interface WarningChecklistProps {
|
||||||
checks: string[];
|
checks: ReactNode[];
|
||||||
onChange: (allChecked: boolean) => void;
|
onChange(allChecked: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WarningChecklist = React.memo(function WarningChecklist(props: WarningChecklistProps) {
|
export const WarningChecklist = React.memo(function WarningChecklist(props: WarningChecklistProps) {
|
||||||
const { checks, onChange } = props;
|
const { checks, onChange } = props;
|
||||||
const [checked, setChecked] = useState<Record<string, boolean>>({});
|
const [checked, setChecked] = useState<Record<number, boolean>>({});
|
||||||
|
|
||||||
function doCheck(check: string) {
|
function doCheck(checkIndex: number) {
|
||||||
const newChecked = { ...checked };
|
const newChecked = { ...checked };
|
||||||
newChecked[check] = !newChecked[check];
|
newChecked[checkIndex] = !newChecked[checkIndex];
|
||||||
setChecked(newChecked);
|
setChecked(newChecked);
|
||||||
|
|
||||||
onChange(checks.every(check => newChecked[check]));
|
onChange(checks.every((_, i) => newChecked[i]));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="warning-checklist">
|
<div className="warning-checklist">
|
||||||
{checks.map((check, i) => (
|
{checks.map((check, i) => (
|
||||||
<Switch key={i} label={check} onChange={() => doCheck(check)} />
|
<Switch key={i} onChange={() => doCheck(i)}>
|
||||||
|
{check}
|
||||||
|
</Switch>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -47,7 +47,7 @@ export interface AsyncActionDialogProps {
|
||||||
intent?: Intent;
|
intent?: Intent;
|
||||||
successText: string;
|
successText: string;
|
||||||
failText: string;
|
failText: string;
|
||||||
warningChecks?: string[];
|
warningChecks?: ReactNode[];
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ export * from './diff-dialog/diff-dialog';
|
||||||
export * from './doctor-dialog/doctor-dialog';
|
export * from './doctor-dialog/doctor-dialog';
|
||||||
export * from './edit-context-dialog/edit-context-dialog';
|
export * from './edit-context-dialog/edit-context-dialog';
|
||||||
export * from './history-dialog/history-dialog';
|
export * from './history-dialog/history-dialog';
|
||||||
|
export * from './kill-datasource-dialog/kill-datasource-dialog';
|
||||||
export * from './lookup-edit-dialog/lookup-edit-dialog';
|
export * from './lookup-edit-dialog/lookup-edit-dialog';
|
||||||
export * from './numeric-input-dialog/numeric-input-dialog';
|
export * from './numeric-input-dialog/numeric-input-dialog';
|
||||||
export * from './overlord-dynamic-config-dialog/overlord-dynamic-config-dialog';
|
export * from './overlord-dynamic-config-dialog/overlord-dynamic-config-dialog';
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Code, Intent } from '@blueprintjs/core';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { FormGroupWithInfo, PopoverText } from '../../components';
|
||||||
|
import { SuggestibleInput } from '../../components/suggestible-input/suggestible-input';
|
||||||
|
import { Api } from '../../singletons';
|
||||||
|
import { uniq } from '../../utils';
|
||||||
|
import { AsyncActionDialog } from '../async-action-dialog/async-action-dialog';
|
||||||
|
|
||||||
|
function getSuggestions(): string[] {
|
||||||
|
// Default to a data 24h ago so as not to cause a conflict between streaming ingestion and kill tasks
|
||||||
|
const end = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
||||||
|
const startOfDay = end.slice(0, 10);
|
||||||
|
const startOfMonth = end.slice(0, 7) + '-01';
|
||||||
|
const startOfYear = end.slice(0, 4) + '-01-01';
|
||||||
|
|
||||||
|
return uniq([
|
||||||
|
`1000-01-01/${startOfDay}`,
|
||||||
|
`1000-01-01/${startOfMonth}`,
|
||||||
|
`1000-01-01/${startOfYear}`,
|
||||||
|
'1000-01-01/3000-01-01',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KillDatasourceDialogProps {
|
||||||
|
datasource: string;
|
||||||
|
onClose(): void;
|
||||||
|
onSuccess(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const KillDatasourceDialog = function KillDatasourceDialog(
|
||||||
|
props: KillDatasourceDialogProps,
|
||||||
|
) {
|
||||||
|
const { datasource, onClose, onSuccess } = props;
|
||||||
|
const suggestions = getSuggestions();
|
||||||
|
const [interval, setInterval] = useState<string>(suggestions[0]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncActionDialog
|
||||||
|
className="kill-datasource-dialog"
|
||||||
|
action={async () => {
|
||||||
|
const resp = await Api.instance.delete(
|
||||||
|
`/druid/coordinator/v1/datasources/${Api.encodePath(
|
||||||
|
datasource,
|
||||||
|
)}?kill=true&interval=${Api.encodePath(interval)}`,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
return resp.data;
|
||||||
|
}}
|
||||||
|
confirmButtonText="Permanently delete unused segments"
|
||||||
|
successText="Kill task was issued. Unused segments in datasource will be deleted"
|
||||||
|
failText="Failed submit kill task"
|
||||||
|
intent={Intent.DANGER}
|
||||||
|
onClose={onClose}
|
||||||
|
onSuccess={onSuccess}
|
||||||
|
warningChecks={[
|
||||||
|
<>
|
||||||
|
I understand that this operation will delete all metadata about the unused segments of{' '}
|
||||||
|
<Code>{datasource}</Code> and removes them from deep storage.
|
||||||
|
</>,
|
||||||
|
'I understand that this operation cannot be undone.',
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Are you sure you want to permanently delete unused segments in <Code>{datasource}</Code>?
|
||||||
|
</p>
|
||||||
|
<p>This action is not reversible and the data deleted will be lost.</p>
|
||||||
|
<FormGroupWithInfo
|
||||||
|
label="Interval to delete"
|
||||||
|
info={
|
||||||
|
<PopoverText>
|
||||||
|
<p>
|
||||||
|
The range of time over which to delete unused segments specified in ISO8601 interval
|
||||||
|
format.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you have streaming ingestion running make sure that your interval range doe not
|
||||||
|
overlap with intervals where streaming data is being added - otherwise the kill task
|
||||||
|
will not start.
|
||||||
|
</p>
|
||||||
|
</PopoverText>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SuggestibleInput
|
||||||
|
value={interval}
|
||||||
|
onValueChange={s => setInterval(s || '')}
|
||||||
|
suggestions={suggestions}
|
||||||
|
/>
|
||||||
|
</FormGroupWithInfo>
|
||||||
|
</AsyncActionDialog>
|
||||||
|
);
|
||||||
|
};
|
|
@ -18,11 +18,7 @@
|
||||||
|
|
||||||
import { CompactionConfig } from '../compaction-config/compaction-config';
|
import { CompactionConfig } from '../compaction-config/compaction-config';
|
||||||
|
|
||||||
import {
|
import { CompactionStatus, formatCompactionInfo, zeroCompactionStatus } from './compaction-status';
|
||||||
CompactionStatus,
|
|
||||||
formatCompactionConfigAndStatus,
|
|
||||||
zeroCompactionStatus,
|
|
||||||
} from './compaction-status';
|
|
||||||
|
|
||||||
describe('compaction status', () => {
|
describe('compaction status', () => {
|
||||||
const BASIC_CONFIG: CompactionConfig = {};
|
const BASIC_CONFIG: CompactionConfig = {};
|
||||||
|
@ -61,27 +57,30 @@ describe('compaction status', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('formatCompactionConfigAndStatus', () => {
|
it('formatCompactionConfigAndStatus', () => {
|
||||||
expect(formatCompactionConfigAndStatus(undefined, undefined)).toEqual('Not enabled');
|
expect(formatCompactionInfo({})).toEqual('Not enabled');
|
||||||
|
|
||||||
expect(formatCompactionConfigAndStatus(BASIC_CONFIG, undefined)).toEqual('Awaiting first run');
|
expect(formatCompactionInfo({ config: BASIC_CONFIG })).toEqual('Awaiting first run');
|
||||||
|
|
||||||
expect(formatCompactionConfigAndStatus(undefined, ZERO_STATUS)).toEqual('Not enabled');
|
expect(formatCompactionInfo({ status: ZERO_STATUS })).toEqual('Not enabled');
|
||||||
|
|
||||||
expect(formatCompactionConfigAndStatus(BASIC_CONFIG, ZERO_STATUS)).toEqual('Running');
|
expect(formatCompactionInfo({ config: BASIC_CONFIG, status: ZERO_STATUS })).toEqual('Running');
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
formatCompactionConfigAndStatus(BASIC_CONFIG, {
|
formatCompactionInfo({
|
||||||
dataSource: 'tbl',
|
config: BASIC_CONFIG,
|
||||||
scheduleStatus: 'RUNNING',
|
status: {
|
||||||
bytesAwaitingCompaction: 0,
|
dataSource: 'tbl',
|
||||||
bytesCompacted: 100,
|
scheduleStatus: 'RUNNING',
|
||||||
bytesSkipped: 0,
|
bytesAwaitingCompaction: 0,
|
||||||
segmentCountAwaitingCompaction: 0,
|
bytesCompacted: 100,
|
||||||
segmentCountCompacted: 10,
|
bytesSkipped: 0,
|
||||||
segmentCountSkipped: 0,
|
segmentCountAwaitingCompaction: 0,
|
||||||
intervalCountAwaitingCompaction: 0,
|
segmentCountCompacted: 10,
|
||||||
intervalCountCompacted: 10,
|
segmentCountSkipped: 0,
|
||||||
intervalCountSkipped: 0,
|
intervalCountAwaitingCompaction: 0,
|
||||||
|
intervalCountCompacted: 10,
|
||||||
|
intervalCountSkipped: 0,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
).toEqual('Fully compacted');
|
).toEqual('Fully compacted');
|
||||||
});
|
});
|
||||||
|
|
|
@ -50,19 +50,19 @@ export function zeroCompactionStatus(compactionStatus: CompactionStatus): boolea
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatCompactionConfigAndStatus(
|
export interface CompactionInfo {
|
||||||
compactionConfig: CompactionConfig | undefined,
|
config?: CompactionConfig;
|
||||||
compactionStatus: CompactionStatus | undefined,
|
status?: CompactionStatus;
|
||||||
) {
|
}
|
||||||
if (compactionConfig) {
|
|
||||||
if (compactionStatus) {
|
export function formatCompactionInfo(compaction: CompactionInfo) {
|
||||||
if (
|
const { config, status } = compaction;
|
||||||
compactionStatus.bytesAwaitingCompaction === 0 &&
|
if (config) {
|
||||||
!zeroCompactionStatus(compactionStatus)
|
if (status) {
|
||||||
) {
|
if (status.bytesAwaitingCompaction === 0 && !zeroCompactionStatus(status)) {
|
||||||
return 'Fully compacted';
|
return 'Fully compacted';
|
||||||
} else {
|
} else {
|
||||||
return capitalizeFirst(compactionStatus.scheduleStatus);
|
return capitalizeFirst(status.scheduleStatus);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return 'Awaiting first run';
|
return 'Awaiting first run';
|
||||||
|
|
|
@ -69,20 +69,9 @@ export const COORDINATOR_DYNAMIC_CONFIG_FIELDS: Field<CoordinatorDynamicConfig>[
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'killAllDataSources',
|
|
||||||
type: 'boolean',
|
|
||||||
defaultValue: false,
|
|
||||||
info: (
|
|
||||||
<>
|
|
||||||
Send kill tasks for ALL dataSources if property <Code>druid.coordinator.kill.on</Code> is
|
|
||||||
true. If this is set to true then <Code>killDataSourceWhitelist</Code> must not be specified
|
|
||||||
or be empty list.
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'killDataSourceWhitelist',
|
name: 'killDataSourceWhitelist',
|
||||||
|
label: 'Kill datasource whitelist',
|
||||||
type: 'string-array',
|
type: 'string-array',
|
||||||
emptyValue: [],
|
emptyValue: [],
|
||||||
info: (
|
info: (
|
||||||
|
|
|
@ -36,12 +36,18 @@ import {
|
||||||
TableColumnSelector,
|
TableColumnSelector,
|
||||||
ViewControlBar,
|
ViewControlBar,
|
||||||
} from '../../components';
|
} from '../../components';
|
||||||
import { AsyncActionDialog, CompactionDialog, RetentionDialog } from '../../dialogs';
|
import {
|
||||||
|
AsyncActionDialog,
|
||||||
|
CompactionDialog,
|
||||||
|
KillDatasourceDialog,
|
||||||
|
RetentionDialog,
|
||||||
|
} from '../../dialogs';
|
||||||
import { DatasourceTableActionDialog } from '../../dialogs/datasource-table-action-dialog/datasource-table-action-dialog';
|
import { DatasourceTableActionDialog } from '../../dialogs/datasource-table-action-dialog/datasource-table-action-dialog';
|
||||||
import {
|
import {
|
||||||
CompactionConfig,
|
CompactionConfig,
|
||||||
|
CompactionInfo,
|
||||||
CompactionStatus,
|
CompactionStatus,
|
||||||
formatCompactionConfigAndStatus,
|
formatCompactionInfo,
|
||||||
QueryWithContext,
|
QueryWithContext,
|
||||||
zeroCompactionStatus,
|
zeroCompactionStatus,
|
||||||
} from '../../druid-models';
|
} from '../../druid-models';
|
||||||
|
@ -208,9 +214,8 @@ function segmentGranularityCountsToRank(row: DatasourceQueryResultRow): number {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Datasource extends DatasourceQueryResultRow {
|
interface Datasource extends DatasourceQueryResultRow {
|
||||||
readonly rules: Rule[];
|
readonly rules?: Rule[];
|
||||||
readonly compactionConfig?: CompactionConfig;
|
readonly compaction?: CompactionInfo;
|
||||||
readonly compactionStatus?: CompactionStatus;
|
|
||||||
readonly unused?: boolean;
|
readonly unused?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,7 +225,7 @@ function makeUnusedDatasource(datasource: string): Datasource {
|
||||||
|
|
||||||
interface DatasourcesAndDefaultRules {
|
interface DatasourcesAndDefaultRules {
|
||||||
readonly datasources: Datasource[];
|
readonly datasources: Datasource[];
|
||||||
readonly defaultRules: Rule[];
|
readonly defaultRules?: Rule[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RetentionDialogOpenOn {
|
interface RetentionDialogOpenOn {
|
||||||
|
@ -433,43 +438,85 @@ ORDER BY 1`;
|
||||||
|
|
||||||
let unused: string[] = [];
|
let unused: string[] = [];
|
||||||
if (showUnused) {
|
if (showUnused) {
|
||||||
const unusedResp = await Api.instance.get<string[]>(
|
try {
|
||||||
'/druid/coordinator/v1/metadata/datasources?includeUnused',
|
unused = (
|
||||||
);
|
await Api.instance.get<string[]>(
|
||||||
unused = unusedResp.data.filter(d => !seen[d]);
|
'/druid/coordinator/v1/metadata/datasources?includeUnused',
|
||||||
|
)
|
||||||
|
).data.filter(d => !seen[d]);
|
||||||
|
} catch {
|
||||||
|
AppToaster.show({
|
||||||
|
icon: IconNames.ERROR,
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
message: 'Could not get the list of unused datasources',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rulesResp = await Api.instance.get<Record<string, Rule[]>>(
|
let rules: Record<string, Rule[]> = {};
|
||||||
'/druid/coordinator/v1/rules',
|
try {
|
||||||
);
|
rules = (await Api.instance.get<Record<string, Rule[]>>('/druid/coordinator/v1/rules'))
|
||||||
const rules = rulesResp.data;
|
.data;
|
||||||
|
} catch {
|
||||||
|
AppToaster.show({
|
||||||
|
icon: IconNames.ERROR,
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
message: 'Could not get load rules',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const compactionConfigsResp = await Api.instance.get<{
|
let compactionConfigs: Record<string, CompactionConfig> | undefined;
|
||||||
compactionConfigs: CompactionConfig[];
|
try {
|
||||||
}>('/druid/coordinator/v1/config/compaction');
|
const compactionConfigsResp = await Api.instance.get<{
|
||||||
const compactionConfigs = lookupBy(
|
compactionConfigs: CompactionConfig[];
|
||||||
compactionConfigsResp.data.compactionConfigs || [],
|
}>('/druid/coordinator/v1/config/compaction');
|
||||||
c => c.dataSource,
|
compactionConfigs = lookupBy(
|
||||||
);
|
compactionConfigsResp.data.compactionConfigs || [],
|
||||||
|
c => c.dataSource,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
AppToaster.show({
|
||||||
|
icon: IconNames.ERROR,
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
message: 'Could not get compaction configs',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const compactionStatusesResp = await Api.instance.get<{ latestStatus: CompactionStatus[] }>(
|
let compactionStatuses: Record<string, CompactionStatus> | undefined;
|
||||||
'/druid/coordinator/v1/compaction/status',
|
if (compactionConfigs) {
|
||||||
);
|
// Don't bother getting the statuses if we can not even get the configs
|
||||||
const compactionStatuses = lookupBy(
|
try {
|
||||||
compactionStatusesResp.data.latestStatus || [],
|
const compactionStatusesResp = await Api.instance.get<{
|
||||||
c => c.dataSource,
|
latestStatus: CompactionStatus[];
|
||||||
);
|
}>('/druid/coordinator/v1/compaction/status');
|
||||||
|
compactionStatuses = lookupBy(
|
||||||
|
compactionStatusesResp.data.latestStatus || [],
|
||||||
|
c => c.dataSource,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
AppToaster.show({
|
||||||
|
icon: IconNames.ERROR,
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
message: 'Could not get compaction statuses',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
datasources: datasources.concat(unused.map(makeUnusedDatasource)).map(ds => {
|
datasources: datasources.concat(unused.map(makeUnusedDatasource)).map(ds => {
|
||||||
return {
|
return {
|
||||||
...ds,
|
...ds,
|
||||||
rules: rules[ds.datasource] || [],
|
rules: rules[ds.datasource],
|
||||||
compactionConfig: compactionConfigs[ds.datasource],
|
compaction:
|
||||||
compactionStatus: compactionStatuses[ds.datasource],
|
compactionConfigs && compactionStatuses
|
||||||
|
? {
|
||||||
|
config: compactionConfigs[ds.datasource],
|
||||||
|
status: compactionStatuses[ds.datasource],
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
defaultRules: rules[DEFAULT_RULES_KEY] || [],
|
defaultRules: rules[DEFAULT_RULES_KEY],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
onStateChange: datasourcesAndDefaultRulesState => {
|
onStateChange: datasourcesAndDefaultRulesState => {
|
||||||
|
@ -633,36 +680,15 @@ ORDER BY 1`;
|
||||||
if (!killDatasource) return;
|
if (!killDatasource) return;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncActionDialog
|
<KillDatasourceDialog
|
||||||
action={async () => {
|
datasource={killDatasource}
|
||||||
const resp = await Api.instance.delete(
|
|
||||||
`/druid/coordinator/v1/datasources/${Api.encodePath(
|
|
||||||
killDatasource,
|
|
||||||
)}?kill=true&interval=1000/3000`,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
return resp.data;
|
|
||||||
}}
|
|
||||||
confirmButtonText="Permanently delete unused segments"
|
|
||||||
successText="Kill task was issued. Unused segments in datasource will be deleted"
|
|
||||||
failText="Failed submit kill task"
|
|
||||||
intent={Intent.DANGER}
|
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
this.setState({ killDatasource: undefined });
|
this.setState({ killDatasource: undefined });
|
||||||
}}
|
}}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
this.fetchDatasourceData();
|
this.fetchDatasourceData();
|
||||||
}}
|
}}
|
||||||
warningChecks={[
|
/>
|
||||||
`I understand that this operation will delete all metadata about the unused segments of ${killDatasource} and removes them from deep storage.`,
|
|
||||||
'I understand that this operation cannot be undone.',
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
{`Are you sure you want to permanently delete unused segments in '${killDatasource}'?`}
|
|
||||||
</p>
|
|
||||||
<p>This action is not reversible and the data deleted will be lost.</p>
|
|
||||||
</AsyncActionDialog>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -756,20 +782,20 @@ ORDER BY 1`;
|
||||||
this.setState({ retentionDialogOpenOn: undefined });
|
this.setState({ retentionDialogOpenOn: undefined });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.setState(state => {
|
this.setState(state => {
|
||||||
const datasourcesAndDefaultRules = state.datasourcesAndDefaultRulesState.data;
|
const defaultRules = state.datasourcesAndDefaultRulesState.data?.defaultRules;
|
||||||
if (!datasourcesAndDefaultRules) return {};
|
if (!defaultRules) return {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
retentionDialogOpenOn: {
|
retentionDialogOpenOn: {
|
||||||
datasource: '_default',
|
datasource: '_default',
|
||||||
rules: datasourcesAndDefaultRules.defaultRules,
|
rules: defaultRules,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, 50);
|
}, 50);
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly saveCompaction = async (compactionConfig: any) => {
|
private readonly saveCompaction = async (compactionConfig: CompactionConfig) => {
|
||||||
if (!compactionConfig) return;
|
if (!compactionConfig) return;
|
||||||
try {
|
try {
|
||||||
await Api.instance.post(`/druid/coordinator/v1/config/compaction`, compactionConfig);
|
await Api.instance.post(`/druid/coordinator/v1/config/compaction`, compactionConfig);
|
||||||
|
@ -819,8 +845,8 @@ ORDER BY 1`;
|
||||||
getDatasourceActions(
|
getDatasourceActions(
|
||||||
datasource: string,
|
datasource: string,
|
||||||
unused: boolean | undefined,
|
unused: boolean | undefined,
|
||||||
rules: Rule[],
|
rules: Rule[] | undefined,
|
||||||
compactionConfig: CompactionConfig | undefined,
|
compactionInfo: CompactionInfo | undefined,
|
||||||
): BasicAction[] {
|
): BasicAction[] {
|
||||||
const { goToQuery, goToTask, capabilities } = this.props;
|
const { goToQuery, goToTask, capabilities } = this.props;
|
||||||
|
|
||||||
|
@ -863,82 +889,83 @@ ORDER BY 1`;
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
return goToActions.concat([
|
return goToActions.concat(
|
||||||
{
|
compact([
|
||||||
icon: IconNames.AUTOMATIC_UPDATES,
|
{
|
||||||
title: 'Edit retention rules',
|
icon: IconNames.AUTOMATIC_UPDATES,
|
||||||
onAction: () => {
|
title: 'Edit retention rules',
|
||||||
this.setState({
|
onAction: () => {
|
||||||
retentionDialogOpenOn: {
|
this.setState({
|
||||||
datasource,
|
retentionDialogOpenOn: {
|
||||||
rules,
|
datasource,
|
||||||
},
|
rules: rules || [],
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
icon: IconNames.REFRESH,
|
||||||
icon: IconNames.REFRESH,
|
title: 'Mark as used all segments (will lead to reapplying retention rules)',
|
||||||
title: 'Mark as used all segments (will lead to reapplying retention rules)',
|
onAction: () =>
|
||||||
onAction: () =>
|
this.setState({
|
||||||
this.setState({
|
datasourceToMarkAllNonOvershadowedSegmentsAsUsedIn: datasource,
|
||||||
datasourceToMarkAllNonOvershadowedSegmentsAsUsedIn: datasource,
|
}),
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: IconNames.COMPRESSED,
|
|
||||||
title: 'Edit compaction configuration',
|
|
||||||
onAction: () => {
|
|
||||||
this.setState({
|
|
||||||
compactionDialogOpenOn: {
|
|
||||||
datasource,
|
|
||||||
compactionConfig,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
compactionInfo
|
||||||
{
|
? {
|
||||||
icon: IconNames.EXPORT,
|
icon: IconNames.COMPRESSED,
|
||||||
title: 'Mark as used segments by interval',
|
title: 'Edit compaction configuration',
|
||||||
|
onAction: () => {
|
||||||
|
this.setState({
|
||||||
|
compactionDialogOpenOn: {
|
||||||
|
datasource,
|
||||||
|
compactionConfig: compactionInfo.config,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
{
|
||||||
|
icon: IconNames.EXPORT,
|
||||||
|
title: 'Mark as used segments by interval',
|
||||||
|
|
||||||
onAction: () =>
|
onAction: () =>
|
||||||
this.setState({
|
this.setState({
|
||||||
datasourceToMarkSegmentsByIntervalIn: datasource,
|
datasourceToMarkSegmentsByIntervalIn: datasource,
|
||||||
useUnuseAction: 'use',
|
useUnuseAction: 'use',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: IconNames.IMPORT,
|
icon: IconNames.IMPORT,
|
||||||
title: 'Mark as unused segments by interval',
|
title: 'Mark as unused segments by interval',
|
||||||
|
|
||||||
onAction: () =>
|
onAction: () =>
|
||||||
this.setState({
|
this.setState({
|
||||||
datasourceToMarkSegmentsByIntervalIn: datasource,
|
datasourceToMarkSegmentsByIntervalIn: datasource,
|
||||||
useUnuseAction: 'unuse',
|
useUnuseAction: 'unuse',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: IconNames.IMPORT,
|
icon: IconNames.IMPORT,
|
||||||
title: 'Mark as unused all segments',
|
title: 'Mark as unused all segments',
|
||||||
intent: Intent.DANGER,
|
intent: Intent.DANGER,
|
||||||
onAction: () => this.setState({ datasourceToMarkAsUnusedAllSegmentsIn: datasource }),
|
onAction: () => this.setState({ datasourceToMarkAsUnusedAllSegmentsIn: datasource }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: IconNames.TRASH,
|
icon: IconNames.TRASH,
|
||||||
title: 'Delete unused segments (issue kill task)',
|
title: 'Delete unused segments (issue kill task)',
|
||||||
intent: Intent.DANGER,
|
intent: Intent.DANGER,
|
||||||
onAction: () => this.setState({ killDatasource: datasource }),
|
onAction: () => this.setState({ killDatasource: datasource }),
|
||||||
},
|
},
|
||||||
]);
|
]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderRetentionDialog(): JSX.Element | undefined {
|
private renderRetentionDialog(): JSX.Element | undefined {
|
||||||
const { retentionDialogOpenOn, tiersState, datasourcesAndDefaultRulesState } = this.state;
|
const { retentionDialogOpenOn, tiersState, datasourcesAndDefaultRulesState } = this.state;
|
||||||
const { defaultRules } = datasourcesAndDefaultRulesState.data || {
|
const defaultRules = datasourcesAndDefaultRulesState.data?.defaultRules;
|
||||||
datasources: [],
|
if (!retentionDialogOpenOn || !defaultRules) return;
|
||||||
defaultRules: [],
|
|
||||||
};
|
|
||||||
if (!retentionDialogOpenOn) return;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RetentionDialog
|
<RetentionDialog
|
||||||
|
@ -969,11 +996,11 @@ ORDER BY 1`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private onDetail(datasource: Datasource): void {
|
private onDetail(datasource: Datasource): void {
|
||||||
const { unused, rules, compactionConfig } = datasource;
|
const { unused, rules, compaction } = datasource;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
datasourceTableActionDialogId: datasource.datasource,
|
datasourceTableActionDialogId: datasource.datasource,
|
||||||
actions: this.getDatasourceActions(datasource.datasource, unused, rules, compactionConfig),
|
actions: this.getDatasourceActions(datasource.datasource, unused, rules, compaction),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -982,9 +1009,7 @@ ORDER BY 1`;
|
||||||
const { datasourcesAndDefaultRulesState, datasourceFilter, showUnused, visibleColumns } =
|
const { datasourcesAndDefaultRulesState, datasourceFilter, showUnused, visibleColumns } =
|
||||||
this.state;
|
this.state;
|
||||||
|
|
||||||
let { datasources, defaultRules } = datasourcesAndDefaultRulesState.data
|
let { datasources, defaultRules } = datasourcesAndDefaultRulesState.data || { datasources: [] };
|
||||||
? datasourcesAndDefaultRulesState.data
|
|
||||||
: { datasources: [], defaultRules: [] };
|
|
||||||
|
|
||||||
if (!showUnused) {
|
if (!showUnused) {
|
||||||
datasources = datasources.filter(d => !d.unused);
|
datasources = datasources.filter(d => !d.unused);
|
||||||
|
@ -1009,8 +1034,8 @@ ORDER BY 1`;
|
||||||
const replicatedSizeValues = datasources.map(d => formatReplicatedSize(d.replicated_size));
|
const replicatedSizeValues = datasources.map(d => formatReplicatedSize(d.replicated_size));
|
||||||
|
|
||||||
const leftToBeCompactedValues = datasources.map(d =>
|
const leftToBeCompactedValues = datasources.map(d =>
|
||||||
d.compactionStatus
|
d.compaction?.status
|
||||||
? formatLeftToBeCompacted(d.compactionStatus.bytesAwaitingCompaction)
|
? formatLeftToBeCompacted(d.compaction?.status.bytesAwaitingCompaction)
|
||||||
: '-',
|
: '-',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1297,24 +1322,26 @@ ORDER BY 1`;
|
||||||
Header: 'Compaction',
|
Header: 'Compaction',
|
||||||
show: capabilities.hasCoordinatorAccess() && visibleColumns.shown('Compaction'),
|
show: capabilities.hasCoordinatorAccess() && visibleColumns.shown('Compaction'),
|
||||||
id: 'compactionStatus',
|
id: 'compactionStatus',
|
||||||
accessor: row => Boolean(row.compactionStatus),
|
accessor: row => Boolean(row.compaction?.status),
|
||||||
filterable: false,
|
filterable: false,
|
||||||
width: 150,
|
width: 150,
|
||||||
Cell: ({ original }) => {
|
Cell: ({ original }) => {
|
||||||
const { datasource, compactionConfig, compactionStatus } = original as Datasource;
|
const { datasource, compaction } = original as Datasource;
|
||||||
return (
|
return (
|
||||||
<TableClickableCell
|
<TableClickableCell
|
||||||
onClick={() =>
|
disabled={!compaction}
|
||||||
|
onClick={() => {
|
||||||
|
if (!compaction) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
compactionDialogOpenOn: {
|
compactionDialogOpenOn: {
|
||||||
datasource,
|
datasource,
|
||||||
compactionConfig,
|
compactionConfig: compaction.config,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}}
|
||||||
hoverIcon={IconNames.EDIT}
|
hoverIcon={IconNames.EDIT}
|
||||||
>
|
>
|
||||||
{formatCompactionConfigAndStatus(compactionConfig, compactionStatus)}
|
{compaction ? formatCompactionInfo(compaction) : 'Could not get compaction info'}
|
||||||
</TableClickableCell>
|
</TableClickableCell>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -1324,17 +1351,22 @@ ORDER BY 1`;
|
||||||
show: capabilities.hasCoordinatorAccess() && visibleColumns.shown('% Compacted'),
|
show: capabilities.hasCoordinatorAccess() && visibleColumns.shown('% Compacted'),
|
||||||
id: 'percentCompacted',
|
id: 'percentCompacted',
|
||||||
width: 200,
|
width: 200,
|
||||||
accessor: ({ compactionStatus }) =>
|
accessor: ({ compaction }) => {
|
||||||
compactionStatus && compactionStatus.bytesCompacted
|
const status = compaction?.status;
|
||||||
? compactionStatus.bytesCompacted /
|
return status?.bytesCompacted
|
||||||
(compactionStatus.bytesAwaitingCompaction + compactionStatus.bytesCompacted)
|
? status.bytesCompacted / (status.bytesAwaitingCompaction + status.bytesCompacted)
|
||||||
: 0,
|
: 0;
|
||||||
|
},
|
||||||
filterable: false,
|
filterable: false,
|
||||||
className: 'padded',
|
className: 'padded',
|
||||||
Cell: ({ original }) => {
|
Cell: ({ original }) => {
|
||||||
const { compactionStatus } = original as Datasource;
|
const { compaction } = original as Datasource;
|
||||||
|
if (!compaction) {
|
||||||
|
return 'Could not get compaction info';
|
||||||
|
}
|
||||||
|
|
||||||
if (!compactionStatus || zeroCompactionStatus(compactionStatus)) {
|
const { status } = compaction;
|
||||||
|
if (!status || zeroCompactionStatus(status)) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BracedText text="-" braces={PERCENT_BRACES} /> {' '}
|
<BracedText text="-" braces={PERCENT_BRACES} /> {' '}
|
||||||
|
@ -1348,10 +1380,14 @@ ORDER BY 1`;
|
||||||
<>
|
<>
|
||||||
<BracedText
|
<BracedText
|
||||||
text={formatPercent(
|
text={formatPercent(
|
||||||
progress(
|
progress(status.bytesCompacted, status.bytesAwaitingCompaction),
|
||||||
compactionStatus.bytesCompacted,
|
)}
|
||||||
compactionStatus.bytesAwaitingCompaction,
|
braces={PERCENT_BRACES}
|
||||||
),
|
/>{' '}
|
||||||
|
{' '}
|
||||||
|
<BracedText
|
||||||
|
text={formatPercent(
|
||||||
|
progress(status.segmentCountCompacted, status.segmentCountAwaitingCompaction),
|
||||||
)}
|
)}
|
||||||
braces={PERCENT_BRACES}
|
braces={PERCENT_BRACES}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
|
@ -1359,18 +1395,8 @@ ORDER BY 1`;
|
||||||
<BracedText
|
<BracedText
|
||||||
text={formatPercent(
|
text={formatPercent(
|
||||||
progress(
|
progress(
|
||||||
compactionStatus.segmentCountCompacted,
|
status.intervalCountCompacted,
|
||||||
compactionStatus.segmentCountAwaitingCompaction,
|
status.intervalCountAwaitingCompaction,
|
||||||
),
|
|
||||||
)}
|
|
||||||
braces={PERCENT_BRACES}
|
|
||||||
/>{' '}
|
|
||||||
{' '}
|
|
||||||
<BracedText
|
|
||||||
text={formatPercent(
|
|
||||||
progress(
|
|
||||||
compactionStatus.intervalCountCompacted,
|
|
||||||
compactionStatus.intervalCountAwaitingCompaction,
|
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
braces={PERCENT_BRACES}
|
braces={PERCENT_BRACES}
|
||||||
|
@ -1385,20 +1411,26 @@ ORDER BY 1`;
|
||||||
capabilities.hasCoordinatorAccess() && visibleColumns.shown('Left to be compacted'),
|
capabilities.hasCoordinatorAccess() && visibleColumns.shown('Left to be compacted'),
|
||||||
id: 'leftToBeCompacted',
|
id: 'leftToBeCompacted',
|
||||||
width: 100,
|
width: 100,
|
||||||
accessor: ({ compactionStatus }) =>
|
accessor: ({ compaction }) => {
|
||||||
(compactionStatus && compactionStatus.bytesAwaitingCompaction) || 0,
|
const status = compaction?.status;
|
||||||
|
return status?.bytesAwaitingCompaction || 0;
|
||||||
|
},
|
||||||
filterable: false,
|
filterable: false,
|
||||||
className: 'padded',
|
className: 'padded',
|
||||||
Cell: ({ original }) => {
|
Cell: ({ original }) => {
|
||||||
const { compactionStatus } = original as Datasource;
|
const { compaction } = original as Datasource;
|
||||||
|
if (!compaction) {
|
||||||
|
return 'Could not get compaction info';
|
||||||
|
}
|
||||||
|
|
||||||
if (!compactionStatus) {
|
const { status } = compaction;
|
||||||
|
if (!status) {
|
||||||
return <BracedText text="-" braces={leftToBeCompactedValues} />;
|
return <BracedText text="-" braces={leftToBeCompactedValues} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BracedText
|
<BracedText
|
||||||
text={formatLeftToBeCompacted(compactionStatus.bytesAwaitingCompaction)}
|
text={formatLeftToBeCompacted(status.bytesAwaitingCompaction)}
|
||||||
braces={leftToBeCompactedValues}
|
braces={leftToBeCompactedValues}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -1408,26 +1440,30 @@ ORDER BY 1`;
|
||||||
Header: 'Retention',
|
Header: 'Retention',
|
||||||
show: capabilities.hasCoordinatorAccess() && visibleColumns.shown('Retention'),
|
show: capabilities.hasCoordinatorAccess() && visibleColumns.shown('Retention'),
|
||||||
id: 'retention',
|
id: 'retention',
|
||||||
accessor: row => row.rules.length,
|
accessor: row => row.rules?.length || 0,
|
||||||
filterable: false,
|
filterable: false,
|
||||||
width: 200,
|
width: 200,
|
||||||
Cell: ({ original }) => {
|
Cell: ({ original }) => {
|
||||||
const { datasource, rules } = original as Datasource;
|
const { datasource, rules } = original as Datasource;
|
||||||
return (
|
return (
|
||||||
<TableClickableCell
|
<TableClickableCell
|
||||||
onClick={() =>
|
disabled={!defaultRules}
|
||||||
|
onClick={() => {
|
||||||
|
if (!defaultRules) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
retentionDialogOpenOn: {
|
retentionDialogOpenOn: {
|
||||||
datasource,
|
datasource,
|
||||||
rules,
|
rules: rules || [],
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}}
|
||||||
hoverIcon={IconNames.EDIT}
|
hoverIcon={IconNames.EDIT}
|
||||||
>
|
>
|
||||||
{rules.length
|
{rules?.length
|
||||||
? DatasourcesView.formatRules(rules)
|
? DatasourcesView.formatRules(rules)
|
||||||
: `Cluster default: ${DatasourcesView.formatRules(defaultRules)}`}
|
: defaultRules
|
||||||
|
? `Cluster default: ${DatasourcesView.formatRules(defaultRules)}`
|
||||||
|
: 'Could not get default rules'}
|
||||||
</TableClickableCell>
|
</TableClickableCell>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -1440,12 +1476,12 @@ ORDER BY 1`;
|
||||||
width: ACTION_COLUMN_WIDTH,
|
width: ACTION_COLUMN_WIDTH,
|
||||||
filterable: false,
|
filterable: false,
|
||||||
Cell: ({ value: datasource, original }) => {
|
Cell: ({ value: datasource, original }) => {
|
||||||
const { unused, rules, compactionConfig } = original as Datasource;
|
const { unused, rules, compaction } = original as Datasource;
|
||||||
const datasourceActions = this.getDatasourceActions(
|
const datasourceActions = this.getDatasourceActions(
|
||||||
datasource,
|
datasource,
|
||||||
unused,
|
unused,
|
||||||
rules,
|
rules,
|
||||||
compactionConfig,
|
compaction,
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<ActionCell
|
<ActionCell
|
||||||
|
|
|
@ -36,11 +36,12 @@ import {
|
||||||
import { AsyncActionDialog } from '../../dialogs';
|
import { AsyncActionDialog } from '../../dialogs';
|
||||||
import { QueryWithContext } from '../../druid-models';
|
import { QueryWithContext } from '../../druid-models';
|
||||||
import { STANDARD_TABLE_PAGE_SIZE, STANDARD_TABLE_PAGE_SIZE_OPTIONS } from '../../react-table';
|
import { STANDARD_TABLE_PAGE_SIZE, STANDARD_TABLE_PAGE_SIZE_OPTIONS } from '../../react-table';
|
||||||
import { Api } from '../../singletons';
|
import { Api, AppToaster } from '../../singletons';
|
||||||
import {
|
import {
|
||||||
Capabilities,
|
Capabilities,
|
||||||
CapabilitiesMode,
|
CapabilitiesMode,
|
||||||
deepGet,
|
deepGet,
|
||||||
|
filterMap,
|
||||||
formatBytes,
|
formatBytes,
|
||||||
formatBytesCompact,
|
formatBytesCompact,
|
||||||
hasPopoverOpen,
|
hasPopoverOpen,
|
||||||
|
@ -117,7 +118,7 @@ export interface ServicesViewState {
|
||||||
visibleColumns: LocalStorageBackedVisibility;
|
visibleColumns: LocalStorageBackedVisibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServiceQueryResultRow {
|
interface ServiceResultRow {
|
||||||
readonly service: string;
|
readonly service: string;
|
||||||
readonly service_type: string;
|
readonly service_type: string;
|
||||||
readonly tier: string;
|
readonly tier: string;
|
||||||
|
@ -127,16 +128,18 @@ interface ServiceQueryResultRow {
|
||||||
readonly max_size: NumberLike;
|
readonly max_size: NumberLike;
|
||||||
readonly plaintext_port: number;
|
readonly plaintext_port: number;
|
||||||
readonly tls_port: number;
|
readonly tls_port: number;
|
||||||
|
loadQueueInfo?: LoadQueueInfo;
|
||||||
|
workerInfo?: WorkerInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoadQueueStatus {
|
interface LoadQueueInfo {
|
||||||
readonly segmentsToDrop: NumberLike;
|
readonly segmentsToDrop: NumberLike;
|
||||||
readonly segmentsToDropSize: NumberLike;
|
readonly segmentsToDropSize: NumberLike;
|
||||||
readonly segmentsToLoad: NumberLike;
|
readonly segmentsToLoad: NumberLike;
|
||||||
readonly segmentsToLoadSize: NumberLike;
|
readonly segmentsToLoadSize: NumberLike;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MiddleManagerQueryResultRow {
|
interface WorkerInfo {
|
||||||
readonly availabilityGroups: string[];
|
readonly availabilityGroups: string[];
|
||||||
readonly blacklistedUntil: string | null;
|
readonly blacklistedUntil: string | null;
|
||||||
readonly currCapacityUsed: NumberLike;
|
readonly currCapacityUsed: NumberLike;
|
||||||
|
@ -153,11 +156,6 @@ interface MiddleManagerQueryResultRow {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServiceResultRow
|
|
||||||
extends ServiceQueryResultRow,
|
|
||||||
Partial<LoadQueueStatus>,
|
|
||||||
Partial<MiddleManagerQueryResultRow> {}
|
|
||||||
|
|
||||||
export class ServicesView extends React.PureComponent<ServicesViewProps, ServicesViewState> {
|
export class ServicesView extends React.PureComponent<ServicesViewProps, ServicesViewState> {
|
||||||
private readonly serviceQueryManager: QueryManager<Capabilities, ServiceResultRow[]>;
|
private readonly serviceQueryManager: QueryManager<Capabilities, ServiceResultRow[]>;
|
||||||
|
|
||||||
|
@ -198,7 +196,7 @@ ORDER BY
|
||||||
) DESC,
|
) DESC,
|
||||||
"service" DESC`;
|
"service" DESC`;
|
||||||
|
|
||||||
static async getServices(): Promise<ServiceQueryResultRow[]> {
|
static async getServices(): Promise<ServiceResultRow[]> {
|
||||||
const allServiceResp = await Api.instance.get('/druid/coordinator/v1/servers?simple');
|
const allServiceResp = await Api.instance.get('/druid/coordinator/v1/servers?simple');
|
||||||
const allServices = allServiceResp.data;
|
const allServices = allServiceResp.data;
|
||||||
return allServices.map((s: any) => {
|
return allServices.map((s: any) => {
|
||||||
|
@ -228,7 +226,7 @@ ORDER BY
|
||||||
|
|
||||||
this.serviceQueryManager = new QueryManager({
|
this.serviceQueryManager = new QueryManager({
|
||||||
processQuery: async capabilities => {
|
processQuery: async capabilities => {
|
||||||
let services: ServiceQueryResultRow[];
|
let services: ServiceResultRow[];
|
||||||
if (capabilities.hasSql()) {
|
if (capabilities.hasSql()) {
|
||||||
services = await queryDruidSql({ query: ServicesView.SERVICE_SQL });
|
services = await queryDruidSql({ query: ServicesView.SERVICE_SQL });
|
||||||
} else if (capabilities.hasCoordinatorAccess()) {
|
} else if (capabilities.hasCoordinatorAccess()) {
|
||||||
|
@ -238,50 +236,49 @@ ORDER BY
|
||||||
}
|
}
|
||||||
|
|
||||||
if (capabilities.hasCoordinatorAccess()) {
|
if (capabilities.hasCoordinatorAccess()) {
|
||||||
const loadQueueResponse = await Api.instance.get(
|
try {
|
||||||
'/druid/coordinator/v1/loadqueue?simple',
|
const loadQueueInfos = (
|
||||||
);
|
await Api.instance.get<Record<string, LoadQueueInfo>>(
|
||||||
const loadQueues: Record<string, LoadQueueStatus> = loadQueueResponse.data;
|
'/druid/coordinator/v1/loadqueue?simple',
|
||||||
services = services.map(s => {
|
)
|
||||||
const loadQueueInfo = loadQueues[s.service];
|
).data;
|
||||||
if (loadQueueInfo) {
|
services.forEach(s => {
|
||||||
s = { ...s, ...loadQueueInfo };
|
s.loadQueueInfo = loadQueueInfos[s.service];
|
||||||
}
|
});
|
||||||
return s;
|
} catch {
|
||||||
});
|
AppToaster.show({
|
||||||
|
icon: IconNames.ERROR,
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
message: 'There was an error getting the load queue info',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (capabilities.hasOverlordAccess()) {
|
if (capabilities.hasOverlordAccess()) {
|
||||||
let middleManagers: MiddleManagerQueryResultRow[];
|
|
||||||
try {
|
try {
|
||||||
const middleManagerResponse = await Api.instance.get('/druid/indexer/v1/workers');
|
const workerInfos = (await Api.instance.get<WorkerInfo[]>('/druid/indexer/v1/workers'))
|
||||||
middleManagers = middleManagerResponse.data;
|
.data;
|
||||||
|
|
||||||
|
const workerInfoLookup: Record<string, WorkerInfo> = lookupBy(
|
||||||
|
workerInfos,
|
||||||
|
m => m.worker?.host,
|
||||||
|
);
|
||||||
|
|
||||||
|
services.forEach(s => {
|
||||||
|
s.workerInfo = workerInfoLookup[s.service];
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// Swallow this error because it simply a reflection of a local task runner.
|
||||||
if (
|
if (
|
||||||
e.response &&
|
deepGet(e, 'response.data.error') !== 'Task Runner does not support worker listing'
|
||||||
typeof e.response.data === 'object' &&
|
|
||||||
e.response.data.error === 'Task Runner does not support worker listing'
|
|
||||||
) {
|
) {
|
||||||
// Swallow this error because it simply a reflection of a local task runner.
|
AppToaster.show({
|
||||||
middleManagers = [];
|
icon: IconNames.ERROR,
|
||||||
} else {
|
intent: Intent.DANGER,
|
||||||
// Otherwise re-throw.
|
message: 'There was an error getting the worker info',
|
||||||
throw e;
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const middleManagersLookup: Record<string, MiddleManagerQueryResultRow> = lookupBy(
|
|
||||||
middleManagers,
|
|
||||||
m => m.worker.host,
|
|
||||||
);
|
|
||||||
|
|
||||||
services = services.map(s => {
|
|
||||||
const middleManagerInfo = middleManagersLookup[s.service];
|
|
||||||
if (middleManagerInfo) {
|
|
||||||
s = { ...s, ...middleManagerInfo };
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
|
@ -372,7 +369,8 @@ ORDER BY
|
||||||
id: 'tier',
|
id: 'tier',
|
||||||
width: 180,
|
width: 180,
|
||||||
accessor: row => {
|
accessor: row => {
|
||||||
return row.tier ? row.tier : row.worker ? row.worker.category : null;
|
if (row.tier) return row.tier;
|
||||||
|
return deepGet(row, 'workerInfo.worker.category');
|
||||||
},
|
},
|
||||||
Cell: this.renderFilterableCell('tier'),
|
Cell: this.renderFilterableCell('tier'),
|
||||||
},
|
},
|
||||||
|
@ -451,9 +449,11 @@ ORDER BY
|
||||||
className: 'padded',
|
className: 'padded',
|
||||||
accessor: row => {
|
accessor: row => {
|
||||||
if (oneOf(row.service_type, 'middle_manager', 'indexer')) {
|
if (oneOf(row.service_type, 'middle_manager', 'indexer')) {
|
||||||
return row.worker
|
const { workerInfo } = row;
|
||||||
? (Number(row.currCapacityUsed) || 0) / Number(row.worker.capacity)
|
if (!workerInfo) return 0;
|
||||||
: null;
|
return (
|
||||||
|
(Number(workerInfo.currCapacityUsed) || 0) / Number(workerInfo.worker?.capacity)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return row.max_size ? Number(row.curr_size) / Number(row.max_size) : null;
|
return row.max_size ? Number(row.curr_size) / Number(row.max_size) : null;
|
||||||
}
|
}
|
||||||
|
@ -469,15 +469,21 @@ ORDER BY
|
||||||
|
|
||||||
case 'indexer':
|
case 'indexer':
|
||||||
case 'middle_manager': {
|
case 'middle_manager': {
|
||||||
const originalMiddleManagers: ServiceResultRow[] = row.subRows.map(
|
const workerInfos: WorkerInfo[] = filterMap(
|
||||||
r => r._original,
|
row.subRows,
|
||||||
|
r => r._original.workerInfo,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!workerInfos.length) {
|
||||||
|
return 'Could not get worker infos';
|
||||||
|
}
|
||||||
|
|
||||||
const totalCurrCapacityUsed = sum(
|
const totalCurrCapacityUsed = sum(
|
||||||
originalMiddleManagers,
|
workerInfos,
|
||||||
s => Number(s.currCapacityUsed) || 0,
|
w => Number(w.currCapacityUsed) || 0,
|
||||||
);
|
);
|
||||||
const totalWorkerCapacity = sum(
|
const totalWorkerCapacity = sum(
|
||||||
originalMiddleManagers,
|
workerInfos,
|
||||||
s => deepGet(s, 'worker.capacity') || 0,
|
s => deepGet(s, 'worker.capacity') || 0,
|
||||||
);
|
);
|
||||||
return `${totalCurrCapacityUsed} / ${totalWorkerCapacity} (total slots)`;
|
return `${totalCurrCapacityUsed} / ${totalWorkerCapacity} (total slots)`;
|
||||||
|
@ -496,8 +502,12 @@ ORDER BY
|
||||||
|
|
||||||
case 'indexer':
|
case 'indexer':
|
||||||
case 'middle_manager': {
|
case 'middle_manager': {
|
||||||
const currCapacityUsed = deepGet(row, 'original.currCapacityUsed') || 0;
|
if (!deepGet(row, 'original.workerInfo')) {
|
||||||
const capacity = deepGet(row, 'original.worker.capacity');
|
return 'Could not get capacity info';
|
||||||
|
}
|
||||||
|
const currCapacityUsed =
|
||||||
|
deepGet(row, 'original.workerInfo.currCapacityUsed') || 0;
|
||||||
|
const capacity = deepGet(row, 'original.workerInfo.worker.capacity');
|
||||||
if (typeof capacity === 'number') {
|
if (typeof capacity === 'number') {
|
||||||
return `Slots used: ${currCapacityUsed} of ${capacity}`;
|
return `Slots used: ${currCapacityUsed} of ${capacity}`;
|
||||||
} else {
|
} else {
|
||||||
|
@ -518,30 +528,58 @@ ORDER BY
|
||||||
filterable: false,
|
filterable: false,
|
||||||
className: 'padded',
|
className: 'padded',
|
||||||
accessor: row => {
|
accessor: row => {
|
||||||
if (oneOf(row.service_type, 'middle_manager', 'indexer')) {
|
switch (row.service_type) {
|
||||||
if (deepGet(row, 'worker.version') === '') return 'Disabled';
|
case 'middle_manager':
|
||||||
|
case 'indexer': {
|
||||||
|
if (deepGet(row, 'worker.version') === '') return 'Disabled';
|
||||||
|
const { workerInfo } = row;
|
||||||
|
if (!workerInfo) {
|
||||||
|
return 'Could not get detail info';
|
||||||
|
}
|
||||||
|
|
||||||
const details: string[] = [];
|
const details: string[] = [];
|
||||||
if (row.lastCompletedTaskTime) {
|
if (workerInfo.lastCompletedTaskTime) {
|
||||||
details.push(`Last completed task: ${row.lastCompletedTaskTime}`);
|
details.push(`Last completed task: ${workerInfo.lastCompletedTaskTime}`);
|
||||||
|
}
|
||||||
|
if (workerInfo.blacklistedUntil) {
|
||||||
|
details.push(`Blacklisted until: ${workerInfo.blacklistedUntil}`);
|
||||||
|
}
|
||||||
|
return details.join(' ');
|
||||||
}
|
}
|
||||||
if (row.blacklistedUntil) {
|
|
||||||
details.push(`Blacklisted until: ${row.blacklistedUntil}`);
|
case 'coordinator':
|
||||||
|
case 'overlord':
|
||||||
|
return row.is_leader === 1 ? 'Leader' : '';
|
||||||
|
|
||||||
|
case 'historical': {
|
||||||
|
const { loadQueueInfo } = row;
|
||||||
|
if (!loadQueueInfo) return 0;
|
||||||
|
return (
|
||||||
|
(Number(loadQueueInfo.segmentsToLoad) || 0) +
|
||||||
|
(Number(loadQueueInfo.segmentsToDrop) || 0)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return details.join(' ');
|
|
||||||
} else if (oneOf(row.service_type, 'coordinator', 'overlord')) {
|
default:
|
||||||
return row.is_leader === 1 ? 'Leader' : '';
|
return 0;
|
||||||
} else {
|
|
||||||
return (Number(row.segmentsToLoad) || 0) + (Number(row.segmentsToDrop) || 0);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Cell: row => {
|
Cell: row => {
|
||||||
if (row.aggregated) return '';
|
if (row.aggregated) return '';
|
||||||
const { service_type } = row.original;
|
const { service_type } = row.original;
|
||||||
switch (service_type) {
|
switch (service_type) {
|
||||||
|
case 'middle_manager':
|
||||||
|
case 'indexer':
|
||||||
|
case 'coordinator':
|
||||||
|
case 'overlord':
|
||||||
|
return row.value;
|
||||||
|
|
||||||
case 'historical': {
|
case 'historical': {
|
||||||
|
const { loadQueueInfo } = row.original;
|
||||||
|
if (!loadQueueInfo) return 'Could not get load queue info';
|
||||||
|
|
||||||
const { segmentsToLoad, segmentsToLoadSize, segmentsToDrop, segmentsToDropSize } =
|
const { segmentsToLoad, segmentsToLoadSize, segmentsToDrop, segmentsToDropSize } =
|
||||||
row.original;
|
loadQueueInfo;
|
||||||
return formatQueues(
|
return formatQueues(
|
||||||
segmentsToLoad,
|
segmentsToLoad,
|
||||||
segmentsToLoadSize,
|
segmentsToLoadSize,
|
||||||
|
@ -550,23 +588,31 @@ ORDER BY
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'indexer':
|
|
||||||
case 'middle_manager':
|
|
||||||
case 'coordinator':
|
|
||||||
case 'overlord':
|
|
||||||
return row.value;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Aggregated: row => {
|
Aggregated: row => {
|
||||||
if (row.row._pivotVal !== 'historical') return '';
|
if (row.row._pivotVal !== 'historical') return '';
|
||||||
const originals: ServiceResultRow[] = row.subRows.map(r => r._original);
|
const loadQueueInfos: LoadQueueInfo[] = filterMap(
|
||||||
const segmentsToLoad = sum(originals, s => Number(s.segmentsToLoad) || 0);
|
row.subRows,
|
||||||
const segmentsToLoadSize = sum(originals, s => Number(s.segmentsToLoadSize) || 0);
|
r => r._original.loadQueueInfo,
|
||||||
const segmentsToDrop = sum(originals, s => Number(s.segmentsToDrop) || 0);
|
);
|
||||||
const segmentsToDropSize = sum(originals, s => Number(s.segmentsToDropSize) || 0);
|
|
||||||
|
if (!loadQueueInfos.length) {
|
||||||
|
return 'Could not get load queue infos';
|
||||||
|
}
|
||||||
|
|
||||||
|
const segmentsToLoad = sum(loadQueueInfos, s => Number(s.segmentsToLoad) || 0);
|
||||||
|
const segmentsToLoadSize = sum(
|
||||||
|
loadQueueInfos,
|
||||||
|
s => Number(s.segmentsToLoadSize) || 0,
|
||||||
|
);
|
||||||
|
const segmentsToDrop = sum(loadQueueInfos, s => Number(s.segmentsToDrop) || 0);
|
||||||
|
const segmentsToDropSize = sum(
|
||||||
|
loadQueueInfos,
|
||||||
|
s => Number(s.segmentsToDropSize) || 0,
|
||||||
|
);
|
||||||
return formatQueues(
|
return formatQueues(
|
||||||
segmentsToLoad,
|
segmentsToLoad,
|
||||||
segmentsToLoadSize,
|
segmentsToLoadSize,
|
||||||
|
@ -580,13 +626,14 @@ ORDER BY
|
||||||
show: capabilities.hasOverlordAccess() && visibleColumns.shown(ACTION_COLUMN_LABEL),
|
show: capabilities.hasOverlordAccess() && visibleColumns.shown(ACTION_COLUMN_LABEL),
|
||||||
id: ACTION_COLUMN_ID,
|
id: ACTION_COLUMN_ID,
|
||||||
width: ACTION_COLUMN_WIDTH,
|
width: ACTION_COLUMN_WIDTH,
|
||||||
accessor: row => row.worker,
|
accessor: row => row.workerInfo,
|
||||||
filterable: false,
|
filterable: false,
|
||||||
Cell: ({ value, aggregated }) => {
|
Cell: ({ value, aggregated }) => {
|
||||||
if (aggregated) return '';
|
if (aggregated) return '';
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
const disabled = value.version === '';
|
const { worker } = value;
|
||||||
const workerActions = this.getWorkerActions(value.host, disabled);
|
const disabled = worker.version === '';
|
||||||
|
const workerActions = this.getWorkerActions(worker.host, disabled);
|
||||||
return <ActionCell actions={workerActions} />;
|
return <ActionCell actions={workerActions} />;
|
||||||
},
|
},
|
||||||
Aggregated: () => '',
|
Aggregated: () => '',
|
||||||
|
|
Loading…
Reference in New Issue