Skip to content

fix(core): job canceled during a running task no longer ends as SUCCESS - #256

Open
samuelvkwong wants to merge 2 commits into
mainfrom
fix/analysis-job-cancel-race
Open

fix(core): job canceled during a running task no longer ends as SUCCESS#256
samuelvkwong wants to merge 2 commits into
mainfrom
fix/analysis-job-cancel-race

Conversation

@samuelvkwong

@samuelvkwong samuelvkwong commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Problem

Canceling an analysis job while a task is running frequently ends with the job in SUCCESS instead of CANCELED.

A worker loads its AnalysisJob instance once at task start and holds it in memory for the whole task run (minutes for LLM batches). If the user cancels during that window, the cancel view writes CANCELING to the DB — but the worker's stale instance still says IN_PROGRESS. When the task finishes, update_job_state() re-queries the tasks but checks self.status == CANCELING against the stale in-memory value, skips the cancel branch, and the final evaluation overwrites CANCELING with SUCCESS (canceled tasks aren't counted at all, so even a 9-of-10-canceled job reports "All tasks succeeded").

Affects every AnalysisJob subclass (extractions, subscriptions, and the upcoming labeling app).

Fix

Two layers in update_job_state():

  1. Refresh the status from the DB before evaluating — fixes the practical case: the cancel landed sometime during the (long) task run.
  2. Guarded final write — the refresh and the save still aren't atomic, so the final status is written via a queryset update that excludes CANCELING/CANCELED rows. Zero updated rows means a concurrent cancel won; the job is then settled to CANCELED and the finished mail is skipped.

Tests

  • test_update_job_state_on_stale_instance_respects_concurrent_cancel — reproduces the reported bug (failed with SUCCESS before the fix)
  • test_final_state_write_guarded_against_cancel_landing_after_refresh — pins the sliver-window guard (failed before the guarded write)

Both written test-first and watched fail. Full core + extractions + subscriptions suites pass (310 tests).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved job state handling when cancellation occurs while processing is completing.
    • Canceled jobs now remain canceled instead of being incorrectly marked successful.
    • Improved reliability when multiple updates occur at nearly the same time.

A worker holds its AnalysisJob instance in memory for the whole task run.
When the user cancels meanwhile, update_job_state evaluated the final
status against the stale in-memory status, skipped the CANCELING branch,
and overwrote the cancel with SUCCESS/WARNING/FAILURE.

Two layers of fix in update_job_state:
- refresh the status from the DB before evaluating, so the normal case
  (cancel during a long task) settles to CANCELED
- write the final status with a guarded queryset update that excludes
  CANCELING/CANCELED rows, so a cancel landing in the remaining sliver
  between refresh and write also wins; zero updated rows settles the job
  to CANCELED and skips the finished mail

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6b5e469b-41d5-4fac-9006-de2016d2c13d

📥 Commits

Reviewing files that changed from the base of the PR and between 9c7e8d9 and ad01b90.

📒 Files selected for processing (1)
  • radis/core/tests/test_models.py

📝 Walkthrough

Walkthrough

AnalysisJob.update_job_state() now refreshes status before evaluation and uses a guarded database update so concurrent cancellation transitions to CANCELED instead of being overwritten. Tests cover stale instances and cancellation during final persistence.

Changes

Analysis job cancellation handling

Layer / File(s) Summary
Refresh job status before evaluation
radis/core/models.py, radis/core/tests/test_models.py
The job status is refreshed from the database before state evaluation. A test covers a stale instance observing a concurrent CANCELING transition.
Guard final status persistence
radis/core/models.py, radis/core/tests/test_models.py
Final writes exclude CANCELING and CANCELED rows. If the guarded update affects no rows, the job transitions to CANCELED. A race-window test validates this behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the race-condition fix that prevents canceled jobs from ending as SUCCESS.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/analysis-job-cancel-race

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
radis/core/models.py (1)

97-107: 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Guard the PENDING and IN_PROGRESS intermediate saves against a concurrent cancel.

refresh_from_db() at line 89 only establishes an old memory snapshot; self.save() is still a full-row update if called without update_fields. Between the tasks.filter().exists() checks and these saves, another request can write CANCELING, and these saved rows can overwrite it back to PENDING or IN_PROGRESS, losing the cancellation intent. Use the same conditional objects.exclude(status__in=[CANCELING, CANCELED]).update(status=...) pattern as the final write so canceling always wins.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@radis/core/models.py` around lines 97 - 107, The PENDING and IN_PROGRESS
branches in the job status update flow can overwrite a concurrent cancellation
because self.save() performs a full-row update. Replace each intermediate
self.status assignment and self.save() with a conditional
AnalysisJob.objects.exclude(status__in=[CANCELING, CANCELED]).update(status=...)
using the same pattern as the final write, while preserving the existing early
returns.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@radis/core/models.py`:
- Around line 97-107: The PENDING and IN_PROGRESS branches in the job status
update flow can overwrite a concurrent cancellation because self.save() performs
a full-row update. Replace each intermediate self.status assignment and
self.save() with a conditional
AnalysisJob.objects.exclude(status__in=[CANCELING, CANCELED]).update(status=...)
using the same pattern as the final write, while preserving the existing early
returns.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2167a8f3-f6c5-4060-95aa-14fd3e112bc5

📥 Commits

Reviewing files that changed from the base of the PR and between d4f5768 and 9c7e8d9.

📒 Files selected for processing (2)
  • radis/core/models.py
  • radis/core/tests/test_models.py

@Me333-jjj

Copy link
Copy Markdown

Saw the issue with incorrect job status after cancellation in Radis. I have robust state-machine logic for managing agent task lifecycles.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants