@@ -21,6 +21,7 @@ package source
2121import (
2222 "context"
2323 "errors"
24+ "fmt"
2425 "os"
2526 "path"
2627 "path/filepath"
@@ -633,6 +634,84 @@ func TestDownloadCacheWorkdirMismatch(t *testing.T) {
633634 assert .Equal (t , destination1 , destination2 )
634635}
635636
637+ // TestConcurrentPolicyCachingRaceCondition reproduces the "file exists" error
638+ // that occurs when multiple workers simultaneously try to create symlinks from
639+ // cached policy downloads to their individual work directories
640+ func TestConcurrentPolicyCachingRaceCondition (t * testing.T ) {
641+ t .Cleanup (func () {
642+ downloadCache = sync.Map {}
643+ })
644+
645+ tmp := t .TempDir ()
646+
647+ source := & mockPolicySource {& mock.Mock {}}
648+ source .On ("PolicyUrl" ).Return ("policy-url" )
649+ source .On ("Subdir" ).Return ("subdir" )
650+
651+ // Simulate a pre-cached policy download in a shared location
652+ // This represents the first worker that successfully downloaded the policy
653+ sharedCacheDir := filepath .Join (tmp , "shared-cache" )
654+ cachedPolicyPath := uniqueDestination (sharedCacheDir , "subdir" , source .PolicyUrl ())
655+ require .NoError (t , os .MkdirAll (cachedPolicyPath , 0755 ))
656+
657+ // Create test policy files
658+ policyFile := filepath .Join (cachedPolicyPath , "policy.rego" )
659+ require .NoError (t , os .WriteFile (policyFile , []byte ("package test" ), 0600 ))
660+
661+ // Pre-populate the cache with the shared policy location
662+ downloadCache .Store ("policy-url" , func () (string , cacheContent ) {
663+ return cachedPolicyPath , cacheContent {}
664+ })
665+
666+ // ALL workers use the same work directory - this forces them to compete
667+ // for the exact same symlink destination path, triggering the race condition
668+ sharedWorkDir := filepath .Join (tmp , "shared-worker-dir" )
669+
670+ // Setup concurrent workers that will try to create the SAME symlink
671+ numWorkers := 50
672+ results := make (chan error , numWorkers )
673+
674+ // Synchronization barrier to maximize race condition probability
675+ startSignal := make (chan struct {})
676+
677+ for i := 0 ; i < numWorkers ; i ++ {
678+ go func (workerID int ) {
679+ // Wait for all workers to be ready
680+ <- startSignal
681+
682+ // All workers use the SAME work directory - this creates the race condition
683+ _ , _ , err := getPolicyThroughCache (
684+ context .Background (),
685+ source ,
686+ sharedWorkDir , // Same workDir for all workers = same destination path
687+ func (sourceURL , destDir string ) (metadata.Metadata , error ) {
688+ // Should not be called since policy is already cached
689+ return nil , fmt .Errorf ("unexpected download call for worker %d" , workerID )
690+ },
691+ )
692+
693+ results <- err
694+ }(i )
695+ }
696+
697+ // Release all workers simultaneously to trigger race condition
698+ close (startSignal )
699+
700+ // Collect errors
701+ var errors []error
702+ for i := 0 ; i < numWorkers ; i ++ {
703+ if err := <- results ; err != nil {
704+ errors = append (errors , err )
705+ }
706+ }
707+
708+ // The test should succeed (no errors) when the race condition is fixed
709+ // Any errors indicate a concurrency bug
710+ if len (errors ) > 0 {
711+ t .Errorf ("Concurrent policy caching failed with %d errors: %v" , len (errors ), errors )
712+ }
713+ }
714+
636715func TestLogMetadata (t * testing.T ) {
637716 tests := []struct {
638717 name string
0 commit comments