-
-
Notifications
You must be signed in to change notification settings - Fork 983
Add queue timeout for stuck pre-running submissions #5144
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
RishabhJain2018
wants to merge
1
commit into
master
Choose a base branch
from
cursor/submission-queue-timeout-e826
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
26 changes: 26 additions & 0 deletions
26
apps/challenges/migrations/0128_challenge_submission_queue_time_limit.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| # Generated by Django 2.2.20 on 2026-07-09 | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ("challenges", "0127_alter_challenge_worker_python_version_choices"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name="challenge", | ||
| name="submission_queue_time_limit", | ||
| field=models.PositiveIntegerField( | ||
| default=7200, | ||
| help_text=( | ||
| "Maximum seconds a submission may remain in a pre-running " | ||
| "queue state (submitted, submitting, queued, resuming) " | ||
| "before it is automatically marked failed. Set to 0 to " | ||
| "disable." | ||
| ), | ||
| ), | ||
| ), | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| from django.core.management.base import BaseCommand | ||
|
|
||
| from jobs.queue_timeout import fail_stuck_submissions | ||
|
|
||
|
|
||
| class Command(BaseCommand): | ||
| help = ( | ||
| "Fail submissions that have remained in a pre-running queue state " | ||
| "(submitted, submitting, queued, resuming) longer than the " | ||
| "challenge's submission_queue_time_limit." | ||
| ) | ||
|
|
||
| def add_arguments(self, parser): | ||
| parser.add_argument( | ||
| "--challenge-pk", | ||
| type=int, | ||
| default=None, | ||
| help="Only process submissions for this challenge.", | ||
| ) | ||
| parser.add_argument( | ||
| "--dry-run", | ||
| action="store_true", | ||
| help="Report submissions that would be failed without updating them.", | ||
| ) | ||
|
|
||
| def handle(self, *args, **options): | ||
| failed_count = fail_stuck_submissions( | ||
| challenge_pk=options["challenge_pk"], | ||
| dry_run=options["dry_run"], | ||
| ) | ||
| if options["dry_run"]: | ||
| self.stdout.write( | ||
| self.style.WARNING( | ||
| "Dry run: {} submission(s) would be failed.".format( | ||
| failed_count | ||
| ) | ||
| ) | ||
| ) | ||
| else: | ||
| self.stdout.write( | ||
| self.style.SUCCESS( | ||
| "Failed {} queue-stuck submission(s).".format(failed_count) | ||
| ) | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| import logging | ||
| from datetime import timedelta | ||
|
|
||
| from challenges.aws_utils import trigger_eks_node_autoscale | ||
| from django.core.files.base import ContentFile | ||
| from django.db.models.functions import Coalesce | ||
| from django.utils import timezone | ||
|
|
||
| from .models import Submission | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| STUCK_QUEUE_STATUSES = ( | ||
| Submission.SUBMITTED, | ||
| Submission.SUBMITTING, | ||
| Submission.QUEUED, | ||
| Submission.RESUMING, | ||
| ) | ||
|
|
||
| QUEUE_TIMEOUT_STDERR = ( | ||
| "Submission exceeded the queue time limit and was not picked up for " | ||
| "evaluation." | ||
| ) | ||
|
|
||
|
|
||
| def get_stuck_submission_queryset(challenge_pk=None): | ||
| """Return submissions in pre-running states that may be queue-stuck.""" | ||
| queryset = ( | ||
| Submission.objects.filter( | ||
| status__in=STUCK_QUEUE_STATUSES, | ||
| ignore_submission=False, | ||
| challenge_phase__challenge__submission_queue_time_limit__gt=0, | ||
| ) | ||
| .select_related("challenge_phase__challenge") | ||
| .annotate(queue_started_at=Coalesce("rerun_resumed_at", "submitted_at")) | ||
| ) | ||
| if challenge_pk is not None: | ||
| queryset = queryset.filter(challenge_phase__challenge__pk=challenge_pk) | ||
| return queryset | ||
|
|
||
|
|
||
| def is_submission_queue_timed_out(submission, now=None): | ||
| """Return True when a submission has exceeded its challenge queue limit.""" | ||
| now = now or timezone.now() | ||
| challenge = submission.challenge_phase.challenge | ||
| queue_time_limit = challenge.submission_queue_time_limit | ||
| if not queue_time_limit: | ||
| return False | ||
|
|
||
| queue_started_at = submission.rerun_resumed_at or submission.submitted_at | ||
| return queue_started_at <= now - timedelta(seconds=queue_time_limit) | ||
|
|
||
|
|
||
| def fail_submission_due_to_queue_timeout(submission, dry_run=False): | ||
| """Mark a queue-stuck submission as failed.""" | ||
| if dry_run: | ||
| return True | ||
|
|
||
| previous_status = submission.status | ||
| submission.status = Submission.FAILED | ||
| submission.completed_at = timezone.now() | ||
| submission.stderr_file.save( | ||
| "stderr.txt", | ||
| ContentFile(QUEUE_TIMEOUT_STDERR.encode("utf-8")), | ||
| ) | ||
| submission.save() | ||
|
|
||
| challenge = submission.challenge_phase.challenge | ||
| trigger_eks_node_autoscale( | ||
| challenge.pk, | ||
| trigger_source="submission_queue_timeout", | ||
| submission_pk=submission.pk, | ||
| submission_status=Submission.FAILED, | ||
| previous_submission_status=previous_status, | ||
| ) | ||
| return True | ||
|
|
||
|
|
||
| def fail_stuck_submissions(challenge_pk=None, dry_run=False): | ||
| """ | ||
| Fail submissions that exceeded their challenge queue time limit. | ||
|
|
||
| Returns the number of submissions that were (or would be) failed. | ||
| """ | ||
| now = timezone.now() | ||
| failed_count = 0 | ||
|
|
||
| for submission in get_stuck_submission_queryset(challenge_pk): | ||
| if not is_submission_queue_timed_out(submission, now=now): | ||
| continue | ||
|
|
||
| previous_status = submission.status | ||
| queue_started_at = submission.rerun_resumed_at or submission.submitted_at | ||
| challenge = submission.challenge_phase.challenge | ||
|
|
||
| if fail_submission_due_to_queue_timeout(submission, dry_run=dry_run): | ||
| failed_count += 1 | ||
| action = "Would fail" if dry_run else "Failed" | ||
| logger.warning( | ||
| "%s queue-stuck submission pk=%s challenge_pk=%s status=%s " | ||
| "queue_started_at=%s queue_time_limit=%ss", | ||
| action, | ||
| submission.pk, | ||
| challenge.pk, | ||
| previous_status, | ||
| queue_started_at, | ||
| challenge.submission_queue_time_limit, | ||
| ) | ||
|
|
||
| if failed_count: | ||
| logger.info( | ||
| "fail_stuck_submissions: %s %d submission(s).", | ||
| "would fail" if dry_run else "failed", | ||
| failed_count, | ||
| ) | ||
| return failed_count | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Missing per-submission error handling in batch loop.
If
fail_submission_due_to_queue_timeoutraises an exception for any single submission (e.g., storage failure onstderr_file.save(), ortrigger_eks_node_autoscaleAWS API error), the exception propagates and terminates the loop. All remaining stuck submissions are skipped until the next 30-minute cycle.The autoscale failure case is particularly concerning: the submission is already persisted as
FAILEDbystderr_file.save(), so the next run won't pick it up (it's no longer inSTUCK_QUEUE_STATUSES), but the autoscale trigger is permanently lost.Wrap each iteration in a try/except, log the error, and continue processing the rest.
🛡️ Proposed fix
for submission in get_stuck_submission_queryset(challenge_pk): if not is_submission_queue_timed_out(submission, now=now): continue previous_status = submission.status queue_started_at = submission.rerun_resumed_at or submission.submitted_at challenge = submission.challenge_phase.challenge - if fail_submission_due_to_queue_timeout(submission, dry_run=dry_run): - failed_count += 1 - action = "Would fail" if dry_run else "Failed" - logger.warning( - "%s queue-stuck submission pk=%s challenge_pk=%s status=%s " - "queue_started_at=%s queue_time_limit=%ss", - action, - submission.pk, - challenge.pk, - previous_status, - queue_started_at, - challenge.submission_queue_time_limit, - ) + try: + if fail_submission_due_to_queue_timeout( + submission, dry_run=dry_run + ): + failed_count += 1 + action = "Would fail" if dry_run else "Failed" + logger.warning( + "%s queue-stuck submission pk=%s challenge_pk=%s " + "status=%s queue_started_at=%s " + "queue_time_limit=%ss", + action, + submission.pk, + challenge.pk, + previous_status, + queue_started_at, + challenge.submission_queue_time_limit, + ) + except Exception: + logger.exception( + "Failed to process queue-stuck submission pk=%s " + "challenge_pk=%s status=%s", + submission.pk, + challenge.pk, + previous_status, + )📝 Committable suggestion
🤖 Prompt for AI Agents