1
1
import decimal
2
2
import re
3
+ import threading
3
4
from datetime import date , datetime
5
+ from functools import wraps
4
6
5
7
import django_filters
6
8
from core .models import ObjectType , ObjectChange
@@ -63,6 +65,23 @@ class UniquenessConstraintTestError(Exception):
63
65
USER_TABLE_DATABASE_NAME_PREFIX = "custom_objects_"
64
66
65
67
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
+
66
85
class CustomObject (
67
86
BookmarksMixin ,
68
87
ChangeLoggingMixin ,
@@ -165,6 +184,7 @@ class CustomObjectType(PrimaryModel):
165
184
_through_model_cache = (
166
185
{}
167
186
) # 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)
168
188
name = models .CharField (
169
189
max_length = 100 ,
170
190
unique = True ,
@@ -437,6 +457,7 @@ def register_custom_object_search_index(self, model):
437
457
label = f"{ APP_LABEL } .{ self .get_table_model_name (self .id ).lower ()} "
438
458
registry ["search" ][label ] = search_index
439
459
460
+ @thread_safe_model_generation
440
461
def get_model (
441
462
self ,
442
463
skip_object_fields = False ,
@@ -451,11 +472,12 @@ def get_model(
451
472
:rtype: Model
452
473
"""
453
474
454
- # Check if we have a cached model for this CustomObjectType
475
+ # Double-check pattern: check cache again after acquiring lock
455
476
if self .is_model_cached (self .id ):
456
477
model = self .get_cached_model (self .id )
457
478
return model
458
479
480
+ # Generate the model inside the lock to prevent race conditions
459
481
model_name = self .get_table_model_name (self .pk )
460
482
461
483
# TODO: Add other fields with "index" specified
0 commit comments