From 6fdaa0947a4f06337994865de98c0757a93e1769 Mon Sep 17 00:00:00 2001 From: Fabiano Honorato Date: Sat, 12 Oct 2024 08:22:57 -0300 Subject: [PATCH] fix: Add retry on locks --- cmd/server.go | 9 + .../events/events_controller_e2e_test.go | 2 +- server/controllers/locks_controller_test.go | 6 +- server/events/delete_lock_command_test.go | 2 +- server/events/mock_workingdir_test.go | 483 ++++++++++-------- .../project_command_builder_internal_test.go | 10 +- server/events/project_command_builder_test.go | 26 +- server/events/project_command_runner_test.go | 18 +- server/events/working_dir_locker.go | 54 +- server/events/working_dir_locker_test.go | 52 +- server/server.go | 2 +- server/user_config.go | 1 + 12 files changed, 396 insertions(+), 269 deletions(-) diff --git a/cmd/server.go b/cmd/server.go index e53ca20418..0a20c2db5b 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -113,6 +113,7 @@ const ( APISecretFlag = "api-secret" HidePrevPlanComments = "hide-prev-plan-comments" QuietPolicyChecks = "quiet-policy-checks" + LockAcquireTimeoutSeconds = "lock-acquire-timeout" LockingDBType = "locking-db-type" LogLevelFlag = "log-level" MarkdownTemplateOverridesDirFlag = "markdown-template-overrides-dir" @@ -173,6 +174,7 @@ const ( DefaultGiteaBaseURL = "https://gitea.com" DefaultGiteaPageSize = 30 DefaultGitlabHostname = "gitlab.com" + DefaultLockAcquireTimeoutSeconds = 900 DefaultLockingDBType = "boltdb" DefaultLogLevel = "info" DefaultMaxCommentsPerCommand = 100 @@ -607,6 +609,10 @@ var intFlags = map[string]intFlag{ " If merge base is further behind than this number of commits from any of branches heads, full fetch will be performed.", defaultValue: DefaultCheckoutDepth, }, + LockAcquireTimeoutSeconds: { + description: fmt.Sprintf("The number of seconds to wait for a lock to be acquired before timing out. The default value is %d", DefaultLockAcquireTimeoutSeconds), + defaultValue: DefaultLockAcquireTimeoutSeconds, + }, MaxCommentsPerCommand: { description: "If non-zero, the maximum number of comments to split command output into before truncating.", defaultValue: DefaultMaxCommentsPerCommand, @@ -930,6 +936,9 @@ func (s *ServerCmd) setDefaults(c *server.UserConfig, v *viper.Viper) { if c.AutoDiscoverModeFlag == "" { c.AutoDiscoverModeFlag = DefaultAutoDiscoverMode } + if c.LockAcquireTimeoutSeconds == 0 { + c.LockAcquireTimeoutSeconds = DefaultLockAcquireTimeoutSeconds + } } func (s *ServerCmd) validate(userConfig server.UserConfig) error { diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index 68e517709c..a73e8cc101 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -1347,7 +1347,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers } defaultTFVersion := terraformClient.DefaultVersion() - locker := events.NewDefaultWorkingDirLocker() + locker := events.NewDefaultWorkingDirLocker(1) parser := &config.ParserValidator{} globalCfgArgs := valid.GlobalCfgArgs{ diff --git a/server/controllers/locks_controller_test.go b/server/controllers/locks_controller_test.go index d878b34e33..fd43d9b041 100644 --- a/server/controllers/locks_controller_test.go +++ b/server/controllers/locks_controller_test.go @@ -280,7 +280,7 @@ func TestDeleteLock_UpdateProjectStatus(t *testing.T) { cp := vcsmocks.NewMockClient() l := mocks2.NewMockDeleteLockCommand() workingDir := mocks2.NewMockWorkingDir() - workingDirLocker := events.NewDefaultWorkingDirLocker() + workingDirLocker := events.NewDefaultWorkingDirLocker(1) pull := models.PullRequest{ BaseRepo: models.Repo{FullName: repoName}, } @@ -345,7 +345,7 @@ func TestDeleteLock_CommentFailed(t *testing.T) { }, nil) cp := vcsmocks.NewMockClient() workingDir := mocks2.NewMockWorkingDir() - workingDirLocker := events.NewDefaultWorkingDirLocker() + workingDirLocker := events.NewDefaultWorkingDirLocker(1) var backend locking.Backend tmp := t.TempDir() backend, err := db.New(tmp) @@ -372,7 +372,7 @@ func TestDeleteLock_CommentSuccess(t *testing.T) { cp := vcsmocks.NewMockClient() dlc := mocks2.NewMockDeleteLockCommand() workingDir := mocks2.NewMockWorkingDir() - workingDirLocker := events.NewDefaultWorkingDirLocker() + workingDirLocker := events.NewDefaultWorkingDirLocker(1) var backend locking.Backend tmp := t.TempDir() backend, err := db.New(tmp) diff --git a/server/events/delete_lock_command_test.go b/server/events/delete_lock_command_test.go index 2e652770b9..a2266190fd 100644 --- a/server/events/delete_lock_command_test.go +++ b/server/events/delete_lock_command_test.go @@ -43,7 +43,7 @@ func TestDeleteLock_Success(t *testing.T) { l := lockmocks.NewMockLocker() When(l.Unlock("id")).ThenReturn(&models.ProjectLock{}, nil) workingDir := events.NewMockWorkingDir() - workingDirLocker := events.NewDefaultWorkingDirLocker() + workingDirLocker := events.NewDefaultWorkingDirLocker(1) workspace := "workspace" path := "path" projectName := "" diff --git a/server/events/mock_workingdir_test.go b/server/events/mock_workingdir_test.go index d298b2cee7..65d5fc00a7 100644 --- a/server/events/mock_workingdir_test.go +++ b/server/events/mock_workingdir_test.go @@ -4,12 +4,11 @@ package events import ( - "reflect" - "time" - pegomock "github.com/petergtz/pegomock/v4" models "github.com/runatlantis/atlantis/server/events/models" logging "github.com/runatlantis/atlantis/server/logging" + "reflect" + "time" ) type MockWorkingDir struct { @@ -31,148 +30,148 @@ func (mock *MockWorkingDir) Clone(logger logging.SimpleLogging, headRepo models. if mock == nil { panic("mock must not be nil. Use myMock := NewMockWorkingDir().") } - params := []pegomock.Param{logger, headRepo, p, workspace} - result := pegomock.GetGenericMockFrom(mock).Invoke("Clone", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 bool - var ret2 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) + _params := []pegomock.Param{logger, headRepo, p, workspace} + _result := pegomock.GetGenericMockFrom(mock).Invoke("Clone", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var _ret0 string + var _ret1 bool + var _ret2 error + if len(_result) != 0 { + if _result[0] != nil { + _ret0 = _result[0].(string) } - if result[1] != nil { - ret1 = result[1].(bool) + if _result[1] != nil { + _ret1 = _result[1].(bool) } - if result[2] != nil { - ret2 = result[2].(error) + if _result[2] != nil { + _ret2 = _result[2].(error) } } - return ret0, ret1, ret2 + return _ret0, _ret1, _ret2 } func (mock *MockWorkingDir) Delete(logger logging.SimpleLogging, r models.Repo, p models.PullRequest) error { if mock == nil { panic("mock must not be nil. Use myMock := NewMockWorkingDir().") } - params := []pegomock.Param{logger, r, p} - result := pegomock.GetGenericMockFrom(mock).Invoke("Delete", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(error) + _params := []pegomock.Param{logger, r, p} + _result := pegomock.GetGenericMockFrom(mock).Invoke("Delete", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) + var _ret0 error + if len(_result) != 0 { + if _result[0] != nil { + _ret0 = _result[0].(error) } } - return ret0 + return _ret0 } func (mock *MockWorkingDir) DeleteForWorkspace(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string) error { if mock == nil { panic("mock must not be nil. Use myMock := NewMockWorkingDir().") } - params := []pegomock.Param{logger, r, p, workspace} - result := pegomock.GetGenericMockFrom(mock).Invoke("DeleteForWorkspace", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(error) + _params := []pegomock.Param{logger, r, p, workspace} + _result := pegomock.GetGenericMockFrom(mock).Invoke("DeleteForWorkspace", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) + var _ret0 error + if len(_result) != 0 { + if _result[0] != nil { + _ret0 = _result[0].(error) } } - return ret0 + return _ret0 } func (mock *MockWorkingDir) DeletePlan(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string, path string, projectName string) error { if mock == nil { panic("mock must not be nil. Use myMock := NewMockWorkingDir().") } - params := []pegomock.Param{logger, r, p, workspace, path, projectName} - result := pegomock.GetGenericMockFrom(mock).Invoke("DeletePlan", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(error) + _params := []pegomock.Param{logger, r, p, workspace, path, projectName} + _result := pegomock.GetGenericMockFrom(mock).Invoke("DeletePlan", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) + var _ret0 error + if len(_result) != 0 { + if _result[0] != nil { + _ret0 = _result[0].(error) } } - return ret0 + return _ret0 } func (mock *MockWorkingDir) GetGitUntrackedFiles(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string) ([]string, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockWorkingDir().") } - params := []pegomock.Param{logger, r, p, workspace} - result := pegomock.GetGenericMockFrom(mock).Invoke("GetGitUntrackedFiles", params, []reflect.Type{reflect.TypeOf((*[]string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 []string - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].([]string) + _params := []pegomock.Param{logger, r, p, workspace} + _result := pegomock.GetGenericMockFrom(mock).Invoke("GetGitUntrackedFiles", _params, []reflect.Type{reflect.TypeOf((*[]string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var _ret0 []string + var _ret1 error + if len(_result) != 0 { + if _result[0] != nil { + _ret0 = _result[0].([]string) } - if result[1] != nil { - ret1 = result[1].(error) + if _result[1] != nil { + _ret1 = _result[1].(error) } } - return ret0, ret1 + return _ret0, _ret1 } func (mock *MockWorkingDir) GetPullDir(r models.Repo, p models.PullRequest) (string, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockWorkingDir().") } - params := []pegomock.Param{r, p} - result := pegomock.GetGenericMockFrom(mock).Invoke("GetPullDir", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) + _params := []pegomock.Param{r, p} + _result := pegomock.GetGenericMockFrom(mock).Invoke("GetPullDir", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var _ret0 string + var _ret1 error + if len(_result) != 0 { + if _result[0] != nil { + _ret0 = _result[0].(string) } - if result[1] != nil { - ret1 = result[1].(error) + if _result[1] != nil { + _ret1 = _result[1].(error) } } - return ret0, ret1 + return _ret0, _ret1 } func (mock *MockWorkingDir) GetWorkingDir(r models.Repo, p models.PullRequest, workspace string) (string, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockWorkingDir().") } - params := []pegomock.Param{r, p, workspace} - result := pegomock.GetGenericMockFrom(mock).Invoke("GetWorkingDir", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) + _params := []pegomock.Param{r, p, workspace} + _result := pegomock.GetGenericMockFrom(mock).Invoke("GetWorkingDir", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var _ret0 string + var _ret1 error + if len(_result) != 0 { + if _result[0] != nil { + _ret0 = _result[0].(string) } - if result[1] != nil { - ret1 = result[1].(error) + if _result[1] != nil { + _ret1 = _result[1].(error) } } - return ret0, ret1 + return _ret0, _ret1 } func (mock *MockWorkingDir) HasDiverged(logger logging.SimpleLogging, cloneDir string) bool { if mock == nil { panic("mock must not be nil. Use myMock := NewMockWorkingDir().") } - params := []pegomock.Param{logger, cloneDir} - result := pegomock.GetGenericMockFrom(mock).Invoke("HasDiverged", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) + _params := []pegomock.Param{logger, cloneDir} + _result := pegomock.GetGenericMockFrom(mock).Invoke("HasDiverged", _params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) + var _ret0 bool + if len(_result) != 0 { + if _result[0] != nil { + _ret0 = _result[0].(bool) } } - return ret0 + return _ret0 } func (mock *MockWorkingDir) SetCheckForUpstreamChanges() { if mock == nil { panic("mock must not be nil. Use myMock := NewMockWorkingDir().") } - params := []pegomock.Param{} - pegomock.GetGenericMockFrom(mock).Invoke("SetCheckForUpstreamChanges", params, []reflect.Type{}) + _params := []pegomock.Param{} + pegomock.GetGenericMockFrom(mock).Invoke("SetCheckForUpstreamChanges", _params, []reflect.Type{}) } func (mock *MockWorkingDir) VerifyWasCalledOnce() *VerifierMockWorkingDir { @@ -213,8 +212,8 @@ type VerifierMockWorkingDir struct { } func (verifier *VerifierMockWorkingDir) Clone(logger logging.SimpleLogging, headRepo models.Repo, p models.PullRequest, workspace string) *MockWorkingDir_Clone_OngoingVerification { - params := []pegomock.Param{logger, headRepo, p, workspace} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Clone", params, verifier.timeout) + _params := []pegomock.Param{logger, headRepo, p, workspace} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Clone", _params, verifier.timeout) return &MockWorkingDir_Clone_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -229,31 +228,39 @@ func (c *MockWorkingDir_Clone_OngoingVerification) GetCapturedArguments() (loggi } func (c *MockWorkingDir_Clone_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]logging.SimpleLogging, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(logging.SimpleLogging) - } - _param1 = make([]models.Repo, len(c.methodInvocations)) - for u, param := range params[1] { - _param1[u] = param.(models.Repo) - } - _param2 = make([]models.PullRequest, len(c.methodInvocations)) - for u, param := range params[2] { - _param2[u] = param.(models.PullRequest) - } - _param3 = make([]string, len(c.methodInvocations)) - for u, param := range params[3] { - _param3[u] = param.(string) + _params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(_params) > 0 { + if len(_params) > 0 { + _param0 = make([]logging.SimpleLogging, len(c.methodInvocations)) + for u, param := range _params[0] { + _param0[u] = param.(logging.SimpleLogging) + } + } + if len(_params) > 1 { + _param1 = make([]models.Repo, len(c.methodInvocations)) + for u, param := range _params[1] { + _param1[u] = param.(models.Repo) + } + } + if len(_params) > 2 { + _param2 = make([]models.PullRequest, len(c.methodInvocations)) + for u, param := range _params[2] { + _param2[u] = param.(models.PullRequest) + } + } + if len(_params) > 3 { + _param3 = make([]string, len(c.methodInvocations)) + for u, param := range _params[3] { + _param3[u] = param.(string) + } } } return } func (verifier *VerifierMockWorkingDir) Delete(logger logging.SimpleLogging, r models.Repo, p models.PullRequest) *MockWorkingDir_Delete_OngoingVerification { - params := []pegomock.Param{logger, r, p} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Delete", params, verifier.timeout) + _params := []pegomock.Param{logger, r, p} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Delete", _params, verifier.timeout) return &MockWorkingDir_Delete_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -268,27 +275,33 @@ func (c *MockWorkingDir_Delete_OngoingVerification) GetCapturedArguments() (logg } func (c *MockWorkingDir_Delete_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]logging.SimpleLogging, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(logging.SimpleLogging) - } - _param1 = make([]models.Repo, len(c.methodInvocations)) - for u, param := range params[1] { - _param1[u] = param.(models.Repo) - } - _param2 = make([]models.PullRequest, len(c.methodInvocations)) - for u, param := range params[2] { - _param2[u] = param.(models.PullRequest) + _params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(_params) > 0 { + if len(_params) > 0 { + _param0 = make([]logging.SimpleLogging, len(c.methodInvocations)) + for u, param := range _params[0] { + _param0[u] = param.(logging.SimpleLogging) + } + } + if len(_params) > 1 { + _param1 = make([]models.Repo, len(c.methodInvocations)) + for u, param := range _params[1] { + _param1[u] = param.(models.Repo) + } + } + if len(_params) > 2 { + _param2 = make([]models.PullRequest, len(c.methodInvocations)) + for u, param := range _params[2] { + _param2[u] = param.(models.PullRequest) + } } } return } func (verifier *VerifierMockWorkingDir) DeleteForWorkspace(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string) *MockWorkingDir_DeleteForWorkspace_OngoingVerification { - params := []pegomock.Param{logger, r, p, workspace} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DeleteForWorkspace", params, verifier.timeout) + _params := []pegomock.Param{logger, r, p, workspace} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DeleteForWorkspace", _params, verifier.timeout) return &MockWorkingDir_DeleteForWorkspace_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -303,31 +316,39 @@ func (c *MockWorkingDir_DeleteForWorkspace_OngoingVerification) GetCapturedArgum } func (c *MockWorkingDir_DeleteForWorkspace_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]logging.SimpleLogging, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(logging.SimpleLogging) - } - _param1 = make([]models.Repo, len(c.methodInvocations)) - for u, param := range params[1] { - _param1[u] = param.(models.Repo) - } - _param2 = make([]models.PullRequest, len(c.methodInvocations)) - for u, param := range params[2] { - _param2[u] = param.(models.PullRequest) - } - _param3 = make([]string, len(c.methodInvocations)) - for u, param := range params[3] { - _param3[u] = param.(string) + _params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(_params) > 0 { + if len(_params) > 0 { + _param0 = make([]logging.SimpleLogging, len(c.methodInvocations)) + for u, param := range _params[0] { + _param0[u] = param.(logging.SimpleLogging) + } + } + if len(_params) > 1 { + _param1 = make([]models.Repo, len(c.methodInvocations)) + for u, param := range _params[1] { + _param1[u] = param.(models.Repo) + } + } + if len(_params) > 2 { + _param2 = make([]models.PullRequest, len(c.methodInvocations)) + for u, param := range _params[2] { + _param2[u] = param.(models.PullRequest) + } + } + if len(_params) > 3 { + _param3 = make([]string, len(c.methodInvocations)) + for u, param := range _params[3] { + _param3[u] = param.(string) + } } } return } func (verifier *VerifierMockWorkingDir) DeletePlan(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string, path string, projectName string) *MockWorkingDir_DeletePlan_OngoingVerification { - params := []pegomock.Param{logger, r, p, workspace, path, projectName} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DeletePlan", params, verifier.timeout) + _params := []pegomock.Param{logger, r, p, workspace, path, projectName} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DeletePlan", _params, verifier.timeout) return &MockWorkingDir_DeletePlan_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -342,39 +363,51 @@ func (c *MockWorkingDir_DeletePlan_OngoingVerification) GetCapturedArguments() ( } func (c *MockWorkingDir_DeletePlan_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []string, _param4 []string, _param5 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]logging.SimpleLogging, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(logging.SimpleLogging) - } - _param1 = make([]models.Repo, len(c.methodInvocations)) - for u, param := range params[1] { - _param1[u] = param.(models.Repo) - } - _param2 = make([]models.PullRequest, len(c.methodInvocations)) - for u, param := range params[2] { - _param2[u] = param.(models.PullRequest) - } - _param3 = make([]string, len(c.methodInvocations)) - for u, param := range params[3] { - _param3[u] = param.(string) - } - _param4 = make([]string, len(c.methodInvocations)) - for u, param := range params[4] { - _param4[u] = param.(string) - } - _param5 = make([]string, len(c.methodInvocations)) - for u, param := range params[5] { - _param5[u] = param.(string) + _params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(_params) > 0 { + if len(_params) > 0 { + _param0 = make([]logging.SimpleLogging, len(c.methodInvocations)) + for u, param := range _params[0] { + _param0[u] = param.(logging.SimpleLogging) + } + } + if len(_params) > 1 { + _param1 = make([]models.Repo, len(c.methodInvocations)) + for u, param := range _params[1] { + _param1[u] = param.(models.Repo) + } + } + if len(_params) > 2 { + _param2 = make([]models.PullRequest, len(c.methodInvocations)) + for u, param := range _params[2] { + _param2[u] = param.(models.PullRequest) + } + } + if len(_params) > 3 { + _param3 = make([]string, len(c.methodInvocations)) + for u, param := range _params[3] { + _param3[u] = param.(string) + } + } + if len(_params) > 4 { + _param4 = make([]string, len(c.methodInvocations)) + for u, param := range _params[4] { + _param4[u] = param.(string) + } + } + if len(_params) > 5 { + _param5 = make([]string, len(c.methodInvocations)) + for u, param := range _params[5] { + _param5[u] = param.(string) + } } } return } func (verifier *VerifierMockWorkingDir) GetGitUntrackedFiles(logger logging.SimpleLogging, r models.Repo, p models.PullRequest, workspace string) *MockWorkingDir_GetGitUntrackedFiles_OngoingVerification { - params := []pegomock.Param{logger, r, p, workspace} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetGitUntrackedFiles", params, verifier.timeout) + _params := []pegomock.Param{logger, r, p, workspace} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetGitUntrackedFiles", _params, verifier.timeout) return &MockWorkingDir_GetGitUntrackedFiles_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -389,31 +422,39 @@ func (c *MockWorkingDir_GetGitUntrackedFiles_OngoingVerification) GetCapturedArg } func (c *MockWorkingDir_GetGitUntrackedFiles_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []models.PullRequest, _param3 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]logging.SimpleLogging, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(logging.SimpleLogging) - } - _param1 = make([]models.Repo, len(c.methodInvocations)) - for u, param := range params[1] { - _param1[u] = param.(models.Repo) - } - _param2 = make([]models.PullRequest, len(c.methodInvocations)) - for u, param := range params[2] { - _param2[u] = param.(models.PullRequest) - } - _param3 = make([]string, len(c.methodInvocations)) - for u, param := range params[3] { - _param3[u] = param.(string) + _params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(_params) > 0 { + if len(_params) > 0 { + _param0 = make([]logging.SimpleLogging, len(c.methodInvocations)) + for u, param := range _params[0] { + _param0[u] = param.(logging.SimpleLogging) + } + } + if len(_params) > 1 { + _param1 = make([]models.Repo, len(c.methodInvocations)) + for u, param := range _params[1] { + _param1[u] = param.(models.Repo) + } + } + if len(_params) > 2 { + _param2 = make([]models.PullRequest, len(c.methodInvocations)) + for u, param := range _params[2] { + _param2[u] = param.(models.PullRequest) + } + } + if len(_params) > 3 { + _param3 = make([]string, len(c.methodInvocations)) + for u, param := range _params[3] { + _param3[u] = param.(string) + } } } return } func (verifier *VerifierMockWorkingDir) GetPullDir(r models.Repo, p models.PullRequest) *MockWorkingDir_GetPullDir_OngoingVerification { - params := []pegomock.Param{r, p} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetPullDir", params, verifier.timeout) + _params := []pegomock.Param{r, p} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetPullDir", _params, verifier.timeout) return &MockWorkingDir_GetPullDir_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -428,23 +469,27 @@ func (c *MockWorkingDir_GetPullDir_OngoingVerification) GetCapturedArguments() ( } func (c *MockWorkingDir_GetPullDir_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.PullRequest) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]models.Repo, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(models.Repo) - } - _param1 = make([]models.PullRequest, len(c.methodInvocations)) - for u, param := range params[1] { - _param1[u] = param.(models.PullRequest) + _params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(_params) > 0 { + if len(_params) > 0 { + _param0 = make([]models.Repo, len(c.methodInvocations)) + for u, param := range _params[0] { + _param0[u] = param.(models.Repo) + } + } + if len(_params) > 1 { + _param1 = make([]models.PullRequest, len(c.methodInvocations)) + for u, param := range _params[1] { + _param1[u] = param.(models.PullRequest) + } } } return } func (verifier *VerifierMockWorkingDir) GetWorkingDir(r models.Repo, p models.PullRequest, workspace string) *MockWorkingDir_GetWorkingDir_OngoingVerification { - params := []pegomock.Param{r, p, workspace} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetWorkingDir", params, verifier.timeout) + _params := []pegomock.Param{r, p, workspace} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetWorkingDir", _params, verifier.timeout) return &MockWorkingDir_GetWorkingDir_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -459,27 +504,33 @@ func (c *MockWorkingDir_GetWorkingDir_OngoingVerification) GetCapturedArguments( } func (c *MockWorkingDir_GetWorkingDir_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.PullRequest, _param2 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]models.Repo, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(models.Repo) - } - _param1 = make([]models.PullRequest, len(c.methodInvocations)) - for u, param := range params[1] { - _param1[u] = param.(models.PullRequest) - } - _param2 = make([]string, len(c.methodInvocations)) - for u, param := range params[2] { - _param2[u] = param.(string) + _params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(_params) > 0 { + if len(_params) > 0 { + _param0 = make([]models.Repo, len(c.methodInvocations)) + for u, param := range _params[0] { + _param0[u] = param.(models.Repo) + } + } + if len(_params) > 1 { + _param1 = make([]models.PullRequest, len(c.methodInvocations)) + for u, param := range _params[1] { + _param1[u] = param.(models.PullRequest) + } + } + if len(_params) > 2 { + _param2 = make([]string, len(c.methodInvocations)) + for u, param := range _params[2] { + _param2[u] = param.(string) + } } } return } func (verifier *VerifierMockWorkingDir) HasDiverged(logger logging.SimpleLogging, cloneDir string) *MockWorkingDir_HasDiverged_OngoingVerification { - params := []pegomock.Param{logger, cloneDir} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "HasDiverged", params, verifier.timeout) + _params := []pegomock.Param{logger, cloneDir} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "HasDiverged", _params, verifier.timeout) return &MockWorkingDir_HasDiverged_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -494,23 +545,27 @@ func (c *MockWorkingDir_HasDiverged_OngoingVerification) GetCapturedArguments() } func (c *MockWorkingDir_HasDiverged_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]logging.SimpleLogging, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(logging.SimpleLogging) - } - _param1 = make([]string, len(c.methodInvocations)) - for u, param := range params[1] { - _param1[u] = param.(string) + _params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(_params) > 0 { + if len(_params) > 0 { + _param0 = make([]logging.SimpleLogging, len(c.methodInvocations)) + for u, param := range _params[0] { + _param0[u] = param.(logging.SimpleLogging) + } + } + if len(_params) > 1 { + _param1 = make([]string, len(c.methodInvocations)) + for u, param := range _params[1] { + _param1[u] = param.(string) + } } } return } func (verifier *VerifierMockWorkingDir) SetCheckForUpstreamChanges() *MockWorkingDir_SetCheckForUpstreamChanges_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SetCheckForUpstreamChanges", params, verifier.timeout) + _params := []pegomock.Param{} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SetCheckForUpstreamChanges", _params, verifier.timeout) return &MockWorkingDir_SetCheckForUpstreamChanges_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } diff --git a/server/events/project_command_builder_internal_test.go b/server/events/project_command_builder_internal_test.go index d020871b31..970bd68791 100644 --- a/server/events/project_command_builder_internal_test.go +++ b/server/events/project_command_builder_internal_test.go @@ -656,7 +656,7 @@ projects: &DefaultProjectFinder{}, vcsClient, workingDir, - NewDefaultWorkingDirLocker(), + NewDefaultWorkingDirLocker(1), globalCfg, &DefaultPendingPlanFinder{}, &CommentParser{ExecutableName: "atlantis"}, @@ -873,7 +873,7 @@ projects: &DefaultProjectFinder{}, vcsClient, workingDir, - NewDefaultWorkingDirLocker(), + NewDefaultWorkingDirLocker(1), globalCfg, &DefaultPendingPlanFinder{}, &CommentParser{ExecutableName: "atlantis"}, @@ -1120,7 +1120,7 @@ workflows: &DefaultProjectFinder{}, vcsClient, workingDir, - NewDefaultWorkingDirLocker(), + NewDefaultWorkingDirLocker(1), globalCfg, &DefaultPendingPlanFinder{}, &CommentParser{ExecutableName: "atlantis"}, @@ -1272,7 +1272,7 @@ projects: &DefaultProjectFinder{}, vcsClient, workingDir, - NewDefaultWorkingDirLocker(), + NewDefaultWorkingDirLocker(1), globalCfg, &DefaultPendingPlanFinder{}, &CommentParser{ExecutableName: "atlantis"}, @@ -1414,7 +1414,7 @@ projects: &DefaultProjectFinder{}, vcsClient, workingDir, - NewDefaultWorkingDirLocker(), + NewDefaultWorkingDirLocker(1), globalCfg, &DefaultPendingPlanFinder{}, &CommentParser{ExecutableName: "atlantis"}, diff --git a/server/events/project_command_builder_test.go b/server/events/project_command_builder_test.go index 7560b5d6de..69dcee2fd7 100644 --- a/server/events/project_command_builder_test.go +++ b/server/events/project_command_builder_test.go @@ -258,7 +258,7 @@ terraform { &events.DefaultProjectFinder{}, vcsClient, workingDir, - events.NewDefaultWorkingDirLocker(), + events.NewDefaultWorkingDirLocker(1), valid.NewGlobalCfgFromArgs(globalCfgArgs), &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, @@ -624,7 +624,7 @@ projects: &events.DefaultProjectFinder{}, vcsClient, workingDir, - events.NewDefaultWorkingDirLocker(), + events.NewDefaultWorkingDirLocker(1), valid.NewGlobalCfgFromArgs(globalCfgArgs), &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, @@ -812,7 +812,7 @@ projects: &events.DefaultProjectFinder{}, vcsClient, workingDir, - events.NewDefaultWorkingDirLocker(), + events.NewDefaultWorkingDirLocker(1), valid.NewGlobalCfgFromArgs(globalCfgArgs), &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, @@ -1141,7 +1141,7 @@ projects: &events.DefaultProjectFinder{}, vcsClient, workingDir, - events.NewDefaultWorkingDirLocker(), + events.NewDefaultWorkingDirLocker(1), valid.NewGlobalCfgFromArgs(globalCfgArgs), &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, @@ -1239,7 +1239,7 @@ func TestDefaultProjectCommandBuilder_BuildMultiApply(t *testing.T) { &events.DefaultProjectFinder{}, nil, workingDir, - events.NewDefaultWorkingDirLocker(), + events.NewDefaultWorkingDirLocker(1), valid.NewGlobalCfgFromArgs(globalCfgArgs), &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, @@ -1325,7 +1325,7 @@ projects: &events.DefaultProjectFinder{}, nil, workingDir, - events.NewDefaultWorkingDirLocker(), + events.NewDefaultWorkingDirLocker(1), valid.NewGlobalCfgFromArgs(globalCfgArgs), &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, @@ -1413,7 +1413,7 @@ func TestDefaultProjectCommandBuilder_EscapeArgs(t *testing.T) { &events.DefaultProjectFinder{}, vcsClient, workingDir, - events.NewDefaultWorkingDirLocker(), + events.NewDefaultWorkingDirLocker(1), valid.NewGlobalCfgFromArgs(globalCfgArgs), &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, @@ -1575,7 +1575,7 @@ projects: &events.DefaultProjectFinder{}, vcsClient, workingDir, - events.NewDefaultWorkingDirLocker(), + events.NewDefaultWorkingDirLocker(1), valid.NewGlobalCfgFromArgs(globalCfgArgs), &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, @@ -1685,7 +1685,7 @@ projects: &events.DefaultProjectFinder{}, vcsClient, workingDir, - events.NewDefaultWorkingDirLocker(), + events.NewDefaultWorkingDirLocker(1), valid.NewGlobalCfgFromArgs(globalCfgArgs), &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, @@ -1754,7 +1754,7 @@ func TestDefaultProjectCommandBuilder_WithPolicyCheckEnabled_BuildAutoplanComman &events.DefaultProjectFinder{}, vcsClient, workingDir, - events.NewDefaultWorkingDirLocker(), + events.NewDefaultWorkingDirLocker(1), globalCfg, &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, @@ -1842,7 +1842,7 @@ func TestDefaultProjectCommandBuilder_BuildVersionCommand(t *testing.T) { &events.DefaultProjectFinder{}, nil, workingDir, - events.NewDefaultWorkingDirLocker(), + events.NewDefaultWorkingDirLocker(1), valid.NewGlobalCfgFromArgs(globalCfgArgs), &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, @@ -1972,7 +1972,7 @@ func TestDefaultProjectCommandBuilder_BuildPlanCommands_Single_With_RestrictFile &events.DefaultProjectFinder{}, vcsClient, workingDir, - events.NewDefaultWorkingDirLocker(), + events.NewDefaultWorkingDirLocker(1), valid.NewGlobalCfgFromArgs(globalCfgArgs), &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, @@ -2083,7 +2083,7 @@ func TestDefaultProjectCommandBuilder_BuildPlanCommands_with_IncludeGitUntracked &events.DefaultProjectFinder{}, vcsClient, workingDir, - events.NewDefaultWorkingDirLocker(), + events.NewDefaultWorkingDirLocker(1), valid.NewGlobalCfgFromArgs(globalCfgArgs), &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, diff --git a/server/events/project_command_runner_test.go b/server/events/project_command_runner_test.go index d241d44569..f1f1552538 100644 --- a/server/events/project_command_runner_test.go +++ b/server/events/project_command_runner_test.go @@ -58,7 +58,7 @@ func TestDefaultProjectCommandRunner_Plan(t *testing.T) { PullApprovedChecker: nil, WorkingDir: mockWorkingDir, Webhooks: nil, - WorkingDirLocker: events.NewDefaultWorkingDirLocker(), + WorkingDirLocker: events.NewDefaultWorkingDirLocker(1), CommandRequirementHandler: mockCommandRequirementHandler, } @@ -248,7 +248,7 @@ func TestDefaultProjectCommandRunner_ApplyNotApproved(t *testing.T) { mockWorkingDir := mocks.NewMockWorkingDir() runner := &events.DefaultProjectCommandRunner{ WorkingDir: mockWorkingDir, - WorkingDirLocker: events.NewDefaultWorkingDirLocker(), + WorkingDirLocker: events.NewDefaultWorkingDirLocker(1), CommandRequirementHandler: &events.DefaultCommandRequirementHandler{ WorkingDir: mockWorkingDir, }, @@ -269,7 +269,7 @@ func TestDefaultProjectCommandRunner_ApplyNotMergeable(t *testing.T) { mockWorkingDir := mocks.NewMockWorkingDir() runner := &events.DefaultProjectCommandRunner{ WorkingDir: mockWorkingDir, - WorkingDirLocker: events.NewDefaultWorkingDirLocker(), + WorkingDirLocker: events.NewDefaultWorkingDirLocker(1), CommandRequirementHandler: &events.DefaultCommandRequirementHandler{ WorkingDir: mockWorkingDir, }, @@ -293,7 +293,7 @@ func TestDefaultProjectCommandRunner_ApplyDiverged(t *testing.T) { mockWorkingDir := mocks.NewMockWorkingDir() runner := &events.DefaultProjectCommandRunner{ WorkingDir: mockWorkingDir, - WorkingDirLocker: events.NewDefaultWorkingDirLocker(), + WorkingDirLocker: events.NewDefaultWorkingDirLocker(1), CommandRequirementHandler: &events.DefaultCommandRequirementHandler{ WorkingDir: mockWorkingDir, }, @@ -415,7 +415,7 @@ func TestDefaultProjectCommandRunner_Apply(t *testing.T) { EnvStepRunner: mockEnv, WorkingDir: mockWorkingDir, Webhooks: mockSender, - WorkingDirLocker: events.NewDefaultWorkingDirLocker(), + WorkingDirLocker: events.NewDefaultWorkingDirLocker(1), CommandRequirementHandler: applyReqHandler, } repoDir := t.TempDir() @@ -496,7 +496,7 @@ func TestDefaultProjectCommandRunner_ApplyRunStepFailure(t *testing.T) { LockURLGenerator: mockURLGenerator{}, ApplyStepRunner: mockApply, WorkingDir: mockWorkingDir, - WorkingDirLocker: events.NewDefaultWorkingDirLocker(), + WorkingDirLocker: events.NewDefaultWorkingDirLocker(1), CommandRequirementHandler: applyReqHandler, Webhooks: mockSender, } @@ -565,7 +565,7 @@ func TestDefaultProjectCommandRunner_RunEnvSteps(t *testing.T) { EnvStepRunner: &env, WorkingDir: mockWorkingDir, Webhooks: nil, - WorkingDirLocker: events.NewDefaultWorkingDirLocker(), + WorkingDirLocker: events.NewDefaultWorkingDirLocker(1), CommandRequirementHandler: mockCommandRequirementHandler, } @@ -699,7 +699,7 @@ func TestDefaultProjectCommandRunner_Import(t *testing.T) { StateRmStepRunner: mockStateRm, WorkingDir: mockWorkingDir, Webhooks: mockSender, - WorkingDirLocker: events.NewDefaultWorkingDirLocker(), + WorkingDirLocker: events.NewDefaultWorkingDirLocker(1), CommandRequirementHandler: applyReqHandler, } ctx := command.ProjectContext{ @@ -1244,7 +1244,7 @@ func TestDefaultProjectCommandRunner_ApprovePolicies(t *testing.T) { EnvStepRunner: mockEnv, WorkingDir: mockWorkingDir, Webhooks: mockSender, - WorkingDirLocker: events.NewDefaultWorkingDirLocker(), + WorkingDirLocker: events.NewDefaultWorkingDirLocker(1), } repoDir := t.TempDir() When(mockWorkingDir.GetWorkingDir( diff --git a/server/events/working_dir_locker.go b/server/events/working_dir_locker.go index 06af44ec5a..27bb797c52 100644 --- a/server/events/working_dir_locker.go +++ b/server/events/working_dir_locker.go @@ -17,6 +17,7 @@ import ( "fmt" "strings" "sync" + "time" ) //go:generate pegomock generate --package mocks -o mocks/mock_working_dir_locker.go WorkingDirLocker @@ -49,11 +50,19 @@ type DefaultWorkingDirLocker struct { // matching to determine if something is locked. It's naive but that's okay // because there won't be many locks at one time. locks []string + + lockAcquireTimeoutSeconds int } // NewDefaultWorkingDirLocker is a constructor. -func NewDefaultWorkingDirLocker() *DefaultWorkingDirLocker { - return &DefaultWorkingDirLocker{} +func NewDefaultWorkingDirLocker(lockAcquireTimeoutSeconds int) *DefaultWorkingDirLocker { + if lockAcquireTimeoutSeconds < 1 { + lockAcquireTimeoutSeconds = 1 + } + + return &DefaultWorkingDirLocker{ + lockAcquireTimeoutSeconds: lockAcquireTimeoutSeconds, + } } func (d *DefaultWorkingDirLocker) TryLockPull(repoFullName string, pullNum int) (func(), error) { @@ -75,22 +84,45 @@ func (d *DefaultWorkingDirLocker) TryLockPull(repoFullName string, pullNum int) } func (d *DefaultWorkingDirLocker) TryLock(repoFullName string, pullNum int, workspace string, path string) (func(), error) { - d.mutex.Lock() - defer d.mutex.Unlock() + ticker := time.NewTicker(500 * time.Millisecond) + timeout := time.NewTimer(time.Duration(d.lockAcquireTimeoutSeconds) * time.Second) - pullKey := d.pullKey(repoFullName, pullNum) workspaceKey := d.workspaceKey(repoFullName, pullNum, workspace, path) - for _, l := range d.locks { - if l == pullKey || l == workspaceKey { + pullKey := d.pullKey(repoFullName, pullNum) + + for { + select { + case <-timeout.C: 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) + case <-ticker.C: + lockAcquired := d.tryAcquireLock(pullKey, workspaceKey) + if lockAcquired { + return func() { + d.unlock(repoFullName, pullNum, workspace, path) + }, nil + } } } - d.locks = append(d.locks, workspaceKey) - return func() { - d.unlock(repoFullName, pullNum, workspace, path) - }, nil +} + +func (d *DefaultWorkingDirLocker) tryAcquireLock(pullKey string, workspaceKey string) bool { + d.mutex.Lock() + defer d.mutex.Unlock() + + acquireLock := true + for _, l := range d.locks { + if l == pullKey || l == workspaceKey { + acquireLock = false + } + } + + if acquireLock { + d.locks = append(d.locks, workspaceKey) + } + + return acquireLock } // Unlock unlocks the workspace for this pull. diff --git a/server/events/working_dir_locker_test.go b/server/events/working_dir_locker_test.go index 11c21ea151..6a0689eb8b 100644 --- a/server/events/working_dir_locker_test.go +++ b/server/events/working_dir_locker_test.go @@ -14,7 +14,9 @@ package events_test import ( + "sync" "testing" + "time" "github.com/runatlantis/atlantis/server/events" . "github.com/runatlantis/atlantis/testing" @@ -25,7 +27,7 @@ var workspace = "default" var path = "." func TestTryLock(t *testing.T) { - locker := events.NewDefaultWorkingDirLocker() + locker := events.NewDefaultWorkingDirLocker(1) // The first lock should succeed. unlockFn, err := locker.TryLock(repo, 1, workspace, path) @@ -43,8 +45,36 @@ func TestTryLock(t *testing.T) { Ok(t, err) } +func TestTryLockConcurrently(t *testing.T) { + locker := events.NewDefaultWorkingDirLocker(1) + + t.Log("a second lock should wait for the first to unlock") + + var wg sync.WaitGroup + + wg.Add(1) + go func() { + t.Log("the first lock should acquire the lock") + defer wg.Done() + unlockFn, err := locker.TryLock(repo, 1, workspace, path) + time.Sleep(10 * time.Millisecond) + unlockFn() + Ok(t, err) + }() + + wg.Add(1) + go func() { + t.Log("and the second lock should wait for the first to unlock") + defer wg.Done() + time.Sleep(5 * time.Millisecond) + _, err := locker.TryLock(repo, 1, workspace, path) + Ok(t, err) + }() + wg.Wait() +} + func TestTryLockDifferentWorkspaces(t *testing.T) { - locker := events.NewDefaultWorkingDirLocker() + locker := events.NewDefaultWorkingDirLocker(1) t.Log("a lock for the same repo and pull but different workspace should succeed") _, err := locker.TryLock(repo, 1, workspace, path) @@ -60,7 +90,7 @@ func TestTryLockDifferentWorkspaces(t *testing.T) { } func TestTryLockDifferentRepo(t *testing.T) { - locker := events.NewDefaultWorkingDirLocker() + locker := events.NewDefaultWorkingDirLocker(1) t.Log("a lock for a different repo but the same workspace and pull should succeed") _, err := locker.TryLock(repo, 1, workspace, path) @@ -77,7 +107,7 @@ func TestTryLockDifferentRepo(t *testing.T) { } func TestTryLockDifferentPulls(t *testing.T) { - locker := events.NewDefaultWorkingDirLocker() + locker := events.NewDefaultWorkingDirLocker(1) t.Log("a lock for a different pull but the same repo and workspace should succeed") _, err := locker.TryLock(repo, 1, workspace, path) @@ -94,7 +124,7 @@ func TestTryLockDifferentPulls(t *testing.T) { } func TestTryLockDifferentPaths(t *testing.T) { - locker := events.NewDefaultWorkingDirLocker() + locker := events.NewDefaultWorkingDirLocker(1) t.Log("a lock for a different path but the same repo, pull, and workspace should succeed") _, err := locker.TryLock(repo, 1, workspace, path) @@ -111,7 +141,7 @@ func TestTryLockDifferentPaths(t *testing.T) { } func TestUnlock(t *testing.T) { - locker := events.NewDefaultWorkingDirLocker() + locker := events.NewDefaultWorkingDirLocker(1) t.Log("unlocking should work") unlockFn, err := locker.TryLock(repo, 1, workspace, path) @@ -122,7 +152,7 @@ func TestUnlock(t *testing.T) { } func TestUnlockDifferentWorkspaces(t *testing.T) { - locker := events.NewDefaultWorkingDirLocker() + locker := events.NewDefaultWorkingDirLocker(1) t.Log("unlocking should work for different workspaces") unlockFn1, err1 := locker.TryLock(repo, 1, workspace, path) Ok(t, err1) @@ -138,7 +168,7 @@ func TestUnlockDifferentWorkspaces(t *testing.T) { } func TestUnlockDifferentRepos(t *testing.T) { - locker := events.NewDefaultWorkingDirLocker() + locker := events.NewDefaultWorkingDirLocker(1) t.Log("unlocking should work for different repos") unlockFn1, err1 := locker.TryLock(repo, 1, workspace, path) Ok(t, err1) @@ -155,7 +185,7 @@ func TestUnlockDifferentRepos(t *testing.T) { } func TestUnlockDifferentPulls(t *testing.T) { - locker := events.NewDefaultWorkingDirLocker() + locker := events.NewDefaultWorkingDirLocker(1) t.Log("unlocking should work for different pulls") unlockFn1, err1 := locker.TryLock(repo, 1, workspace, path) Ok(t, err1) @@ -172,7 +202,7 @@ func TestUnlockDifferentPulls(t *testing.T) { } func TestLockPull(t *testing.T) { - locker := events.NewDefaultWorkingDirLocker() + locker := events.NewDefaultWorkingDirLocker(1) unlock, err := locker.TryLockPull("owner/repo", 1) Ok(t, err) @@ -202,7 +232,7 @@ func TestLockPull(t *testing.T) { // If the workspace was locked first, we shouldn't be able to get the pull lock. func TestLockPull_WorkspaceFirst(t *testing.T) { - locker := events.NewDefaultWorkingDirLocker() + locker := events.NewDefaultWorkingDirLocker(1) unlock, err := locker.TryLock("owner/repo", 1, "workspace", path) Ok(t, err) diff --git a/server/server.go b/server/server.go index af940f3787..a1954bdc23 100644 --- a/server/server.go +++ b/server/server.go @@ -489,7 +489,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { } applyLockingClient = locking.NewApplyClient(backend, disableApply, disableGlobalApplyLock) - workingDirLocker := events.NewDefaultWorkingDirLocker() + workingDirLocker := events.NewDefaultWorkingDirLocker(userConfig.LockAcquireTimeoutSeconds) var workingDir events.WorkingDir = &events.FileWorkspace{ DataDir: userConfig.DataDir, diff --git a/server/user_config.go b/server/user_config.go index 9a6247ae09..d4d5127e9b 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -71,6 +71,7 @@ type UserConfig struct { IncludeGitUntrackedFiles bool `mapstructure:"include-git-untracked-files"` APISecret string `mapstructure:"api-secret"` HidePrevPlanComments bool `mapstructure:"hide-prev-plan-comments"` + LockAcquireTimeoutSeconds int `mapstructure:"lock-acquire-timeout-seconds"` LockingDBType string `mapstructure:"locking-db-type"` LogLevel string `mapstructure:"log-level"` MarkdownTemplateOverridesDir string `mapstructure:"markdown-template-overrides-dir"`