Skip to content

Conversation

@ArmanJR
Copy link

@ArmanJR ArmanJR commented Aug 13, 2025

Currently, in saving processed image samples, if the pipeline successfully saves the first file (e.g., CT.nii.gz) but then fails while saving the second file (e.g., RTSTRUCT.nii.gz), the process aborts, leaving an incomplete and corrupt sample in the output directory. This leads to inconsistent datasets that are not machine-learning-ready and can silently corrupt analysis downstream.

This PR fixes the lack of transactional behavior when saving image samples. In both the Autopipeline and nnUNetPipeline, a single sample can consist of multiple files (e.g., a CT scan and its corresponding segmentation mask). The saving process writes these files sequentially.

Summary by CodeRabbit

  • New Features

    • Save operations now return a combined result listing saved files alongside any per-file errors for clearer feedback.
  • Bug Fixes

    • Saving outputs is now atomic via staging, preventing partial or corrupted files and ensuring cleanup on failure.
    • Improved reliability when saving masks and images, with automatic creation of destination folders.
    • Consolidated and clearer error reporting during batch saves.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 13, 2025

📝 Walkthrough

Walkthrough

Implements atomic, two-phase save workflows in nnUNetOutput and SampleOutput by staging writes to a temporary directory, then moving to final destinations. Updates nnUNetOutput.call to return AnnotatedPathSequence, adds broader error collection, ensures directory creation, and introduces a temp writer. SampleOutput adopts batch-level atomic error handling.

Changes

Cohort / File(s) Summary of changes
Atomic staging and return-type revision (nnUNetOutput)
src/imgtools/io/nnunet_output.py
Reworks call to stage all outputs to a temp directory, then move to final paths; ensures parent dirs exist; logs and aggregates per-file save errors; returns AnnotatedPathSequence instead of Sequence[Path]; restructures mask saving via strategy map; adds tempfile/shutil usage and cleanup.
Atomic staging with batch error handling (SampleOutput)
src/imgtools/io/sample_output.py
Adds two-phase save with a temp writer; stages VectorMask and MedImage outputs, commits via moves, and cleans up temp dir; simplifies error handling to a single batch-level failure record; minor refactors to context assembly; imports tempfile/shutil.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

hackathon

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@ArmanJR ArmanJR changed the title Transactional savings in Autopipeline & nnUNetPipeline fix: transactional savings in Autopipeline & nnUNetPipeline Aug 13, 2025
@strixy16 strixy16 requested a review from JoshuaSiraj August 13, 2025 18:11
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (5)
src/imgtools/io/sample_output.py (2)

195-195: Consider using a more secure temporary directory prefix

The prefix .tmp_sample_ starts with a dot, making these directories hidden on Unix-like systems. While this can reduce clutter, it may make debugging harder if cleanup fails. Consider using a more descriptive prefix without the leading dot, such as tmp_sample_atomic_.

-            temp_dir = Path(tempfile.mkdtemp(prefix=".tmp_sample_", dir=self.writer.root_directory))
+            temp_dir = Path(tempfile.mkdtemp(prefix="tmp_sample_atomic_", dir=self.writer.root_directory))

243-248: Consider preserving the original exception type

While catching all exceptions ensures cleanup happens, the generic error message loses valuable debugging context. Consider preserving the original exception type and details in the error message.

         except Exception as e:
-            errmsg = f"Failed to save sample atomically: {e}"
+            errmsg = f"Failed to save sample atomically: {type(e).__name__}: {e}"
             image_context = data[0] if data else None
src/imgtools/io/nnunet_output.py (3)

345-345: Use consistent temporary directory naming

For consistency with sample_output.py, consider using a similar prefix pattern without the leading dot for better visibility during debugging.

-            temp_dir = Path(tempfile.mkdtemp(prefix=".tmp_nnunet_", dir=self.writer.root_directory))
+            temp_dir = Path(tempfile.mkdtemp(prefix="tmp_nnunet_atomic_", dir=self.writer.root_directory))

393-394: Type checking could be more specific

The error message mentions "Expected Scan or VectorMask" but the actual logic only processes Scan images here (VectorMask is handled earlier). Consider making the error message more accurate or restructuring the logic.

                 elif not isinstance(image, VectorMask):
-                    raise TypeError(f"Unsupported image type: {type(image)}. Expected Scan or VectorMask.")
+                    # VectorMask already processed above, only Scan expected here
+                    logger.debug(f"Skipping non-Scan image of type {type(image).__name__}")

402-407: Preserve exception details for better debugging

Similar to the comment in sample_output.py, consider preserving the exception type in the error message for better debugging context.

         except Exception as e:
-            errmsg = f"Failed to save nnUNet sample atomically: {e}"
+            errmsg = f"Failed to save nnUNet sample atomically: {type(e).__name__}: {e}"
             image_context = data[0] if data else None
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04bb636 and 59a5258.

📒 Files selected for processing (2)
  • src/imgtools/io/nnunet_output.py (4 hunks)
  • src/imgtools/io/sample_output.py (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
src/**/*.py

⚙️ CodeRabbit Configuration File

Review the Python code for compliance with PEP 8 and PEP 257 (docstring conventions). Ensure the following: - Variables and functions follow meaningful naming conventions. - Docstrings are present, accurate, and align with the implementation. - Code is efficient and avoids redundancy while adhering to DRY principles. - Consider suggestions to enhance readability and maintainability. - Highlight any potential performance issues, edge cases, or logical errors. - Ensure all imported libraries are used and necessary.

Files:

  • src/imgtools/io/sample_output.py
  • src/imgtools/io/nnunet_output.py
🧠 Learnings (1)
📚 Learning: 2024-11-29T21:18:38.153Z
Learnt from: jjjermiah
PR: bhklab/med-imagetools#145
File: src/imgtools/utils/nnunet.py:0-0
Timestamp: 2024-11-29T21:18:38.153Z
Learning: Suggestions to modify the `save_json` function in `src/imgtools/utils/nnunet.py` to fix type annotations or add error handling are considered out of scope.

Applied to files:

  • src/imgtools/io/nnunet_output.py
🧬 Code Graph Analysis (2)
src/imgtools/io/sample_output.py (5)
src/imgtools/io/writers/nifti_writer.py (2)
  • NIFTIWriter (34-224)
  • save (135-224)
src/imgtools/io/writers/abstract_base_writer.py (3)
  • ExistingFileMode (57-79)
  • AbstractBaseWriter (90-601)
  • save (205-216)
src/imgtools/coretypes/base_masks.py (2)
  • VectorMask (42-470)
  • iter_masks (190-203)
src/imgtools/coretypes/base_medimage.py (1)
  • MedImage (21-235)
src/imgtools/autopipeline.py (1)
  • process_one_sample (162-278)
src/imgtools/io/nnunet_output.py (5)
src/imgtools/io/sample_output.py (3)
  • FailedToSaveSingleImageError (29-34)
  • AnnotatedPathSequence (37-76)
  • writer (165-169)
src/imgtools/io/writers/nifti_writer.py (1)
  • NIFTIWriter (34-224)
src/imgtools/io/writers/abstract_base_writer.py (1)
  • ExistingFileMode (57-79)
src/imgtools/coretypes/base_masks.py (4)
  • to_label_image (285-312)
  • to_sparse_mask (263-283)
  • to_region_mask (314-354)
  • VectorMask (42-470)
src/imgtools/coretypes/imagetypes/scan.py (1)
  • Scan (50-148)
🔇 Additional comments (6)
src/imgtools/io/sample_output.py (3)

201-201: Comment accuracy: ExistingFileMode.FAIL is appropriate for temp directories

The inline comment "Should not have conflicts in a new temp dir" is correct. Since each transaction gets a unique temporary directory via mkdtemp, file conflicts are impossible, making FAIL the right choice.


238-241: Good atomic commit implementation!

The two-phase commit pattern (staging to temp, then moving to final) ensures atomicity. Using shutil.move is appropriate as it provides atomic semantics when source and destination are on the same filesystem. The explicit directory creation before moving prevents potential failures.


250-253: Cleanup implementation looks solid

The finally block ensures cleanup even on exceptions, and the existence check prevents errors if temp_dir creation failed. Good defensive programming.

src/imgtools/io/nnunet_output.py (3)

361-365: Clever use of dictionary dispatch for mask strategy

The dictionary-based dispatch pattern elegantly maps strategies to their corresponding methods. This is more maintainable than if-elif chains.


396-400: Excellent atomic commit pattern

The staging and commit implementation properly ensures atomicity. Creating parent directories before moving and using shutil.move for atomic filesystem operations is the right approach.


410-412: Proper cleanup in finally block

The cleanup logic correctly ensures the temporary directory is removed even if an exception occurs, preventing accumulation of temporary files.

Comment on lines +367 to +368
if mask is None:
raise MaskSavingStrategyError(f"Unknown mask saving strategy: {self.mask_saving_strategy}")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Dead code: mask will never be None

The dictionary lookup on lines 361-365 will always return a callable method (to_label_image, to_sparse_mask, or to_region_mask) because self.mask_saving_strategy is validated as an enum. The .get() call with no default will return None only for invalid enum values, which can't happen. This check is unreachable.

-            if mask is None:
-                raise MaskSavingStrategyError(f"Unknown mask saving strategy: {self.mask_saving_strategy}")
+            # No need to check for None - enum validation ensures valid strategy

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/imgtools/io/nnunet_output.py around lines 367-368, the if-check raising
MaskSavingStrategyError when mask is None is dead code because the dict lookup
for mask saving strategy always returns a callable for validated enum values;
remove this unreachable if-block and its raise, and if you need a defensive
guarantee for static analysis keep a short assert like "assert callable(mask)"
immediately after the lookup (or use typing.cast) instead of the conditional
raise.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant