diff --git a/netbox/utilities/counters.py b/netbox/utilities/counters.py index 589dacbdbc5..b396d4617c4 100644 --- a/netbox/utilities/counters.py +++ b/netbox/utilities/counters.py @@ -77,7 +77,7 @@ def post_delete_receiver(sender, instance, origin, **kwargs): parent_pk = getattr(instance, field_name, None) # Decrement the parent's counter by one - if parent_pk is not None and not hasattr(instance, "_previously_removed"): + if parent_pk is not None and not hasattr(instance, '_previously_removed'): update_counter(parent_model, parent_pk, counter_name, -1) @@ -87,38 +87,48 @@ def post_delete_receiver(sender, instance, origin, **kwargs): def connect_counters(*models): """ - Register counter fields and connect post_save & post_delete signal handlers for the affected models. + Register counter fields and connect signal handlers for their child models. + Ensures exactly one receiver per child (sender), even when multiple counters + reference the same sender (e.g., Device). """ - for model in models: + connected = set() # child models we've already connected + for model in models: # Find all CounterCacheFields on the model - counter_fields = [ - field for field in model._meta.get_fields() if type(field) is CounterCacheField - ] + counter_fields = [field for field in model._meta.get_fields() if isinstance(field, CounterCacheField)] for field in counter_fields: to_model = apps.get_model(field.to_model_name) # Register the counter in the registry change_tracking_fields = registry['counter_fields'][to_model] - change_tracking_fields[f"{field.to_field_name}_id"] = field.name + change_tracking_fields[f'{field.to_field_name}_id'] = field.name + + # Connect signals once per child model + if to_model in connected: + continue + + # Ensure dispatch_uid is unique per model (sender), not per field + uid_base = f'countercache.{to_model._meta.label_lower}' # Connect the post_save and post_delete handlers post_save.connect( post_save_receiver, sender=to_model, weak=False, - dispatch_uid=f'{model._meta.label}.{field.name}' + dispatch_uid=f'{uid_base}.post_save', ) pre_delete.connect( pre_delete_receiver, sender=to_model, weak=False, - dispatch_uid=f'{model._meta.label}.{field.name}' + dispatch_uid=f'{uid_base}.pre_delete', ) post_delete.connect( post_delete_receiver, sender=to_model, weak=False, - dispatch_uid=f'{model._meta.label}.{field.name}' + dispatch_uid=f'{uid_base}.post_delete', ) + + connected.add(to_model)