diff --git a/README.md b/README.md index 803b242..509b285 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,12 @@ One of the following is required. * If the lock is in the claimed state we will wait for it to be unclaimed and proceed to update it as above. +* `check`: If set, we will check the lock status of a specified lock from the pool. + This is the atomic equivalent of performing a `claim` on that lock, followed + by a `release`. Like `claim`, will retry until the lock becomes available. + Note that this will, in fact, perform locking and unlocking operations, and will + produce new commits in the Git repository. + The value is the path to a directory containing the files `name` and `metadata` which should contain the name of your new lock and the contents you would like in the lock, respectively. diff --git a/cmd/out/main.go b/cmd/out/main.go index a386afa..ed29a34 100644 --- a/cmd/out/main.go +++ b/cmd/out/main.go @@ -102,6 +102,18 @@ func main() { } } + if request.Params.Check != "" { + lock = request.Params.Check + version, err = lockPool.ClaimLock(lock) + if err != nil { + fatal("claiming lock for check", err) + } + _, version, err = lockPool.UnclaimLock(lock) + if err != nil { + fatal("unclaiming lock for check", err) + } + } + err = json.NewEncoder(os.Stdout).Encode(out.OutResponse{ Version: version, Metadata: []out.MetadataPair{ diff --git a/integration/out_test.go b/integration/out_test.go index 7ad4377..b5f46ba 100644 --- a/integration/out_test.go +++ b/integration/out_test.go @@ -133,7 +133,7 @@ func itWorksWithBranch(branchName string) { It("complains about it", func() { errorMessages := string(session.Err.Contents()) - Ω(errorMessages).Should(ContainSubstring("invalid payload (missing acquire, release, remove, claim, add, or add_claimed)")) + Ω(errorMessages).Should(ContainSubstring("invalid payload (missing acquire, release, remove, claim, check, add, or add_claimed)")) }) }) }) @@ -1140,5 +1140,126 @@ func itWorksWithBranch(branchName string) { }) }) }) + + Context("when checking a lock", func() { + BeforeEach(func() { + outRequest = out.OutRequest{ + Source: out.Source{ + URI: bareGitRepo, + Branch: branchName, + Pool: "lock-pool", + RetryDelay: 100 * time.Millisecond, + }, + Params: out.OutParams{ + Check: "some-lock", + }, + } + session := runOut(outRequest, sourceDir) + <-session.Exited + Expect(session.ExitCode()).To(Equal(0)) + + err := json.Unmarshal(session.Out.Contents(), &outResponse) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("exits leaving it unclaimed", func() { + version := getVersion(bareGitRepo, "origin/"+branchName) + + reCloneRepo, err := ioutil.TempDir("", "git-version-repo") + Ω(err).ShouldNot(HaveOccurred()) + + defer os.RemoveAll(reCloneRepo) + + reClone := exec.Command("git", "clone", "--branch", branchName, bareGitRepo, ".") + reClone.Dir = reCloneRepo + err = reClone.Run() + Ω(err).ShouldNot(HaveOccurred()) + + _, err = ioutil.ReadFile(filepath.Join(reCloneRepo, "lock-pool", "unclaimed", "some-lock")) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(outResponse).Should(Equal(out.OutResponse{ + Version: version, + Metadata: []out.MetadataPair{ + {Name: "lock_name", Value: "some-lock"}, + {Name: "pool_name", Value: "lock-pool"}, + }, + })) + + }) + + It("actually does claim it", func() { + log := exec.Command("git", "log", "--oneline", "-2", "--reverse", outResponse.Version.Ref) + log.Dir = bareGitRepo + + session, err := gexec.Start(log, GinkgoWriter, GinkgoWriter) + Ω(err).ShouldNot(HaveOccurred()) + + <-session.Exited + + Ω(session).Should(gbytes.Say("pipeline-name/job-name build 42 claiming: some-lock")) + Ω(session).Should(gbytes.Say("pipeline-name/job-name build 42 unclaiming: some-lock")) + }) + + Context("when the specific lock is already claimed", func() { + var unclaimLockDir string + BeforeEach(func() { + var err error + unclaimLockDir, err = ioutil.TempDir("", "claiming-locks") + Ω(err).ShouldNot(HaveOccurred()) + + claimRequest := out.OutRequest{ + Source: out.Source{ + URI: bareGitRepo, + Branch: branchName, + Pool: "lock-pool", + RetryDelay: 100 * time.Millisecond, + }, + Params: out.OutParams{ + Claim: "some-lock", + }, + } + + session := runOut(claimRequest, sourceDir) + <-session.Exited + Expect(session.ExitCode()).To(Equal(0)) + + err = json.Unmarshal(session.Out.Contents(), &outResponse) + Ω(err).ShouldNot(HaveOccurred()) + }) + + AfterEach(func() { + err := os.RemoveAll(unclaimLockDir) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("continues to acquire the same lock", func() { + checkSession := runOut(outRequest, sourceDir) + Consistently(checkSession).ShouldNot(gexec.Exit(0)) + + unclaimLock := exec.Command("bash", "-e", "-c", fmt.Sprintf(` + git clone --branch %s %s . + + git config user.email "ginkgo@localhost" + git config user.name "Ginkgo Local" + + git mv lock-pool/claimed/some-lock lock-pool/unclaimed/ + git commit -am "unclaim some-lock" + git push + `, branchName, bareGitRepo)) + + unclaimLock.Stdout = GinkgoWriter + unclaimLock.Stderr = GinkgoWriter + unclaimLock.Dir = unclaimLockDir + + err := unclaimLock.Run() + Ω(err).ShouldNot(HaveOccurred()) + + <-checkSession.Exited + Expect(checkSession.ExitCode()).To(Equal(0)) + }) + + }) + }) }) } diff --git a/out/lock_pool.go b/out/lock_pool.go index 0548597..cc66ae8 100644 --- a/out/lock_pool.go +++ b/out/lock_pool.go @@ -110,10 +110,14 @@ func (lp *LockPool) ReleaseLock(inDir string) (string, Version, error) { } lockName := strings.TrimSpace(string(nameFileContents)) + return lp.UnclaimLock(lockName) +} + +func (lp *LockPool) UnclaimLock(lockName string) (string, Version, error) { fmt.Fprintf(lp.Output, "releasing lock: %s on pool: %s\n", lockName, lp.Source.Pool) var ref string - err = lp.performRobustAction(func() (bool, error) { + err := lp.performRobustAction(func() (bool, error) { var err error ref, err = lp.LockHandler.UnclaimLock(lockName) diff --git a/out/lock_pool_test.go b/out/lock_pool_test.go index 924abab..23ddef4 100644 --- a/out/lock_pool_test.go +++ b/out/lock_pool_test.go @@ -513,6 +513,91 @@ var _ = Describe("Lock Pool", func() { }) }) + Context("Unclaiming a lock", func() { + var lockDir string + + BeforeEach(func() { + var err error + lockDir, err = ioutil.TempDir("", "lock-dir") + Ω(err).ShouldNot(HaveOccurred()) + + }) + + AfterEach(func() { + err := os.RemoveAll(lockDir) + Ω(err).ShouldNot(HaveOccurred()) + }) + + Context("when setup fails", func() { + BeforeEach(func() { + fakeLockHandler.SetupReturns(errors.New("some-error")) + }) + + It("returns an error", func() { + _, _, err := lockPool.UnclaimLock("imaginary") + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("when setup succeeds", func() { + Context("when the lock doesn't exist", func() { + BeforeEach(func() { + fakeLockHandler.UnclaimLockReturns("", errors.New("lock not found")) + }) + + It("returns an error", func() { + _, _, err := lockPool.UnclaimLock("imaginary") + Ω(err).Should(HaveOccurred()) + }) + }) + + Context("when the lock does exist", func() { + BeforeEach(func() { + fakeLockHandler.UnclaimLockReturns("some-ref", nil) + err := ioutil.WriteFile(filepath.Join(lockDir, "name"), []byte("some-lock"), 0755) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("tries to unclaim it", func() { + _, _, err := lockPool.UnclaimLock("some-lock") + Ω(err).ShouldNot(HaveOccurred()) + + Ω(fakeLockHandler.UnclaimLockCallCount()).Should(Equal(1)) + lockName := fakeLockHandler.UnclaimLockArgsForCall(0) + Ω(lockName).Should(Equal("some-lock")) + }) + + It("tries to broadcast to the lock pool", func() { + _, _, err := lockPool.UnclaimLock("some-lock") + Ω(err).ShouldNot(HaveOccurred()) + + Ω(fakeLockHandler.BroadcastLockPoolCallCount()).Should(Equal(1)) + }) + + ValidateSharedBehaviorDuringBroadcastFailures( + func() error { + _, _, err := lockPool.UnclaimLock("some-lock") + return err + }, func(expectedNumberOfInteractions int) { + Ω(fakeLockHandler.ResetLockCallCount()).Should(Equal(expectedNumberOfInteractions)) + Ω(fakeLockHandler.UnclaimLockCallCount()).Should(Equal(expectedNumberOfInteractions)) + }) + + Context("when broadcasting succeeds", func() { + It("returns the lockname, and a version", func() { + lockName, version, err := lockPool.UnclaimLock("some-lock") + + Ω(err).ShouldNot(HaveOccurred()) + Ω(lockName).Should(Equal("some-lock")) + Ω(version).Should(Equal(out.Version{ + Ref: "some-ref", + })) + }) + }) + }) + }) + }) + Context("adding an initially unclaimed lock", func() { var lockDir string @@ -790,7 +875,7 @@ var _ = Describe("Lock Pool", func() { }) }) - Context( "when resetting the lock succeeds", func() { + Context("when resetting the lock succeeds", func() { It("tries to update the lock it found in the name file", func() { _, _, err := lockPool.UpdateLock(lockDir) Ω(err).ShouldNot(HaveOccurred()) diff --git a/out/out_request.go b/out/out_request.go index f0f6ff6..6d10c0d 100644 --- a/out/out_request.go +++ b/out/out_request.go @@ -54,6 +54,7 @@ type OutParams struct { Remove string `json:"remove"` Claim string `json:"claim"` Update string `json:"update"` + Check string `json:"check"` } func (request OutRequest) Validate() []string { @@ -77,8 +78,9 @@ func (request OutRequest) Validate() []string { request.Params.AddClaimed == "" && request.Params.Remove == "" && request.Params.Claim == "" && - request.Params.Update == "" { - errorMessages = append(errorMessages, "invalid payload (missing acquire, release, remove, claim, add, or add_claimed)") + request.Params.Update == "" && + request.Params.Check == "" { + errorMessages = append(errorMessages, "invalid payload (missing acquire, release, remove, claim, check, add, or add_claimed)") } return errorMessages