11import decimal
22import re
3+ import threading
34from datetime import date , datetime
5+ from functools import wraps
46
57import django_filters
68from core .models import ObjectType , ObjectChange
@@ -63,6 +65,23 @@ class UniquenessConstraintTestError(Exception):
6365USER_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+
6685class 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
0 commit comments