Refs #39253 - perf 2/4 - Replace interval polling with setTimeout chains - #1048
Refs #39253 - perf 2/4 - Replace interval polling with setTimeout chains #1048adamruzicka wants to merge 4 commits into
Conversation
4cf6700 to
afc6f1c
Compare
41b9287 to
2c65b51
Compare
Replace withInterval/setInterval polling with recursive setTimeout chains for job invocation and template invocation detail pages. This prevents request pileup by waiting for the previous response before scheduling the next poll. - Remove updateJob action (stored under unused UPDATE_JOB key) - Remove getTask action, expose cancellable via task node in RABL - Add extractErrorMessage helper consolidating duplicated error handling - Fix sprintf injection in error toast messages - Stop template invocation polling on finished/error states Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adopt the cancel-first pattern from JobInvocationHostTable to prevent overlapping polls when getJobInvocation is called multiple times. This ensures that any pending timeout is cancelled before starting a new polling cycle, preventing race conditions and duplicate API calls.
|
@coderabbitai fullreview |
|
✅ Action performedFull review finished. |
WalkthroughJob invocation and template invocation polling now use recursive five-second timeouts with cleanup on terminal responses, errors, unmounts, and expansion changes. Task cancellability is returned by the API and consumed by selectors. Obsolete task-fetching actions were removed. Recurring actions no longer receive a job ID. Tests cover polling, cleanup, expansion, cancellation, and recurring-action updates. Estimated code review effort: 4 (Complex) | ~45 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
webpack/JobInvocationDetail/JobInvocationActions.js (1)
14-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExport
isJobFinishedto avoid duplicating the terminal-status list.
index.jsrecomputes the sameSTATUS.FAILED/STATUS.SUCCEEDED/STATUS.CANCELLEDcheck inline for itsfinishedvariable. If the set of terminal statuses ever changes here, the UI's "finished" flag can silently drift out of sync with when polling actually stops.♻️ Proposed fix
-const isJobFinished = statusLabel => +export const isJobFinished = statusLabel => statusLabel === STATUS.FAILED || statusLabel === STATUS.SUCCEEDED || statusLabel === STATUS.CANCELLED;Then in
index.js:- const finished = - statusLabel === STATUS.FAILED || - statusLabel === STATUS.SUCCEEDED || - statusLabel === STATUS.CANCELLED; + const finished = isJobFinished(statusLabel);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@webpack/JobInvocationDetail/JobInvocationActions.js` around lines 14 - 17, Export the isJobFinished helper from JobInvocationActions.js and update index.js to use it for the finished value instead of duplicating the STATUS.FAILED, STATUS.SUCCEEDED, and STATUS.CANCELLED checks. Keep the terminal-status logic centralized in isJobFinished.webpack/JobInvocationDetail/TemplateInvocation.js (1)
15-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winImport the shared refresh interval instead of redefining it.
AUTO_REFRESH_INTERVAL_MSis already exported fromwebpack/JobInvocationDetail/JobInvocationConstants.jswith the same value (5000). Redefining it locally here creates two independent sources of truth for the same timing value. If the interval changes in one place, job and template polling will drift out of sync silently.♻️ Proposed fix
-const AUTO_REFRESH_INTERVAL_MS = 5000; +import { AUTO_REFRESH_INTERVAL_MS } from './JobInvocationConstants';🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@webpack/JobInvocationDetail/TemplateInvocation.js` around lines 15 - 16, Remove the local AUTO_REFRESH_INTERVAL_MS definition in TemplateInvocation.js and import the shared AUTO_REFRESH_INTERVAL_MS export from JobInvocationConstants.js, ensuring the existing refresh logic uses that shared constant.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@webpack/JobInvocationDetail/JobInvocationActions.js`:
- Around line 19-23: Update the fallback returned by extractErrorMessage to pass
'Unknown error.' through the existing __() translation helper, while preserving
the current response error-message precedence.
- Around line 25-55: Update getJobInvocation and stopJobInvocationPolling to
invalidate prior poll cycles, not just clear their timers. Create a per-cycle
request/token guard tied to pollTimeoutRef so handleSuccess and handleError from
an older URL or cleanup cannot reschedule polling or mutate shared state; ensure
the active cycle still polls and clears normally. Add coverage for resolving an
earlier request after a new getJobInvocation call starts.
In `@webpack/JobInvocationDetail/TemplateInvocation.js`:
- Around line 88-125: Guard schedulePoll against cancelled effect runs before
dispatching, and make handleError ignore responses from cancelled runs so they
cannot clear a newer poll’s timeout. Update the schedulePoll and handleError
callbacks while preserving the existing cancellation cleanup and active-poll
behavior.
---
Nitpick comments:
In `@webpack/JobInvocationDetail/JobInvocationActions.js`:
- Around line 14-17: Export the isJobFinished helper from
JobInvocationActions.js and update index.js to use it for the finished value
instead of duplicating the STATUS.FAILED, STATUS.SUCCEEDED, and STATUS.CANCELLED
checks. Keep the terminal-status logic centralized in isJobFinished.
In `@webpack/JobInvocationDetail/TemplateInvocation.js`:
- Around line 15-16: Remove the local AUTO_REFRESH_INTERVAL_MS definition in
TemplateInvocation.js and import the shared AUTO_REFRESH_INTERVAL_MS export from
JobInvocationConstants.js, ensuring the existing refresh logic uses that shared
constant.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 79743ae0-a143-4270-a79c-483bd376e92a
📒 Files selected for processing (11)
app/views/api/v2/job_invocations/main.json.rablwebpack/JobInvocationDetail/JobInvocationActions.jswebpack/JobInvocationDetail/JobInvocationConstants.jswebpack/JobInvocationDetail/JobInvocationSelectors.jswebpack/JobInvocationDetail/JobInvocationToolbarButtons.jswebpack/JobInvocationDetail/TemplateInvocation.jswebpack/JobInvocationDetail/__tests__/JobInvocationPolling.test.jswebpack/JobInvocationDetail/__tests__/MainInformation.test.jswebpack/JobInvocationDetail/__tests__/TemplateInvocationPolling.test.jswebpack/JobInvocationDetail/__tests__/fixtures.jswebpack/JobInvocationDetail/index.js
💤 Files with no reviewable changes (1)
- webpack/JobInvocationDetail/JobInvocationConstants.js
There was a problem hiding this comment.
Don't know what's up with this preview. Anyway, STATUS should be removed.
- Cancel pending timeout before starting new poll to prevent overlaps - Import AUTO_REFRESH_INTERVAL_MS from constants instead of redefining - Guard schedulePoll and handleError against cancelled effect runs - Wrap 'Unknown error.' fallback in __() for translation extraction - Export and reuse isJobFinished helper to centralize terminal-status logic - Add cancellation token to invalidate in-flight callbacks from previous poll cycles Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
|
Claude review, seems like a valid suggestion to me but let me know your thoughts: The cancelPreviousCycle() canceller was only invoked when a new getJobInvocation call started, so a request still in flight when the component unmounted (no new call ever follows) could resolve afterward and revive a zombie polling loop via setTimeout. The fix moves that cancellation into stopJobInvocationPolling itself, since both retirement paths (new cycle and unmount) already call it, so the active cycle is always invalidated regardless of which path retires it. diff --git a/webpack/JobInvocationDetail/JobInvocationActions.js b/webpack/JobInvocationDetail/JobInvocationActions.js
index 978a6acc..dcedea06 100644
--- a/webpack/JobInvocationDetail/JobInvocationActions.js
+++ b/webpack/JobInvocationDetail/JobInvocationActions.js
@@ -52,10 +52,6 @@ export const getJobInvocation = (url, pollTimeoutRef) => dispatch => {
stopJobInvocationPolling(pollTimeoutRef);
- // Cancel previous poll cycle's in-flight requests
- if (pollTimeoutRef.cancelPreviousCycle) {
- pollTimeoutRef.cancelPreviousCycle();
- }
pollTimeoutRef.cancelPreviousCycle = () => {
cancelled = true;
};
@@ -66,6 +62,13 @@ export const getJobInvocation = (url, pollTimeoutRef) => dispatch => {
export const stopJobInvocationPolling = pollTimeoutRef => {
clearTimeout(pollTimeoutRef.current);
pollTimeoutRef.current = null;
+ // Invalidate any request still in flight from a previous poll cycle
+ // (e.g. the job id changed, or the component unmounted) so its
+ // response can no longer reschedule polling or mutate shared state.
+ if (pollTimeoutRef.cancelPreviousCycle) {
+ pollTimeoutRef.cancelPreviousCycle();
+ pollTimeoutRef.cancelPreviousCycle = null;
+ }
};
export const cancelJob = (jobId, force) => dispatch => {
diff --git a/webpack/JobInvocationDetail/__tests__/JobInvocationPolling.test.js b/webpack/JobInvocationDetail/__tests__/JobInvocationPolling.test.js
index e00520fe..b423de03 100644
--- a/webpack/JobInvocationDetail/__tests__/JobInvocationPolling.test.js
+++ b/webpack/JobInvocationDetail/__tests__/JobInvocationPolling.test.js
@@ -197,4 +197,27 @@ describe('job invocation polling', () => {
// Timeout should not be set by the stale callback
expect(ref.current).toBeNull();
});
+
+ it('ignores a stale response resolving after stopJobInvocationPolling (e.g. unmount)', () => {
+ let capturedHandleSuccess;
+ apiGetSpy = jest
+ .spyOn(APIActions, 'get')
+ .mockImplementation(({ handleSuccess }) => dispatch => {
+ capturedHandleSuccess = handleSuccess;
+ return dispatch({ type: 'MOCK_GET' });
+ });
+
+ const store = makeStore();
+ store.dispatch(getJobInvocation(url, ref));
+ expect(apiGetSpy).toHaveBeenCalledTimes(1);
+
+ // Simulate unmount: stop polling while the request is still in flight.
+ stopJobInvocationPolling(ref);
+
+ // The in-flight request resolves after the "unmount".
+ capturedHandleSuccess({ data: runningData });
+
+ // The stale response must not revive polling.
+ expect(ref.current).toBeNull();
+ });
}); |
|
The comments can be plucked out ^ |
Seems valid, added. |
|
Actually, sorry to bother but let's drop it. It breaks the UI and fixing it correctly seems non-trivial. I'd rather not inflate this PR more than it is, we can mark it down for future. |
Follows #1047
TODOs: