diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.fixture.ts b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.fixture.ts index 47124953d7e0..46263f718cd8 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.fixture.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.fixture.ts @@ -247,6 +247,7 @@ const diagnosticsReportsInProgress: IStatementDiagnosticsReport[] = [ ]; const aggregatedTs = Date.parse("Sep 15 2021 01:00:00 GMT") * 1e-3; +const lastUpdated = moment("Sep 15 2021 01:30:00 GMT"); const aggregationInterval = 3600; // 1 hour const statementsPagePropsFixture: StatementsPageProps = { @@ -285,6 +286,7 @@ const statementsPagePropsFixture: StatementsPageProps = { regions: "", nodes: "", }, + lastUpdated, // Aggregate key values in these statements will need to change if implementation // of 'statementKey' in appStats.ts changes. statements: [ diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts index 5e171e7776f3..510f6e9db8df 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.selectors.ts @@ -46,6 +46,11 @@ export interface StatementsSummaryData { stats: StatementStatistics[]; } +export const selectStatementsLastUpdated = createSelector( + sqlStatsSelector, + sqlStats => sqlStats.lastUpdated, +); + // selectApps returns the array of all apps with statement statistics present // in the data. export const selectApps = createSelector(sqlStatsSelector, sqlStatsState => { diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx index 236076971bc6..8fff93a08e5b 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPage.tsx @@ -122,6 +122,7 @@ export interface StatementsPageDispatchProps { export interface StatementsPageStateProps { statements: AggregateStatistics[]; + lastUpdated: moment.Moment | null; timeScale: TimeScale; statementsError: Error | null; apps: string[]; @@ -182,6 +183,8 @@ export class StatementsPage extends React.Component< StatementsPageState > { activateDiagnosticsRef: React.RefObject; + refreshDataTimeout: NodeJS.Timeout; + constructor(props: StatementsPageProps) { super(props); const defaultState = { @@ -281,10 +284,26 @@ export class StatementsPage extends React.Component< }); }; + clearRefreshDataTimeout() { + if (this.refreshDataTimeout) { + clearTimeout(this.refreshDataTimeout); + } + } + refreshStatements = (): void => { + this.clearRefreshDataTimeout(); + const req = statementsRequestFromProps(this.props); this.props.refreshStatements(req); + + if (this.props.timeScale.key !== "Custom") { + this.refreshDataTimeout = setTimeout( + this.refreshStatements, + 300000, // 5 minutes + ); + } }; + resetSQLStats = (): void => { const req = statementsRequestFromProps(this.props); this.props.resetSQLStats(req); @@ -297,7 +316,23 @@ export class StatementsPage extends React.Component< this.setState({ startRequest: new Date(), }); - this.refreshStatements(); + + // For the first data fetch for this page, we refresh if there are: + // - Last updated is null (no statements fetched previously) + // - The time interval is not custom, i.e. we have a moving window + // in which case we poll every 5 minutes. For the first fetch we will + // calculate the next time to refresh based on when the data was last + // udpated. + if (this.props.timeScale.key !== "Custom" || !this.props.lastUpdated) { + const now = moment(); + const nextRefresh = + this.props.lastUpdated?.clone().add(5, "minutes") || now; + setTimeout( + this.refreshStatements, + Math.max(0, nextRefresh.diff(now, "milliseconds")), + ); + } + this.props.refreshUserSQLRoles(); if (!this.props.isTenant && !this.props.hasViewActivityRedactedRole) { this.props.refreshStatementDiagnosticsRequests(); @@ -345,6 +380,7 @@ export class StatementsPage extends React.Component< componentWillUnmount(): void { this.props.dismissAlertMessage(); + this.clearRefreshDataTimeout(); } onChangePage = (current: number): void => { diff --git a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx index e5d9c211be32..926164bad035 100644 --- a/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/statementsPage/statementsPageConnected.tsx @@ -33,6 +33,7 @@ import { selectSortSetting, selectFilters, selectSearch, + selectStatementsLastUpdated, } from "./statementsPage.selectors"; import { selectIsTenant, @@ -99,6 +100,7 @@ export const ConnectedStatementsPage = withRouter( search: selectSearch(state), sortSetting: selectSortSetting(state), statements: selectStatements(state, props), + lastUpdated: selectStatementsLastUpdated(state), statementsError: selectStatementsLastError(state), totalFingerprints: selectTotalFingerprints(state), }, diff --git a/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.reducer.ts b/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.reducer.ts index 1a00165fdeb2..b5bf976f5a8a 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.reducer.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.reducer.ts @@ -13,6 +13,7 @@ import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; import { DOMAIN_NAME } from "../utils"; import { StatementsRequest } from "src/api/statementsApi"; import { TimeScale } from "../../timeScaleDropdown"; +import moment from "moment"; export type StatementsResponse = cockroach.server.serverpb.StatementsResponse; @@ -20,12 +21,15 @@ export type SQLStatsState = { data: StatementsResponse; lastError: Error; valid: boolean; + lastUpdated: moment.Moment | null; }; const initialState: SQLStatsState = { data: null, lastError: null, + // Data is valid if not in flight and no error was encountered. valid: true, + lastUpdated: null, }; export type UpdateTimeScalePayload = { @@ -40,10 +44,12 @@ const sqlStatsSlice = createSlice({ state.data = action.payload; state.valid = true; state.lastError = null; + state.lastUpdated = moment.utc(); }, failed: (state, action: PayloadAction) => { state.valid = false; state.lastError = action.payload; + state.lastUpdated = moment.utc(); }, invalidated: state => { state.valid = false; diff --git a/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.sagas.spec.ts b/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.sagas.spec.ts index a00434df68ee..e9cb9ace51cd 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.sagas.spec.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.sagas.spec.ts @@ -20,7 +20,6 @@ import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; import { getCombinedStatements } from "src/api/statementsApi"; import { resetSQLStats } from "src/api/sqlStatsApi"; import { - receivedSQLStatsSaga, refreshSQLStatsSaga, requestSQLStatsSaga, resetSQLStatsSaga, @@ -89,26 +88,6 @@ describe("SQLStats sagas", () => { }); }); - describe("receivedSQLStatsSaga", () => { - it("sets valid status to false after specified period of time", () => { - const timeout = 500; - return expectSaga(receivedSQLStatsSaga, timeout) - .delay(timeout) - .put(actions.invalidated()) - .withReducer(reducer, { - data: sqlStatsResponse, - lastError: null, - valid: true, - }) - .hasFinalState({ - data: sqlStatsResponse, - lastError: null, - valid: false, - }) - .run(1000); - }); - }); - describe("resetSQLStatsSaga", () => { const resetSQLStatsResponse = new cockroach.server.serverpb.ResetSQLStatsResponse(); @@ -116,14 +95,13 @@ describe("SQLStats sagas", () => { it("successfully resets SQL stats", () => { return expectSaga(resetSQLStatsSaga, payload) .provide([[matchers.call.fn(resetSQLStats), resetSQLStatsResponse]]) - .put(actions.invalidated()) .put(sqlDetailsStatsActions.invalidateAll()) .put(actions.refresh()) .withReducer(reducer) .hasFinalState({ data: null, lastError: null, - valid: false, + valid: true, }) .run(); }); diff --git a/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.sagas.ts b/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.sagas.ts index baa0be4f7a30..c46978abb5dc 100644 --- a/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.sagas.ts +++ b/pkg/ui/workspaces/cluster-ui/src/store/sqlStats/sqlStats.sagas.ts @@ -9,14 +9,7 @@ // licenses/APL.txt. import { PayloadAction } from "@reduxjs/toolkit"; -import { - all, - call, - put, - delay, - takeLatest, - takeEvery, -} from "redux-saga/effects"; +import { all, call, put, takeLatest, takeEvery } from "redux-saga/effects"; import Long from "long"; import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; import { @@ -30,8 +23,6 @@ import { UpdateTimeScalePayload, } from "./sqlStats.reducer"; import { actions as sqlDetailsStatsActions } from "../statementDetails/statementDetails.reducer"; -import { rootActions } from "../reducers"; -import { CACHE_INVALIDATION_PERIOD, throttleWithReset } from "src/store/utils"; import { toDateRange } from "../../timeScaleDropdown"; export function* refreshSQLStatsSaga(action: PayloadAction) { @@ -41,6 +32,7 @@ export function* refreshSQLStatsSaga(action: PayloadAction) { export function* requestSQLStatsSaga( action: PayloadAction, ): any { + yield put(sqlStatsActions.invalidated()); try { const result = yield call(getCombinedStatements, action.payload); yield put(sqlStatsActions.received(result)); @@ -49,11 +41,6 @@ export function* requestSQLStatsSaga( } } -export function* receivedSQLStatsSaga(delayMs: number) { - yield delay(delayMs); - yield put(sqlStatsActions.invalidated()); -} - export function* updateSQLStatsTimeScaleSaga( action: PayloadAction, ) { @@ -64,7 +51,6 @@ export function* updateSQLStatsTimeScaleSaga( value: ts, }), ); - yield put(sqlStatsActions.invalidated()); const [start, end] = toDateRange(ts); const req = new cockroach.server.serverpb.StatementsRequest({ combined: true, @@ -77,7 +63,6 @@ export function* updateSQLStatsTimeScaleSaga( export function* resetSQLStatsSaga(action: PayloadAction) { try { yield call(resetSQLStats); - yield put(sqlStatsActions.invalidated()); yield put(sqlDetailsStatsActions.invalidateAll()); yield put(sqlStatsActions.refresh(action.payload)); } catch (e) { @@ -85,22 +70,10 @@ export function* resetSQLStatsSaga(action: PayloadAction) { } } -export function* sqlStatsSaga( - cacheInvalidationPeriod: number = CACHE_INVALIDATION_PERIOD, -) { +export function* sqlStatsSaga() { yield all([ - throttleWithReset( - cacheInvalidationPeriod, - sqlStatsActions.refresh, - [sqlStatsActions.invalidated, rootActions.resetState], - refreshSQLStatsSaga, - ), + takeLatest(sqlStatsActions.refresh, refreshSQLStatsSaga), takeLatest(sqlStatsActions.request, requestSQLStatsSaga), - takeLatest( - sqlStatsActions.received, - receivedSQLStatsSaga, - cacheInvalidationPeriod, - ), takeLatest(sqlStatsActions.updateTimeScale, updateSQLStatsTimeScaleSaga), takeEvery(sqlStatsActions.reset, resetSQLStatsSaga), ]); diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx index 3f75a47bcedd..b48e2396151e 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPage.tsx @@ -85,6 +85,7 @@ interface TState { export interface TransactionsPageStateProps { columns: string[]; data: IStatementsResponse; + lastUpdated: moment.Moment | null; timeScale: TimeScale; error?: Error | null; filters: Filters; @@ -127,6 +128,8 @@ export class TransactionsPage extends React.Component< TransactionsPageProps, TState > { + refreshDataTimeout: NodeJS.Timeout; + constructor(props: TransactionsPageProps) { super(props); this.state = { @@ -185,17 +188,51 @@ export class TransactionsPage extends React.Component< }; }; + clearRefreshDataTimeout() { + if (this.refreshDataTimeout) { + clearTimeout(this.refreshDataTimeout); + } + } + refreshData = (): void => { + this.clearRefreshDataTimeout(); + const req = statementsRequestFromProps(this.props); this.props.refreshData(req); + + if (this.props.timeScale.key !== "Custom") { + this.refreshDataTimeout = setTimeout( + this.refreshData, + 300000, // 5 minutes + ); + } }; + resetSQLStats = (): void => { const req = statementsRequestFromProps(this.props); this.props.resetSQLStats(req); }; componentDidMount(): void { - this.refreshData(); + // For the first data fetch for this page, we refresh if there are: + // - Last updated is null (no statements fetched previously) + // - The time interval is not custom, i.e. we have a moving window + // in which case we poll every 5 minutes. For the first fetch we will + // calculate the next time to refresh based on when the data was last + // udpated. + if (this.props.timeScale.key !== "Custom" || !this.props.lastUpdated) { + const now = moment(); + const nextRefresh = + this.props.lastUpdated?.clone().add(5, "minutes") || now; + setTimeout( + this.refreshData, + Math.max(0, nextRefresh.diff(now, "milliseconds")), + ); + } + } + + componentWillUnmount(): void { + this.clearRefreshDataTimeout(); } updateQueryParams(): void { diff --git a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPageConnected.tsx b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPageConnected.tsx index eba0bd180659..ac8555ab7a94 100644 --- a/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPageConnected.tsx +++ b/pkg/ui/workspaces/cluster-ui/src/transactionsPage/transactionsPageConnected.tsx @@ -28,7 +28,10 @@ import { } from "./transactionsPage.selectors"; import { selectIsTenant } from "../store/uiConfig"; import { nodeRegionsByIDSelector } from "../store/nodes"; -import { selectTimeScale } from "src/statementsPage/statementsPage.selectors"; +import { + selectTimeScale, + selectStatementsLastUpdated, +} from "src/statementsPage/statementsPage.selectors"; import { StatementsRequest } from "src/api/statementsApi"; import { actions as localStorageActions } from "../store/localStorage"; import { Filters } from "../queryFilter"; @@ -69,6 +72,7 @@ export const TransactionsPageConnected = withRouter( ...props, columns: selectTxnColumns(state), data: selectTransactionsData(state), + lastUpdated: selectStatementsLastUpdated(state), timeScale: selectTimeScale(state), error: selectTransactionsLastError(state), filters: selectFilters(state), diff --git a/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts b/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts index 4a2951780c72..1d0e82b3c93e 100644 --- a/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts +++ b/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts @@ -315,7 +315,7 @@ export const refreshStores = storesReducerObj.refresh; const queriesReducerObj = new CachedDataReducer( api.getCombinedStatements, "statements", - moment.duration(5, "m"), + null, moment.duration(30, "m"), ); export const invalidateStatements = queriesReducerObj.invalidateData; diff --git a/pkg/ui/workspaces/db-console/src/selectors/executionFingerprintsSelectors.tsx b/pkg/ui/workspaces/db-console/src/selectors/executionFingerprintsSelectors.tsx new file mode 100644 index 000000000000..4f5a4ec45642 --- /dev/null +++ b/pkg/ui/workspaces/db-console/src/selectors/executionFingerprintsSelectors.tsx @@ -0,0 +1,14 @@ +// Copyright 2022 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +import { AdminUIState } from "../redux/state"; + +export const selectStatementsLastUpdated = (state: AdminUIState) => + state.cachedData.statements?.setAt?.utc(); diff --git a/pkg/ui/workspaces/db-console/src/views/statements/statementsPage.tsx b/pkg/ui/workspaces/db-console/src/views/statements/statementsPage.tsx index 459905c7662b..a4d9119e1347 100644 --- a/pkg/ui/workspaces/db-console/src/views/statements/statementsPage.tsx +++ b/pkg/ui/workspaces/db-console/src/views/statements/statementsPage.tsx @@ -62,6 +62,7 @@ import { mapStateToActiveStatementViewProps, } from "./activeStatementsSelectors"; import { selectTimeScale } from "src/redux/timeScale"; +import { selectStatementsLastUpdated } from "src/selectors/executionFingerprintsSelectors"; type ICollectedStatementStatistics = protos.cockroach.server.serverpb.StatementsResponse.ICollectedStatementStatistics; @@ -360,6 +361,7 @@ export default withRouter( search: searchLocalSetting.selector(state), sortSetting: sortSettingLocalSetting.selector(state), statements: selectStatements(state, props), + lastUpdated: selectStatementsLastUpdated(state), statementsError: state.cachedData.statements.lastError, totalFingerprints: selectTotalFingerprints(state), hasViewActivityRedactedRole: selectHasViewActivityRedactedRole(state), diff --git a/pkg/ui/workspaces/db-console/src/views/transactions/transactionsPage.tsx b/pkg/ui/workspaces/db-console/src/views/transactions/transactionsPage.tsx index 235a744fab65..70b7b3289e4e 100644 --- a/pkg/ui/workspaces/db-console/src/views/transactions/transactionsPage.tsx +++ b/pkg/ui/workspaces/db-console/src/views/transactions/transactionsPage.tsx @@ -39,6 +39,7 @@ import { mapStateToActiveTransactionsPageProps, } from "./activeTransactionsSelectors"; import { selectTimeScale } from "src/redux/timeScale"; +import { selectStatementsLastUpdated } from "src/selectors/executionFingerprintsSelectors"; // selectStatements returns the array of AggregateStatistics to show on the // TransactionsPage, based on if the appAttr route parameter is set. @@ -139,6 +140,7 @@ const TransactionsPageConnected = withRouter( ...props, columns: transactionColumnsLocalSetting.selectorToArray(state), data: selectData(state), + lastUpdated: selectStatementsLastUpdated(state), timeScale: selectTimeScale(state), error: selectLastError(state), filters: filtersLocalSetting.selector(state),