Skip to content

Commit 26ef957

Browse files
committed
lock get_model
1 parent bebb504 commit 26ef957

File tree

2 files changed

+43
-9
lines changed

2 files changed

+43
-9
lines changed

netbox_custom_objects/models.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import decimal
22
import re
3+
import threading
34
from datetime import date, datetime
5+
from functools import wraps
46

57
import django_filters
68
from core.models import ObjectType, ObjectChange
@@ -63,6 +65,23 @@ class UniquenessConstraintTestError(Exception):
6365
USER_TABLE_DATABASE_NAME_PREFIX = "custom_objects_"
6466

6567

68+
def thread_safe_model_generation(func):
69+
"""
70+
Decorator to ensure thread-safe model generation.
71+
72+
This decorator prevents race conditions when multiple threads try to generate
73+
the same custom object model simultaneously. It uses a class-level reentrant
74+
lock to ensure only one thread can generate a model at a time, while allowing
75+
recursive calls from the same thread (e.g., during Django startup).
76+
"""
77+
@wraps(func)
78+
def wrapper(self, *args, **kwargs):
79+
# Use the class-level lock for thread safety
80+
with self._model_cache_lock:
81+
return func(self, *args, **kwargs)
82+
return wrapper
83+
84+
6685
class CustomObject(
6786
BookmarksMixin,
6887
ChangeLoggingMixin,
@@ -165,6 +184,7 @@ class CustomObjectType(PrimaryModel):
165184
_through_model_cache = (
166185
{}
167186
) # Now stores {custom_object_type_id: {through_model_name: through_model}}
187+
_model_cache_lock = threading.RLock() # Reentrant lock for model cache operations (allows recursive calls)
168188
name = models.CharField(
169189
max_length=100,
170190
unique=True,
@@ -437,6 +457,7 @@ def register_custom_object_search_index(self, model):
437457
label = f"{APP_LABEL}.{self.get_table_model_name(self.id).lower()}"
438458
registry["search"][label] = search_index
439459

460+
@thread_safe_model_generation
440461
def get_model(
441462
self,
442463
skip_object_fields=False,
@@ -451,11 +472,12 @@ def get_model(
451472
:rtype: Model
452473
"""
453474

454-
# Check if we have a cached model for this CustomObjectType
475+
# Double-check pattern: check cache again after acquiring lock
455476
if self.is_model_cached(self.id):
456477
model = self.get_cached_model(self.id)
457478
return model
458479

480+
# Generate the model inside the lock to prevent race conditions
459481
model_name = self.get_table_model_name(self.pk)
460482

461483
# TODO: Add other fields with "index" specified

netbox_custom_objects/templatetags/custom_object_utils.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,34 @@ def get_field_type_verbose_name(field: CustomObjectTypeField) -> str:
2828

2929
@register.filter(name="get_field_value")
3030
def get_field_value(obj, field: CustomObjectTypeField) -> str:
31-
return getattr(obj, field.name)
31+
try:
32+
return getattr(obj, field.name)
33+
except AttributeError:
34+
# Handle race condition where model doesn't have the field yet
35+
return ""
3236

3337

3438
@register.filter(name="get_field_is_ui_visible")
3539
def get_field_is_ui_visible(obj, field: CustomObjectTypeField) -> bool:
3640
if field.ui_visible == CustomFieldUIVisibleChoices.ALWAYS:
3741
return True
38-
if field.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
39-
field_value = getattr(obj, field.name).exists()
40-
else:
41-
field_value = getattr(obj, field.name)
42-
if field.ui_visible == CustomFieldUIVisibleChoices.IF_SET and field_value:
43-
return True
42+
try:
43+
if field.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
44+
field_value = getattr(obj, field.name).exists()
45+
else:
46+
field_value = getattr(obj, field.name)
47+
if field.ui_visible == CustomFieldUIVisibleChoices.IF_SET and field_value:
48+
return True
49+
except AttributeError:
50+
# Handle race condition where model doesn't have the field yet
51+
pass
4452
return False
4553

4654

4755
@register.filter(name="get_child_relations")
4856
def get_child_relations(obj, field: CustomObjectTypeField):
49-
return getattr(obj, field.name).all()
57+
try:
58+
return getattr(obj, field.name).all()
59+
except AttributeError:
60+
# Handle race condition where model doesn't have the field yet
61+
return []

0 commit comments

Comments
 (0)