feat(mass_transfer): resumable automatic retries (durable volume progress) - #374
feat(mass_transfer): resumable automatic retries (durable volume progress)#374samuelvkwong wants to merge 11 commits into
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…empt A mass transfer task processes its partition's series sequentially. When a single series exhausted the stamina network retries, the resulting RetriableDicomError was re-raised, aborting the whole task. Each Procrastinate retry re-fetched the entire partition and hit the same dead series again, so the task ended FAILURE even when only 1-2 of its volumes were truly unrecoverable (e.g. series on archived/offline PACS storage). On the final attempt only, mark the dead volume ERROR and continue with the remaining volumes, so the partition completes as WARNING instead of FAILURE. Non-final attempts are unchanged: a transient PACS outage can still recover during the task-level retry waits. Implements the approved design in docs/superpowers/specs/2026-06-12-mass-transfer-final-attempt-continue-design.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Supersedes the final-attempt-continue stop-gap design: automatic Procrastinate retries resume from persisted MassTransferVolume rows instead of wiping and re-fetching the whole partition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eries folders - Delete obsolete _is_final_attempt() helper method - Update _transfer_single_series to never raise RetriableDicomError: instead, mark the volume as ERROR with retriable=True, allowing process() to schedule a task retry after the whole partition was attempted - Add per-series folder cleanup before export: if output_path exists from a previous partial attempt, delete it (folder exports are not atomic) - Add "retriable" to volume.save() update_fields Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Code Review
This pull request implements resumable automatic retries for mass transfer tasks, allowing them to resume from persisted volume progress instead of restarting the entire partition. It introduces a retriable field on MassTransferVolume and an anonymizer_seed on MassTransferTask, passes is_final_attempt from the runner to the processor, and updates the transfer loop to continue past retriable errors, raising a single aggregated error at the end. Feedback on the changes points out a potential network connection leak where the source DicomOperator is not closed if all volumes are already completed in a resumed run, suggesting it be closed in the finally block of process().
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| self.mass_task.save(update_fields=["anonymizer_seed"]) | ||
| pseudonymizer = Pseudonymizer(seed=self.mass_task.anonymizer_seed) | ||
|
|
||
| operator = DicomOperator(source_node.dicomserver, persistent=True) |
There was a problem hiding this comment.
The operator (source DicomOperator) is initialized with persistent=True. In a resumed run where all volumes are already completed (i.e., volumes is not empty, but pending is empty), the else block is skipped, and the loop inside _transfer_grouped_series does not execute. As a result, operator.close() is never called, leading to a potential network connection/association leak on the PACS server.
To prevent this, consider initializing operator = None at the start of process(), and ensuring it is closed in the finally block of process():
finally:
if operator:
operator.close()
if dest_operator:
dest_operator.close()
Summary
Makes automatic Procrastinate retries of mass transfer tasks resume from persisted per-series progress instead of wiping the destination partition and re-fetching everything. One dead series no longer costs the whole partition: it is retried alone, and the task ends
WARNINGwith only the truly dead volumes markedERROR.Supersedes the final-attempt-continue stop-gap on the base branch: the
_is_final_attempt()special case (and its cancel→resume edge case) is deleted; every attempt now behaves identically.How it works
DicomTask.attempts <= 1): fresh jobs and UI Retry/Restart (which resetattemptsto 0) keep clean-slate semantics. Automatic retries (attempts ≥ 2) skip the wipe and discovery and reload the persistedMassTransferVolumerows.MassTransferVolume.retriableflag: a per-volumeRetriableDicomErrormarks the volumeERROR+retriable=Trueand the loop continues. On resume, only retriable/pending volumes are re-transferred;EXPORTED/CONVERTED/SKIPPED/permanent-ERRORvolumes (rows and files) are untouched.RetriableDicomErrorso Procrastinate retries with backoff. On the final attempt it returns the whole-partition summary instead.is_final_attemptcomes from the runner:_run_dicom_taskcomputes it from Procrastinate's per-job counter (the arithmetic already used for the PENDING/FAILURE decision) and injects it into the processor subprocess — no more guessing from the cumulativeDicomTask.attempts.rmtreeas the defense against partially-written series.MassTransferTask.anonymizer_seed(generated on the fresh cycle, reused by retries) so a resumed attempt produces the same pseudonymized UIDs/date shifts as the volumes already transferred.bulk_createis wrapped in a transaction so a worker kill mid-create rolls back to zero rows and the next attempt re-discovers._run_dicom_task's final save writes only the fields it owns (status,message,log,end) so it cannot clobber fields the processor subprocess persisted mid-run.Bonus: cancel → resume now actually resumes the partition instead of redoing it.
Migrations
Two backward-compatible
AddFieldmigrations (0006MassTransferVolume.retriable,0007MassTransferTask.anonymizer_seed).Deploy note (one-time transitional behavior)
A task that failed retriably under the old code and auto-retries after deploy resumes from rows written by the old code (
retriable=False), so its interrupted volume is treated as permanent and the task endsWARNINGwithout retrying that volume. Self-healing via the job Retry button.Test plan
uv run cli lint(ruff + pyright + djlint) clean.docs/superpowers/specs/2026-07-16-mass-transfer-resumable-retries-design.md(spec) anddocs/superpowers/plans/2026-07-16-mass-transfer-resumable-retries.md(plan).Non-blocking follow-ups (from review)
adit/core/tests/test_tasks.py(three inline copies now).🤖 Generated with Claude Code