diff --git a/ami/jobs/models.py b/ami/jobs/models.py index e662707f4..d0e3b0c02 100644 --- a/ami/jobs/models.py +++ b/ami/jobs/models.py @@ -79,7 +79,9 @@ class JobState(str, OrderedEnum): @classmethod def running_states(cls): - return [cls.CREATED, cls.PENDING, cls.STARTED, cls.RETRY, cls.CANCELING, cls.UNKNOWN] + # Dispatched states the reapers/reconcilers select from. CREATED is not + # here: a not-yet-enqueued job has no Celery task to reap. See #1354. + return [cls.PENDING, cls.STARTED, cls.RETRY, cls.CANCELING, cls.UNKNOWN] @classmethod def final_states(cls): @@ -96,8 +98,9 @@ def active_states(cls): @classmethod def finalizable_states(cls): - # running_states() minus CANCELING (don't resurrect a cancel in progress) - # and UNKNOWN (never served; shouldn't auto-finalize). See #1337. + # States a job may auto-finalize from. Excludes CANCELING (don't resurrect + # a cancel in progress) and UNKNOWN (never served; shouldn't auto-finalize). + # See #1337. return [cls.CREATED, cls.PENDING, cls.STARTED, cls.RETRY] diff --git a/ami/jobs/tests/test_update_stale_jobs.py b/ami/jobs/tests/test_update_stale_jobs.py index 0f0a2a215..a64c18e68 100644 --- a/ami/jobs/tests/test_update_stale_jobs.py +++ b/ami/jobs/tests/test_update_stale_jobs.py @@ -121,3 +121,21 @@ def test_skips_recent_and_final_state_jobs(self, mock_cleanup): self.assertEqual(results, []) mock_cleanup.assert_not_called() + + @patch("ami.jobs.tasks.cleanup_async_job_if_needed") + def test_skips_created_but_unstarted_jobs(self, mock_cleanup): + """A job created but never enqueued is left alone regardless of age. + + Jobs can be pre-configured and started later, so a CREATED job has no + Celery task and no async resources to reconcile. The staleness cutoff + (measured against ``updated_at``) does not apply to it — it waits in + CREATED until a user starts it. See #1354. + """ + job = self._create_job(status=JobState.CREATED, minutes_ago=300) + + results = check_stale_jobs() + + self.assertEqual(results, []) + job.refresh_from_db() + self.assertEqual(job.status, JobState.CREATED.value) + mock_cleanup.assert_not_called()