diff --git a/packages/compass-global-writes/src/components/index.tsx b/packages/compass-global-writes/src/components/index.tsx index 77aa286de46..12031c5656f 100644 --- a/packages/compass-global-writes/src/components/index.tsx +++ b/packages/compass-global-writes/src/components/index.tsx @@ -5,6 +5,7 @@ import { spacing, WorkspaceContainer, SpinLoaderWithLabel, + ConfirmationModalArea, } from '@mongodb-js/compass-components'; import type { RootState, ShardingStatus } from '../store/reducer'; import { ShardingStatuses } from '../store/reducer'; @@ -55,7 +56,10 @@ function ShardingStateView({ return ; } - if (shardingStatus === ShardingStatuses.SHARDING) { + if ( + shardingStatus === ShardingStatuses.SHARDING || + shardingStatus === ShardingStatuses.CANCELLING_SHARDING + ) { return ; } @@ -73,7 +77,9 @@ export function GlobalWrites({ shardingStatus }: GlobalWritesProps) { return (
- + + +
); diff --git a/packages/compass-global-writes/src/components/states/sharding.spec.tsx b/packages/compass-global-writes/src/components/states/sharding.spec.tsx index c3d8fb542be..69b767fe87e 100644 --- a/packages/compass-global-writes/src/components/states/sharding.spec.tsx +++ b/packages/compass-global-writes/src/components/states/sharding.spec.tsx @@ -1,13 +1,20 @@ import React from 'react'; import { expect } from 'chai'; -import { screen } from '@mongodb-js/testing-library-compass'; +import { screen, userEvent } from '@mongodb-js/testing-library-compass'; import { ShardingState } from './sharding'; import { renderWithStore } from '../../../tests/create-store'; +import Sinon from 'sinon'; function renderWithProps( props?: Partial> ) { - return renderWithStore(); + return renderWithStore( + {}} + isCancellingSharding={false} + {...props} + /> + ); } describe('Sharding', function () { @@ -15,4 +22,31 @@ describe('Sharding', function () { await renderWithProps(); expect(screen.getByRole('alert')).to.exist; }); + + it('sharding request can be cancelled', async function () { + const onCancelSharding = Sinon.spy(); + await renderWithProps({ + onCancelSharding, + }); + const btn = screen.getByRole('button', { name: 'Cancel Request' }); + expect(btn).to.be.visible; + + userEvent.click(btn); + + expect(onCancelSharding).to.have.been.calledOnce; + }); + + it('when cancelling is in progress, it cannot be triggered again', async function () { + const onCancelSharding = Sinon.spy(); + await renderWithProps({ + isCancellingSharding: true, + onCancelSharding, + }); + const btn = screen.getByTestId('cancel-sharding-btn'); + expect(btn.getAttribute('aria-disabled')).to.equal('true'); + + userEvent.click(btn); + + expect(onCancelSharding).not.to.have.been.calledOnce; + }); }); diff --git a/packages/compass-global-writes/src/components/states/sharding.tsx b/packages/compass-global-writes/src/components/states/sharding.tsx index f64e788a4ab..3bef1cf75d8 100644 --- a/packages/compass-global-writes/src/components/states/sharding.tsx +++ b/packages/compass-global-writes/src/components/states/sharding.tsx @@ -3,11 +3,17 @@ import { Banner, BannerVariant, Body, + Button, css, Link, spacing, } from '@mongodb-js/compass-components'; import { connect } from 'react-redux'; +import { + cancelSharding, + type RootState, + ShardingStatuses, +} from '../../store/reducer'; const nbsp = '\u00a0'; @@ -17,12 +23,33 @@ const containerStyles = css({ gap: spacing[400], }); -export function ShardingState() { +const btnStyles = css({ + float: 'right', + height: spacing[600], +}); + +interface ShardingStateProps { + isCancellingSharding: boolean; + onCancelSharding: () => void; +} + +export function ShardingState({ + isCancellingSharding, + onCancelSharding, +}: ShardingStateProps) { return (
Sharding your collection … {nbsp}this should not take too long. + Once your collection is sharded, this tab will show instructions on @@ -39,4 +66,11 @@ export function ShardingState() { ); } -export default connect()(ShardingState); +export default connect( + (state: RootState) => ({ + isCancellingSharding: state.status === ShardingStatuses.CANCELLING_SHARDING, + }), + { + onCancelSharding: cancelSharding, + } +)(ShardingState); diff --git a/packages/compass-global-writes/src/plugin-title.spec.tsx b/packages/compass-global-writes/src/plugin-title.spec.tsx new file mode 100644 index 00000000000..ce4e843a822 --- /dev/null +++ b/packages/compass-global-writes/src/plugin-title.spec.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { expect } from 'chai'; +import { PluginTitle } from './plugin-title'; +import { render, screen } from '@mongodb-js/testing-library-compass'; + +describe('PluginTitle', function () { + it('Renders a warning', function () { + render(); + expect(screen.getByLabelText('warning')).to.be.visible; + }); + + it('Does not render a warning', function () { + render(); + expect(screen.queryByLabelText('warning')).not.to.exist; + }); +}); diff --git a/packages/compass-global-writes/src/plugin-title.tsx b/packages/compass-global-writes/src/plugin-title.tsx index 0c484aecaac..9106ae55c29 100644 --- a/packages/compass-global-writes/src/plugin-title.tsx +++ b/packages/compass-global-writes/src/plugin-title.tsx @@ -30,7 +30,7 @@ const iconStylesDark = css({ color: palette.yellow.base, }); -const PluginTitle = ({ showWarning }: { showWarning: boolean }) => { +export const PluginTitle = ({ showWarning }: { showWarning: boolean }) => { const darkMode = useDarkMode(); return (
@@ -50,6 +50,7 @@ const PluginTitle = ({ showWarning }: { showWarning: boolean }) => { > false, + hasShardingError = () => false, + hasShardKey = () => false, + failsOnShardingRequest = () => false, + authenticatedFetchStub, +}: + | { + isNamespaceManaged?: () => boolean; + hasShardingError?: () => boolean; + hasShardKey?: () => boolean; + failsOnShardingRequest?: () => boolean; + authenticatedFetchStub?: never; + } + | { + isNamespaceManaged?: never; + hasShardingError?: never; + hasShardKey?: () => boolean; + failsOnShardingRequest?: never; + authenticatedFetchStub?: () => void; + } = {}): GlobalWritesStore { + const atlasService = { + authenticatedFetch: (uri: string) => { + if (uri.includes(`/geoSharding`) && failsOnShardingRequest()) { + return Promise.reject(new Error('Failed to shard')); + } + + if (uri.includes('/clusters/')) { + return createAuthFetchResponse({ + ...clusterDetails, + geoSharding: { + ...clusterDetails.geoSharding, + managedNamespaces: isNamespaceManaged() ? [managedNamespace] : [], + }, + }); + } + + if (uri.includes('/deploymentStatus/')) { + return createAuthFetchResponse({ + automationStatus: { + processes: hasShardingError() ? [failedShardingProcess] : [], + }, + }); + } + + return createAuthFetchResponse({}); + }, + automationAgentRequest: (_meta: unknown, type: string) => ({ + _id: '123', + requestType: type, + }), + automationAgentAwait: (_meta: unknown, type: string) => { + if (type === 'getShardKey') { + return { + response: hasShardKey() + ? [ + { + key: { + location: 'range', + secondary: shardKeyData.isCustomShardKeyHashed + ? 'hashed' + : 'range', + }, + unique: true, + }, + ] + : [], + }; + } + }, + } as any; + + if (authenticatedFetchStub) + atlasService.authenticatedFetch = authenticatedFetchStub; + return setupStore( { namespace: NS, @@ -69,6 +150,20 @@ function createStore(atlasService: any = {}): GlobalWritesStore { } describe('GlobalWritesStore Store', function () { + let confirmationStub: Sinon.SinonStub; + let clock: Sinon.SinonFakeTimers; + + beforeEach(() => { + confirmationStub = sinon + .stub(globalWritesReducer, 'showConfirmation') + .resolves(true); + }); + + afterEach(() => { + sinon.restore(); + clock && clock.restore(); + }); + it('sets the initial state', function () { const store = createStore(); expect(store.getState().namespace).to.equal(NS); @@ -76,88 +171,184 @@ describe('GlobalWritesStore Store', function () { }); context('scenarios', function () { - it('not managed -> sharding', async function () { + it('not managed -> sharding -> valid shard key', async function () { + let mockShardKey = false; + // initial state === unsharded const store = createStore({ - authenticatedFetch: () => createAuthFetchResponse(clusterDetails), + hasShardKey: Sinon.fake(() => mockShardKey), + }); + await waitFor(() => { + expect(store.getState().status).to.equal('UNSHARDED'); + expect(store.getState().managedNamespace).to.equal(undefined); }); - await store.dispatch(fetchClusterShardingData()); - expect(store.getState().status).to.equal('UNSHARDED'); - expect(store.getState().managedNamespace).to.equal(undefined); + // user requests sharding + clock = sinon.useFakeTimers({ + shouldAdvanceTime: true, + }); const promise = store.dispatch(createShardKey(shardKeyData)); expect(store.getState().status).to.equal('SUBMITTING_FOR_SHARDING'); await promise; expect(store.getState().status).to.equal('SHARDING'); + + // sharding ends with a shardKey + mockShardKey = true; + clock.tick(POLLING_INTERVAL); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); + }); }); - it('not managed -> failed sharding attempt', async function () { + it('not managed -> sharding -> sharding error', async function () { + let mockFailure = false; + // initial state === unsharded const store = createStore({ - authenticatedFetch: (uri: string) => { - if (uri.includes('/geoSharding')) { - return Promise.reject(new Error('Failed to shard')); - } + hasShardingError: Sinon.fake(() => mockFailure), + }); + await waitFor(() => { + expect(store.getState().status).to.equal('UNSHARDED'); + expect(store.getState().managedNamespace).to.equal(undefined); + }); - return createAuthFetchResponse(clusterDetails); - }, + // user requests sharding + clock = sinon.useFakeTimers({ + shouldAdvanceTime: true, }); - await store.dispatch(fetchClusterShardingData()); - expect(store.getState().status).to.equal('UNSHARDED'); - expect(store.getState().managedNamespace).to.equal(undefined); + const promise = store.dispatch(createShardKey(shardKeyData)); + expect(store.getState().status).to.equal('SUBMITTING_FOR_SHARDING'); + await promise; + expect(store.getState().status).to.equal('SHARDING'); + + // sharding ends with an error + mockFailure = true; + clock.tick(POLLING_INTERVAL); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARDING_ERROR'); + }); + }); + it('not managed -> not managed (failed sharding request)', async function () { + // initial state === not managed + const store = createStore({ + failsOnShardingRequest: () => true, + }); + await waitFor(() => { + expect(store.getState().status).to.equal('UNSHARDED'); + expect(store.getState().managedNamespace).to.equal(undefined); + }); + + // user tries to submit for sharding, but the request fails const promise = store.dispatch(createShardKey(shardKeyData)); expect(store.getState().status).to.equal('SUBMITTING_FOR_SHARDING'); await promise; expect(store.getState().status).to.equal('UNSHARDED'); }); - it('when the namespace is managed', async function () { + it('sharding -> valid shard key', async function () { + let mockShardKey = false; + // initial state === sharding + clock = sinon.useFakeTimers({ + shouldAdvanceTime: true, + }); + const store = createStore({ + isNamespaceManaged: () => true, + hasShardKey: Sinon.fake(() => mockShardKey), + }); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARDING'); + expect(store.getState().managedNamespace).to.equal(managedNamespace); + }); + + // sharding ends with a shardKey + mockShardKey = true; + clock.tick(POLLING_INTERVAL); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); + }); + }); + + it('sharding -> cancelling request -> not managed', async function () { + let mockManagedNamespace = true; + confirmationStub.resolves(true); + // initial state === sharding + const store = createStore({ + isNamespaceManaged: Sinon.fake(() => mockManagedNamespace), + }); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARDING'); + expect(store.getState().pollingTimeout).not.to.be.undefined; + expect(store.getState().managedNamespace).to.equal(managedNamespace); + }); + + // user cancels the sharding request + const promise = store.dispatch(cancelSharding()); + mockManagedNamespace = false; + await promise; + expect(store.getState().status).to.equal('UNSHARDED'); + expect(store.getState().pollingTimeout).to.be.undefined; + expect(confirmationStub).to.have.been.called; + }); + + it('valid shard key', async function () { const store = createStore({ - authenticatedFetch: (uri: string) => { - if (uri.includes('/clusters/')) { - return createAuthFetchResponse({ - ...clusterDetails, - geoSharding: { - ...clusterDetails.geoSharding, - managedNamespaces: [managedNamespace], - }, - }); - } - - if (uri.includes('/deploymentStatus/')) { - return createAuthFetchResponse({ - automationStatus: { - processes: [], - }, - }); - } - - return createAuthFetchResponse({}); - }, - automationAgentRequest: (_meta: unknown, type: string) => ({ - _id: '123', - requestType: type, - }), - automationAgentAwait: (_meta: unknown, type: string) => { - if (type === 'getShardKey') { - return { - response: [ - { - key: { - location: 'HASHED', - secondary: 'HASHED', - }, - unique: false, - }, - ], - }; - } - }, + isNamespaceManaged: () => true, + hasShardKey: () => true, + }); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); + expect(store.getState().managedNamespace).to.equal(managedNamespace); + }); + }); + + it('valid shard key -> not managed', async function () { + // initial state === shard key correct + const store = createStore({ + isNamespaceManaged: () => true, + hasShardKey: () => true, + }); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); + expect(store.getState().managedNamespace).to.equal(managedNamespace); + }); + + // user asks to unmanage + const promise = store.dispatch(unmanageNamespace()); + expect(store.getState().status).to.equal('UNMANAGING_NAMESPACE'); + await promise; + expect(store.getState().status).to.equal('UNSHARDED'); + }); + + it('valid shard key -> valid shard key (failed unmanage attempt)', async function () { + // initial state === shard key correct + let mockFailure = false; + const store = createStore({ + isNamespaceManaged: () => true, + hasShardKey: () => true, + failsOnShardingRequest: Sinon.fake(() => mockFailure), }); - await store.dispatch(fetchClusterShardingData()); + await waitFor(() => { expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); expect(store.getState().managedNamespace).to.equal(managedNamespace); }); + + // user asks to unmanage + mockFailure = true; + const promise = store.dispatch(unmanageNamespace()); + expect(store.getState().status).to.equal('UNMANAGING_NAMESPACE'); + await promise; + expect(store.getState().status).to.equal('SHARD_KEY_CORRECT'); + }); + + it('sharding error', async function () { + const store = createStore({ + isNamespaceManaged: () => true, + hasShardingError: () => true, + }); + await waitFor(() => { + expect(store.getState().status).to.equal('SHARDING_ERROR'); + expect(store.getState().managedNamespace).to.equal(managedNamespace); + }); }); it('sends correct data to the server when creating a shard key', async function () { @@ -165,7 +356,7 @@ describe('GlobalWritesStore Store', function () { { db: 'test', collection: 'one', - customShardKey: 'test', + customShardKey: 'secondary', isCustomShardKeyHashed: true, isShardKeyUnique: false, numInitialChunks: 1, @@ -196,7 +387,7 @@ describe('GlobalWritesStore Store', function () { .resolves(); const store = createStore({ - authenticatedFetch: fetchStub, + authenticatedFetchStub: fetchStub, }); await store.dispatch(createShardKey(shardKeyData)); diff --git a/packages/compass-global-writes/src/store/reducer.ts b/packages/compass-global-writes/src/store/reducer.ts index 85981a039df..d596d52d51c 100644 --- a/packages/compass-global-writes/src/store/reducer.ts +++ b/packages/compass-global-writes/src/store/reducer.ts @@ -1,8 +1,13 @@ import type { Action, Reducer } from 'redux'; import type { GlobalWritesThunkAction } from '.'; -import { openToast } from '@mongodb-js/compass-components'; +import { + openToast, + showConfirmation as showConfirmationModal, +} from '@mongodb-js/compass-components'; import type { ManagedNamespace } from '../services/atlas-global-writes-service'; +export const POLLING_INTERVAL = 5000; + export function isAction( action: Action, type: A['type'] @@ -30,6 +35,13 @@ enum GlobalWritesActionTypes { SubmittingForShardingFinished = 'global-writes/SubmittingForShardingFinished', SubmittingForShardingErrored = 'global-writes/SubmittingForShardingErrored', + CancellingShardingStarted = 'global-writes/CancellingShardingStarted', + CancellingShardingFinished = 'global-writes/CancellingShardingFinished', + CancellingShardingErrored = 'global-writes/CancellingShardingErrored', + + NextPollingTimeoutSet = 'global-writes/NextPollingTimeoutSet', + NextPollingTimeoutCleared = 'global-writes/NextPollingTimeoutCleared', + UnmanagingNamespaceStarted = 'global-writes/UnmanagingNamespaceStarted', UnmanagingNamespaceFinished = 'global-writes/UnmanagingNamespaceFinished', UnmanagingNamespaceErrored = 'global-writes/UnmanagingNamespaceErrored', @@ -68,6 +80,28 @@ type SubmittingForShardingErroredAction = { type: GlobalWritesActionTypes.SubmittingForShardingErrored; }; +type CancellingShardingStartedAction = { + type: GlobalWritesActionTypes.CancellingShardingStarted; +}; + +type CancellingShardingFinishedAction = { + type: GlobalWritesActionTypes.CancellingShardingFinished; + managedNamespace?: ManagedNamespace; +}; + +type CancellingShardingErroredAction = { + type: GlobalWritesActionTypes.CancellingShardingErrored; +}; + +type NextPollingTimeoutSetAction = { + type: GlobalWritesActionTypes.NextPollingTimeoutSet; + timeout: NodeJS.Timeout; +}; + +type NextPollingTimeoutClearedAction = { + type: GlobalWritesActionTypes.NextPollingTimeoutCleared; +}; + type UnmanagingNamespaceStartedAction = { type: GlobalWritesActionTypes.UnmanagingNamespaceStarted; }; @@ -102,6 +136,12 @@ export enum ShardingStatuses { */ SHARDING = 'SHARDING', + /** + * State when user cancels the sharding and + * we are waiting for server to accept the request. + */ + CANCELLING_SHARDING = 'CANCELLING_SHARDING', + /** * Sharding failed. */ @@ -157,23 +197,36 @@ export type RootState = { status: ShardingStatuses.NOT_READY; shardKey?: never; shardingError?: never; + pollingTimeout?: never; } | { status: | ShardingStatuses.UNSHARDED | ShardingStatuses.SUBMITTING_FOR_SHARDING - | ShardingStatuses.SHARDING; + | ShardingStatuses.CANCELLING_SHARDING; /** * note: shardKey might exist even for unsharded. * if the collection was sharded previously and then unmanaged */ shardKey?: ShardKey; shardingError?: never; + pollingTimeout?: never; + } + | { + status: ShardingStatuses.SHARDING; + /** + * note: shardKey might exist + * if the collection was sharded previously and then unmanaged + */ + shardKey?: ShardKey; + shardingError?: never; + pollingTimeout?: NodeJS.Timeout; } | { status: ShardingStatuses.SHARDING_ERROR; shardKey?: never; shardingError: string; + pollingTimeout?: never; } | { status: @@ -183,6 +236,7 @@ export type RootState = { | ShardingStatuses.UNMANAGING_NAMESPACE; shardKey: ShardKey; shardingError?: never; + pollingTimeout?: never; } ); @@ -214,13 +268,18 @@ const reducer: Reducer = (state = initialState, action) => { action, GlobalWritesActionTypes.NamespaceShardingErrorFetched ) && - state.status === ShardingStatuses.NOT_READY + (state.status === ShardingStatuses.NOT_READY || + state.status === ShardingStatuses.SHARDING) ) { + if (state.pollingTimeout) { + throw new Error('Polling was not stopped'); + } return { ...state, status: ShardingStatuses.SHARDING_ERROR, shardKey: undefined, shardingError: action.error, + pollingTimeout: state.pollingTimeout, }; } @@ -229,13 +288,18 @@ const reducer: Reducer = (state = initialState, action) => { action, GlobalWritesActionTypes.NamespaceShardKeyFetched ) && - state.status === ShardingStatuses.NOT_READY + (state.status === ShardingStatuses.NOT_READY || + state.status === ShardingStatuses.SHARDING) ) { + if (state.pollingTimeout) { + throw new Error('Polling was not stopped'); + } return { ...state, status: getStatusFromShardKey(action.shardKey, state.managedNamespace), shardKey: action.shardKey, shardingError: undefined, + pollingTimeout: state.pollingTimeout, }; } @@ -269,7 +333,8 @@ const reducer: Reducer = (state = initialState, action) => { action, GlobalWritesActionTypes.SubmittingForShardingFinished ) && - state.status === ShardingStatuses.SUBMITTING_FOR_SHARDING + (state.status === ShardingStatuses.SUBMITTING_FOR_SHARDING || + state.status === ShardingStatuses.NOT_READY) ) { return { ...state, @@ -278,6 +343,78 @@ const reducer: Reducer = (state = initialState, action) => { }; } + if ( + isAction( + action, + GlobalWritesActionTypes.NextPollingTimeoutSet + ) && + state.status === ShardingStatuses.SHARDING + ) { + return { + ...state, + pollingTimeout: action.timeout, + }; + } + + if ( + isAction( + action, + GlobalWritesActionTypes.NextPollingTimeoutCleared + ) && + state.status === ShardingStatuses.SHARDING + ) { + return { + ...state, + pollingTimeout: undefined, + }; + } + + if ( + isAction( + action, + GlobalWritesActionTypes.CancellingShardingStarted + ) && + state.status === ShardingStatuses.SHARDING + ) { + if (state.pollingTimeout) { + throw new Error('Polling was not stopped'); + } + return { + ...state, + status: ShardingStatuses.CANCELLING_SHARDING, + pollingTimeout: state.pollingTimeout, + }; + } + + if ( + isAction( + action, + GlobalWritesActionTypes.CancellingShardingErrored + ) && + state.status === ShardingStatuses.CANCELLING_SHARDING + ) { + return { + ...state, + status: ShardingStatuses.SHARDING, + }; + } + + if ( + isAction( + action, + GlobalWritesActionTypes.CancellingShardingFinished + ) && + (state.status === ShardingStatuses.CANCELLING_SHARDING || + state.status === ShardingStatuses.SHARDING_ERROR) + // the error might come before the cancel request was processed + ) { + return { + ...state, + status: ShardingStatuses.UNSHARDED, + shardingError: undefined, + }; + } + if ( isAction( action, @@ -433,6 +570,64 @@ export const createShardKey = ( }; }; +// Exporting this for test only to stub it and set +// its value. This enables to test cancelSharding action. +export const showConfirmation = showConfirmationModal; + +export const cancelSharding = (): GlobalWritesThunkAction< + Promise, + | CancellingShardingStartedAction + | CancellingShardingFinishedAction + | CancellingShardingErroredAction +> => { + return async (dispatch, getState, { atlasGlobalWritesService, logger }) => { + const confirmed = await showConfirmation({ + title: 'Confirmation', + description: 'Are you sure you want to cancel the sharding request?', + }); + + if (!confirmed) { + return; + } + + const { namespace, status } = getState(); + + if (status === ShardingStatuses.SHARDING) { + dispatch(stopPollingForShardKey()); + } + dispatch({ + type: GlobalWritesActionTypes.CancellingShardingStarted, + }); + + try { + await atlasGlobalWritesService.unmanageNamespace(namespace); + dispatch({ + type: GlobalWritesActionTypes.CancellingShardingFinished, + }); + } catch (error) { + logger.log.error( + logger.mongoLogId(1_001_000_334), + 'AtlasFetchError', + 'Error cancelling the sharding process', + { + error: (error as Error).message, + } + ); + openToast('global-writes-cancel-sharding-error', { + title: `Failed to cancel the sharding process: ${ + (error as Error).message + }`, + dismissible: true, + timeout: 5000, + variant: 'important', + }); + dispatch({ + type: GlobalWritesActionTypes.CancellingShardingErrored, + }); + } + }; +}; + const setNamespaceBeingSharded = ( managedNamespace?: ManagedNamespace ): GlobalWritesThunkAction => { @@ -441,6 +636,41 @@ const setNamespaceBeingSharded = ( type: GlobalWritesActionTypes.SubmittingForShardingFinished, managedNamespace, }); + dispatch(pollForShardKey()); + }; +}; + +const pollForShardKey = (): GlobalWritesThunkAction< + void, + NextPollingTimeoutSetAction +> => { + return (dispatch, getState) => { + const { pollingTimeout } = getState(); + if ( + pollingTimeout // prevent double polling + ) { + return; + } + const timeout = setTimeout(() => { + void dispatch(fetchNamespaceShardKey()); + }, POLLING_INTERVAL); + + dispatch({ + type: GlobalWritesActionTypes.NextPollingTimeoutSet, + timeout, + }); + }; +}; + +const stopPollingForShardKey = (): GlobalWritesThunkAction< + void, + NextPollingTimeoutClearedAction +> => { + return (dispatch, getState) => { + const { pollingTimeout } = getState(); + if (!pollingTimeout) return; + clearTimeout(pollingTimeout); + dispatch({ type: GlobalWritesActionTypes.NextPollingTimeoutCleared }); }; }; @@ -453,7 +683,7 @@ export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< getState, { atlasGlobalWritesService, logger, connectionInfoRef } ) => { - const { namespace } = getState(); + const { namespace, status } = getState(); try { const [shardingError, shardKey] = await Promise.all([ @@ -462,6 +692,9 @@ export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< ]); if (shardingError) { + if (status === ShardingStatuses.SHARDING) { + dispatch(stopPollingForShardKey()); + } dispatch({ type: GlobalWritesActionTypes.NamespaceShardingErrorFetched, error: shardingError, @@ -474,6 +707,9 @@ export const fetchNamespaceShardKey = (): GlobalWritesThunkAction< return; } + if (status === ShardingStatuses.SHARDING) { + dispatch(stopPollingForShardKey()); + } dispatch({ type: GlobalWritesActionTypes.NamespaceShardKeyFetched, shardKey,