Repository: pydicom/pynetdicom
Pull request #468 (2020-03-13 06:04:20)

[MRG] Fix sending two A-ABORT messages
#### Reference issue
Related to #460

#### Tasks
 - [x] Unit tests added that reproduce issue or prove feature is working
 - [x] Fix or feature added
 - [x] Documentation and examples updated (if relevant)
 - [x] Unit tests passing and coverage at 100% after adding fix/feature



Patch:
diff --git a/docs/changelog/v1.5.0.rst b/docs/changelog/v1.5.0.rst
index 1c41bf8b9..e979ac6d1 100644
--- a/docs/changelog/v1.5.0.rst
+++ b/docs/changelog/v1.5.0.rst
@@ -24,6 +24,9 @@ Fixes
   transfer syntax rather than the first matching supported transfer syntax
   during presentation context negotiation (:issue:`445`)
 * Fixed incorrect Pending status value for Modality Worklist (:issue:`462`)
+* Fixed an ``InvalidEventError`` due to two A-ABORT messages being sent. Issue
+  is triggered when the DIMSE and network timeouts have the same value and
+  both expire while sending a DIMSE request. (:issue:`460`)
 
 
 Enhancements
diff --git a/pynetdicom/association.py b/pynetdicom/association.py
index cac8e1f56..6caa29011 100644
--- a/pynetdicom/association.py
+++ b/pynetdicom/association.py
@@ -107,6 +107,9 @@ class Association(threading.Thread):
         self.is_aborted = False
         self.is_released = False
 
+        # Track whether we've sent an abort or not for the abort() method
+        self._sent_abort = False
+
         # Accepted and rejected presentation contexts
         self._accepted_cx = {}
         self._rejected_cx = []
@@ -144,11 +147,18 @@ class Association(threading.Thread):
     def abort(self):
         """Send an A-ABORT to the remote AE and kill the :class:`Association`.
         """
+        # Only allow a single abort message to be sent
+        if self._sent_abort:
+            return
+
         if not self.is_released:
+            # Set before restarting the reactor to prevent race condition
+            self._sent_abort = True
             # Ensure the reactor is running so it can be exited
             self._reactor_checkpoint.set()
             LOGGER.info('Aborting Association')
             self.acse.send_abort(0x00)
+
             # Event handler - association aborted
             evt.trigger(self, evt.EVT_ABORTED, {})
             self.kill()


Test patch:
diff --git a/pynetdicom/tests/test_service_verification.py b/pynetdicom/tests/test_service_verification.py
index 6f62084b0..7f20379c0 100644
--- a/pynetdicom/tests/test_service_verification.py
+++ b/pynetdicom/tests/test_service_verification.py
@@ -373,3 +373,32 @@ class TestVerificationServiceClass(object):
 
         assert "DIMSE timeout reached" in caplog.text
         assert "Aborting Association" in caplog.text
+
+    def test_dimse_network_timeout(self, caplog):
+        """Regression test for #460: invalid second abort."""
+        def handle(event):
+            time.sleep(0.1)
+            return 0x0000
+
+        handlers = [(evt.EVT_C_ECHO, handle)]
+
+        self.ae = ae = AE()
+        ae.dimse_timeout = 0.05
+        ae.network_timeout = 0.05
+        ae.add_supported_context(VerificationSOPClass)
+        ae.add_requested_context(VerificationSOPClass)
+        scp = ae.start_server(('', 11112), block=False, evt_handlers=handlers)
+
+        assoc = ae.associate('localhost', 11112)
+        assert assoc.is_established
+        with caplog.at_level(logging.DEBUG, logger='pynetdicom'):
+            rsp = assoc.send_c_echo()
+        assert rsp == Dataset()
+
+        time.sleep(0.1)
+        assert assoc.is_aborted
+        scp.shutdown()
+
+        assert "Invalid event 'Evt15' for the current state" not in caplog.text
+        assert "DIMSE timeout reached" in caplog.text
+        assert "Aborting Association" in caplog.text