Files
atlantis/server/events/working_dir_locker.go
Andrew Carter dcae47bddb fix: parallel plan and apply also in a single workspace (rebased) (#5264)
Signed-off-by: Andrew Carter <andrew@emailcarter.com>
Signed-off-by: Luke Massa <lukefrederickmassa@gmail.com>
Co-authored-by: Finn Arne Gangstad <finnag@gmail.com>
Co-authored-by: Rui Chen <rui@chenrui.dev>
Co-authored-by: PePe Amengual <2208324+jamengual@users.noreply.github.com>
Co-authored-by: Luke Massa <lukefrederickmassa@gmail.com>
2025-04-22 13:57:17 -04:00

78 lines
3.1 KiB
Go

// Copyright 2017 HootSuite Media Inc.
//
// Licensed under the Apache License, Version 2.0 (the License);
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an AS IS BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Modified hereafter by contributors to runatlantis/atlantis.
package events
import (
"fmt"
"sync"
)
//go:generate pegomock generate --package mocks -o mocks/mock_working_dir_locker.go WorkingDirLocker
// WorkingDirLocker is used to prevent multiple commands from executing
// at the same time for a single repo, pull, and workspace. We need to prevent
// this from happening because a specific repo/pull/workspace has a single workspace
// on disk and we haven't written Atlantis (yet) to handle concurrent execution
// within this workspace.
type WorkingDirLocker interface {
// TryLock tries to acquire a lock for this repo, pull, workspace, and path.
// It returns a function that should be used to unlock the workspace and
// an error if the workspace is already locked. The error is expected to
// be printed to the pull request.
TryLock(repoFullName string, pullNum int, workspace string, path string) (func(), error)
}
// DefaultWorkingDirLocker implements WorkingDirLocker.
type DefaultWorkingDirLocker struct {
// mutex prevents against multiple threads calling functions on this struct
// concurrently. It's only used for entry/exit to each function.
mutex sync.Mutex
// locks is a set of the keys that are locked.
locks map[string]struct{}
}
// NewDefaultWorkingDirLocker is a constructor.
func NewDefaultWorkingDirLocker() *DefaultWorkingDirLocker {
return &DefaultWorkingDirLocker{locks: make(map[string]struct{})}
}
func (d *DefaultWorkingDirLocker) TryLock(repoFullName string, pullNum int, workspace string, path string) (func(), error) {
d.mutex.Lock()
defer d.mutex.Unlock()
workspaceKey := d.workspaceKey(repoFullName, pullNum, workspace, path)
if _, exists := d.locks[workspaceKey]; exists {
return func() {}, fmt.Errorf("the %s workspace at path %s is currently locked by another"+
" command that is running for this pull request.\n"+
"Wait until the previous command is complete and try again", workspace, path)
}
d.locks[workspaceKey] = struct{}{}
return func() {
d.unlock(repoFullName, pullNum, workspace, path)
}, nil
}
// Unlock unlocks the workspace for this pull.
func (d *DefaultWorkingDirLocker) unlock(repoFullName string, pullNum int, workspace string, path string) {
d.mutex.Lock()
defer d.mutex.Unlock()
workspaceKey := d.workspaceKey(repoFullName, pullNum, workspace, path)
delete(d.locks, workspaceKey)
}
func (d *DefaultWorkingDirLocker) workspaceKey(repo string, pull int, workspace string, path string) string {
return fmt.Sprintf("%s/%d/%s/%s", repo, pull, workspace, path)
}