Skip to content

CANCELLED jobs leak through /next filter, starve newer async_api jobs #1282

Description

@mihow

Bug

/next (claim-next-job, currently list(ids_only=1) in ami/jobs/views.py:296 get_queryset()) hands out CANCELLED async_api jobs forever. The exclude filter does:

jobs.exclude(
    status=JobState.failed_states(),
    updated_at__lt=cutoff_datetime,
)

Two problems:

  1. JobState.failed_states() does not include CANCELLED — only [FAILURE, REVOKED, UNKNOWN]. CANCELLED jobs are treated as still-active and offered to workers indefinitely.
  2. status= is the wrong lookup for a list-valued return — should be status__in=. As written, Django compares the column to the list as a single value; the exclude effectively never matches.

The two combine: any user-cancelled async_api job becomes a permanent zombie that workers serially "claim" every poll, then immediately bail because the corresponding NATS stream is empty/exhausted. Newer async_api jobs created after the zombies are starved out — workers never see them.

Symptom

A real async_api job sits with collect stage = SUCCESS, process stage = CREATED 0% indefinitely. NATS stream for that job ID has num_pending = total_tasks, delivered = 0. Worker stdout cycles:

Processing job <old_cancelled_id> with pipeline ...
DataLoader subprocess 0/1 starting iteration for job <old_cancelled_id>
No jobs found, sleeping for 5 seconds

…on the same handful of stale CANCELLED job IDs every 5s, never picking up the new job.

Manual unblock

Bump CANCELLED zombies to REVOKED so they fall in failed_states(). .update() bypasses auto_now, so updated_at stays at the original old value (already past the 72h FAILED_JOBS_DISPLAY_MAX_HOURS cutoff):

from django.utils import timezone
Job.objects.filter(pk__in=[<zombie ids>]).update(
    status="REVOKED", finished_at=timezone.now()
)

Workers stop cycling within ~30s.

Suggested fix

Either:

  • Add CANCELLED to JobState.failed_states(), and change status= to status__in= in the exclude so the comparison actually works.
  • Or short-circuit in the /next handler with an explicit exclude(status__in=[CANCELLED, FAILURE, REVOKED, UNKNOWN]).

Links / urgency

Related to #1265 (dedicated /next action; the temporary list(ids_only=1) claim-semantics).

This bit a production deployment today — workers stuck cycling 3 zombies from 2+ weeks ago, blocking a freshly-started job. Would be good to merge alongside the next deploy today.

Metadata

Metadata

Assignees

No one assigned

    Labels

    PSv2Async & distributed ML backend (PSv2): job state, NATS dispatch, result handling. Umbrella #515.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions