Skip to content

Commit 40fd512

Browse files
committed
more locking functions
1 parent 41c2c41 commit 40fd512

File tree

4 files changed

+78
-59
lines changed

4 files changed

+78
-59
lines changed

netbox_custom_objects/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,28 @@
11
import sys
2+
import threading
23
import warnings
4+
from functools import wraps
35

46
from django.db import transaction
57
from django.db.utils import DatabaseError, OperationalError, ProgrammingError
68
from netbox.plugins import PluginConfig
79

810
from .constants import APP_LABEL as APP_LABEL
911

12+
# Global lock for plugin ready method
13+
_plugin_ready_lock = threading.RLock()
14+
15+
16+
def thread_safe_plugin_ready(func):
17+
"""
18+
Decorator to ensure thread-safe plugin ready method execution.
19+
"""
20+
@wraps(func)
21+
def wrapper(self, *args, **kwargs):
22+
with _plugin_ready_lock:
23+
return func(self, *args, **kwargs)
24+
return wrapper
25+
1026

1127
def is_running_migration():
1228
"""
@@ -51,6 +67,7 @@ class CustomObjectsPluginConfig(PluginConfig):
5167
required_settings = []
5268
template_extensions = "template_content.template_extensions"
5369

70+
@thread_safe_plugin_ready
5471
def ready(self):
5572
from .models import CustomObjectType
5673
from netbox_custom_objects.api.serializers import get_serializer_class
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import threading
2+
from functools import wraps
3+
4+
5+
def thread_safe_model_generation(func):
6+
"""
7+
Decorator to ensure thread-safe model generation.
8+
9+
This decorator prevents race conditions when multiple threads try to generate
10+
the same custom object model simultaneously. It uses per-model locks to ensure
11+
only one thread can generate a specific model at a time, while allowing
12+
different models to be generated concurrently and preventing deadlocks.
13+
"""
14+
@wraps(func)
15+
def wrapper(self, *args, **kwargs):
16+
# Get or create a lock for this specific model
17+
with self._global_lock:
18+
if self.id not in self._model_cache_locks:
19+
self._model_cache_locks[self.id] = threading.RLock()
20+
model_lock = self._model_cache_locks[self.id]
21+
22+
# Use the per-model lock for thread safety
23+
with model_lock:
24+
return func(self, *args, **kwargs)
25+
return wrapper

netbox_custom_objects/models.py

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import re
33
import threading
44
from datetime import date, datetime
5-
from functools import wraps
65

76
import django_filters
87
from core.models import ObjectType, ObjectChange
@@ -52,6 +51,7 @@
5251
from utilities.validators import validate_regex
5352

5453
from netbox_custom_objects.constants import APP_LABEL, RESERVED_FIELD_NAMES
54+
from netbox_custom_objects.decorators import thread_safe_model_generation
5555
from netbox_custom_objects.field_types import FIELD_TYPE_CLASS
5656
from netbox_custom_objects.utilities import generate_model
5757

@@ -65,29 +65,6 @@ class UniquenessConstraintTestError(Exception):
6565
USER_TABLE_DATABASE_NAME_PREFIX = "custom_objects_"
6666

6767

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 per-model locks to ensure
74-
only one thread can generate a specific model at a time, while allowing
75-
different models to be generated concurrently and preventing deadlocks.
76-
"""
77-
@wraps(func)
78-
def wrapper(self, *args, **kwargs):
79-
# Get or create a lock for this specific model
80-
with self._global_lock:
81-
if self.id not in self._model_cache_locks:
82-
self._model_cache_locks[self.id] = threading.RLock()
83-
model_lock = self._model_cache_locks[self.id]
84-
85-
# Use the per-model lock for thread safety
86-
with model_lock:
87-
return func(self, *args, **kwargs)
88-
return wrapper
89-
90-
9168
class CustomObject(
9269
BookmarksMixin,
9370
ChangeLoggingMixin,
@@ -573,6 +550,14 @@ def wrapped_post_through_setup(self, cls):
573550

574551
return model
575552

553+
@thread_safe_model_generation
554+
def get_model_with_serializer(self):
555+
from netbox_custom_objects.api.serializers import get_serializer_class
556+
model = self.get_model()
557+
get_serializer_class(model)
558+
self.register_custom_object_search_index(model)
559+
return model
560+
576561
def create_model(self):
577562
# Get the model and ensure it's registered
578563
model = self.get_model()

netbox_custom_objects/views.py

Lines changed: 27 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -142,14 +142,6 @@ def get_table(self, data, request, bulk_actions=True):
142142
return super().get_table(data, request, bulk_actions=bulk_actions)
143143

144144

145-
class CustomObjectModelMixin:
146-
def get_model(self, custom_object_type):
147-
model = custom_object_type.get_model()
148-
get_serializer_class(model)
149-
custom_object_type.register_custom_object_search_index(model)
150-
return model
151-
152-
153145
#
154146
# Custom Object Types
155147
#
@@ -164,17 +156,17 @@ class CustomObjectTypeListView(generic.ObjectListView):
164156

165157

166158
@register_model_view(CustomObjectType)
167-
class CustomObjectTypeView(CustomObjectTableMixin, CustomObjectModelMixin, generic.ObjectView):
159+
class CustomObjectTypeView(CustomObjectTableMixin, generic.ObjectView):
168160
queryset = CustomObjectType.objects.all()
169161

170162
def get_table(self, data, request, bulk_actions=True):
171163
self.custom_object_type = self.get_object(**self.kwargs)
172-
model = self.get_model(self.custom_object_type)
164+
model = self.custom_object_type.get_model_with_serializer()
173165
data = model.objects.all()
174166
return super().get_table(data, request, bulk_actions=False)
175167

176168
def get_extra_context(self, request, instance):
177-
model = self.get_model(instance)
169+
model = instance.get_model_with_serializer()
178170

179171
# Get fields and group them by group_name
180172
fields = instance.fields.all().order_by("group_name", "weight", "name")
@@ -202,13 +194,13 @@ class CustomObjectTypeEditView(generic.ObjectEditView):
202194

203195

204196
@register_model_view(CustomObjectType, "delete")
205-
class CustomObjectTypeDeleteView(CustomObjectModelMixin, generic.ObjectDeleteView):
197+
class CustomObjectTypeDeleteView(generic.ObjectDeleteView):
206198
queryset = CustomObjectType.objects.all()
207199
default_return_url = "plugins:netbox_custom_objects:customobjecttype_list"
208200

209201
def _get_dependent_objects(self, obj):
210202
dependent_objects = super()._get_dependent_objects(obj)
211-
model = self.get_model(obj)
203+
model = obj.get_model_with_serializer()
212204
dependent_objects[model] = list(model.objects.all())
213205

214206
# Find CustomObjectTypeFields that reference this CustomObjectType
@@ -235,7 +227,7 @@ class CustomObjectTypeFieldEditView(generic.ObjectEditView):
235227

236228

237229
@register_model_view(CustomObjectTypeField, "delete")
238-
class CustomObjectTypeFieldDeleteView(CustomObjectModelMixin, generic.ObjectDeleteView):
230+
class CustomObjectTypeFieldDeleteView(generic.ObjectDeleteView):
239231
template_name = "netbox_custom_objects/field_delete.html"
240232
queryset = CustomObjectTypeField.objects.all()
241233

@@ -252,7 +244,7 @@ def get(self, request, *args, **kwargs):
252244
obj = self.get_object(**kwargs)
253245
form = ConfirmationForm(initial=request.GET)
254246

255-
model = self.get_model(obj.custom_object_type)
247+
model = obj.custom_object_type.get_model_with_serializer()
256248
kwargs = {
257249
f"{obj.name}__isnull": False,
258250
}
@@ -289,7 +281,7 @@ def get(self, request, *args, **kwargs):
289281

290282
def _get_dependent_objects(self, obj):
291283
dependent_objects = super()._get_dependent_objects(obj)
292-
model = self.get_model(obj.custom_object_type)
284+
model = obj.custom_object_type.get_model_with_serializer()
293285
kwargs = {
294286
f"{obj.name}__isnull": False,
295287
}
@@ -323,7 +315,7 @@ class CustomObjectTypeBulkDeleteView(generic.BulkDeleteView):
323315
#
324316

325317

326-
class CustomObjectListView(CustomObjectTableMixin, CustomObjectModelMixin, generic.ObjectListView):
318+
class CustomObjectListView(CustomObjectTableMixin, generic.ObjectListView):
327319
queryset = None
328320
custom_object_type = None
329321
template_name = "netbox_custom_objects/custom_object_list.html"
@@ -341,7 +333,7 @@ def get_queryset(self, request):
341333
self.custom_object_type = get_object_or_404(
342334
CustomObjectType, slug=custom_object_type
343335
)
344-
model = self.get_model(self.custom_object_type)
336+
model = self.custom_object_type.get_model_with_serializer()
345337
return model.objects.all()
346338

347339
def get_filterset(self):
@@ -379,23 +371,23 @@ def get_extra_context(self, request):
379371

380372

381373
@register_model_view(CustomObject)
382-
class CustomObjectView(CustomObjectModelMixin, generic.ObjectView):
374+
class CustomObjectView(generic.ObjectView):
383375
template_name = "netbox_custom_objects/customobject.html"
384376

385377
def get_queryset(self, request):
386378
custom_object_type = self.kwargs.get("custom_object_type", None)
387379
object_type = get_object_or_404(
388380
CustomObjectType, slug=custom_object_type
389381
)
390-
model = self.get_model(object_type)
382+
model = object_type.get_model_with_serializer()
391383
return model.objects.all()
392384

393385
def get_object(self, **kwargs):
394386
custom_object_type = self.kwargs.get("custom_object_type", None)
395387
object_type = get_object_or_404(
396388
CustomObjectType, slug=custom_object_type
397389
)
398-
model = self.get_model(object_type)
390+
model = object_type.get_model_with_serializer()
399391
# Filter out custom_object_type from kwargs for the object lookup
400392
lookup_kwargs = {
401393
k: v for k, v in self.kwargs.items() if k != "custom_object_type"
@@ -422,7 +414,7 @@ def get_extra_context(self, request, instance):
422414

423415

424416
@register_model_view(CustomObject, "edit")
425-
class CustomObjectEditView(CustomObjectModelMixin, generic.ObjectEditView):
417+
class CustomObjectEditView(generic.ObjectEditView):
426418
template_name = "netbox_custom_objects/customobject_edit.html"
427419
form = None
428420
queryset = None
@@ -445,7 +437,7 @@ def get_object(self, **kwargs):
445437
object_type = get_object_or_404(
446438
CustomObjectType, slug=custom_object_type
447439
)
448-
model = self.get_model(object_type)
440+
model = object_type.get_model_with_serializer()
449441

450442
if not self.kwargs.get("pk", None):
451443
# We're creating a new object
@@ -583,7 +575,7 @@ def custom_save(self, commit=True):
583575

584576

585577
@register_model_view(CustomObject, "delete")
586-
class CustomObjectDeleteView(CustomObjectModelMixin, generic.ObjectDeleteView):
578+
class CustomObjectDeleteView(generic.ObjectDeleteView):
587579
queryset = None
588580
object = None
589581
default_return_url = "plugins:netbox_custom_objects:customobject_list"
@@ -603,7 +595,7 @@ def get_object(self, **kwargs):
603595
object_type = get_object_or_404(
604596
CustomObjectType, slug=custom_object_type
605597
)
606-
model = self.get_model(object_type)
598+
model = object_type.get_model_with_serializer()
607599
return get_object_or_404(model.objects.all(), **self.kwargs)
608600

609601
def get_return_url(self, request, obj=None):
@@ -624,7 +616,7 @@ def get_return_url(self, request, obj=None):
624616

625617

626618
@register_model_view(CustomObject, "bulk_edit", path="edit", detail=False)
627-
class CustomObjectBulkEditView(CustomObjectTableMixin, CustomObjectModelMixin, generic.BulkEditView):
619+
class CustomObjectBulkEditView(CustomObjectTableMixin, generic.BulkEditView):
628620
queryset = None
629621
custom_object_type = None
630622
table = None
@@ -643,7 +635,7 @@ def get_queryset(self, request):
643635
self.custom_object_type = CustomObjectType.objects.get(
644636
slug=custom_object_type
645637
)
646-
model = self.get_model(self.custom_object_type)
638+
model = self.custom_object_type.get_model_with_serializer()
647639
return model.objects.all()
648640

649641
def get_form(self, queryset):
@@ -683,7 +675,7 @@ def get_form(self, queryset):
683675

684676

685677
@register_model_view(CustomObject, "bulk_delete", path="delete", detail=False)
686-
class CustomObjectBulkDeleteView(CustomObjectTableMixin, CustomObjectModelMixin, generic.BulkDeleteView):
678+
class CustomObjectBulkDeleteView(CustomObjectTableMixin, generic.BulkDeleteView):
687679
queryset = None
688680
custom_object_type = None
689681
table = None
@@ -701,12 +693,12 @@ def get_queryset(self, request):
701693
self.custom_object_type = CustomObjectType.objects.get(
702694
slug=custom_object_type
703695
)
704-
model = self.get_model(self.custom_object_type)
696+
model = self.custom_object_type.get_model_with_serializer()
705697
return model.objects.all()
706698

707699

708700
@register_model_view(CustomObject, "bulk_import", path="import", detail=False)
709-
class CustomObjectBulkImportView(CustomObjectModelMixin, generic.BulkImportView):
701+
class CustomObjectBulkImportView(generic.BulkImportView):
710702
queryset = None
711703
model_form = None
712704

@@ -730,7 +722,7 @@ def get_queryset(self, request):
730722
self.custom_object_type = CustomObjectType.objects.get(
731723
name__iexact=custom_object_type
732724
)
733-
model = self.get_model(self.custom_object_type)
725+
model = self.custom_object_type.get_model_with_serializer()
734726
return model.objects.all()
735727

736728
def get_model_form(self, queryset):
@@ -766,7 +758,7 @@ def get_model_form(self, queryset):
766758
return form
767759

768760

769-
class CustomObjectJournalView(ConditionalLoginRequiredMixin, CustomObjectModelMixin, View):
761+
class CustomObjectJournalView(ConditionalLoginRequiredMixin, View):
770762
"""
771763
Custom journal view for CustomObject instances.
772764
Shows all journal entries for a custom object.
@@ -782,7 +774,7 @@ def get(self, request, custom_object_type, **kwargs):
782774
object_type = get_object_or_404(
783775
CustomObjectType, slug=custom_object_type
784776
)
785-
model = self.get_model(object_type)
777+
model = object_type.get_model_with_serializer()
786778

787779
# Get the specific object
788780
lookup_kwargs = {k: v for k, v in kwargs.items() if k != "custom_object_type"}
@@ -838,7 +830,7 @@ def get(self, request, custom_object_type, **kwargs):
838830
)
839831

840832

841-
class CustomObjectChangeLogView(ConditionalLoginRequiredMixin, CustomObjectModelMixin, View):
833+
class CustomObjectChangeLogView(ConditionalLoginRequiredMixin, View):
842834
"""
843835
Custom changelog view for CustomObject instances.
844836
Shows all changes made to a custom object.
@@ -854,7 +846,7 @@ def get(self, request, custom_object_type, **kwargs):
854846
object_type = get_object_or_404(
855847
CustomObjectType, slug=custom_object_type
856848
)
857-
model = self.get_model(object_type)
849+
model = object_type.get_model_with_serializer()
858850

859851
# Get the specific object
860852
lookup_kwargs = {k: v for k, v in kwargs.items() if k != "custom_object_type"}

0 commit comments

Comments
 (0)