Skip to content

Commit fe47f4d

Browse files
adi-herwana-nuscysjonathan
authored andcommitted
fix(programming): fix evaluation polling after user navigate away
- polling now handled by SubmissionEditIndex component, timers started on mount and cleared on unmount - autograding job polling (from page load and "Run Code" button) moved from pollJob to component timers
1 parent b1fcfdf commit fe47f4d

File tree

8 files changed

+118
-111
lines changed

8 files changed

+118
-111
lines changed

client/app/bundles/course/assessment/submission/actions/answers/index.js

Lines changed: 11 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { getClientVersionForAnswerId } from '../../selectors/answers';
1111
import translations from '../../translations';
1212
import { convertAnswerDataToInitialValue } from '../../utils/answers';
1313
import { buildErrorMessage, formatAnswer } from '../utils';
14-
import { fetchSubmission, getEvaluationResult } from '..';
14+
import { fetchSubmission } from '..';
1515

1616
const JOB_POLL_DELAY_MS = 500;
1717
export const STALE_ANSWER_ERR = 'stale_answer';
@@ -33,28 +33,10 @@ export const updateClientVersion = (answerId, clientVersion) => (dispatch) =>
3333
payload: { answer: { id: answerId, clientVersion } },
3434
});
3535

36-
const pollAutogradingJob =
37-
(jobUrl, submissionId, questionId, answerId) => (dispatch) => {
38-
pollJob(
39-
jobUrl,
40-
() => dispatch(getEvaluationResult(submissionId, answerId, questionId)),
41-
(errorData) => {
42-
dispatch({
43-
type: actionTypes.AUTOGRADE_FAILURE,
44-
questionId,
45-
payload: errorData,
46-
});
47-
dispatch(setNotification(translations.requestFailure));
48-
},
49-
JOB_POLL_DELAY_MS,
50-
);
51-
};
52-
53-
export function submitAnswer(submissionId, answerId, rawAnswer, resetField) {
36+
export function submitAnswer(questionId, answerId, rawAnswer, resetField) {
5437
const currentTime = Date.now();
5538
const answer = formatAnswer(rawAnswer, currentTime);
5639
const payload = { answer };
57-
const questionId = answer.questionId;
5840

5941
return (dispatch) => {
6042
dispatch(updateClientVersion(answerId, currentTime));
@@ -73,16 +55,14 @@ export function submitAnswer(submissionId, answerId, rawAnswer, resetField) {
7355
if (data.newSessionUrl) {
7456
window.location = data.newSessionUrl;
7557
} else if (data.jobUrl) {
76-
pollAutogradingJob(
77-
data.jobUrl,
78-
submissionId,
79-
questionId,
80-
answerId,
81-
)(dispatch);
58+
dispatch({
59+
type: actionTypes.AUTOGRADE_SUBMITTED,
60+
payload: { questionId, jobUrl: data.jobUrl },
61+
});
8262
} else {
8363
dispatch({
8464
type: actionTypes.AUTOGRADE_SUCCESS,
85-
payload: data,
65+
payload: { ...data, answerId },
8666
});
8767
// When an answer is submitted, the value of that field needs to be updated.
8868
resetField(`${answerId}`, {
@@ -359,20 +339,10 @@ export function reevaluateAnswer(submissionId, answerId, questionId) {
359339
if (data.newSessionUrl) {
360340
window.location = data.newSessionUrl;
361341
} else if (data.jobUrl) {
362-
pollJob(
363-
data.jobUrl,
364-
() =>
365-
dispatch(getEvaluationResult(submissionId, answerId, questionId)),
366-
(errorData) => {
367-
dispatch({
368-
type: actionTypes.REEVALUATE_FAILURE,
369-
questionId,
370-
payload: errorData,
371-
});
372-
dispatch(setNotification(translations.requestFailure));
373-
},
374-
JOB_POLL_DELAY_MS,
375-
);
342+
dispatch({
343+
type: actionTypes.REEVALUATE_SUBMITTED,
344+
payload: { questionId, jobUrl: data.jobUrl },
345+
});
376346
} else {
377347
dispatch({
378348
type: actionTypes.REEVALUATE_SUCCESS,

client/app/bundles/course/assessment/submission/actions/index.js

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import GlobalAPI from 'api';
12
import CourseAPI from 'api/course';
23
import { setNotification } from 'lib/actions';
34
import pollJob from 'lib/helpers/jobHelpers';
@@ -12,7 +13,6 @@ import translations from '../translations';
1213
import { buildErrorMessage, formatAnswers } from './utils';
1314

1415
const JOB_POLL_DELAY_MS = 500;
15-
const JOB_STAGGER_DELAY_MS = 400;
1616

1717
export function getEvaluationResult(submissionId, answerId, questionId) {
1818
return (dispatch) => {
@@ -22,16 +22,20 @@ export function getEvaluationResult(submissionId, answerId, questionId) {
2222
.then((data) => {
2323
dispatch({
2424
type: actionTypes.AUTOGRADE_SUCCESS,
25-
payload: data,
25+
payload: { ...data, answerId },
2626
});
2727
})
2828
.catch(() => {
2929
dispatch(setNotification(translations.requestFailure));
30-
dispatch({ type: actionTypes.AUTOGRADE_FAILURE, questionId });
30+
dispatch({ type: actionTypes.AUTOGRADE_FAILURE, questionId, answerId });
3131
});
3232
};
3333
}
3434

35+
export function getJobStatus(jobUrl) {
36+
return GlobalAPI.jobs.get(jobUrl);
37+
}
38+
3539
export function fetchSubmission(id, onGetMonitoringSessionId) {
3640
return (dispatch) => {
3741
dispatch({ type: actionTypes.FETCH_SUBMISSION_REQUEST });
@@ -48,29 +52,6 @@ export function fetchSubmission(id, onGetMonitoringSessionId) {
4852
window.location = data.newSessionUrl;
4953
return;
5054
}
51-
data.answers
52-
.filter((a) => a.autograding && a.autograding.path)
53-
.forEach((answer, index) => {
54-
setTimeout(() => {
55-
pollJob(
56-
answer.autograding.path,
57-
() =>
58-
dispatch(
59-
getEvaluationResult(
60-
id,
61-
answer.fields.id,
62-
answer.questionId,
63-
),
64-
),
65-
() =>
66-
dispatch({
67-
type: actionTypes.AUTOGRADE_FAILURE,
68-
questionId: answer.questionId,
69-
}),
70-
JOB_POLL_DELAY_MS,
71-
);
72-
}, JOB_STAGGER_DELAY_MS * index);
73-
});
7455
if (data.monitoringSessionId !== undefined)
7556
onGetMonitoringSessionId?.(data.monitoringSessionId);
7657
dispatch({

client/app/bundles/course/assessment/submission/constants.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ export const BUFFER_TIME_TO_FORCE_SUBMIT_MS = 5 * 1000;
2222
// to still be considered a "newly created" submission
2323
export const TIME_LAPSE_NEW_SUBMISSION_MS = 10 * 1000;
2424

25-
export const POLL_INTERVAL_MILLISECONDS = 2000;
25+
export const EVALUATE_POLL_INTERVAL_MILLISECONDS = 500;
26+
export const FEEDBACK_POLL_INTERVAL_MILLISECONDS = 2000;
2627

2728
export const workflowStates = {
2829
Unstarted: 'unstarted' as const,
@@ -160,6 +161,7 @@ const actionTypes = mirrorCreator([
160161
'UNSUBMIT_SUCCESS',
161162
'UNSUBMIT_FAILURE',
162163
'AUTOGRADE_REQUEST',
164+
'AUTOGRADE_SUBMITTED',
163165
'AUTOGRADE_SUCCESS',
164166
'AUTOGRADE_FAILURE',
165167
'AUTOGRADE_SAVING_SUCCESS',
@@ -168,6 +170,7 @@ const actionTypes = mirrorCreator([
168170
'FEEDBACK_SUCCESS',
169171
'FEEDBACK_FAILURE',
170172
'REEVALUATE_REQUEST',
173+
'REEVALUATE_SUBMITTED',
171174
'REEVALUATE_SUCCESS',
172175
'REEVALUATE_FAILURE',
173176
'RESET_REQUEST',

client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/SubmissionForm.tsx

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,21 @@ import usePrompt from 'lib/hooks/router/usePrompt';
77
import { useAppDispatch, useAppSelector } from 'lib/hooks/store';
88
import useTranslation from 'lib/hooks/useTranslation';
99

10-
import { finalise } from '../../actions';
10+
import { finalise, getEvaluationResult, getJobStatus } from '../../actions';
1111
import { fetchLiveFeedback } from '../../actions/answers';
1212
import WarningDialog from '../../components/WarningDialog';
13-
import {
13+
import actionTypes, {
14+
EVALUATE_POLL_INTERVAL_MILLISECONDS,
15+
FEEDBACK_POLL_INTERVAL_MILLISECONDS,
1416
formNames,
15-
POLL_INTERVAL_MILLISECONDS,
1617
workflowStates,
1718
} from '../../constants';
1819
import GradingPanel from '../../containers/GradingPanel';
1920
import { getInitialAnswer } from '../../selectors/answers';
2021
import { getAssessment } from '../../selectors/assessments';
2122
import { getAttachments } from '../../selectors/attachments';
2223
import { getLiveFeedbacks } from '../../selectors/liveFeedbacks';
24+
import { getQuestionFlags } from '../../selectors/questionFlags';
2325
import { getQuestions } from '../../selectors/questions';
2426
import { getSubmission } from '../../selectors/submissions';
2527
import translations from '../../translations';
@@ -51,6 +53,7 @@ const SubmissionForm: FC<Props> = (props) => {
5153
const assessment = useAppSelector(getAssessment);
5254
const submission = useAppSelector(getSubmission);
5355
const questions = useAppSelector(getQuestions);
56+
const questionFlags = useAppSelector(getQuestionFlags);
5457
const attachments = useAppSelector(getAttachments);
5558
const liveFeedbacks = useAppSelector(getLiveFeedbacks);
5659
const initialValues = useAppSelector(getInitialAnswer);
@@ -147,7 +150,8 @@ const SubmissionForm: FC<Props> = (props) => {
147150
}
148151
});
149152

150-
const pollerRef = useRef<NodeJS.Timeout | null>(null);
153+
const feedbackPollerRef = useRef<NodeJS.Timeout | null>(null);
154+
const evaluatePollerRef = useRef<NodeJS.Timeout | null>(null);
151155
const pollAllFeedback = (): void => {
152156
questionIds.forEach((id) => {
153157
const question = questions[id];
@@ -159,17 +163,59 @@ const SubmissionForm: FC<Props> = (props) => {
159163
});
160164
};
161165

166+
const handleEvaluationPolling = (): void => {
167+
Object.values(questions).forEach((question) => {
168+
if (
169+
questionFlags[question.id]?.isAutograding &&
170+
questionFlags[question.id]?.jobUrl
171+
) {
172+
getJobStatus(questionFlags[question.id].jobUrl).then((response) => {
173+
switch (response.data.status) {
174+
case 'submitted':
175+
break;
176+
case 'completed':
177+
dispatch(
178+
getEvaluationResult(
179+
submissionId,
180+
question.answerId,
181+
question.id,
182+
),
183+
);
184+
break;
185+
case 'errored':
186+
dispatch({
187+
type: actionTypes.AUTOGRADE_FAILURE,
188+
answerId: question.answerId,
189+
questionId: question.id,
190+
});
191+
break;
192+
default:
193+
throw new Error('Unknown job status');
194+
}
195+
});
196+
}
197+
});
198+
};
199+
162200
useEffect(() => {
163201
// check for feedback from Codaveri on page load for each question
164-
pollerRef.current = setInterval(
202+
feedbackPollerRef.current = setInterval(
165203
pollAllFeedback,
166-
POLL_INTERVAL_MILLISECONDS,
204+
FEEDBACK_POLL_INTERVAL_MILLISECONDS,
205+
);
206+
207+
evaluatePollerRef.current = setInterval(
208+
handleEvaluationPolling,
209+
EVALUATE_POLL_INTERVAL_MILLISECONDS,
167210
);
168211

169212
// clean up poller on unmount
170213
return () => {
171-
if (pollerRef.current) {
172-
clearInterval(pollerRef.current);
214+
if (feedbackPollerRef.current) {
215+
clearInterval(feedbackPollerRef.current);
216+
}
217+
if (evaluatePollerRef.current) {
218+
clearInterval(evaluatePollerRef.current);
173219
}
174220
};
175221
});

client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/components/button/RunCodeButton.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { getSubmissionFlags } from 'course/assessment/submission/selectors/submi
99
import { getSubmission } from 'course/assessment/submission/selectors/submissions';
1010
import translations from 'course/assessment/submission/translations';
1111
import LoadingIndicator from 'lib/components/core/LoadingIndicator';
12-
import { getSubmissionId } from 'lib/helpers/url-helpers';
1312
import { useAppDispatch, useAppSelector } from 'lib/hooks/store';
1413
import useTranslation from 'lib/hooks/useTranslation';
1514

@@ -30,7 +29,6 @@ const RunCodeButton: FC<Props> = (props) => {
3029
const { resetField, getValues } = useFormContext();
3130

3231
const question = questions[questionId];
33-
const submissionId = getSubmissionId();
3432

3533
const { answerId, attemptsLeft, attemptLimit } = question;
3634
const { isAutograding, isResetting } = questionFlags[questionId] || {};
@@ -39,12 +37,7 @@ const RunCodeButton: FC<Props> = (props) => {
3937

4038
const onSubmitAnswer = (): void => {
4139
dispatch(
42-
submitAnswer(
43-
submissionId,
44-
answerId,
45-
getValues(`${answerId}`),
46-
resetField,
47-
),
40+
submitAnswer(question.id, answerId, getValues(`${answerId}`), resetField),
4841
);
4942
};
5043

client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/components/button/SubmitButton.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { getQuestions } from 'course/assessment/submission/selectors/questions';
1212
import { getSubmissionFlags } from 'course/assessment/submission/selectors/submissionFlags';
1313
import translations from 'course/assessment/submission/translations';
1414
import LoadingIndicator from 'lib/components/core/LoadingIndicator';
15-
import { getSubmissionId } from 'lib/helpers/url-helpers';
1615
import { useAppDispatch, useAppSelector } from 'lib/hooks/store';
1716
import useTranslation from 'lib/hooks/useTranslation';
1817

@@ -33,8 +32,6 @@ const SubmitButton: FC<Props> = (props) => {
3332

3433
const { resetField, getValues } = useFormContext();
3534

36-
const submissionId = getSubmissionId();
37-
3835
const question = questions[questionId];
3936

4037
const { answerId, autogradable, type } = question;
@@ -51,12 +48,7 @@ const SubmitButton: FC<Props> = (props) => {
5148

5249
const onSubmitAnswer = (): void => {
5350
dispatch(
54-
submitAnswer(
55-
submissionId,
56-
answerId,
57-
getValues(`${answerId}`),
58-
resetField,
59-
),
51+
submitAnswer(question.id, answerId, getValues(`${answerId}`), resetField),
6052
);
6153
};
6254

0 commit comments

Comments
 (0)