Closes #22104: Avoid retracing paths when deleting Cables (#22167)

This commit is contained in:
Jeremy Stretch 2026-05-13 09:50:32 -04:00 committed by GitHub
parent b2a1e94508
commit b1412514f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 44 additions and 0 deletions

View File

@ -1,5 +1,6 @@
import itertools
import logging
import threading
from collections import Counter
from django.contrib.contenttypes.fields import GenericForeignKey
@ -76,6 +77,11 @@ class Cable(PrimaryModel):
"""
A physical connection between two endpoints.
"""
# Per-thread tracking of Cable PKs currently in delete(); referenced by
# dcim.signals.nullify_connected_endpoints to skip per-CableTermination
# cable path retracing during cascade (retrace_cable_paths handles it once).
_deletion_tracking = threading.local()
type = models.CharField(
verbose_name=_('type'),
max_length=50,
@ -343,6 +349,26 @@ class Cable(PrimaryModel):
except UnsupportedCablePath as e:
raise AbortRequest(e)
def delete(self, *args, **kwargs):
# Track this Cable as being deleted so the post_delete signal handler
# for cascaded CableTerminations can skip redundant path retracing;
# retrace_cable_paths() will retrace each affected path once after the
# Cable itself is deleted. Cache the PK locally because super().delete()
# clears self.pk before the finally block runs. The tracking set lives
# on a threading.local() to isolate concurrent deletions across threads.
if not hasattr(Cable._deletion_tracking, 'pks'):
Cable._deletion_tracking.pks = set()
pk = self.pk
Cable._deletion_tracking.pks.add(pk)
try:
return super().delete(*args, **kwargs)
finally:
Cable._deletion_tracking.pks.discard(pk)
@classmethod
def _is_being_deleted(cls, pk):
return pk in getattr(cls._deletion_tracking, 'pks', ())
def clone(self):
"""
Return attributes suitable for cloning this cable.

View File

@ -185,6 +185,12 @@ def nullify_connected_endpoints(instance, **kwargs):
model = instance.termination_type.model_class()
model.objects.filter(pk=instance.termination_id).update(cable=None, cable_end='')
# If the parent Cable is being deleted in this same operation, skip the
# per-termination retrace; retrace_cable_paths() will retrace each affected
# path once after the Cable is deleted.
if Cable._is_being_deleted(instance.cable_id):
return
for cablepath in CablePath.objects.filter(_nodes__contains=instance.cable):
# Remove the deleted CableTermination if it's one of the path's originating nodes
if instance.termination in cablepath.origins:

View File

@ -55,6 +55,18 @@ class LegacyCablePathTestCase(CablePathTestCase):
# Check that all CablePaths have been deleted
self.assertEqual(CablePath.objects.count(), 0)
# Check that connected interfaces are fully cleaned up
interface1.refresh_from_db()
interface2.refresh_from_db()
self.assertIsNone(interface1.cable_id)
self.assertEqual(interface1.cable_end, '')
self.assertPathIsNotSet(interface1)
self.assertIsNone(interface2.cable_id)
self.assertEqual(interface2.cable_end, '')
self.assertPathIsNotSet(interface2)
def test_102_consoleport_to_consoleserverport(self):
"""
[CP1] --C1-- [CSP1]