fix: mark an answer failed when its last grading write fails
WR-07: when storing the grader's score fails on the last River attempt, record the answer as failed instead of leaving it pending with no job. The terminal failure write runs on a context detached from the job's, so a timed-out or cancelled job still records it.
This commit is contained in:
@@ -189,11 +189,12 @@ func fireAfterCommit[T any](ctx context.Context, s *Service, tx *gorm.DB, name s
|
||||
// A missing answer, one that is no longer pending (graded, failed or
|
||||
// reviewed by the owner), one whose grading_seq differs from seq (the
|
||||
// student wrote a newer text) and one whose question is no longer open are
|
||||
// left alone, as is an answer whose quiz has no registered grader. Each run counts an attempt for the current
|
||||
// text first. A grader error, or a score ClampOpenPoints refuses, is
|
||||
// returned for River to retry while fewer than GradeMaxAttempts attempts
|
||||
// were made; the last one marks the answer failed with the error and
|
||||
// returns nil (D-17). A score is stored as graded with points clamped to
|
||||
// left alone, as is an answer whose quiz has no registered grader. Each
|
||||
// run counts an attempt for the current text first. A grader error, or a
|
||||
// score ClampOpenPoints refuses, is returned for River to retry while
|
||||
// fewer than GradeMaxAttempts attempts were made; the last one marks the
|
||||
// answer failed with the error and returns nil (D-17), as does an error
|
||||
// storing the score on the last attempt. A score is stored as graded with points clamped to
|
||||
// 0..question points in steps of 0.5, the attempt's score is recalculated
|
||||
// when it is completed, and AnswerGradedEvent fires after the commit.
|
||||
func (s *Service) GradeAnswer(ctx context.Context, answerID uint, seq int) error {
|
||||
@@ -266,9 +267,32 @@ WHERE id = ? AND grading_status = ? AND grading_seq = ? RETURNING grading_attemp
|
||||
case GradingRetry:
|
||||
return gradeErr
|
||||
case GradingFail:
|
||||
return s.failGrading(ctx, row.ID, attempt.ID, seq, grader.Code(), gradeErr)
|
||||
return s.failLast(ctx, row.ID, attempt.ID, seq, grader.Code(), gradeErr)
|
||||
case GradingStore:
|
||||
return s.storeGrade(ctx, row.ID, seq, &attempt, question, grader.Code(), points, res.Feedback)
|
||||
err := s.storeGrade(ctx, row.ID, seq, &attempt, question, grader.Code(), points, res.Feedback)
|
||||
if err != nil && attempts >= GradeMaxAttempts {
|
||||
// River will not run the job again: without this the answer
|
||||
// would stay pending with no job (D-17).
|
||||
return s.failLast(ctx, row.ID, attempt.ID, seq, grader.Code(), err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// failLastTimeout bounds the write that marks an answer failed after its
|
||||
// last attempt.
|
||||
const failLastTimeout = 30 * time.Second
|
||||
|
||||
// failLast marks an answer failed after its last grading attempt on a
|
||||
// context detached from the job's, so a job that timed out or was
|
||||
// cancelled still records the failure. If even that write fails, both
|
||||
// errors are returned and the answer stays pending for a regrade.
|
||||
func (s *Service) failLast(ctx context.Context, answerID, attemptID uint, seq int, code string, cause error) error {
|
||||
fctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), failLastTimeout)
|
||||
defer cancel()
|
||||
if err := s.failGrading(fctx, answerID, attemptID, seq, code, cause); err != nil {
|
||||
return errors.Join(cause, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user