Skip to content

Commit

Permalink
[dagit] More cleanup of query callsites performing background polling (
Browse files Browse the repository at this point in the history
  • Loading branch information
bengotow committed Apr 21, 2022
1 parent a4618b4 commit d7ff7bc
Show file tree
Hide file tree
Showing 16 changed files with 130 additions and 112 deletions.
35 changes: 34 additions & 1 deletion js_modules/dagit/packages/core/src/app/QueryRefresh.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ export interface QueryRefreshState {
* You can choose to use this hook alone (no UI) or pass the returned refreshState object to
* <QueryRefreshCountdown /> to display the refresh status.
*
* Important: Required useQuery Options:
*
* - When using this hook, pass useQuery the `notifyOnNetworkStatusChange: true` option.
* This allows the hook to observe how long the request spends in-flight. This option
* is NOT necessary if you pass cache-and-network, but IS necessary if you use network-only
* or the default cache-first fetchPolicy.
*
*/
export function useQueryRefreshAtInterval(queryResult: QueryResult<any, any>, intervalMs: number) {
const timer = React.useRef<number>();
Expand Down Expand Up @@ -78,7 +85,7 @@ export function useQueryRefreshAtInterval(queryResult: QueryResult<any, any>, in

// Schedule the next refretch
timer.current = window.setTimeout(() => {
if (document.hidden) {
if (document.visibilityState === 'hidden') {
// If the document is no longer visible, mark that we have skipped an update rather
// then updating in the background. We'll refresh when we return to the foreground.
documentVisiblityDidInterrupt.current = true;
Expand Down Expand Up @@ -111,6 +118,32 @@ export function useQueryRefreshAtInterval(queryResult: QueryResult<any, any>, in
);
}

/**
* This hook allows you to hook a single QueryRefreshCountdown component to more than
* one useQueryRefreshAtInterval. The QueryRefreshCountdown will reflect the countdown
* state of the FIRST query, but clicking the refresh button will trigger them all.
*
* Note: If you use this hook, you should pass the same interval to each
* useQueryRefreshAtInterval.
*/
export function useMergedRefresh(
...args: [QueryRefreshState, ...QueryRefreshState[]]
): QueryRefreshState {
return React.useMemo(() => {
const refetch: ObservableQuery['refetch'] = async () => {
const [ar] = await Promise.all(args.map((s) => s?.refetch()));
return ar!;
};
return {
nextFireMs: args[0].nextFireMs,
nextFireDelay: args[0].nextFireDelay,
networkStatus: args[0].networkStatus,
refetch,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, args);
}

export const QueryRefreshCountdown = ({refreshState}: {refreshState: QueryRefreshState}) => {
const status = refreshState.networkStatus === NetworkStatus.ready ? 'counting' : 'idle';
const timeRemaining = useCountdown({duration: refreshState.nextFireDelay, status});
Expand Down
7 changes: 5 additions & 2 deletions js_modules/dagit/packages/core/src/assets/AssetView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import * as React from 'react';
import {
FIFTEEN_SECONDS,
QueryRefreshCountdown,
useMergedRefresh,
useQueryRefreshAtInterval,
} from '../app/QueryRefresh';
import {Timestamp} from '../app/time/Timestamp';
Expand Down Expand Up @@ -88,8 +89,10 @@ export const AssetView: React.FC<Props> = ({assetKey}) => {
notifyOnNetworkStatusChange: true,
});

const refreshState = useQueryRefreshAtInterval(queryResult, FIFTEEN_SECONDS);
useQueryRefreshAtInterval(liveQueryResult, FIFTEEN_SECONDS);
const refreshState = useMergedRefresh(
useQueryRefreshAtInterval(queryResult, FIFTEEN_SECONDS),
useQueryRefreshAtInterval(liveQueryResult, FIFTEEN_SECONDS),
);

// Refresh immediately when a run is launched from this page
useDidLaunchEvent(queryResult.refetch);
Expand Down
8 changes: 6 additions & 2 deletions js_modules/dagit/packages/core/src/gantt/RunGroupPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import styled from 'styled-components/macro';

import {showCustomAlert} from '../app/CustomAlertProvider';
import {PYTHON_ERROR_FRAGMENT} from '../app/PythonErrorInfo';
import {FIFTEEN_SECONDS, useQueryRefreshAtInterval} from '../app/QueryRefresh';
import {SidebarSection} from '../pipelines/SidebarComponents';
import {RunStatusIndicator} from '../runs/RunStatusDots';
import {DagsterTag} from '../runs/RunTag';
Expand All @@ -28,15 +29,18 @@ export const RunGroupPanel: React.FC<{runId: string; runStatusLastChangedAt: num
runId,
runStatusLastChangedAt,
}) => {
const {data, refetch} = useQuery<RunGroupPanelQuery, RunGroupPanelQueryVariables>(
const queryResult = useQuery<RunGroupPanelQuery, RunGroupPanelQueryVariables>(
RUN_GROUP_PANEL_QUERY,
{
variables: {runId},
fetchPolicy: 'cache-and-network',
pollInterval: 15000, // 15s
notifyOnNetworkStatusChange: true,
},
);

const {data, refetch} = queryResult;
useQueryRefreshAtInterval(queryResult, FIFTEEN_SECONDS);

// Because the RunGroupPanel makes it's own query for the runs and their statuses,
// the log + gantt chart UI can show that the run is "completed" for up to 15s before
// it's reflected in the sidebar. Observing this single timestamp from our parent
Expand Down
20 changes: 16 additions & 4 deletions js_modules/dagit/packages/core/src/hooks/useDocumentVisibility.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import React from 'react';

// Note: This is a workaround for a problem observed in Firefox - registering
// two visibilitychange event listeners is fine, but if you add a third one
// it is not called reliably (maybe there's an execution time limit before
// the page's JS is paused?)
//
let callbacks: (() => void)[] = [];
document.addEventListener('visibilitychange', () => {
callbacks.forEach((c) => c());
});

export function useDocumentVisibility() {
const [documentVisible, setDocumentVisible] = React.useState(true);
const [documentVisible, setDocumentVisible] = React.useState(
document.visibilityState !== 'hidden',
);
React.useEffect(() => {
const handler = () => {
setDocumentVisible(!document.hidden);
setDocumentVisible(document.visibilityState !== 'hidden');
};
document.addEventListener('visibilitychange', handler);
callbacks.push(handler);
return () => {
document.removeEventListener('visibilitychange', handler);
callbacks = callbacks.filter((c) => c !== handler);
};
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const InstanceConfigStyle = createGlobalStyle`
export const InstanceConfig = React.memo(() => {
const queryResult = useQuery<InstanceConfigQuery>(INSTANCE_CONFIG_QUERY, {
fetchPolicy: 'cache-and-network',
notifyOnNetworkStatusChange: true,
});

const refreshState = useQueryRefreshAtInterval(queryResult, FIFTEEN_SECONDS);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {gql, useLazyQuery, useQuery} from '@apollo/client';
import {gql, useQuery} from '@apollo/client';
import {
Box,
Button,
Expand All @@ -17,7 +17,7 @@ import {
import * as React from 'react';

import {PYTHON_ERROR_FRAGMENT} from '../app/PythonErrorInfo';
import {FIFTEEN_SECONDS, useQueryRefreshAtInterval} from '../app/QueryRefresh';
import {FIFTEEN_SECONDS, useMergedRefresh, useQueryRefreshAtInterval} from '../app/QueryRefresh';
import {isAssetGroup} from '../asset-graph/Utils';
import {ScheduleOrSensorTag} from '../nav/ScheduleOrSensorTag';
import {LegacyPipelineTag} from '../pipelines/LegacyPipelineTag';
Expand Down Expand Up @@ -104,25 +104,28 @@ const initialState: State = {
export const InstanceOverviewPage = () => {
const [state, dispatch] = React.useReducer(reducer, initialState);
const {allRepos, visibleRepos} = React.useContext(WorkspaceContext);
const {searchValue} = state;

const queryResultOverview = useQuery<InstanceOverviewInitialQuery>(
INSTANCE_OVERVIEW_INITIAL_QUERY,
{
fetchPolicy: 'network-only',
notifyOnNetworkStatusChange: true,
},
);
const {data, loading} = queryResultOverview;

const queryResult = useQuery<InstanceOverviewInitialQuery>(INSTANCE_OVERVIEW_INITIAL_QUERY, {
const queryResultLastRuns = useQuery<LastTenRunsPerJobQuery>(LAST_TEN_RUNS_PER_JOB_QUERY, {
fetchPolicy: 'network-only',
notifyOnNetworkStatusChange: true,
});
const refreshState = useQueryRefreshAtInterval(queryResult, FIFTEEN_SECONDS);
const {data, loading} = queryResult;
const {data: lastTenRunsData} = queryResultLastRuns;

const [retrieveLastTenRuns, {data: lastTenRunsData}] = useLazyQuery<LastTenRunsPerJobQuery>(
LAST_TEN_RUNS_PER_JOB_QUERY,
{fetchPolicy: 'network-only', pollInterval: FIFTEEN_SECONDS},
const refreshState = useMergedRefresh(
useQueryRefreshAtInterval(queryResultLastRuns, FIFTEEN_SECONDS),
useQueryRefreshAtInterval(queryResultOverview, FIFTEEN_SECONDS),
);

const {searchValue} = state;

React.useEffect(() => {
retrieveLastTenRuns();
}, [retrieveLastTenRuns]);

const bucketed = React.useMemo(() => {
const failed = [];
const inProgress = [];
Expand Down
20 changes: 12 additions & 8 deletions js_modules/dagit/packages/core/src/instigation/TickHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import styled from 'styled-components/macro';

import {SharedToaster} from '../app/DomUtils';
import {PythonErrorInfo, PYTHON_ERROR_FRAGMENT} from '../app/PythonErrorInfo';
import {ONE_MONTH, useQueryRefreshAtInterval} from '../app/QueryRefresh';
import {useCopyToClipboard} from '../app/browser';
import {useQueryPersistedState} from '../hooks/useQueryPersistedState';
import {useCursorPaginatedQuery} from '../runs/useCursorPaginatedQuery';
Expand Down Expand Up @@ -258,15 +259,18 @@ export const TickHistoryTimeline = ({
const [pollingPaused, pausePolling] = React.useState<boolean>(false);

const instigationSelector = {...repoAddressToSelector(repoAddress), name};
const {data} = useQuery<TickHistoryQuery, TickHistoryQueryVariables>(JOB_TICK_HISTORY_QUERY, {
variables: {
instigationSelector,
limit: 15,
const queryResult = useQuery<TickHistoryQuery, TickHistoryQueryVariables>(
JOB_TICK_HISTORY_QUERY,
{
variables: {instigationSelector, limit: 15},
fetchPolicy: 'cache-and-network',
partialRefetch: true,
notifyOnNetworkStatusChange: true,
},
fetchPolicy: 'cache-and-network',
partialRefetch: true,
pollInterval: !pollingPaused ? 1000 : 0,
});
);

useQueryRefreshAtInterval(queryResult, pollingPaused ? ONE_MONTH : 1000);
const {data} = queryResult;

if (!data) {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {gql, useQuery} from '@apollo/client';
import {Colors, Icon} from '@dagster-io/ui';
import * as React from 'react';

import {FIFTEEN_SECONDS, useQueryRefreshAtInterval} from '../app/QueryRefresh';
import {INSTANCE_HEALTH_FRAGMENT} from '../instance/InstanceHealthFragment';
import {useRepositoryOptions} from '../workspace/WorkspaceContext';

Expand All @@ -10,11 +11,15 @@ import {InstanceWarningQuery} from './types/InstanceWarningQuery';

export const InstanceWarningIcon = React.memo(() => {
const {options} = useRepositoryOptions();
const {data: healthData} = useQuery<InstanceWarningQuery>(INSTANCE_WARNING_QUERY, {
const queryResult = useQuery<InstanceWarningQuery>(INSTANCE_WARNING_QUERY, {
fetchPolicy: 'cache-and-network',
pollInterval: 15 * 1000,
notifyOnNetworkStatusChange: true,
});

useQueryRefreshAtInterval(queryResult, FIFTEEN_SECONDS);

const {data: healthData} = queryResult;

const {anySchedules, anySensors} = React.useMemo(() => {
let anySchedules = false;
let anySensors = false;
Expand Down
2 changes: 1 addition & 1 deletion js_modules/dagit/packages/core/src/nav/LatestRunTag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const TIME_FORMAT = {showSeconds: true, showTimezone: false};
export const LatestRunTag: React.FC<{pipelineName: string}> = ({pipelineName}) => {
const lastRunQuery = useQuery<LatestRunTagQuery, LatestRunTagQueryVariables>(
LATEST_RUN_TAG_QUERY,
{variables: {runsFilter: {pipelineName}}},
{variables: {runsFilter: {pipelineName}}, notifyOnNetworkStatusChange: true},
);

useQueryRefreshAtInterval(lastRunQuery, FIFTEEN_SECONDS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const AllScheduledTicks = () => {
const queryResult = useQuery<SchedulerInfoQuery>(SCHEDULER_INFO_QUERY, {
fetchPolicy: 'cache-and-network',
partialRefetch: true,
notifyOnNetworkStatusChange: true,
});

useQueryRefreshAtInterval(queryResult, FIFTEEN_SECONDS);
Expand Down
25 changes: 4 additions & 21 deletions js_modules/dagit/packages/core/src/schedules/ScheduleDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,9 @@ import {
Box,
ButtonLink,
Colors,
CountdownStatus,
useCountdown,
Group,
MetadataTableWIP,
PageHeader,
RefreshableCountdown,
Tag,
Code,
Heading,
Expand All @@ -16,6 +13,7 @@ import {
} from '@dagster-io/ui';
import * as React from 'react';

import {QueryRefreshCountdown, QueryRefreshState} from '../app/QueryRefresh';
import {useCopyToClipboard} from '../app/browser';
import {TickTag} from '../instigation/InstigationTick';
import {RepositoryLink} from '../nav/RepositoryLink';
Expand All @@ -35,11 +33,9 @@ const TIME_FORMAT = {showSeconds: false, showTimezone: true};
export const ScheduleDetails: React.FC<{
schedule: ScheduleFragment;
repoAddress: RepoAddress;
countdownDuration: number;
countdownStatus: CountdownStatus;
onRefresh: () => void;
refreshState: QueryRefreshState;
}> = (props) => {
const {repoAddress, schedule, countdownDuration, countdownStatus, onRefresh} = props;
const {repoAddress, schedule, refreshState} = props;
const {cronSchedule, executionTimezone, futureTicks, name, partitionSet, pipelineName} = schedule;
const copyToClipboard = useCopyToClipboard();

Expand All @@ -48,11 +44,6 @@ export const ScheduleDetails: React.FC<{

const [copyText, setCopyText] = React.useState('Click to copy');

const timeRemaining = useCountdown({
duration: countdownDuration,
status: countdownStatus,
});

// Restore the tooltip text after a delay.
React.useEffect(() => {
let token: any;
Expand All @@ -76,8 +67,6 @@ export const ScheduleDetails: React.FC<{
};

const running = status === InstigationStatus.RUNNING;
const countdownRefreshing = countdownStatus === 'idle' || timeRemaining === 0;
const seconds = Math.floor(timeRemaining / 1000);

return (
<>
Expand Down Expand Up @@ -112,13 +101,7 @@ export const ScheduleDetails: React.FC<{
</Box>
</>
}
right={
<RefreshableCountdown
refreshing={countdownRefreshing}
seconds={seconds}
onRefresh={onRefresh}
/>
}
right={<QueryRefreshCountdown refreshState={refreshState} />}
/>
<MetadataTableWIP>
<tbody>
Expand Down

0 comments on commit d7ff7bc

Please sign in to comment.