@@ -493,7 +493,7 @@ def test_copr_build_race_condition_concurrent_packages(
493493 """
494494 Test that three concurrent copr_build_handler tasks for different packages
495495 (containers-common-fedora, containers-common-eln, containers-common-centos)
496- with the same commit_sha trigger a race condition when creating CoprBuildGroups.
496+ with the same commit_sha do not trigger a race condition when creating CoprBuildGroups.
497497
498498 This test simulates the race condition where multiple handlers try to create
499499 a CoprBuildGroup for the same pipeline concurrently. The reverted code checks
@@ -605,6 +605,78 @@ def patched_create_from_config_file(*args, **kwargs):
605605 # This ensures they execute concurrently and increases the chance of a race condition
606606 barrier = threading .Barrier (3 , timeout = 30 )
607607
608+ # CRITICAL: Mock SRPMBuildModel.create_with_new_run() to return the shared pipeline
609+ # This ensures all threads use the same pipeline, triggering the race condition
610+ # when they all try to create a CoprBuildGroup for it concurrently
611+
612+ def mock_create_with_new_run (* args , ** kwargs ):
613+ # Query fresh objects from this thread's session to avoid SQLAlchemy
614+ # "already attached to session" errors. All threads will get objects
615+ # representing the same database row (same pipeline_id), which is what
616+ # we need to trigger the race condition.
617+ #
618+ # The race condition happens because:
619+ # 1. All threads query the same pipeline (same pipeline_id)
620+ # 2. Each thread checks run_model.copr_build_group (in-memory attribute)
621+ # 3. All see None (because they're checking their own session's object state)
622+ # 4. All try to set run_model.copr_build_group = build_group
623+ # 5. Only the last commit wins, causing the race condition
624+ #
625+ # The issue: CoprBuildGroupModel.create() checks run_model.copr_build_group
626+ # before merging the object into its session. If the object is detached,
627+ # this triggers a lazy load which fails. We need to ensure the relationship
628+ # is accessible without lazy loading. We'll use getattr with a default to
629+ # avoid triggering lazy loads, or we can ensure the object is properly merged.
630+ #
631+ # Actually, the best approach is to query the object fresh in each thread's
632+ # session, commit it (so it's persisted), then return it.
633+ # When CoprBuildGroupModel.create()
634+ # uses it, it will merge it into its own session. But we need to ensure the
635+ # relationship check doesn't trigger a lazy load.
636+ #
637+ # Solution: Query with the relationship already loaded (eager load), then
638+ # after commit and expunge, the relationship value is already in the object's
639+ # __dict__, so accessing it won't trigger a lazy load.
640+ with sa_session_transaction (commit = True ) as session :
641+ # Query with eager loading to populate all relationships in __dict__
642+ # This prevents lazy load errors when the object is detached
643+ from sqlalchemy .orm import joinedload
644+
645+ srpm = session .query (SRPMBuildModel ).filter_by (id = shared_srpm_id ).first ()
646+ pipeline = (
647+ session .query (PipelineModel )
648+ .options (
649+ joinedload (PipelineModel .copr_build_group ),
650+ joinedload (PipelineModel .project_event ),
651+ joinedload (PipelineModel .srpm_build ),
652+ )
653+ .filter_by (id = shared_pipeline_id )
654+ .first ()
655+ )
656+
657+ thread_id = threading .current_thread ().ident
658+ pkg_name = thread_packages .get (thread_id , "unknown" )
659+ # Update package_name to match this thread's package
660+ # (This simulates different packages trying to use the same pipeline)
661+ # Note: Since all threads update the same shared pipeline, the last one wins
662+ pipeline .package_name = pkg_name
663+ session .add (pipeline )
664+ session .commit ()
665+ # Access all relationships to ensure they're loaded into __dict__
666+ # This prevents lazy load errors when the object is detached
667+ _ = pipeline .copr_build_group
668+ _ = pipeline .project_event
669+ _ = pipeline .srpm_build
670+
671+ session .expunge (srpm )
672+ session .expunge (pipeline )
673+ return srpm , pipeline
674+
675+ # Applying the mock to the class correctly
676+ flexmock (SRPMBuildModel ).should_receive ("create_with_new_run" ).replace_with (
677+ mock_create_with_new_run
678+ )
679+
608680 def run_copr_build_handler_for_package (package_name : str ):
609681 """Helper function to run copr_build_handler in a thread"""
610682 try :
@@ -842,78 +914,6 @@ def mock_get_valid_build_targets(self, *args, **kwargs):
842914 "/tmp/test.srpm"
843915 )
844916
845- # CRITICAL: Mock SRPMBuildModel.create_with_new_run() to return the shared pipeline
846- # This ensures all threads use the same pipeline, triggering the race condition
847- # when they all try to create a CoprBuildGroup for it concurrently
848-
849- from packit_service .models import SRPMBuildModel
850-
851- def mock_create_with_new_run (cls , * args , ** kwargs ):
852- # Query fresh objects from this thread's session to avoid SQLAlchemy
853- # "already attached to session" errors. All threads will get objects
854- # representing the same database row (same pipeline_id), which is what
855- # we need to trigger the race condition.
856- #
857- # The race condition happens because:
858- # 1. All threads query the same pipeline (same pipeline_id)
859- # 2. Each thread checks run_model.copr_build_group (in-memory attribute)
860- # 3. All see None (because they're checking their own session's object state)
861- # 4. All try to set run_model.copr_build_group = build_group
862- # 5. Only the last commit wins, causing the race condition
863- #
864- # The issue: CoprBuildGroupModel.create() checks run_model.copr_build_group
865- # before merging the object into its session. If the object is detached,
866- # this triggers a lazy load which fails. We need to ensure the relationship
867- # is accessible without lazy loading. We'll use getattr with a default to
868- # avoid triggering lazy loads, or we can ensure the object is properly merged.
869- #
870- # Actually, the best approach is to query the object fresh in each thread's
871- # session, commit it (so it's persisted), then return it.
872- # When CoprBuildGroupModel.create()
873- # uses it, it will merge it into its own session. But we need to ensure the
874- # relationship check doesn't trigger a lazy load.
875- #
876- # Solution: Query with the relationship already loaded (eager load), then
877- # after commit and expunge, the relationship value is already in the object's
878- # __dict__, so accessing it won't trigger a lazy load.
879- with sa_session_transaction (commit = True ) as session :
880- # Query with eager loading to populate all relationships in __dict__
881- # This prevents lazy load errors when the object is detached
882- from sqlalchemy .orm import joinedload
883-
884- shared_srpm = session .query (SRPMBuildModel ).filter_by (id = shared_srpm_id ).first ()
885- shared_pipeline = (
886- session .query (PipelineModel )
887- .options (
888- joinedload (PipelineModel .copr_build_group ),
889- joinedload (PipelineModel .project_event ),
890- joinedload (PipelineModel .srpm_build ),
891- )
892- .filter_by (id = shared_pipeline_id )
893- .first ()
894- )
895- # Update package_name to match this thread's package
896- # (This simulates different packages trying to use the same pipeline)
897- # Note: Since all threads update the same shared pipeline, the last one wins
898- shared_pipeline .package_name = package_name
899- session .add (shared_pipeline )
900- session .commit ()
901- # Access all relationships to ensure they're loaded into __dict__
902- # This prevents lazy load errors when the object is detached
903- _ = shared_pipeline .copr_build_group # Load it now
904- _ = shared_pipeline .project_event # Load it now (needed for cloning)
905- _ = shared_pipeline .srpm_build # Load it now (needed for cloning)
906- # Expunge to detach - but the relationship values are in __dict__
907- session .expunge (shared_srpm )
908- session .expunge (shared_pipeline )
909- return shared_srpm , shared_pipeline
910-
911- # Patch the classmethod directly (similar to
912- # how CoprClient.create_from_config_file is patched)
913- # create_with_new_run is a classmethod, so wrap it properly
914- # Intentional patching for testing - ignore mypy errors
915- SRPMBuildModel .create_with_new_run = classmethod (mock_create_with_new_run ) # type: ignore
916-
917917 # Mock celery_app.send_task to prevent actual task sending
918918 from packit_service .celerizer import celery_app
919919
0 commit comments