diff --git a/web/vtadmin/src/components/pips/Pip.module.scss b/web/vtadmin/src/components/pips/Pip.module.scss index ece8042179f..341ed6ad314 100644 --- a/web/vtadmin/src/components/pips/Pip.module.scss +++ b/web/vtadmin/src/components/pips/Pip.module.scss @@ -35,4 +35,8 @@ &.danger { background: var(--colorError50); } + + &.throttled { + background: var(--grey900); + } } diff --git a/web/vtadmin/src/components/pips/Pip.tsx b/web/vtadmin/src/components/pips/Pip.tsx index 197249967aa..13d0e71fb3f 100644 --- a/web/vtadmin/src/components/pips/Pip.tsx +++ b/web/vtadmin/src/components/pips/Pip.tsx @@ -22,7 +22,7 @@ interface Props { state?: PipState; } -export type PipState = 'primary' | 'success' | 'warning' | 'danger' | null | undefined; +export type PipState = 'primary' | 'success' | 'warning' | 'danger' | 'throttled' | null | undefined; export const Pip = ({ className, state }: Props) => { return
; diff --git a/web/vtadmin/src/components/pips/StreamStatePip.tsx b/web/vtadmin/src/components/pips/StreamStatePip.tsx index e68d358b654..be0a7f9157b 100644 --- a/web/vtadmin/src/components/pips/StreamStatePip.tsx +++ b/web/vtadmin/src/components/pips/StreamStatePip.tsx @@ -25,6 +25,7 @@ const STREAM_STATES: { [key: string]: PipState } = { Error: 'danger', Running: 'success', Stopped: null, + Throttled: 'throttled', }; export const StreamStatePip = ({ state }: Props) => { diff --git a/web/vtadmin/src/components/routes/Workflows.tsx b/web/vtadmin/src/components/routes/Workflows.tsx index b9a85a8e3e4..32ddfcfb825 100644 --- a/web/vtadmin/src/components/routes/Workflows.tsx +++ b/web/vtadmin/src/components/routes/Workflows.tsx @@ -36,6 +36,8 @@ import { KeyspaceLink } from '../links/KeyspaceLink'; import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder'; import { UseQueryResult } from 'react-query'; +export const ThrottleThresholdSeconds = 60; + export const Workflows = () => { useDocumentTitle('Workflows'); const workflowsQuery = useWorkflows(); @@ -67,7 +69,6 @@ export const Workflows = () => { row.clusterID && row.keyspace && row.name ? `/workflow/${row.clusterID}/${row.keyspace}/${row.name}` : null; - return ( @@ -112,13 +113,52 @@ export const Workflows = () => { {/* TODO(doeg): add a protobuf enum for this (https://github.com/vitessio/vitess/projects/12#card-60190340) */} {['Error', 'Copying', 'Running', 'Stopped'].map((streamState) => { if (streamState in row.streams) { + var numThrottled = 0; + var throttledApp: string | undefined = ''; const streamCount = row.streams[streamState].length; + var streamDescription: string; + switch (streamState) { + case 'Error': + streamDescription = 'failed'; + break; + case 'Running': + case 'Copying': + const streams = row.streams[streamState]; + if (streams !== undefined && streams !== null) { + for (const stream of streams) { + if ( + stream?.throttler_status?.time_throttled !== null && + stream?.throttler_status?.time_throttled !== undefined && + // If the stream has been throttled recently, treat it as throttled. + Number(stream?.throttler_status?.time_throttled?.seconds) > + Date.now() / 1000 - ThrottleThresholdSeconds + ) { + numThrottled++; + // In case of multiple streams, show the first throttled app and time. + // The detail page will show each stream separately. + if (numThrottled === 1) { + throttledApp = + stream?.throttler_status?.component_throttled?.toString(); + } + } + } + } + streamDescription = streamState.toLocaleLowerCase(); + if (numThrottled > 0) { + streamState = 'Throttled'; + } + break; + default: + streamDescription = streamState.toLocaleLowerCase(); + } const tooltip = [ streamCount, - streamState === 'Error' ? 'failed' : streamState.toLocaleLowerCase(), + streamDescription, streamCount === 1 ? 'stream' : 'streams', + numThrottled > 0 + ? '(' + numThrottled + ' throttled in ' + throttledApp + ')' + : '', ].join(' '); - return ( diff --git a/web/vtadmin/src/components/routes/workflow/Workflow.module.scss b/web/vtadmin/src/components/routes/workflow/Workflow.module.scss index 65691d00e4c..0aa70d66828 100644 --- a/web/vtadmin/src/components/routes/workflow/Workflow.module.scss +++ b/web/vtadmin/src/components/routes/workflow/Workflow.module.scss @@ -32,3 +32,11 @@ content: none; } } + +.headingMetaContainer div { + width: '100%'; + display: 'flex'; + justify-content: 'space-between'; + padding-top: '0px'; + padding-bottom: '0px'; +} diff --git a/web/vtadmin/src/components/routes/workflow/Workflow.tsx b/web/vtadmin/src/components/routes/workflow/Workflow.tsx index 5abbcb25e83..a81901786d8 100644 --- a/web/vtadmin/src/components/routes/workflow/Workflow.tsx +++ b/web/vtadmin/src/components/routes/workflow/Workflow.tsx @@ -53,16 +53,21 @@ export const Workflow = () => { {name} -
- - Cluster: {clusterID} - - - Target keyspace:{' '} - - {keyspace} - - +
+
+ + Cluster: {clusterID} + + + Target keyspace:{' '} + + {keyspace} + + +
+
diff --git a/web/vtadmin/src/components/routes/workflow/WorkflowStreams.tsx b/web/vtadmin/src/components/routes/workflow/WorkflowStreams.tsx index 6c9bcae2dfa..2d86e1141a6 100644 --- a/web/vtadmin/src/components/routes/workflow/WorkflowStreams.tsx +++ b/web/vtadmin/src/components/routes/workflow/WorkflowStreams.tsx @@ -14,14 +14,14 @@ * limitations under the License. */ -import { orderBy, groupBy } from 'lodash-es'; +import { groupBy, orderBy } from 'lodash-es'; import React, { useMemo } from 'react'; import { Link } from 'react-router-dom'; import { useWorkflow } from '../../../hooks/api'; import { formatAlias } from '../../../util/tablets'; -import { formatDateTime } from '../../../util/time'; -import { getStreams, formatStreamKey, getStreamSource, getStreamTarget } from '../../../util/workflows'; +import { formatDateTime, formatRelativeTime } from '../../../util/time'; +import { formatStreamKey, getStreams, getStreamSource, getStreamTarget } from '../../../util/workflows'; import { DataCell } from '../../dataTable/DataCell'; import { DataTable } from '../../dataTable/DataTable'; import { TabletLink } from '../../links/TabletLink'; @@ -29,6 +29,7 @@ import { StreamStatePip } from '../../pips/StreamStatePip'; import { WorkflowStreamsLagChart } from '../../charts/WorkflowStreamsLagChart'; import { ShardLink } from '../../links/ShardLink'; import { env } from '../../../util/env'; +import { ThrottleThresholdSeconds } from '../Workflows'; interface Props { clusterID: string; @@ -61,17 +62,25 @@ export const WorkflowStreams = ({ clusterID, keyspace, name }: Props) => { const source = getStreamSource(row); const target = getStreamTarget(row, keyspace); - + var isThrottled = + Number(row?.throttler_status?.time_throttled?.seconds) > Date.now() / 1000 - ThrottleThresholdSeconds; + const rowState = isThrottled ? 'Throttled' : row.state; return ( - {' '} + {' '} {row.key}
Updated {formatDateTime(row.time_updated?.seconds)}
+ {isThrottled ? ( +
+ Throttled: + in {row.throttler_status?.component_throttled} +
+ ) : null}
{source ? ( @@ -114,7 +123,9 @@ export const WorkflowStreams = ({ clusterID, keyspace, name }: Props) => { )} -

Streams

+

+ Streams +

{/* TODO(doeg): add a protobuf enum for this (https://github.com/vitessio/vitess/projects/12#card-60190340) */} {['Error', 'Copying', 'Running', 'Stopped'].map((streamState) => { if (!Array.isArray(streamsByState[streamState])) {