Skip to content

Commit

Permalink
feat(Operations): preempted jobs [YTFRONT-4641]
Browse files Browse the repository at this point in the history
  • Loading branch information
SimbiozizV committed Feb 13, 2025
1 parent b05e92f commit b8c5dc7
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 71 deletions.
36 changes: 32 additions & 4 deletions packages/ui/src/ui/pages/job/JobGeneral/JobGeneral.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import cn from 'bem-cn-lite';
import {useSelector} from 'react-redux';
import {Redirect, Route, Switch, useRouteMatch} from 'react-router';

import {Link} from '@gravity-ui/uikit';
import {Alert, Flex, Link} from '@gravity-ui/uikit';

import Specification from '../../../pages/job/tabs/Specification/Specification';
import ErrorBoundary from '../../../components/ErrorBoundary/ErrorBoundary';
Expand Down Expand Up @@ -76,6 +76,7 @@ export default function JobGeneral() {
monitoring_descriptor,
pool_tree,
is_stale,
interruption_info,
} = job;
const operationUrl = `/${cluster}/${Page.OPERATIONS}/${operationID}/jobs?jobId=${jobID}`;
const path = `/${cluster}/${Page.JOB}/${operationID}/${jobID}`;
Expand All @@ -84,6 +85,7 @@ export default function JobGeneral() {
const isSpeculativeJob = jobCompetitionId && jobCompetitionId !== id;

const jobShellCommand = `yt --proxy ${cluster} run-job-shell ${id}`;
const preemptionReason = hammer.format['ReadableField'](interruption_info?.preemption_reason);

return (
<ErrorBoundary>
Expand All @@ -92,9 +94,18 @@ export default function JobGeneral() {
<span className={block('heading')}>
{hammer.format['ReadableField'](type)} job
</span>

<Statuslabel label={state} renderPlaque />

<Flex alignItems="center" gap={1}>
<Statuslabel label={state} renderPlaque />
{Boolean(interruption_info) && (
<Label theme="warning">
Interrupted (
{hammer.format['ReadableField'](
interruption_info?.interruption_reason,
)}
)
</Label>
)}
</Flex>
<JobActions className={block('actions')} />
</div>

Expand Down Expand Up @@ -271,6 +282,23 @@ export default function JobGeneral() {
]}
/>

{Boolean(preemptionReason) && (
<Alert
theme="info"
layout="horizontal"
message={preemptionReason}
actions={
<ClipboardButton
title="Copy reason"
view="flat-secondary"
size="s"
text={preemptionReason}
className={block('popup-button')}
/>
}
/>
)}

<div className={block('tabs')}>
<Tabs {...tabsProps} active={DEFAULT_TAB} routed size={UI_TAB_SIZE} />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, {FC} from 'react';
import {Tooltip} from '../../../../../../components/Tooltip/Tooltip';
import ypath from '../../../../../../common/thor/ypath';
import hammer from '../../../../../../common/hammer';
import {Flex, Icon} from '@gravity-ui/uikit';
import CircleQuestionIcon from '@gravity-ui/icons/svgs/circle-question.svg';
import map_ from 'lodash/map';
import MetaTable from '../../../../../../components/MetaTable/MetaTable';
import {RawJob} from '../../../../../../types/operations/job';

type Props = {
statistics: RawJob['statistics'];
type: string;
};

const prepareStatistics = (statistics: RawJob['statistics']) => {
return map_(ypath.getValue(statistics), (value, key: string) => {
let result = hammer.format.Number(value);

if (key.endsWith('_data_size') || key.endsWith('_data_weight')) {
result = hammer.format.Bytes(value);
} else if (key.endsWith('_time')) {
result = hammer.format.TimeDuration(value, {format: 'milliseconds'});
}

return {
key: hammer.format.Readable(key),
value: <div style={{textAlign: 'right'}}>{result}</div>,
};
});
};

export const JobDetails: FC<Props> = ({statistics, type}) => {
if (!statistics || type === 'vanilla') return null;

const [rowCount, dataSize] = ypath.getValues(statistics, [
'/processed_input_row_count',
'/processed_input_uncompressed_data_size',
]);

if (!rowCount && !dataSize) return null;

return (
<Tooltip content={<MetaTable items={prepareStatistics(statistics)} />}>
<Flex gap={2} alignItems="center">
<span>
{hammer.format['Number'](rowCount)} ({hammer.format['Bytes'](dataSize)})
</span>
<Icon data={CircleQuestionIcon} size={16} />
</Flex>
</Tooltip>
);
};
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import React from 'react';
import ypath from '@ytsaurus/interface-helpers/lib/ypath';
import {Progress} from '@gravity-ui/uikit';

import hammer from '../../../../../../common/hammer';
import Link from '../../../../../../components/Link/Link';
import {Template} from '../../../../../../components/MetaTable/templates/Template';
import {showErrorModal} from '../../../../../../store/actions/actions';
Expand Down Expand Up @@ -62,33 +59,9 @@ function JobProgress({state, progress}: {state: JobProgressState; progress: numb

/* ----------------------------------------------------------------------------------------------------------------- */

function renderStatistics(state: JobProgressState, statistics: Job['brief_statistics']) {
const [rowCount, dataSize] = ypath.getValues(statistics, [
'/processed_input_row_count',
'/processed_input_uncompressed_data_size',
]);
return (
(state === 'running' || state === 'completed') &&
`${hammer.format['Number'](rowCount)} (${hammer.format['Bytes'](dataSize)})`
);
}

function JobStatistics({
state,
statistics,
}: {
state: JobProgressState;
statistics: Job['brief_statistics'];
}) {
return statistics ? renderStatistics(state, statistics) : null;
}

/* ----------------------------------------------------------------------------------------------------------------- */

export default function JobTemplate() {}

JobTemplate.Error = JobError;
JobTemplate.InputPaths = JobInputPaths;
JobTemplate.DebugInfo = JobDebugInfo;
JobTemplate.Progress = JobProgress;
JobTemplate.Statistics = JobStatistics;
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, {Fragment} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import {Button, DropdownMenu} from '@gravity-ui/uikit';
import {Button, DropdownMenu, Icon} from '@gravity-ui/uikit';
import cn from 'bem-cn-lite';

import map_ from 'lodash/map';
Expand All @@ -18,9 +18,7 @@ import ClipboardButton from '../../../../../../components/ClipboardButton/Clipbo
import ChartLink from '../../../../../../components/ChartLink/ChartLink';
import MetaTable from '../../../../../../components/MetaTable/MetaTable';
import Yson from '../../../../../../components/Yson/Yson';
import Icon from '../../../../../../components/Icon/Icon';
import Link from '../../../../../../components/Link/Link';
import {Tooltip} from '../../../../../../components/Tooltip/Tooltip';
import CollapsibleSection from '../../../../../../components/CollapsibleSection/CollapsibleSection';

import {
Expand All @@ -46,11 +44,12 @@ import {StaleJobIcon} from '../StaleJobIcon';
import JobTemplate from './JobTemplate';
import './OperationJobsTable.scss';
import {UI_COLLAPSIBLE_SIZE} from '../../../../../../constants/global';
import {JobDetails} from './JobDetails';
import EllipsisIcon from '@gravity-ui/icons/svgs/ellipsis.svg';
import {StatusInfo} from './StatusInfo';

const block = cn('operation-detail-jobs');

const BriefStatisticsDetailsMemo = React.memo(BriefStatisticsDetails);

class OperationJobsTable extends React.Component {
static propTypes = {
isLoading: PropTypes.bool.isRequired,
Expand Down Expand Up @@ -250,7 +249,7 @@ class OperationJobsTable extends React.Component {

const button = (
<Button view="flat-secondary" title="Show actions">
<Icon awesome="ellipsis-h" />
<Icon data={EllipsisIcon} size={16} />
</Button>
);
const firstGroup = map_(this.preparedActions, ({action, text}) => ({
Expand Down Expand Up @@ -388,25 +387,17 @@ class OperationJobsTable extends React.Component {
}

renderProgress(item) {
const {state, progress, brief_statistics: statistics} = item;
const {state, progress, brief_statistics, type, interruption_info} = item;

return (
<div className={block('state')}>
<div className={block('state-section', 'elements-ellipsis')}>
{hammer.format['ReadableField'](state)}
<StatusInfo info={interruption_info} state={state} />
</div>
<div className={block('state-section')}>
<JobTemplate.Progress state={state} progress={progress} />
</div>
<Tooltip
className={block('state-section', 'elements-ellipsis')}
content={<BriefStatisticsDetailsMemo data={item.brief_statistics} />}
>
<span>
<JobTemplate.Statistics state={state} statistics={statistics} />
</span>
<Icon className={block('state-icon')} awesome={'question-circle'} />
</Tooltip>
<JobDetails statistics={brief_statistics} type={type} />
</div>
);
}
Expand Down Expand Up @@ -551,26 +542,3 @@ const mapDispatchToProps = {
};

export default connect(mapStateToProps, mapDispatchToProps)(OperationJobsTable);

function BriefStatisticsDetails({data}) {
const items = React.useMemo(
() =>
map_(ypath.getValue(data), (value, key) => {
let result = hammer.format.Number(value);

if (key.endsWith('_data_size') || key.endsWith('_data_weight')) {
result = hammer.format.Bytes(value);
} else if (key.endsWith('_time')) {
result = hammer.format.TimeDuration(value, {format: 'milliseconds'});
}

return {
key: hammer.format.Readable(key),
value: <div className={block('state-value')}>{result}</div>,
};
}),
[data],
);

return <MetaTable items={items} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.yt-job-status-info {
&__popup {
padding: 10px;
max-width: 400px;
display: flex;
gap: 5px;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, {FC, useRef} from 'react';
import {RawJob} from '../../../../../../types/operations/job';
import hammer from '../../../../../../common/hammer';
import {ClipboardButton, Flex, Label, Popup} from '@gravity-ui/uikit';
import {useToggle} from 'react-use';
import './StatusInfo.scss';
import cn from 'bem-cn-lite';

const block = cn('yt-job-status-info');

type Props = {
state: RawJob['state'];
info: RawJob['interruption_info'];
};

export const StatusInfo: FC<Props> = ({state, info}) => {
const [open, toggleOpen] = useToggle(false);
const anchorRef = useRef<HTMLDivElement>(null);
const reason = hammer.format['ReadableField'](
info?.preemption_reason || info?.interruption_reason,
);

return (
<Flex alignItems="center" gap={1} className={block()}>
{hammer.format['ReadableField'](state)}
{Boolean(reason) && (
<>
<div onMouseEnter={toggleOpen}>
<Label theme="warning" ref={anchorRef}>
Interrupted
</Label>
</div>

<Popup open={open} hasArrow anchorRef={anchorRef} onMouseLeave={toggleOpen}>
<div className={block('popup')}>
<div>{reason}</div>
<ClipboardButton
title="Copy reason"
view="flat-secondary"
size="s"
text={reason}
/>
</div>
</Popup>
</>
)}
</Flex>
);
};
9 changes: 9 additions & 0 deletions packages/ui/src/ui/types/operations/job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ export interface RawJob {
pool_tree?: string;
is_stale?: boolean;
archive_features?: {has_trace?: boolean};
interruption_info?: {
interruption_reason: string;
interruption_timeout?: number;
preempted_for?: {
allocation_id?: string;
operation_id?: string;
};
preemption_reason?: string;
};
}

export type JobState =
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit b8c5dc7

Please sign in to comment.