Skip to content

Commit 20915fa

Browse files
authored
Fix polling in case of error response; continue status polling on startup (#1160)
1 parent 70d0287 commit 20915fa

File tree

4 files changed

+129
-5
lines changed

4 files changed

+129
-5
lines changed

src/content/app/tools/vep/state/vep-action-listeners/vepActionListeners.ts

+47-2
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@ import { vepFormSubmit } from 'src/content/app/tools/vep/state/vep-api/vepApiSli
2020
import {
2121
updateSubmission,
2222
changeSubmissionId,
23-
deleteSubmission
23+
deleteSubmission,
24+
restoreVepSubmissions,
25+
type VepSubmissionsState
2426
} from 'src/content/app/tools/vep/state/vep-submissions/vepSubmissionsSlice';
2527

2628
import VepSubmissionStatusPolling from 'src/content/app/tools/vep/state/vep-action-listeners/vepSubmissionStatusPolling';
2729

30+
import type { VepSubmissionWithoutInputFile } from 'src/content/app/tools/vep/types/vepSubmission';
2831
import type {
2932
AppStartListening,
3033
AppListenerEffectAPI
@@ -98,6 +101,48 @@ const vepFormUnsuccessfulSubmissionListener = {
98101
}
99102
};
100103

104+
const vepSubmissionsRestoreListener = {
105+
actionCreator: restoreVepSubmissions.fulfilled,
106+
effect: async (
107+
action: PayloadAction<VepSubmissionsState>,
108+
listenerApi: AppListenerEffectAPI
109+
) => {
110+
// only respond to the first action
111+
// (this action should happen only once over the lifetime of the page; but due to double execution useEffect in React StrictMode, it will be called twice in dev)
112+
listenerApi.unsubscribe();
113+
const { dispatch } = listenerApi;
114+
115+
const unresolvedSubmissions: VepSubmissionWithoutInputFile[] = [];
116+
const interruptedSubmissions: VepSubmissionWithoutInputFile[] = [];
117+
118+
for (const submission of Object.values(action.payload)) {
119+
if (submission.status === 'SUBMITTING') {
120+
interruptedSubmissions.push(submission);
121+
} else if (['SUBMITTED', 'RUNNING'].includes(submission.status)) {
122+
unresolvedSubmissions.push(submission);
123+
}
124+
}
125+
126+
vepSubmissionStatusPolling.processSubmissionsOnStartup({
127+
submissions: unresolvedSubmissions,
128+
dispatch
129+
});
130+
131+
// If at startup there are submissions with a "SUBMITTING" status,
132+
// it means that the browser was refreshed/closed
133+
// before submission data has been successfully transmitted to the server.
134+
// Mark these submissions as failed.
135+
for (const submission of interruptedSubmissions) {
136+
await dispatch(
137+
updateSubmission({
138+
submissionId: submission.id,
139+
fragment: { status: 'UNSUCCESSFUL_SUBMISSION' }
140+
})
141+
);
142+
}
143+
}
144+
};
145+
101146
const vepSubmissionDeleteListener = {
102147
actionCreator: deleteSubmission.fulfilled,
103148
effect: async (
@@ -111,8 +156,8 @@ const vepSubmissionDeleteListener = {
111156
};
112157

113158
export const startVepListeners = (startListening: AppStartListening) => {
114-
// startListening(vepFormConfigQueryListener);
115159
startListening(vepFormSuccessfulSubmissionListener);
116160
startListening(vepFormUnsuccessfulSubmissionListener as any);
117161
startListening(vepSubmissionDeleteListener);
162+
startListening(vepSubmissionsRestoreListener);
118163
};

src/content/app/tools/vep/state/vep-action-listeners/vepSubmissionStatusPolling.test.ts

+77
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,83 @@ describe('getSubmissionStatusFetcher', () => {
174174
});
175175
});
176176

177+
it('continues polling if server responds with a 500 error', async () => {
178+
// Arrange
179+
const submissionId = 'my-submission';
180+
let actualStatusPollCount = 0;
181+
182+
server.use(
183+
http.get(
184+
'http://tools-api-url/vep/submissions/:submissionId/status',
185+
() => {
186+
actualStatusPollCount++;
187+
188+
if (actualStatusPollCount === 1) {
189+
// return a 500 error
190+
return new HttpResponse(null, { status: 500 });
191+
} else {
192+
return HttpResponse.json({ status: 'SUCCEEDED' });
193+
}
194+
}
195+
)
196+
);
197+
198+
// Act
199+
const vepStatusPolling = new VepSubmissionStatusPolling();
200+
vepStatusPolling.enqueueSubmission({
201+
submission: { id: submissionId, status: 'SUBMITTED' },
202+
dispatch: jest.fn()
203+
});
204+
205+
// Assert
206+
await jest.runAllTimersAsync();
207+
208+
expect(actualStatusPollCount).toBe(2);
209+
expect(updateSubmission as any).toHaveBeenCalledWith({
210+
submissionId,
211+
fragment: { status: 'SUCCEEDED' }
212+
});
213+
});
214+
215+
it('fails submission if server responds with a 404 error', async () => {
216+
// Arrange
217+
const submissionId = 'my-submission';
218+
let actualStatusPollCount = 0;
219+
220+
server.use(
221+
http.get(
222+
'http://tools-api-url/vep/submissions/:submissionId/status',
223+
() => {
224+
actualStatusPollCount++;
225+
226+
if (actualStatusPollCount === 1) {
227+
// return a 404 error
228+
return new HttpResponse(null, { status: 404 });
229+
} else {
230+
// this should not be reached
231+
return HttpResponse.json({ status: 'SUCCEEDED' });
232+
}
233+
}
234+
)
235+
);
236+
237+
// Act
238+
const vepStatusPolling = new VepSubmissionStatusPolling();
239+
vepStatusPolling.enqueueSubmission({
240+
submission: { id: submissionId, status: 'SUBMITTED' },
241+
dispatch: jest.fn()
242+
});
243+
244+
// Assert
245+
await jest.runAllTimersAsync();
246+
247+
expect(actualStatusPollCount).toBe(1);
248+
expect(updateSubmission as any).toHaveBeenCalledWith({
249+
submissionId,
250+
fragment: { status: 'FAILED' }
251+
});
252+
});
253+
177254
it('checks statuses of all submissions without artificial delay when they are submitted as a batch', async () => {
178255
// Arrange
179256
const lateSubmissionId = 'late-submission-id';

src/content/app/tools/vep/state/vep-action-listeners/vepSubmissionStatusPolling.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import type { SubmissionStatus } from 'src/content/app/tools/vep/types/vepSubmis
2323

2424
export const POLLING_INTERVAL = 15 * 1000;
2525

26-
type PolledSubmission = {
26+
export type PolledSubmission = {
2727
id: string;
2828
status: SubmissionStatus;
2929
};
@@ -109,12 +109,14 @@ class VepSubmissionStatusPolling {
109109
if (response.status >= 500) {
110110
// try later
111111
this.queue.unshift(submissionId);
112-
} else if (response.status === 404)
112+
} else if (response.status === 404) {
113113
// fail submission
114114
this.reportSubmissionStatus({
115115
submissionId,
116116
status: 'FAILED'
117117
});
118+
}
119+
return;
118120
}
119121

120122
const { status } = await response.json();

src/content/app/tools/vep/state/vep-submissions/vepSubmissionsSlice.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929

3030
import type { VepSubmissionWithoutInputFile } from 'src/content/app/tools/vep/types/vepSubmission';
3131

32-
type VepSubmissionsState = Record<string, VepSubmissionWithoutInputFile>;
32+
export type VepSubmissionsState = Record<string, VepSubmissionWithoutInputFile>;
3333

3434
export const restoreVepSubmissions = createAsyncThunk(
3535
'vep-submissions/restoreSubmissions',

0 commit comments

Comments
 (0)