Plugin Version
0.6.0
NetBox Version
4.6.2
Python Version
3.12
Steps to Reproduce
I am encountering a RecursionError: maximum recursion depth exceeded when accessing Custom Object Types which have relational links involving polymorphic fields.
This appears to be related to the dynamic model/relation graph recursion described in #685 but the recursion enters through a different path.
In my case, NetBox starts normally. The error occurs later when acting upon a dynamically generated Custom Object model which has relational links, including relationships multiple nodes away. That is, attempting to save, modify, or delete any field on the COT or COTs on a related node upon graph construction causes infinite recursion.
The significant schema changes immediately preceding the problem were:
- Changing
internal_name values on Custom Object Type Fields
- Explicitly setting/changing
related_name values on relational fields, including polymorphic fields
No other structural schema changes were made and my schema was previously stable and functional.
Expected Behavior
Various related names can be successfully saved on polymorphic fields of COTs.
Observed Behavior
The problem appears to be associated with polymorphic relationship handling and the reverse-descriptor wiring introduced/fixed in the current plugin code. The recursion follows this path:
django.db.models.options.Options._relation_tree
↓
django.db.models.options.Options._populate_directed_relation_graph
↓
django.apps.registry.Apps.get_models
↓
netbox_custom_objects.CustomObjectsPluginConfig.get_models
↓
CustomObjectType.get_model
↓
CustomObjectType._after_model_generation
↓
_wire_polymorphic_reverse_descriptors
↓
field_instance.related_object_types.all()
↓
Django QuerySet evaluation
↓
Query.add_q()
↓
Query.names_to_path()
↓
Options.get_field()
↓
Options.fields_map
↓
Options._get_fields
↓
Options._relation_tree
↓
django.apps.registry.Apps.get_models
↓
...
The relevant portion of the traceback is:
File ".../django/db/models/options.py", line 810, in _populate_directed_relation_graph
all_models = self.apps.get_models(include_auto_created=True)
File ".../django/apps/registry.py", line 185, in get_models
result.extend(app_config.get_models(include_auto_created, include_swapped))
File ".../netbox_custom_objects/init.py", line 563, in get_models
model = custom_type.get_model()
File ".../netbox_custom_objects/models.py", line 1751, in get_model
self._after_model_generation(attrs, model)
File ".../netbox_custom_objects/models.py", line 1495, in _after_model_generation
_wire_polymorphic_reverse_descriptors(inbound_field)
File ".../netbox_custom_objects/models.py", line 3768, in _wire_polymorphic_reverse_descriptors
for ot in field_instance.related_object_types.all():
File ".../django/db/models/query.py", line 390, in iter
self._fetch_all()
...
File ".../django/db/models/options.py", line 843, in _relation_tree
return self._populate_directed_relation_graph()
File ".../django/db/models/options.py", line 810, in _populate_directed_relation_graph
all_models = self.apps.get_models(include_auto_created=True)
File ".../django/apps/registry.py", line 185, in get_models
result.extend(app_config.get_models(include_auto_created, include_swapped))
File ".../netbox_custom_objects/init.py", line 563, in get_models
model = custom_type.get_model()
...
This then repeats until Python raises maximum recursion depth exceeded. I am not sure if it is related to the number of of polymorphic fields that we are using or the structure, but I have verified that we have no structural integrity issues with the COTs themselves and the related_names that we are setting are all unique per reverse target related COT.
Suspected Cause
The immediate point of re-entry appears to be _wire_polymorphic_reverse_descriptors():
def _wire_polymorphic_reverse_descriptors(field_instance):
"""..."""
if not field_instance.related_name:
return
descriptor = _make_reverse_descriptor(field_instance)
if descriptor is None:
return
related_name = field_instance.related_name
for ot in field_instance.related_object_types.all():
target_cls = ot.model_class()
if target_cls is None:
continue
existing = getattr(target_cls, related_name, None)
if existing is not None:
if not isinstance(
existing,
(
PolymorphicObjectReverseDescriptor,
PolymorphicMultiObjectReverseDescriptor,
),
):
continue
if not _descriptor_matches_field(existing, field_instance):
continue
setattr(target_cls, related_name, descriptor)
Pertinently, the recursion seems to occur while evaluating field_instance.related_object_types.all() as the ORM operation eventually requires Django to resolve model metadata and populate _relation_tree, which calls apps.get_models(). The Custom Objects plugin overrides get_models() for dynamic registration/reconstruction and consequently begins generating COT models again. I also inspected the generated model metadata for one of my affected COTs, end_user. For a reverse relation from COT id 30 (teams_voice_end_user_delegation) to COT id 25 (end_user) duplicate relation objects are reported from nbshell when examining a particular reverse relation:
relations = [
r
for r in model._meta.related_objects
if r.name == "delegatees"
]
print(len(relations))
Result: 2
Both entries were the same underlying relation object:
Relation 1
object id : 139759371543776
relation class : ManyToManyRel
source field : delegatee
source model : <class 'database.models.Table30Model'>
target model : <class 'database.models.Table30Model'>
related_name : delegatees
field object id: 139759371546464
Relation 2
object id : 139759371543776
relation class : ManyToManyRel
source field : delegatee
source model : <class 'database.models.Table30Model'>
target model : <class 'database.models.Table30Model'>
related_name : delegatees
field object id: 139759371546464
I also found multiple duplicated field/relation object IDs when inspecting the result of _meta.get_fields(). I had manually assigned related_name values to a number of relational fields. A query of the COT fields shows several names being used by multiple fields. For example,
RELATED NAME: assigned_caller_id_policy
COT 28 - Field 562 - assigned_shared_device_accounts --> table27model
COT 28 - Field 563 - assigned_end_users --> table25model
COT 28 - Field 568 - assigned_groups --> table29model
RELATED NAME: auto_attendant
COT 35 - Field 682 - business_hours --> table33model
COT 35 - Field 699 - resource_accounts --> table26model
RELATED NAME: auto_attendants
COT 35 - Field 668 - tenant_groups --> tenancy.tenant_group
COT 35 - Field 669 - tenants --> tenancy.tenant
COT 35 - Field 693 - holidays_list --> table31model
I first suspected that I might have related_name conflicts. However, after verifying that all of my related names per target model were distinct, the behavior appears more closely related to the fact that a non-empty related_name causes _wire_polymorphic_reverse_descriptors() to execute, even on non-polymorphic relations:
if not field_instance.related_name:
return
The entry path for this issue is slightly different than what is described in #685 . In that issue, the recursion described is:
get_models()
-> get_model()
-> _after_model_generation()
-> ObjectType.objects.get_for_model()
-> ObjectType.objects.create()
-> model._meta._reverse_on_to_one_field_names
-> _relation_tree
-> get_models()
-> ....
The recursion observed in this case is:
get_models()
-> get_model()
-> _after_model_generation()
-> _wire_polymorphic_reverse_descriptors()
-> related_object_types.all()
-> ...Django relation metadata...
-> _relation_tree
-> get_models()
-> ....
So, while both failures appear to involve the same fundamental re-entrancy problem, this one appears to expose another path into it through polymorphic reverse-descriptor wiring. Once I removed the related_name values from all of my polymorphic fields on COTs, my COT structure stabilized and I do not receive the error any more.
Could _wire_polymorphic_reverse_descriptors() be causing Django's relation graph to be recursively re-entered by evaluating field_instance.related_object_types.all() while get_models() is itself being invoked as part of _relation_tree construction? If so, would this need a possible re-entrancy guard similar to the proposal from #685 ?
I am happy to provide the affected COT/field definitions and a complete dependency graph if that would help reproduce the issue on your end.
Proposed Fix
No response
Plugin Version
0.6.0
NetBox Version
4.6.2
Python Version
3.12
Steps to Reproduce
I am encountering a RecursionError:
maximum recursion depth exceededwhen accessing Custom Object Types which have relational links involving polymorphic fields.This appears to be related to the dynamic model/relation graph recursion described in #685 but the recursion enters through a different path.
In my case, NetBox starts normally. The error occurs later when acting upon a dynamically generated Custom Object model which has relational links, including relationships multiple nodes away. That is, attempting to save, modify, or delete any field on the COT or COTs on a related node upon graph construction causes infinite recursion.
The significant schema changes immediately preceding the problem were:
internal_namevalues on Custom Object Type Fieldsrelated_namevalues on relational fields, including polymorphic fieldsNo other structural schema changes were made and my schema was previously stable and functional.
Expected Behavior
Various related names can be successfully saved on polymorphic fields of COTs.
Observed Behavior
The problem appears to be associated with polymorphic relationship handling and the reverse-descriptor wiring introduced/fixed in the current plugin code. The recursion follows this path:
django.db.models.options.Options._relation_tree↓
django.db.models.options.Options._populate_directed_relation_graph↓
django.apps.registry.Apps.get_models↓
netbox_custom_objects.CustomObjectsPluginConfig.get_models↓
CustomObjectType.get_model↓
CustomObjectType._after_model_generation↓
_wire_polymorphic_reverse_descriptors↓
field_instance.related_object_types.all()↓
Django QuerySet evaluation↓
Query.add_q()↓
Query.names_to_path()↓
Options.get_field()↓
Options.fields_map↓
Options._get_fields↓
Options._relation_tree↓
django.apps.registry.Apps.get_models↓
...
The relevant portion of the traceback is:
File ".../django/db/models/options.py", line 810, in _populate_directed_relation_graph
all_models = self.apps.get_models(include_auto_created=True)
File ".../django/apps/registry.py", line 185, in get_models
result.extend(app_config.get_models(include_auto_created, include_swapped))
File ".../netbox_custom_objects/init.py", line 563, in get_models
model = custom_type.get_model()
File ".../netbox_custom_objects/models.py", line 1751, in get_model
self._after_model_generation(attrs, model)
File ".../netbox_custom_objects/models.py", line 1495, in _after_model_generation
_wire_polymorphic_reverse_descriptors(inbound_field)
File ".../netbox_custom_objects/models.py", line 3768, in _wire_polymorphic_reverse_descriptors
for ot in field_instance.related_object_types.all():
File ".../django/db/models/query.py", line 390, in iter
self._fetch_all()
...
File ".../django/db/models/options.py", line 843, in _relation_tree
return self._populate_directed_relation_graph()
File ".../django/db/models/options.py", line 810, in _populate_directed_relation_graph
all_models = self.apps.get_models(include_auto_created=True)
File ".../django/apps/registry.py", line 185, in get_models
result.extend(app_config.get_models(include_auto_created, include_swapped))
File ".../netbox_custom_objects/init.py", line 563, in get_models
model = custom_type.get_model()
...
This then repeats until Python raises
maximum recursion depth exceeded. I am not sure if it is related to the number of of polymorphic fields that we are using or the structure, but I have verified that we have no structural integrity issues with the COTs themselves and the related_names that we are setting are all unique per reverse target related COT.Suspected Cause
The immediate point of re-entry appears to be
_wire_polymorphic_reverse_descriptors():Pertinently, the recursion seems to occur while evaluating
field_instance.related_object_types.all()as the ORM operation eventually requires Django to resolve model metadata and populate_relation_tree, which callsapps.get_models(). The Custom Objects plugin overridesget_models()for dynamic registration/reconstruction and consequently begins generating COT models again. I also inspected the generated model metadata for one of my affected COTs,end_user. For a reverse relation from COT id 30 (teams_voice_end_user_delegation) to COT id 25 (end_user) duplicate relation objects are reported fromnbshellwhen examining a particular reverse relation:Result: 2
Both entries were the same underlying relation object:
Relation 1
object id : 139759371543776
relation class : ManyToManyRel
source field : delegatee
source model : <class 'database.models.Table30Model'>
target model : <class 'database.models.Table30Model'>
related_name : delegatees
field object id: 139759371546464
Relation 2
object id : 139759371543776
relation class : ManyToManyRel
source field : delegatee
source model : <class 'database.models.Table30Model'>
target model : <class 'database.models.Table30Model'>
related_name : delegatees
field object id: 139759371546464
I also found multiple duplicated field/relation object IDs when inspecting the result of
_meta.get_fields(). I had manually assignedrelated_namevalues to a number of relational fields. A query of the COT fields shows several names being used by multiple fields. For example,RELATED NAME:
assigned_caller_id_policyCOT 28 - Field 562 -
assigned_shared_device_accounts-->table27modelCOT 28 - Field 563 -
assigned_end_users-->table25modelCOT 28 - Field 568 -
assigned_groups-->table29modelRELATED NAME:
auto_attendantCOT 35 - Field 682 -
business_hours-->table33modelCOT 35 - Field 699 -
resource_accounts-->table26modelRELATED NAME:
auto_attendantsCOT 35 - Field 668 -
tenant_groups-->tenancy.tenant_groupCOT 35 - Field 669 -
tenants-->tenancy.tenantCOT 35 - Field 693 -
holidays_list-->table31modelI first suspected that I might have
related_nameconflicts. However, after verifying that all of my related names per target model were distinct, the behavior appears more closely related to the fact that a non-emptyrelated_namecauses_wire_polymorphic_reverse_descriptors()to execute, even on non-polymorphic relations:The entry path for this issue is slightly different than what is described in #685 . In that issue, the recursion described is:
The recursion observed in this case is:
So, while both failures appear to involve the same fundamental re-entrancy problem, this one appears to expose another path into it through polymorphic reverse-descriptor wiring. Once I removed the
related_namevalues from all of my polymorphic fields on COTs, my COT structure stabilized and I do not receive the error any more.Could
_wire_polymorphic_reverse_descriptors()be causing Django's relation graph to be recursively re-entered by evaluatingfield_instance.related_object_types.all()whileget_models()is itself being invoked as part of_relation_treeconstruction? If so, would this need a possible re-entrancy guard similar to the proposal from #685 ?I am happy to provide the affected COT/field definitions and a complete dependency graph if that would help reproduce the issue on your end.
Proposed Fix
No response