Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ public interface JobRepository {
*/
boolean updateJob(Job job);

/**
* Unstuck jobs that have been stuck in {@code JobStatus.PROCESSING} for atleast 1 hour(s).
*
* @note Jobs will not be picked up immediately. The job processor's rules still apply, which at the time of
* writing, will either run anytime a new submission is picked up or scheduled every 30 minutes, whichever comes
* first.
* @return number of jobs that we have restarted.
*/
int unstickStuckJobs();

/**
* Deletes a job by its ID.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,23 @@ public boolean deleteAllJobs() {
int rowsAffected = jdbcClient.sql(sql).update();
return rowsAffected == 1;
}

@Override
public int unstickStuckJobs() {
String sql = """
UPDATE
"Job"
SET
status = 'INCOMPLETE',
"processedAt" = NULL,
"nextAttemptAt" = NOW()
WHERE
status = 'PROCESSING'
AND
"processedAt" < NOW() - INTERVAL '1 hour';
""";

int rowsAffected = jdbcClient.sql(sql).update();
return rowsAffected;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ public class GetSubmissionDetails {

public static final String QUERY = """
#graphql
query submissionDetails($submissionId: Int!) {
submissionDetails(submissionId: $submissionId) {
query submissionDetails($submissionId: ID!) {
submissionDetails(submissionIdV2: $submissionId) {
runtime
runtimeDisplay
runtimePercentile
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,11 @@ public void refetchIncompleteQuestions() {
jobRepository.createJob(newJob);
}
}

@Scheduled(initialDelay = 0, fixedDelay = 24, timeUnit = TimeUnit.HOURS)
public void unstickStuckQuestionJobs() {
int unstuckJobs = jobRepository.unstickStuckJobs();

LOGGER.info("unstuck {} jobs that were stuck", unstuckJobs);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

Expand Down Expand Up @@ -112,4 +113,58 @@ void testFindJobsByQuestionId() {
boolean allMatchId = jobs.stream().allMatch(j -> j.getQuestionId().equals(mockQuestionId));
assertTrue(allMatchId, "All returned jobs must match the requested question ID");
}

@Test
@Order(5)
void testUnstickStuckJobs() {
Job stuckJob = Job.builder()
.questionId(mockQuestionId)
.status(JobStatus.PROCESSING)
.build();
Job recentlyProcessingJob = Job.builder()
.questionId(mockQuestionId)
.status(JobStatus.PROCESSING)
.build();
Job oldCompletedJob = Job.builder()
.questionId(mockQuestionId)
.status(JobStatus.COMPLETE)
.build();
jobRepository.createJob(stuckJob);
jobRepository.createJob(recentlyProcessingJob);
jobRepository.createJob(oldCompletedJob);

try {
var now = StandardizedOffsetDateTime.now();

stuckJob.setProcessedAt(now.minusHours(2));
stuckJob.setNextAttemptAt(now.plusDays(1));
recentlyProcessingJob.setProcessedAt(now.minusMinutes(30));
oldCompletedJob.setProcessedAt(now.minusHours(2));
assertTrue(jobRepository.updateJob(stuckJob));
assertTrue(jobRepository.updateJob(recentlyProcessingJob));
assertTrue(jobRepository.updateJob(oldCompletedJob));

int unstuckCount = jobRepository.unstickStuckJobs();
assertTrue(unstuckCount >= 1, "At least the stuck test job should have been unstuck");

Job unstuck = jobRepository.findJobById(stuckJob.getId());
assertEquals(JobStatus.INCOMPLETE, unstuck.getStatus());
assertNull(unstuck.getProcessedAt());
assertTrue(
unstuck.getNextAttemptAt().isBefore(now.plusMinutes(1)),
"nextAttemptAt should be reset to now so the job is picked up again");

Job stillProcessing = jobRepository.findJobById(recentlyProcessingJob.getId());
assertEquals(JobStatus.PROCESSING, stillProcessing.getStatus());
assertEquals(recentlyProcessingJob.getProcessedAt(), stillProcessing.getProcessedAt());

Job stillCompleted = jobRepository.findJobById(oldCompletedJob.getId());
assertEquals(JobStatus.COMPLETE, stillCompleted.getStatus());
assertEquals(oldCompletedJob.getProcessedAt(), stillCompleted.getProcessedAt());
} finally {
jobRepository.deleteJobById(stuckJob.getId());
jobRepository.deleteJobById(recentlyProcessingJob.getId());
jobRepository.deleteJobById(oldCompletedJob.getId());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -165,4 +167,27 @@ void handleDuplicateKeyError() {
jobRepository.deleteJobById(secondJob.getId());
questionRepository.deleteQuestionById(collisionQuestion.getId());
}

@Test
void unstickStuckQuestionJobsResetsStuckJob() {
Job stuckJob = Job.builder()
.questionId(testQuestion.getId())
.status(JobStatus.PROCESSING)
.build();
jobRepository.createJob(stuckJob);

try {
stuckJob.setProcessedAt(StandardizedOffsetDateTime.now().minusHours(2));
assertTrue(jobRepository.updateJob(stuckJob));

service.unstickStuckQuestionJobs();

Job unstuck = jobRepository.findJobById(stuckJob.getId());
assertNotNull(unstuck);
assertEquals(JobStatus.INCOMPLETE, unstuck.getStatus());
assertNull(unstuck.getProcessedAt());
} finally {
jobRepository.deleteJobById(stuckJob.getId());
}
}
}
Loading