diff --git a/cmd/root.go b/cmd/root.go index 1fbb55bcfe1..48a2a7b52e0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -649,7 +649,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str if input.useNewActionCache || len(input.localRepository) > 0 { if input.actionOfflineMode { config.ActionCache = &runner.GoGitActionCacheOfflineMode{ - Parent: runner.GoGitActionCache{ + Parent: &runner.GoGitActionCache{ Path: config.ActionCacheDir, }, } diff --git a/pkg/runner/action_cache.go b/pkg/runner/action_cache.go index 5c61e1826e4..f3c7ccf9ea6 100644 --- a/pkg/runner/action_cache.go +++ b/pkg/runner/action_cache.go @@ -11,6 +11,7 @@ import ( "io/fs" "path" "strings" + "sync" "time" git "github.com/go-git/go-git/v5" @@ -28,16 +29,28 @@ type ActionCache interface { } type GoGitActionCache struct { - Path string + Path string + locks sync.Map } -func (c GoGitActionCache) Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error) { +// repoMu returns a per-gitPath mutex to serialize concurrent access to the +// same bare git repository. Different repos are not contended. +func (c *GoGitActionCache) repoMu(gitPath string) *sync.Mutex { + v, _ := c.locks.LoadOrStore(gitPath, &sync.Mutex{}) + return v.(*sync.Mutex) +} + +func (c *GoGitActionCache) Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error) { logger := common.Logger(ctx) gitPath := path.Join(c.Path, safeFilename(cacheDir)+".git") logger.Infof("GoGitActionCache fetch %s with ref %s at %s", url, ref, gitPath) + mu := c.repoMu(gitPath) + mu.Lock() + defer mu.Unlock() + gogitrepo, err := git.PlainInit(gitPath, true) if errors.Is(err, git.ErrRepositoryAlreadyExists) { logger.Debugf("GoGitActionCache cache hit %s with ref %s at %s", url, ref, gitPath) @@ -127,13 +140,17 @@ func (g *GitFileInfo) Sys() any { return nil } -func (c GoGitActionCache) GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error) { +func (c *GoGitActionCache) GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error) { logger := common.Logger(ctx) gitPath := path.Join(c.Path, safeFilename(cacheDir)+".git") logger.Infof("GoGitActionCache get content %s with sha %s subpath '%s' at %s", cacheDir, sha, includePrefix, gitPath) + mu := c.repoMu(gitPath) + mu.Lock() + defer mu.Unlock() + gogitrepo, err := git.PlainOpen(gitPath) if err != nil { return nil, fmt.Errorf("GoGitActionCache failed to open bare git %s with sha %s subpath '%s' at %s: %w", cacheDir, sha, includePrefix, gitPath, err) diff --git a/pkg/runner/action_cache_offline_mode.go b/pkg/runner/action_cache_offline_mode.go index a6926af0b46..6c6815d1f89 100644 --- a/pkg/runner/action_cache_offline_mode.go +++ b/pkg/runner/action_cache_offline_mode.go @@ -11,7 +11,7 @@ import ( ) type GoGitActionCacheOfflineMode struct { - Parent GoGitActionCache + Parent *GoGitActionCache } func (c GoGitActionCacheOfflineMode) Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error) { diff --git a/pkg/runner/runner_test.go b/pkg/runner/runner_test.go index 5c3959af2cd..89b1e66154f 100644 --- a/pkg/runner/runner_test.go +++ b/pkg/runner/runner_test.go @@ -353,8 +353,8 @@ func TestRunEvent(t *testing.T) { if yaml.Unmarshal(file, testConfig) == nil { if testConfig.LocalRepositories != nil { config.ActionCache = &LocalRepositoryCache{ - Parent: GoGitActionCache{ - path.Clean(path.Join(workdir, "cache")), + Parent: &GoGitActionCache{ + Path: path.Clean(path.Join(workdir, "cache")), }, LocalRepositories: testConfig.LocalRepositories, CacheDirCache: map[string]string{}, @@ -386,7 +386,7 @@ func TestPullAndPostStepFailureIsJobFailure(t *testing.T) { } defCache := &GoGitActionCache{ - path.Clean(path.Join(workdir, "cache")), + Path: path.Clean(path.Join(workdir, "cache")), } mockCache := &mockCache{}