From 0ed5edf68cee91ecebcb8aff63efc649f309d5c9 Mon Sep 17 00:00:00 2001 From: Tahmid Ahmed Date: Sat, 10 Oct 2026 19:46:45 -0400 Subject: [PATCH 1/3] TAN-1090: use new v2 submission details query --- .../common/leetcode/queries/GetSubmissionDetails.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/patinanetwork/codebloom/common/leetcode/queries/GetSubmissionDetails.java b/src/main/java/org/patinanetwork/codebloom/common/leetcode/queries/GetSubmissionDetails.java index 4f5a968bb..09381bc37 100644 --- a/src/main/java/org/patinanetwork/codebloom/common/leetcode/queries/GetSubmissionDetails.java +++ b/src/main/java/org/patinanetwork/codebloom/common/leetcode/queries/GetSubmissionDetails.java @@ -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 From 457045dd88db0be9944e86d7c1fa7ba19b5a50e6 Mon Sep 17 00:00:00 2001 From: Tahmid Ahmed Date: Sat, 10 Oct 2026 19:46:56 -0400 Subject: [PATCH 2/3] TAN-1090: Add a method that can rerun stuck jobs --- .../common/db/repos/job/JobRepository.java | 10 ++++++++++ .../common/db/repos/job/JobSqlRepository.java | 19 +++++++++++++++++++ .../RefetchIncompleteQuestionsService.java | 7 +++++++ 3 files changed, 36 insertions(+) diff --git a/src/main/java/org/patinanetwork/codebloom/common/db/repos/job/JobRepository.java b/src/main/java/org/patinanetwork/codebloom/common/db/repos/job/JobRepository.java index 41c889fa1..cdb2d0bfb 100644 --- a/src/main/java/org/patinanetwork/codebloom/common/db/repos/job/JobRepository.java +++ b/src/main/java/org/patinanetwork/codebloom/common/db/repos/job/JobRepository.java @@ -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. * diff --git a/src/main/java/org/patinanetwork/codebloom/common/db/repos/job/JobSqlRepository.java b/src/main/java/org/patinanetwork/codebloom/common/db/repos/job/JobSqlRepository.java index 4256b8759..7b5b0f915 100644 --- a/src/main/java/org/patinanetwork/codebloom/common/db/repos/job/JobSqlRepository.java +++ b/src/main/java/org/patinanetwork/codebloom/common/db/repos/job/JobSqlRepository.java @@ -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; + } } diff --git a/src/main/java/org/patinanetwork/codebloom/scheduled/leetcode/RefetchIncompleteQuestionsService.java b/src/main/java/org/patinanetwork/codebloom/scheduled/leetcode/RefetchIncompleteQuestionsService.java index 8c2b28b78..9afa62cc6 100644 --- a/src/main/java/org/patinanetwork/codebloom/scheduled/leetcode/RefetchIncompleteQuestionsService.java +++ b/src/main/java/org/patinanetwork/codebloom/scheduled/leetcode/RefetchIncompleteQuestionsService.java @@ -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); + } } From 5a1a8e83be54e5c56086fda163c90109e45dbef0 Mon Sep 17 00:00:00 2001 From: Tahmid Ahmed Date: Sat, 10 Oct 2026 22:05:37 -0400 Subject: [PATCH 3/3] TAN-1090: Add tests --- .../db/repos/job/JobRepositoryTest.java | 55 +++++++++++++++++++ ...RefetchIncompleteQuestionsServiceTest.java | 25 +++++++++ 2 files changed, 80 insertions(+) diff --git a/src/test/java/org/patinanetwork/codebloom/common/db/repos/job/JobRepositoryTest.java b/src/test/java/org/patinanetwork/codebloom/common/db/repos/job/JobRepositoryTest.java index 55d21bf2a..8afb9cbdf 100644 --- a/src/test/java/org/patinanetwork/codebloom/common/db/repos/job/JobRepositoryTest.java +++ b/src/test/java/org/patinanetwork/codebloom/common/db/repos/job/JobRepositoryTest.java @@ -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; @@ -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()); + } + } } diff --git a/src/test/java/org/patinanetwork/codebloom/scheduled/leetcode/RefetchIncompleteQuestionsServiceTest.java b/src/test/java/org/patinanetwork/codebloom/scheduled/leetcode/RefetchIncompleteQuestionsServiceTest.java index 7a02ed128..2e3083bcd 100644 --- a/src/test/java/org/patinanetwork/codebloom/scheduled/leetcode/RefetchIncompleteQuestionsServiceTest.java +++ b/src/test/java/org/patinanetwork/codebloom/scheduled/leetcode/RefetchIncompleteQuestionsServiceTest.java @@ -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; @@ -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()); + } + } }