Skip to content

Conversation

@samuelvkwong
Copy link
Collaborator

@samuelvkwong samuelvkwong commented Jan 13, 2026

Summary

Fixes a critical bug where DICOM connection retries would fail with AssertionError: A former connection was not closed properly. This prevented the stamina retry mechanism from recovering from transient network errors.

Problem

The abort_connection() method in DimseConnector was not resetting self.assoc = None after aborting a connection. When a DICOM operation failed and the retry mechanism attempted to reconnect, open_connection() would detect the stale association reference and raise an AssertionError, preventing any retries from succeeding.

Error Stack Trace

File "/app/adit/core/utils/dimse_connector.py", line 134, in open_connection
    raise AssertionError("A former connection was not closed properly.")
AssertionError: A former connection was not closed properly.

Root Cause

Inconsistency between two connection cleanup methods:

  • close_connection() (line 202): Correctly sets self.assoc = None after releasing
  • abort_connection() (line 207): Missing self.assoc = None after aborting

This caused the @connect_to_server decorator's retry logic to fail because self.assoc and self.assoc.is_alive() check would find a non-None association that was no longer alive.

Solution

Added self.assoc = None to abort_connection() after calling abort(), matching the behavior of close_connection(). This allows the retry decorator to correctly detect that no active connection exists and establish a new one.

Code Change

def abort_connection(self):
    if self.assoc:
        logger.debug("Aborting connection to DICOM server %s.", self.server.ae_title)
        self.assoc.abort()
        self.assoc = None  # Added this line

Testing

  • Added comprehensive test suite in test_dimse_connector.py with 4 tests
  • test_abort_connection_resets_assoc_allowing_retry: Verifies retry works after abort
  • test_close_connection_properly_resets_assoc: Confirms normal close path works
  • test_open_connection_fails_if_previous_not_closed: Validates the safety check
  • test_abort_connection_with_no_connection: Tests edge case handling
  • All tests pass ✅

Impact

  • Before: Transient network errors would cause task failures even with 5 retry attempts configured
  • After: Retry mechanism works as intended, recovering from transient failures

Related

Resolves the error reported in the production logs where DICOM C-GET operations were failing with connection cleanup assertions.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced connection state management for improved recovery from transient DICOM server communication failures
  • Tests
    • Added comprehensive unit tests validating connection retry behavior and lifecycle management edge cases

✏️ Tip: You can customize this high-level summary in your review settings.

## Problem
When abort_connection() was called during error handling, it would abort
the DICOM association but fail to reset self.assoc to None. This caused
subsequent retry attempts to fail with AssertionError("A former connection
was not closed properly.") because open_connection() detected a stale
association reference.

## Root Cause
The abort_connection() method (line 204-207) was missing the critical
`self.assoc = None` cleanup that close_connection() correctly performed
(line 202). This inconsistency prevented the stamina retry mechanism from
working properly - retries would fail immediately at the connection level
instead of establishing a new connection.

## Solution
Added `self.assoc = None` to abort_connection() after calling abort(),
matching the behavior of close_connection(). This allows retry decorators
to detect that no connection exists and successfully open a new connection
on retry attempts.

## Testing
- Added comprehensive test suite in test_dimse_connector.py
- Tests verify retry behavior works correctly with the fix
- All existing tests continue to pass

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Jan 13, 2026

📝 Walkthrough

Walkthrough

A 1-line fix to DimseConnector.abort_connection now clears the association reference by setting self.assoc = None. A new comprehensive test module verifies retry behavior after retriable errors, proper closure resets, and connection lifecycle edge cases using mocks and fixtures.

Changes

Cohort / File(s) Summary
Production Fix
adit/core/utils/dimse_connector.py
abort_connection method now sets self.assoc = None to reset association state after aborting.
Test Coverage
adit/core/tests/utils/test_dimse_connector.py
New test module with TestDimseConnectorRetry and TestDimseConnectorConnectionLifecycle classes. Validates retry flow recovery, association lifecycle (abort/release), and edge cases like operations without active connections. Uses mocks for AE.associate, association objects, and C-GET responses.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A rabbit hops through retry flows so neat,
With associations freed when errors greet,
Each test case mocked and staged with care,
Connection lifecycle—handled fair! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main change: fixing DimseConnector retry failure by resetting assoc in abort_connection, which is the core bug fix in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d37e04 and 38e14f0.

📒 Files selected for processing (2)
  • adit/core/tests/utils/test_dimse_connector.py
  • adit/core/utils/dimse_connector.py
🧰 Additional context used
🧬 Code graph analysis (1)
adit/core/tests/utils/test_dimse_connector.py (4)
adit/core/errors.py (1)
  • RetriableDicomError (14-28)
adit/core/utils/dicom_dataset.py (4)
  • QueryDataset (140-252)
  • create (165-197)
  • PatientID (26-27)
  • StudyInstanceUID (53-54)
adit/core/utils/dimse_connector.py (4)
  • DimseConnector (109-471)
  • send_c_get (264-293)
  • open_connection (132-140)
  • abort_connection (204-208)
adit/core/utils/testing_helpers.py (2)
  • create_association_mock (69-72)
  • create_successful_c_get_response (51-56)
🪛 Ruff (0.14.10)
adit/core/tests/utils/test_dimse_connector.py

43-43: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: claude-review
  • GitHub Check: ci
🔇 Additional comments (6)
adit/core/utils/dimse_connector.py (1)

204-208: LGTM! This fix correctly resets the association state after abort.

The addition of self.assoc = None after self.assoc.abort() ensures the connector properly cleans up its state, mirroring the behavior in close_connection(). This allows the connect_to_server decorator's retry path to successfully re-establish connections after transient failures.

adit/core/tests/utils/test_dimse_connector.py (5)

1-16: Good test file organization.

Clean imports and appropriate use of pytest markers with Django DB integration. The test class naming clearly communicates the test intent.


18-91: Well-structured retry test with comprehensive assertions.

The test effectively simulates the retry scenario by:

  1. Creating associations that track their lifecycle via abort_side_effect
  2. Using nonlocal call_count to control first-fail/then-succeed behavior
  3. Verifying the complete association lifecycle (abort on first, release on second)

The assertions at lines 83-91 properly validate the fix by confirming two associations were created and the final state is clean.


93-131: Good coverage of the normal close path.

This test validates that the existing close_connection() behavior remains correct. The assertions confirm the association is properly released and connector.assoc is reset to None.


137-148: Effective edge case test.

This test validates the defensive check in open_connection() that prevents connection leaks. Using pytest.raises with match ensures the correct exception is raised.


150-160: Good idempotency test.

This test validates that abort_connection() safely handles the case when no connection exists, ensuring the if self.assoc: guard works correctly.


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 and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello @samuelvkwong, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves a significant issue preventing DICOM operation retries from functioning as intended. Previously, transient network failures would lead to an AssertionError during retry attempts because the DimseConnector retained a stale association reference. The change ensures that connections are properly cleared upon abortion, allowing the retry mechanism to successfully establish new connections and recover from temporary disruptions, thereby enhancing the reliability of DICOM communications.

Highlights

  • Critical Bug Fix: Fixed a critical bug in DimseConnector where DICOM connection retries would fail with an AssertionError due to an improperly closed association, preventing recovery from transient network errors.
  • Connection Reset Logic: Ensured that the abort_connection() method now correctly resets self.assoc = None after aborting a DICOM association, aligning its behavior with close_connection() and allowing the retry mechanism to establish new connections.
  • Comprehensive Testing: Introduced a comprehensive test suite in test_dimse_connector.py with four new tests to validate the fix, confirm proper connection lifecycle management, and cover edge cases.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request addresses a critical bug where DICOM connection retries failed due to an improperly cleared association reference in abort_connection. The fix is simple and correct, adding self.assoc = None to ensure the connection state is properly reset. The accompanying new test suite is comprehensive, verifying the fix, confirming existing behavior, and testing edge cases. My review found the code changes to be of high quality. I've only added a couple of minor suggestions in the new test file to improve mocking consistency.

class TestDimseConnectorRetry:
"""Test retry behavior and connection cleanup in DimseConnector."""

@patch("adit.core.utils.retry_config.settings.ENABLE_STAMINA_RETRY", True)

Choose a reason for hiding this comment

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

medium

For consistency with the use of the mocker fixture from pytest-mock throughout the test, consider using mocker.patch to modify the settings instead of the unittest.mock.patch decorator. This keeps the mocking style uniform within the test function.

For example, you could remove this decorator and add the following line at the beginning of the test body:

mocker.patch("adit.core.utils.retry_config.settings.ENABLE_STAMINA_RETRY", True)

# Operation should have succeeded on retry
assert call_count == 2

@patch("adit.core.utils.retry_config.settings.ENABLE_STAMINA_RETRY", True)

Choose a reason for hiding this comment

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

medium

For consistency with the use of the mocker fixture from pytest-mock throughout the test, consider using mocker.patch to modify the settings instead of the unittest.mock.patch decorator. This keeps the mocking style uniform within the test function.

For example, you could remove this decorator and add the following line at the beginning of the test body:

mocker.patch("adit.core.utils.retry_config.settings.ENABLE_STAMINA_RETRY", True)

@claude
Copy link

claude bot commented Jan 13, 2026

Code Review - PR #283: Fix DimseConnector retry failure

Summary

This PR fixes a critical bug where DICOM connection retries would fail after abort_connection() due to stale association references. The fix is minimal, correct, and well-tested. Approved with minor suggestions.


✅ Strengths

  1. Excellent Problem Analysis: The PR description clearly explains the root cause with line numbers and stack traces. The comparison between close_connection() and abort_connection() makes the bug obvious.

  2. Minimal, Surgical Fix: Single line addition (self.assoc = None) is exactly what's needed. No over-engineering or scope creep.

  3. Comprehensive Test Coverage: Four well-structured tests covering happy path with retry, normal close path, assertion validation, and edge cases.

  4. Good Test Design: Tests correctly use @patch to enable stamina retry for controlled testing. Mock setup properly simulates failure-then-success scenario.


🔍 Code Quality Assessment

The Fix (dimse_connector.py:208) - Rating: 5/5

  • Correct, consistent with existing code, properly placed
  • Matches the pattern in close_connection() perfectly

Test Quality (test_dimse_connector.py) - Rating: 4.5/5

  • Comprehensive and well-structured

🐛 Potential Issues

None identified. The fix is correct and safe.


🎯 Suggestions for Improvement

1. Consider Refactoring Duplicate Code (Low Priority)
The @connect_to_server decorator has duplicate connection management logic in gen_wrapper and func_wrapper (lines 65-100). Consider extracting connection lifecycle management to reduce duplication in a future PR.

2. Test Mock Complexity (Minor)
In test_abort_connection_resets_assoc_allowing_retry, consider adding a docstring comment explaining why is_alive() needs to transition from True → False during abort to help future maintainers.

3. Test Documentation Enhancement (Minor)
The test docstrings are clear but could be more concise. Consider simplifying to focus on the regression being tested.


🔒 Security Considerations

No concerns. This is defensive code that prevents stale state - actually improves reliability.


⚡ Performance Considerations

Positive impact. The fix enables proper retry behavior, reducing task failures and improving overall system resilience. No performance regression risk.


📊 Test Coverage

Excellent. All critical paths covered:

  • ✅ Main bug scenario (retry after abort)
  • ✅ Normal path (close)
  • ✅ Error detection (stale connection assertion)
  • ✅ Edge case (abort with no connection)

Coverage delta: +160 lines (all test code), 0 uncovered lines in implementation.


📋 Alignment with Project Standards

Checking against CLAUDE.md guidelines:

  • ✅ Google Python Style Guide: Followed
  • ✅ Line Length: 100 chars (compliant)
  • ✅ Type Hints: Not needed for simple None assignment
  • ✅ Testing Framework: pytest with proper fixtures and mocking
  • ✅ Django Conventions: N/A (no model changes)
  • ✅ Docstrings: Present and clear

🎓 Architecture Fit

This fix correctly handles the network layer retry (stamina) as documented in retry_config.py. The bug was preventing stamina from executing its retry strategy. Now it works as designed:

Network error → abort_connection() → self.assoc=None → open_connection() succeeds → retry continues

✅ Final Verdict

APPROVED - This is an excellent bug fix PR that:

  1. Solves a real production issue
  2. Uses minimal, correct code
  3. Includes comprehensive tests
  4. Has clear documentation
  5. Follows project conventions

The suggestions above are minor polish opportunities, not blockers.


🤖 Review completed using repository guidelines from CLAUDE.md

@samuelvkwong samuelvkwong merged commit 14ed626 into main Jan 13, 2026
6 checks passed
@samuelvkwong samuelvkwong deleted the fix/dimse-connector-abort-assoc-reset branch January 13, 2026 15:04
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.

2 participants