feat: Add job cancellation support and update job status handling

This commit introduces a new `CANCELLED` status to the job management system, allowing users to cancel ongoing jobs. The frontend is updated to handle job cancellation requests, providing user feedback during the cancellation process. Additionally, the backend is enhanced to manage job statuses more effectively, ensuring that jobs can be marked as cancelled and that appropriate messages are displayed to users. This improvement enhances the overall user experience by providing better control over job processing.
This commit is contained in:
Tanya
2026-01-05 13:09:32 -05:00
parent 03d3a28b21
commit 0b95cd2492
8 changed files with 749 additions and 335 deletions
+52 -10
View File
@@ -54,7 +54,14 @@ def get_job(job_id: str) -> JobResponse:
# Check if job was cancelled
if job.meta and job.meta.get("cancelled", False):
job_status = JobStatus.FAILURE
# If job finished gracefully after cancellation, mark as CANCELLED
if rq_status == "finished":
job_status = JobStatus.CANCELLED
# If still running, show current status but with cancellation message
elif rq_status == "started":
job_status = JobStatus.PROGRESS if progress > 0 else JobStatus.STARTED
else:
job_status = JobStatus.CANCELLED
message = job.meta.get("message", "Cancelled by user")
# If job failed, include error message
@@ -93,19 +100,31 @@ def stream_job_progress(job_id: str):
while True:
try:
job = Job.fetch(job_id, connection=redis_conn)
rq_status = job.get_status()
status_map = {
"queued": JobStatus.PENDING,
"started": JobStatus.STARTED,
"finished": JobStatus.SUCCESS,
"failed": JobStatus.FAILURE,
}
job_status = status_map.get(job.get_status(), JobStatus.PENDING)
job_status = status_map.get(rq_status, JobStatus.PENDING)
# Check if job was cancelled first
# Check if job was cancelled - this takes priority
if job.meta and job.meta.get("cancelled", False):
job_status = JobStatus.FAILURE
# If job is finished and was cancelled, it completed gracefully
if rq_status == "finished":
job_status = JobStatus.CANCELLED
progress = job.meta.get("progress", 0) if job.meta else 0
# If job is still running but cancellation was requested, keep it as PROGRESS/STARTED
# until it actually stops
elif rq_status == "started":
# Job is still running - let it finish current photo
progress = job.meta.get("progress", 0) if job.meta else 0
job_status = JobStatus.PROGRESS if progress > 0 else JobStatus.STARTED
else:
job_status = JobStatus.CANCELLED
progress = job.meta.get("progress", 0) if job.meta else 0
message = job.meta.get("message", "Cancelled by user")
progress = job.meta.get("progress", 0) if job.meta else 0
else:
progress = 0
if job_status == JobStatus.STARTED:
@@ -140,8 +159,8 @@ def stream_job_progress(job_id: str):
last_progress = progress
last_message = message
# Stop streaming if job is complete or failed
if job_status in (JobStatus.SUCCESS, JobStatus.FAILURE):
# Stop streaming if job is complete, failed, or cancelled
if job_status in (JobStatus.SUCCESS, JobStatus.FAILURE, JobStatus.CANCELLED):
break
time.sleep(0.5) # Poll every 500ms
@@ -164,8 +183,10 @@ def cancel_job(job_id: str) -> dict:
The job will check this flag and exit gracefully.
"""
try:
print(f"[Jobs API] Cancel request for job_id={job_id}")
job = Job.fetch(job_id, connection=redis_conn)
rq_status = job.get_status()
print(f"[Jobs API] Job {job_id} current status: {rq_status}")
if rq_status == "finished":
return {
@@ -182,6 +203,7 @@ def cancel_job(job_id: str) -> dict:
if rq_status == "queued":
# Cancel queued job - remove from queue
job.cancel()
print(f"[Jobs API] ✓ Cancelled queued job {job_id}")
return {
"message": f"Job {job_id} cancelled (was queued)",
"status": "cancelled",
@@ -190,30 +212,50 @@ def cancel_job(job_id: str) -> dict:
if rq_status == "started":
# For running jobs, set cancellation flag in metadata
# The task will check this and exit gracefully
print(f"[Jobs API] Setting cancellation flag for running job {job_id}")
if job.meta is None:
job.meta = {}
job.meta["cancelled"] = True
job.meta["message"] = "Cancellation requested..."
# CRITICAL: Save metadata immediately and verify it was saved
job.save_meta()
print(f"[Jobs API] Saved metadata for job {job_id}")
# Verify the flag was saved by fetching fresh
try:
fresh_job = Job.fetch(job_id, connection=redis_conn)
if not fresh_job.meta or not fresh_job.meta.get("cancelled", False):
print(f"[Jobs API] ❌ WARNING: Cancellation flag NOT found after save for job {job_id}")
print(f"[Jobs API] Fresh job meta: {fresh_job.meta}")
else:
print(f"[Jobs API] ✓ Verified: Cancellation flag is set for job {job_id}")
except Exception as verify_error:
print(f"[Jobs API] ⚠️ Could not verify cancellation flag: {verify_error}")
# Also try to cancel the job (which will interrupt it if possible)
# This sends a signal to the worker process
try:
job.cancel()
except Exception:
# Job might already be running, that's OK
pass
print(f"[Jobs API] ✓ RQ cancel() called for job {job_id}")
except Exception as cancel_error:
# Job might already be running, that's OK - metadata flag will be checked
print(f"[Jobs API] Note: RQ cancel() raised exception (may be expected): {cancel_error}")
return {
"message": f"Job {job_id} cancellation requested",
"status": "cancelling",
}
print(f"[Jobs API] Job {job_id} in unexpected status: {rq_status}")
return {
"message": f"Job {job_id} status: {rq_status}",
"status": rq_status,
}
except Exception as e:
print(f"[Jobs API] ❌ Error cancelling job {job_id}: {e}")
import traceback
traceback.print_exc()
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Job {job_id} not found: {str(e)}",