Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(System/Masters): allow to 'Switch leader' for 'Secondary masters' and 'Timestamp provider' [YTFRONT-4214] #925

Merged
merged 3 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions packages/ui/src/ui/containers/ClusterPage/ClusterPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ import {setSetting} from '../../store/actions/settings';
import {unmountCluster, updateCluster} from '../../store/actions/cluster-params';
import {updateTitle} from '../../store/actions/global';
import {getClusterUiConfig} from '../../store/selectors/global';
import {isQueryTrackerAllowed} from '../../store/selectors/global/experimental-pages';
import {
isExperimentalPagesReady,
isQueryTrackerAllowed,
} from '../../store/selectors/global/experimental-pages';
import {getClusterConfig} from '../../utils';
import {NAMESPACES, SettingName} from '../../../shared/constants/settings';
import {getClusterPagePaneSizes, getStartingPage} from '../../store/selectors/settings';
Expand Down Expand Up @@ -88,6 +91,7 @@ class ClusterPage extends Component {

allowChyt: PropTypes.bool,
allowQueryTracker: PropTypes.bool,
allowStartPageRedirect: PropTypes.bool,
};

state = {
Expand Down Expand Up @@ -193,6 +197,7 @@ class ClusterPage extends Component {
paramsError,
allowChyt,
allowQueryTracker,
allowStartPageRedirect,
} = this.props;

return isLoaded && !this.isParamsLoading() ? (
Expand Down Expand Up @@ -238,7 +243,9 @@ class ClusterPage extends Component {
to={`/:cluster/${Page.COMPONENTS}/versions`}
/>
{makeExtraPageRoutes()}
<Redirect from={`/${cluster}/`} to={`/${cluster}/${startingPage}`} />
{allowStartPageRedirect && (
<Redirect from={`/${cluster}/`} to={`/${cluster}/${startingPage}`} />
)}
</Switch>

<Route path="/:cluster/:page/:tab?" component={PageTracker} />
Expand Down Expand Up @@ -337,6 +344,7 @@ function mapStateToProps(state) {
paramsCluster,
allowQueryTracker: isQueryTrackerAllowed(state),
allowChyt: Boolean(getClusterUiConfig(state).chyt_controller_base_url),
allowStartPageRedirect: isExperimentalPagesReady(state),
};
}

Expand Down
15 changes: 13 additions & 2 deletions packages/ui/src/ui/hooks/use-updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,27 @@ export type UseUpdaterOptions = {
* if `true` then `fn()` will be called only once
*/
onlyOnce?: boolean;
/**
* Enforces to ignore 'Use auto refresh' user option
*/
forceAutoRefresh?: boolean;
};

export function useUpdater(
fn?: () => unknown,
{timeout = DEFAULT_UPDATER_TIMEOUT, destructFn, onlyOnce}: UseUpdaterOptions = {},
{
timeout = DEFAULT_UPDATER_TIMEOUT,
destructFn,
onlyOnce,
forceAutoRefresh,
}: UseUpdaterOptions = {},
) {
const useAutoRefresh = useSelector(getUseAutoRefresh) as boolean;
const optionsRef = React.useRef({skipNextCall: !useAutoRefresh});

optionsRef.current.skipNextCall = !useAutoRefresh;
const allowAutoRefresh = forceAutoRefresh ?? useAutoRefresh;

optionsRef.current.skipNextCall = !allowAutoRefresh;

React.useEffect(() => {
let updater: Updater | undefined;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useEffect, useRef, useState} from 'react';
import React, {useRef, useState} from 'react';
import cn from 'bem-cn-lite';

import MetaTable from '../../../components/MetaTable/MetaTable';
Expand All @@ -8,11 +8,12 @@ import {getStateForHost, loadMasters} from '../../../store/actions/system/master
import {useDispatch} from 'react-redux';
import moment from 'moment';
import './SwitchLeaderShortInfo.scss';
import {useUpdater} from '../../../hooks/use-updater';

const block = cn('switch-leader-short-info');

interface Props {
newLeaderAddress: string;
newLeaderPath: string;
}

export function SwitchLeaderShortInfo(props: Props) {
Expand All @@ -21,44 +22,26 @@ export function SwitchLeaderShortInfo(props: Props) {
const [finishTime, setFinishTime] = useState<any>();
const dispatch = useDispatch();

useEffect(() => {
const intervalId = setInterval(() => {
const updateCurrentTime = React.useCallback(() => {
if (!finishTime) {
setCurrentTime(moment());
}
}, [finishTime]);
useUpdater(updateCurrentTime, {timeout: 1000, forceAutoRefresh: true});

if (finishTime) {
clearInterval(intervalId);
}
}, 1 * 1000);
const updateFn = React.useCallback(async () => {
if (finishTime) {
return;
}

return () => {
clearInterval(intervalId);
};
}, []);
const hostState = await getStateForHost(props.newLeaderPath);

useEffect(() => {
let stillMounted = true;

const waitForState = async () => {
try {
const hostState = await getStateForHost(props.newLeaderAddress);

if (hostState === 'leading') {
setFinishTime(moment());
dispatch(loadMasters());
}
} catch {
if (stillMounted) {
waitForState();
}
}
};

waitForState();

return () => {
stillMounted = false;
};
}, [props.newLeaderAddress]);
if (hostState === 'leading') {
setFinishTime(moment());
dispatch(loadMasters());
}
}, [props.newLeaderPath, finishTime, dispatch]);
useUpdater(updateFn, {timeout: 3000, forceAutoRefresh: true});

return (
<div className={block()}>
Expand Down
16 changes: 1 addition & 15 deletions packages/ui/src/ui/pages/system/Masters/MasterGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,6 @@ class MasterGroup extends Component {
'no-quorum': 'missing',
unknown: 'unknown',
};
let leadingHost = '';
const hosts = instances.map(({$address, state}) => {
if (state === 'leading') {
leadingHost = $address;
}

return $address;
});

return (
<Fragment>
Expand Down Expand Up @@ -96,13 +88,7 @@ class MasterGroup extends Component {
<div className={b('quorum-cell')} title={cellTitle}>
{cellTag && <Icon className={b('icon-glyph')} face="solid" awesome="tag" />}
{hammer.format['Hex'](cellTag)}
{cellId && (
<SwitchLeaderButton
cellId={cellId}
hosts={hosts}
leadingHost={leadingHost}
/>
)}
{cellId && <SwitchLeaderButton cellId={cellId} hosts={instances} />}
</div>
</div>
</Fragment>
Expand Down
47 changes: 23 additions & 24 deletions packages/ui/src/ui/pages/system/Masters/SwitchLeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ type SwitchLeaderDialogProps = {
confirm: (newLeader: string) => Promise<void>;
visible: boolean;
cellId: string;
hosts: string[];
leadingHost: string;
hosts: Array<{getPath: () => string; state: 'leading'}>;
};

type FormValues = {
Expand All @@ -24,20 +23,24 @@ const SwitchLeaderDialog = (props: SwitchLeaderDialogProps) => {
const [error, setError] = useState(undefined);

const selectLeadingHostOptions = props.hosts.map((host) => {
const path = host.getPath();
return {
value: host,
content: host,
value: path,
content: path.split('/').pop(),
};
});

const leader = props.hosts.find(({state}) => state === 'leading');
const leaderPath = leader?.getPath();

return (
<YTDFDialog<FormValues>
visible={props.visible}
headerProps={{
title: `Switch leader for ${props.cellId}`,
}}
initialValues={{
leading_primary_master: [props.leadingHost],
leading_primary_master: leaderPath ? [leaderPath] : [],
}}
fields={[
{
Expand Down Expand Up @@ -79,27 +82,22 @@ const SwitchLeaderDialog = (props: SwitchLeaderDialogProps) => {
type SwitchLeaderButtonProps = {
className: string;
cellId: string;
hosts: string[];
leadingHost: string;
hosts: Array<{getPath: () => string; state: 'leading'}>;
};

export const SwitchLeaderButton = ({
cellId,
hosts,
leadingHost,
className,
}: SwitchLeaderButtonProps) => {
export const SwitchLeaderButton = ({cellId, hosts, className}: SwitchLeaderButtonProps) => {
const [visible, setVisible] = useState(false);

const handleClick = () => {
setVisible(true);
};

const handleConfirm = async (newLeader: string) => {
const handleConfirm = async (newLeaderPath: string) => {
const leaderAddress = newLeaderPath.split('/').pop();
const switchLeader = () => {
return ytApiV4Id.switchLeader(YTApiId.switchLeader, {
cell_id: cellId,
new_leader_address: newLeader,
new_leader_address: leaderAddress,
});
};

Expand All @@ -108,7 +106,7 @@ export const SwitchLeaderButton = ({
successContent() {
return (
<AppStoreProvider>
<SwitchLeaderShortInfo newLeaderAddress={newLeader} />
<SwitchLeaderShortInfo newLeaderPath={newLeaderPath} />
</AppStoreProvider>
);
},
Expand All @@ -134,14 +132,15 @@ export const SwitchLeaderButton = ({
>
<Icon awesome="crowndiamond" />
</Button>
<SwitchLeaderDialog
cellId={cellId}
hosts={hosts}
leadingHost={leadingHost}
confirm={handleConfirm}
cancel={handleCancel}
visible={visible}
/>
{visible && (
<SwitchLeaderDialog
visible
cellId={cellId}
hosts={hosts}
confirm={handleConfirm}
cancel={handleCancel}
/>
)}
</React.Fragment>
);
};
12 changes: 9 additions & 3 deletions packages/ui/src/ui/store/actions/global/experimental-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ import {getCurrentUserName} from '../../../store/selectors/global';
import {GLOBAL_PARTIAL} from '../../../constants/global';
import UIFactory from '../../../UIFactory';
import {YTThunkAction} from '.';
import {rumLogError} from '../../../rum/rum-counter';

export function loadAllowedExperimentalPages(): YTThunkAction {
return (dispatch, getState) => {
const login = getCurrentUserName(getState());
return UIFactory.getAllowedExperimentalPages(login).then((allowedExperimentalPages) => {
dispatch({type: GLOBAL_PARTIAL, data: {allowedExperimentalPages}});
});
return UIFactory.getAllowedExperimentalPages(login)
.then((allowedExperimentalPages) => {
dispatch({type: GLOBAL_PARTIAL, data: {allowedExperimentalPages}});
})
.catch((error) => {
rumLogError({message: 'Failed to get experimental pages'}, error);
dispatch({type: GLOBAL_PARTIAL, data: {allowedExperimentalPages: []}});
});
};
}
38 changes: 31 additions & 7 deletions packages/ui/src/ui/store/actions/system/masters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ async function loadMastersConfig(): Promise<[MastersConfigResponse, MasterAlert[
const primaryMaster = primaryMasterResult.output;
const secondaryMasters = secondaryMastersResult.output;

const timestampProvierCellId = ypath.getValue(timestampProviderCellTag.output)?.cell_id;
const timestampProviders = !timestampProvidersResult.output
? {}
: {
Expand All @@ -190,8 +191,10 @@ async function loadMastersConfig(): Promise<[MastersConfigResponse, MasterAlert[
attributes: ypath.getValue(value, '/@'),
};
}),
cellId: ypath.getValue(timestampProviderCellTag.output)?.cell_id,
cellTag: getCellIdTag(ypath.getValue(timestampProviderCellTag.output)?.cell_id),
cellId: isSameClusterByCellId(masterCellId, timestampProvierCellId)
? timestampProvierCellId
: undefined,
cellTag: getCellIdTag(timestampProvierCellId),
};

const mainResult: MastersConfigResponse = {
Expand All @@ -218,6 +221,7 @@ async function loadMastersConfig(): Promise<[MastersConfigResponse, MasterAlert[
attributes: ypath.getValue(value, '/@'),
};
}),
cellId: replaceCellIdTag(masterCellId, Number(cellTag).toString(16)),
cellTag: Number(cellTag),
};
}),
Expand Down Expand Up @@ -352,16 +356,13 @@ function loadHydra(
}

export const getStateForHost = async (
host: string,
path: string,
): Promise<'leading' | 'following' | undefined> => {
const cypressPath = '//sys/primary_masters';
const hydraPath = '/orchid/monitoring/hydra';

const masterDataRequests: BatchSubRequest[] = [
{
command: 'get' as const,
parameters: {
path: cypressPath + '/' + host + hydraPath,
path: `${path}/orchid/monitoring/hydra`,
...USE_SUPRESS_SYNC,
},
},
Expand Down Expand Up @@ -512,3 +513,26 @@ function getCellIdTag(uuid?: string): number | undefined {
const [, , third = ''] = uuid.split('-');
return Number(`0x${third.substring(0, third.length - 4)}`);
}

function replaceCellIdTag(uuid?: string, newCellIdTag?: string) {
if (!uuid || !newCellIdTag) {
return undefined;
}

const [first, second, third, ...rest] = uuid.split('-');
const newThird = newCellIdTag + third.substring(third.length - 4);
return [first, second, newThird, ...rest].join('-');
}

function isSameClusterByCellId(lCell_id?: string, rClell_id?: string) {
if (!lCell_id || !rClell_id) {
return false;
}

return removeCellTag(lCell_id) === removeCellTag(rClell_id);
}

function removeCellTag(cellId: string) {
const [first, second, third, ...rest] = cellId.split('-');
return [first, second, third.substring(third.length - 4), ...rest].join('-');
}
2 changes: 1 addition & 1 deletion packages/ui/src/ui/store/reducers/global/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const initialState = {

asideHeaderWidth: 56,

allowedExperimentalPages: [],
allowedExperimentalPages: undefined,
};

function updatedTitle(state, {cluster, page, path, clusters}) {
Expand Down
Loading
Loading