Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement an atomic lock check action. #37

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 12 additions & 0 deletions cmd/out/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
123 changes: 122 additions & 1 deletion integration/out_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)"))
})
})
})
Expand Down Expand Up @@ -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))
})

})
})
})
}
6 changes: 5 additions & 1 deletion out/lock_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
87 changes: 86 additions & 1 deletion out/lock_pool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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())
Expand Down
6 changes: 4 additions & 2 deletions out/out_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down