Skip to content

Commit 70bc1c2

Browse files
pheusjeremystretch
authored andcommitted
fix(utilities): Ensure unique signal handlers for counter models
Updates `connect_counters` to prevent duplicate signal handlers by using consistent `dispatch_uid` values per sender. Adds a check to avoid reconnecting models already processed during registration. Fixes #20697
1 parent 6a21459 commit 70bc1c2

File tree

1 file changed

+20
-10
lines changed

1 file changed

+20
-10
lines changed

netbox/utilities/counters.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def post_delete_receiver(sender, instance, origin, **kwargs):
7777
parent_pk = getattr(instance, field_name, None)
7878

7979
# Decrement the parent's counter by one
80-
if parent_pk is not None and not hasattr(instance, "_previously_removed"):
80+
if parent_pk is not None and not hasattr(instance, '_previously_removed'):
8181
update_counter(parent_model, parent_pk, counter_name, -1)
8282

8383

@@ -87,38 +87,48 @@ def post_delete_receiver(sender, instance, origin, **kwargs):
8787

8888
def connect_counters(*models):
8989
"""
90-
Register counter fields and connect post_save & post_delete signal handlers for the affected models.
90+
Register counter fields and connect signal handlers for their child models.
91+
Ensures exactly one receiver per child (sender), even when multiple counters
92+
reference the same sender (e.g., Device).
9193
"""
92-
for model in models:
94+
connected = set() # child models we've already connected
9395

96+
for model in models:
9497
# Find all CounterCacheFields on the model
95-
counter_fields = [
96-
field for field in model._meta.get_fields() if type(field) is CounterCacheField
97-
]
98+
counter_fields = [field for field in model._meta.get_fields() if isinstance(field, CounterCacheField)]
9899

99100
for field in counter_fields:
100101
to_model = apps.get_model(field.to_model_name)
101102

102103
# Register the counter in the registry
103104
change_tracking_fields = registry['counter_fields'][to_model]
104-
change_tracking_fields[f"{field.to_field_name}_id"] = field.name
105+
change_tracking_fields[f'{field.to_field_name}_id'] = field.name
106+
107+
# Connect signals once per child model
108+
if to_model in connected:
109+
continue
110+
111+
# Ensure dispatch_uid is unique per model (sender), not per field
112+
uid_base = f'countercache.{to_model._meta.label_lower}'
105113

106114
# Connect the post_save and post_delete handlers
107115
post_save.connect(
108116
post_save_receiver,
109117
sender=to_model,
110118
weak=False,
111-
dispatch_uid=f'{model._meta.label}.{field.name}'
119+
dispatch_uid=f'{uid_base}.post_save',
112120
)
113121
pre_delete.connect(
114122
pre_delete_receiver,
115123
sender=to_model,
116124
weak=False,
117-
dispatch_uid=f'{model._meta.label}.{field.name}'
125+
dispatch_uid=f'{uid_base}.pre_delete',
118126
)
119127
post_delete.connect(
120128
post_delete_receiver,
121129
sender=to_model,
122130
weak=False,
123-
dispatch_uid=f'{model._meta.label}.{field.name}'
131+
dispatch_uid=f'{uid_base}.post_delete',
124132
)
133+
134+
connected.add(to_model)

0 commit comments

Comments
 (0)