diff options
| author | Kamil Trzciński <ayufan@ayufan.eu> | 2019-07-30 13:59:21 +0000 |
|---|---|---|
| committer | Kamil Trzciński <ayufan@ayufan.eu> | 2019-07-30 13:59:21 +0000 |
| commit | c19e7f224070016cb61f2877b4f50fc7787a29a3 (patch) | |
| tree | 6e4bc753ec674705f90ed5e60d6ca628160e6d03 | |
| parent | Merge branch 'windows-sh' into 'master' (diff) | |
| parent | Prevent running multiple instances of the gitlab-runner process using the sam... (diff) | |
| download | gitlab-runner-c19e7f224070016cb61f2877b4f50fc7787a29a3.tar.gz | |
Merge branch '4407-prevent-running-multiple-runner-process-instances' into 'master'
Prevent running multiple instances of the gitlab-runner process using the same configuration file
Closes #4407
See merge request gitlab-org/gitlab-runner!1496
| -rw-r--r-- | Gopkg.lock | 9 | ||||
| -rw-r--r-- | Gopkg.toml | 4 | ||||
| -rw-r--r-- | Makefile | 1 | ||||
| -rw-r--r-- | commands/config.go | 5 | ||||
| -rw-r--r-- | commands/multi.go | 17 | ||||
| -rw-r--r-- | commands/multi_test.go | 77 | ||||
| -rw-r--r-- | helpers/fslocker/errors.go | 17 | ||||
| -rw-r--r-- | helpers/fslocker/fslocker.go | 38 | ||||
| -rw-r--r-- | helpers/fslocker/fslocker_test.go | 110 | ||||
| -rw-r--r-- | helpers/fslocker/mock_locker.go | 47 | ||||
| -rw-r--r-- | vendor/github.com/gofrs/flock/LICENSE | 27 | ||||
| -rw-r--r-- | vendor/github.com/gofrs/flock/flock.go | 127 | ||||
| -rw-r--r-- | vendor/github.com/gofrs/flock/flock_example_test.go | 72 | ||||
| -rw-r--r-- | vendor/github.com/gofrs/flock/flock_test.go | 290 | ||||
| -rw-r--r-- | vendor/github.com/gofrs/flock/flock_unix.go | 195 | ||||
| -rw-r--r-- | vendor/github.com/gofrs/flock/flock_winapi.go | 76 | ||||
| -rw-r--r-- | vendor/github.com/gofrs/flock/flock_windows.go | 140 |
17 files changed, 1250 insertions, 2 deletions
@@ -275,6 +275,14 @@ revision = "20b96f641a5ea98f2f8619ff4f3e061cff4833bd" [[projects]] + branch = "master" + digest = "1:e27d558706a107ae82e91ac3010bb15d3978faab98ded7435d640e28007e611c" + name = "github.com/gofrs/flock" + packages = ["."] + pruneopts = "N" + revision = "5135e617513b1e6e205a3a89b042249dee6730c8" + +[[projects]] digest = "1:809e332c8b79399d55f1fd11f5e45b892bc0fb25c7c912898ad307a8d0506164" name = "github.com/gogo/protobuf" packages = [ @@ -1052,6 +1060,7 @@ "github.com/docker/go-units", "github.com/docker/machine/commands/mcndirs", "github.com/getsentry/raven-go", + "github.com/gofrs/flock", "github.com/golang/mock/gomock", "github.com/gorhill/cronexpr", "github.com/gorilla/mux", @@ -162,6 +162,10 @@ ignored = ["test", "appengine"] name = "golang.org/x/sys" branch = "master" +[[constraint]] + name = "github.com/gofrs/flock" + branch = "master" + ## ## Refrain innovations ;) ## @@ -146,6 +146,7 @@ mocks: $(MOCKERY) GOPATH=$(ORIGINAL_GOPATH) mockery $(MOCKERY_FLAGS) -dir=./vendor/github.com/ayufan/golang-kardianos-service -output=./helpers/service/mocks -name='(Interface)' GOPATH=$(ORIGINAL_GOPATH) mockery $(MOCKERY_FLAGS) -dir=./helpers/docker -all -inpkg GOPATH=$(ORIGINAL_GOPATH) mockery $(MOCKERY_FLAGS) -dir=./helpers/certificate -all -inpkg + GOPATH=$(ORIGINAL_GOPATH) mockery $(MOCKERY_FLAGS) -dir=./helpers/fslocker -all -inpkg GOPATH=$(ORIGINAL_GOPATH) mockery $(MOCKERY_FLAGS) -dir=./executors/docker -all -inpkg GOPATH=$(ORIGINAL_GOPATH) mockery $(MOCKERY_FLAGS) -dir=./executors/custom -all -inpkg GOPATH=$(ORIGINAL_GOPATH) mockery $(MOCKERY_FLAGS) -dir=./cache -all -inpkg diff --git a/commands/config.go b/commands/config.go index 7f2eddca..dd176437 100644 --- a/commands/config.go +++ b/commands/config.go @@ -10,6 +10,7 @@ import ( "github.com/sirupsen/logrus" "gitlab.com/gitlab-org/gitlab-runner/common" + "gitlab.com/gitlab-org/gitlab-runner/helpers/fslocker" "gitlab.com/gitlab-org/gitlab-runner/network" ) @@ -41,6 +42,10 @@ func (c *configOptions) loadConfig() error { return nil } +func (c *configOptions) inLock(fn func()) error { + return fslocker.InLock(c.ConfigFile, fn) +} + func (c *configOptions) RunnerByName(name string) (*common.RunnerConfig, error) { if c.config == nil { return nil, fmt.Errorf("config has not been loaded") diff --git a/commands/multi.go b/commands/multi.go index d272ab08..e67eccc4 100644 --- a/commands/multi.go +++ b/commands/multi.go @@ -403,7 +403,7 @@ func (mr *RunCommand) Start(s service.Service) error { } // Start should not block. Do the actual work async. - go mr.Run() + go mr.RunWithLock() return nil } @@ -575,6 +575,19 @@ func (mr *RunCommand) setupSessionServer() { Info("Session server listening") } +func (mr *RunCommand) RunWithLock() { + log := mr.log().WithFields(logrus.Fields{ + "file": mr.ConfigFile, + "pid": os.Getpid(), + }) + log.Info("Locking configuration file") + + err := mr.inLock(mr.Run) + if err != nil { + log.WithError(err).Fatal("Could not handle configuration file locking") + } +} + func (mr *RunCommand) Run() { mr.setupMetricsAndDebugServer() mr.setupSessionServer() @@ -609,7 +622,7 @@ func (mr *RunCommand) Run() { mr.currentWorkers-- } mr.log().Println("All workers stopped. Can exit now") - mr.runFinished <- true + close(mr.runFinished) } func (mr *RunCommand) interruptRun() { diff --git a/commands/multi_test.go b/commands/multi_test.go index 0d635967..0f1c1a79 100644 --- a/commands/multi_test.go +++ b/commands/multi_test.go @@ -1,14 +1,20 @@ package commands import ( + "io/ioutil" + "os" "sync" "sync/atomic" "testing" "time" + "github.com/gofrs/flock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitlab-runner/common" + "gitlab.com/gitlab-org/gitlab-runner/helpers" ) func TestProcessRunner_BuildLimit(t *testing.T) { @@ -110,3 +116,74 @@ func TestProcessRunner_BuildLimit(t *testing.T) { // Wait for all builds to finish. wg.Wait() } + +func runFileLockingCmd(t *testing.T, wg *sync.WaitGroup, started chan bool, stop chan bool, filePath string, shouldPanic bool) { + cmd := &RunCommand{ + configOptionsWithListenAddress: configOptionsWithListenAddress{ + configOptions: configOptions{ + ConfigFile: filePath, + config: &common.Config{ + Concurrent: 5, + }, + }, + }, + stopSignals: make(chan os.Signal), + reloadSignal: make(chan os.Signal, 1), + runFinished: make(chan bool, 1), + } + + go func() { + if shouldPanic { + assert.Panics(t, cmd.RunWithLock, "Expected the Runner to create a new lock") + } else { + assert.NotPanics(t, cmd.RunWithLock, "Expected the Runner to reject creating a new lock") + } + wg.Done() + }() + + close(started) + + <-stop + cmd.stopSignal = os.Kill +} + +func TestMulti_RunWithLock(t *testing.T) { + defer helpers.MakeFatalToPanic()() + + file, err := ioutil.TempFile("", "config.toml") + require.NoError(t, err) + + err = file.Close() + require.NoError(t, err) + + filePath := file.Name() + + wg := new(sync.WaitGroup) + stop := make(chan bool) + + wg.Add(2) + + started := make(chan bool) + go runFileLockingCmd(t, wg, started, stop, filePath, false) + <-started + + time.Sleep(1 * time.Second) + + started = make(chan bool) + go runFileLockingCmd(t, wg, started, stop, filePath, true) + <-started + + time.Sleep(1 * time.Second) + + close(stop) + wg.Wait() + + // Try to lock the file to check if it was properly unlocked while + // finishing cmd.RunWithLock() call + fl := flock.New(filePath) + locked, err := fl.TryLock() + defer fl.Unlock() + + assert.True(t, locked, "File was not unlocked!") + assert.NoError(t, err) +} diff --git a/helpers/fslocker/errors.go b/helpers/fslocker/errors.go new file mode 100644 index 00000000..fe7b57e3 --- /dev/null +++ b/helpers/fslocker/errors.go @@ -0,0 +1,17 @@ +package fslocker + +import ( + "github.com/pkg/errors" +) + +func errFileInUse(path string) error { + return errors.Errorf("file %q is locked by another process", path) +} + +func errCantAcquireLock(inner error, path string) error { + return errors.Wrapf(inner, "can't acquire file lock on %q file", path) +} + +func errCantReleaseLock(inner error, path string) error { + return errors.Wrapf(inner, "can't release file lock on %q file", path) +} diff --git a/helpers/fslocker/fslocker.go b/helpers/fslocker/fslocker.go new file mode 100644 index 00000000..f8ae41ba --- /dev/null +++ b/helpers/fslocker/fslocker.go @@ -0,0 +1,38 @@ +package fslocker + +import ( + "github.com/gofrs/flock" +) + +type locker interface { + TryLock() (bool, error) + Unlock() error +} + +var defaultLocker = func(path string) locker { + return flock.New(path) +} + +func InLock(filePath string, fn func()) (err error) { + locker := defaultLocker(filePath) + + lock, err := locker.TryLock() + if err != nil { + return errCantAcquireLock(err, filePath) + } + + if !lock { + return errFileInUse(filePath) + } + + defer func() { + unlockErr := locker.Unlock() + if unlockErr != nil { + err = errCantReleaseLock(unlockErr, filePath) + } + }() + + fn() + + return +} diff --git a/helpers/fslocker/fslocker_test.go b/helpers/fslocker/fslocker_test.go new file mode 100644 index 00000000..91f54838 --- /dev/null +++ b/helpers/fslocker/fslocker_test.go @@ -0,0 +1,110 @@ +package fslocker + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +type mockFn struct { + mock.Mock +} + +func (l *mockFn) Run() { + l.Called() +} + +func mockDefaultLocker(t *testing.T, expectedPath string) (*mockLocker, func()) { + fsLockerMock := new(mockLocker) + + oldLocker := defaultLocker + cleanup := func() { + defaultLocker = oldLocker + fsLockerMock.AssertExpectations(t) + } + + defaultLocker = func(path string) locker { + assert.Equal(t, expectedPath, path) + + return fsLockerMock + } + + return fsLockerMock, cleanup +} + +func TestInLock(t *testing.T) { + filePath := "/some/path/to/config/file" + testError := errors.New("test-error") + + tests := map[string]struct { + fileLocker locker + prepareMockAssertions func(locker *mockLocker, fn *mockFn) + expectedError error + }{ + "error on file locking": { + prepareMockAssertions: func(locker *mockLocker, fn *mockFn) { + locker.On("TryLock"). + Return(true, testError). + Once() + }, + expectedError: errCantAcquireLock(testError, filePath), + }, + "can't lock the file": { + prepareMockAssertions: func(locker *mockLocker, fn *mockFn) { + locker.On("TryLock"). + Return(false, nil). + Once() + + }, + expectedError: errFileInUse(filePath), + }, + "file locked properly, fails on unlock": { + prepareMockAssertions: func(locker *mockLocker, fn *mockFn) { + locker.On("TryLock"). + Return(true, nil). + Once() + locker.On("Unlock"). + Return(testError). + Once() + + fn.On("Run").Once() + }, + expectedError: errCantReleaseLock(testError, filePath), + }, + "file locked and unlocked properly": { + prepareMockAssertions: func(locker *mockLocker, fn *mockFn) { + locker.On("TryLock"). + Return(true, nil). + Once() + locker.On("Unlock"). + Return(nil). + Once() + + fn.On("Run").Once() + }, + }, + } + + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + fnMock := new(mockFn) + defer fnMock.AssertExpectations(t) + + fsLockerMock, cleanup := mockDefaultLocker(t, filePath) + defer cleanup() + + testCase.prepareMockAssertions(fsLockerMock, fnMock) + + err := InLock(filePath, fnMock.Run) + + if testCase.expectedError == nil { + assert.NoError(t, err) + return + } + + assert.EqualError(t, err, testCase.expectedError.Error()) + }) + } +} diff --git a/helpers/fslocker/mock_locker.go b/helpers/fslocker/mock_locker.go new file mode 100644 index 00000000..1e979768 --- /dev/null +++ b/helpers/fslocker/mock_locker.go @@ -0,0 +1,47 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +// This comment works around https://github.com/vektra/mockery/issues/155 + +package fslocker + +import mock "github.com/stretchr/testify/mock" + +// mockLocker is an autogenerated mock type for the locker type +type mockLocker struct { + mock.Mock +} + +// TryLock provides a mock function with given fields: +func (_m *mockLocker) TryLock() (bool, error) { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Unlock provides a mock function with given fields: +func (_m *mockLocker) Unlock() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/vendor/github.com/gofrs/flock/LICENSE b/vendor/github.com/gofrs/flock/LICENSE new file mode 100644 index 00000000..aff7d358 --- /dev/null +++ b/vendor/github.com/gofrs/flock/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2015, Tim Heckman +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of linode-netint nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/gofrs/flock/flock.go b/vendor/github.com/gofrs/flock/flock.go new file mode 100644 index 00000000..8f109b8a --- /dev/null +++ b/vendor/github.com/gofrs/flock/flock.go @@ -0,0 +1,127 @@ +// Copyright 2015 Tim Heckman. All rights reserved. +// Use of this source code is governed by the BSD 3-Clause +// license that can be found in the LICENSE file. + +// Package flock implements a thread-safe interface for file locking. +// It also includes a non-blocking TryLock() function to allow locking +// without blocking execution. +// +// Package flock is released under the BSD 3-Clause License. See the LICENSE file +// for more details. +// +// While using this library, remember that the locking behaviors are not +// guaranteed to be the same on each platform. For example, some UNIX-like +// operating systems will transparently convert a shared lock to an exclusive +// lock. If you Unlock() the flock from a location where you believe that you +// have the shared lock, you may accidentally drop the exclusive lock. +package flock + +import ( + "context" + "os" + "sync" + "time" +) + +// Flock is the struct type to handle file locking. All fields are unexported, +// with access to some of the fields provided by getter methods (Path() and Locked()). +type Flock struct { + path string + m sync.RWMutex + fh *os.File + l bool + r bool +} + +// New returns a new instance of *Flock. The only parameter +// it takes is the path to the desired lockfile. +func New(path string) *Flock { + return &Flock{path: path} +} + +// NewFlock returns a new instance of *Flock. The only parameter +// it takes is the path to the desired lockfile. +// +// Deprecated: Use New instead. +func NewFlock(path string) *Flock { + return New(path) +} + +// Close is equivalent to calling Unlock. +// +// This will release the lock and close the underlying file descriptor. +// It will not remove the file from disk, that's up to your application. +func (f *Flock) Close() error { + return f.Unlock() +} + +// Path returns the path as provided in NewFlock(). +func (f *Flock) Path() string { + return f.path +} + +// Locked returns the lock state (locked: true, unlocked: false). +// +// Warning: by the time you use the returned value, the state may have changed. +func (f *Flock) Locked() bool { + f.m.RLock() + defer f.m.RUnlock() + return f.l +} + +// RLocked returns the read lock state (locked: true, unlocked: false). +// +// Warning: by the time you use the returned value, the state may have changed. +func (f *Flock) RLocked() bool { + f.m.RLock() + defer f.m.RUnlock() + return f.r +} + +func (f *Flock) String() string { + return f.path +} + +// TryLockContext repeatedly tries to take an exclusive lock until one of the +// conditions is met: TryLock succeeds, TryLock fails with error, or Context +// Done channel is closed. +func (f *Flock) TryLockContext(ctx context.Context, retryDelay time.Duration) (bool, error) { + return tryCtx(ctx, f.TryLock, retryDelay) +} + +// TryRLockContext repeatedly tries to take a shared lock until one of the +// conditions is met: TryRLock succeeds, TryRLock fails with error, or Context +// Done channel is closed. +func (f *Flock) TryRLockContext(ctx context.Context, retryDelay time.Duration) (bool, error) { + return tryCtx(ctx, f.TryRLock, retryDelay) +} + +func tryCtx(ctx context.Context, fn func() (bool, error), retryDelay time.Duration) (bool, error) { + if ctx.Err() != nil { + return false, ctx.Err() + } + for { + if ok, err := fn(); ok || err != nil { + return ok, err + } + select { + case <-ctx.Done(): + return false, ctx.Err() + case <-time.After(retryDelay): + // try again + } + } +} + +func (f *Flock) setFh() error { + // open a new os.File instance + // create it if it doesn't exist, and open the file read-only. + fh, err := os.OpenFile(f.path, os.O_CREATE|os.O_RDONLY, os.FileMode(0600)) + if err != nil { + return err + } + + // set the filehandle on the struct + f.fh = fh + return nil +} diff --git a/vendor/github.com/gofrs/flock/flock_example_test.go b/vendor/github.com/gofrs/flock/flock_example_test.go new file mode 100644 index 00000000..0c2afa5c --- /dev/null +++ b/vendor/github.com/gofrs/flock/flock_example_test.go @@ -0,0 +1,72 @@ +// Copyright 2015 Tim Heckman. All rights reserved. +// Copyright 2018 The Gofrs. All rights reserved. +// Use of this source code is governed by the BSD 3-Clause +// license that can be found in the LICENSE file. + +package flock_test + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/gofrs/flock" +) + +func ExampleFlock_Locked() { + f := flock.New(os.TempDir() + "/go-lock.lock") + f.TryLock() // unchecked errors here + + fmt.Printf("locked: %v\n", f.Locked()) + + f.Unlock() + + fmt.Printf("locked: %v\n", f.Locked()) + // Output: locked: true + // locked: false +} + +func ExampleFlock_TryLock() { + // should probably put these in /var/lock + fileLock := flock.New(os.TempDir() + "/go-lock.lock") + + locked, err := fileLock.TryLock() + + if err != nil { + // handle locking error + } + + if locked { + fmt.Printf("path: %s; locked: %v\n", fileLock.Path(), fileLock.Locked()) + + if err := fileLock.Unlock(); err != nil { + // handle unlock error + } + } + + fmt.Printf("path: %s; locked: %v\n", fileLock.Path(), fileLock.Locked()) +} + +func ExampleFlock_TryLockContext() { + // should probably put these in /var/lock + fileLock := flock.New(os.TempDir() + "/go-lock.lock") + + lockCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + locked, err := fileLock.TryLockContext(lockCtx, 678*time.Millisecond) + + if err != nil { + // handle locking error + } + + if locked { + fmt.Printf("path: %s; locked: %v\n", fileLock.Path(), fileLock.Locked()) + + if err := fileLock.Unlock(); err != nil { + // handle unlock error + } + } + + fmt.Printf("path: %s; locked: %v\n", fileLock.Path(), fileLock.Locked()) +} diff --git a/vendor/github.com/gofrs/flock/flock_test.go b/vendor/github.com/gofrs/flock/flock_test.go new file mode 100644 index 00000000..e277e525 --- /dev/null +++ b/vendor/github.com/gofrs/flock/flock_test.go @@ -0,0 +1,290 @@ +// Copyright 2015 Tim Heckman. All rights reserved. +// Copyright 2018 The Gofrs. All rights reserved. +// Use of this source code is governed by the BSD 3-Clause +// license that can be found in the LICENSE file. + +package flock_test + +import ( + "context" + "io/ioutil" + "os" + "testing" + "time" + + "github.com/gofrs/flock" + + . "gopkg.in/check.v1" +) + +type TestSuite struct { + path string + flock *flock.Flock +} + +var _ = Suite(&TestSuite{}) + +func Test(t *testing.T) { TestingT(t) } + +func (t *TestSuite) SetUpTest(c *C) { + tmpFile, err := ioutil.TempFile(os.TempDir(), "go-flock-") + c.Assert(err, IsNil) + c.Assert(tmpFile, Not(IsNil)) + + t.path = tmpFile.Name() + + defer os.Remove(t.path) + tmpFile.Close() + + t.flock = flock.New(t.path) +} + +func (t *TestSuite) TearDownTest(c *C) { + t.flock.Unlock() + os.Remove(t.path) +} + +func (t *TestSuite) TestNew(c *C) { + var f *flock.Flock + + f = flock.New(t.path) + c.Assert(f, Not(IsNil)) + c.Check(f.Path(), Equals, t.path) + c.Check(f.Locked(), Equals, false) + c.Check(f.RLocked(), Equals, false) +} + +func (t *TestSuite) TestFlock_Path(c *C) { + var path string + path = t.flock.Path() + c.Check(path, Equals, t.path) +} + +func (t *TestSuite) TestFlock_Locked(c *C) { + var locked bool + locked = t.flock.Locked() + c.Check(locked, Equals, false) +} + +func (t *TestSuite) TestFlock_RLocked(c *C) { + var locked bool + locked = t.flock.RLocked() + c.Check(locked, Equals, false) +} + +func (t *TestSuite) TestFlock_String(c *C) { + var str string + str = t.flock.String() + c.Assert(str, Equals, t.path) +} + +func (t *TestSuite) TestFlock_TryLock(c *C) { + c.Assert(t.flock.Locked(), Equals, false) + c.Assert(t.flock.RLocked(), Equals, false) + + var locked bool + var err error + + locked, err = t.flock.TryLock() + c.Assert(err, IsNil) + c.Check(locked, Equals, true) + c.Check(t.flock.Locked(), Equals, true) + c.Check(t.flock.RLocked(), Equals, false) + + locked, err = t.flock.TryLock() + c.Assert(err, IsNil) + c.Check(locked, Equals, true) + + // make sure we just return false with no error in cases + // where we would have been blocked + locked, err = flock.New(t.path).TryLock() + c.Assert(err, IsNil) + c.Check(locked, Equals, false) +} + +func (t *TestSuite) TestFlock_TryRLock(c *C) { + c.Assert(t.flock.Locked(), Equals, false) + c.Assert(t.flock.RLocked(), Equals, false) + + var locked bool + var err error + + locked, err = t.flock.TryRLock() + c.Assert(err, IsNil) + c.Check(locked, Equals, true) + c.Check(t.flock.Locked(), Equals, false) + c.Check(t.flock.RLocked(), Equals, true) + + locked, err = t.flock.TryRLock() + c.Assert(err, IsNil) + c.Check(locked, Equals, true) + + // shared lock should not block. + flock2 := flock.New(t.path) + locked, err = flock2.TryRLock() + c.Assert(err, IsNil) + c.Check(locked, Equals, true) + + // make sure we just return false with no error in cases + // where we would have been blocked + t.flock.Unlock() + flock2.Unlock() + t.flock.Lock() + locked, err = flock.New(t.path).TryRLock() + c.Assert(err, IsNil) + c.Check(locked, Equals, false) +} + +func (t *TestSuite) TestFlock_TryLockContext(c *C) { + // happy path + ctx, cancel := context.WithCancel(context.Background()) + locked, err := t.flock.TryLockContext(ctx, time.Second) + c.Assert(err, IsNil) + c.Check(locked, Equals, true) + + // context already canceled + cancel() + locked, err = flock.New(t.path).TryLockContext(ctx, time.Second) + c.Assert(err, Equals, context.Canceled) + c.Check(locked, Equals, false) + + // timeout + ctx, cancel = context.WithTimeout(context.Background(), 10*time.Millisecond) + defer cancel() + locked, err = flock.New(t.path).TryLockContext(ctx, time.Second) + c.Assert(err, Equals, context.DeadlineExceeded) + c.Check(locked, Equals, false) +} + +func (t *TestSuite) TestFlock_TryRLockContext(c *C) { + // happy path + ctx, cancel := context.WithCancel(context.Background()) + locked, err := t.flock.TryRLockContext(ctx, time.Second) + c.Assert(err, IsNil) + c.Check(locked, Equals, true) + + // context already canceled + cancel() + locked, err = flock.New(t.path).TryRLockContext(ctx, time.Second) + c.Assert(err, Equals, context.Canceled) + c.Check(locked, Equals, false) + + // timeout + t.flock.Unlock() + t.flock.Lock() + ctx, cancel = context.WithTimeout(context.Background(), 10*time.Millisecond) + defer cancel() + locked, err = flock.New(t.path).TryRLockContext(ctx, time.Second) + c.Assert(err, Equals, context.DeadlineExceeded) + c.Check(locked, Equals, false) +} + +func (t *TestSuite) TestFlock_Unlock(c *C) { + var err error + + err = t.flock.Unlock() + c.Assert(err, IsNil) + + // get a lock for us to unlock + locked, err := t.flock.TryLock() + c.Assert(err, IsNil) + c.Assert(locked, Equals, true) + c.Assert(t.flock.Locked(), Equals, true) + c.Check(t.flock.RLocked(), Equals, false) + + _, err = os.Stat(t.path) + c.Assert(os.IsNotExist(err), Equals, false) + + err = t.flock.Unlock() + c.Assert(err, IsNil) + c.Check(t.flock.Locked(), Equals, false) + c.Check(t.flock.RLocked(), Equals, false) +} + +func (t *TestSuite) TestFlock_Lock(c *C) { + c.Assert(t.flock.Locked(), Equals, false) + c.Check(t.flock.RLocked(), Equals, false) + + var err error + + err = t.flock.Lock() + c.Assert(err, IsNil) + c.Check(t.flock.Locked(), Equals, true) + c.Check(t.flock.RLocked(), Equals, false) + + // test that the short-circuit works + err = t.flock.Lock() + c.Assert(err, IsNil) + + // + // Test that Lock() is a blocking call + // + ch := make(chan error, 2) + gf := flock.New(t.path) + defer gf.Unlock() + + go func(ch chan<- error) { + ch <- nil + ch <- gf.Lock() + close(ch) + }(ch) + + errCh, ok := <-ch + c.Assert(ok, Equals, true) + c.Assert(errCh, IsNil) + + err = t.flock.Unlock() + c.Assert(err, IsNil) + + errCh, ok = <-ch + c.Assert(ok, Equals, true) + c.Assert(errCh, IsNil) + c.Check(t.flock.Locked(), Equals, false) + c.Check(t.flock.RLocked(), Equals, false) + c.Check(gf.Locked(), Equals, true) + c.Check(gf.RLocked(), Equals, false) +} + +func (t *TestSuite) TestFlock_RLock(c *C) { + c.Assert(t.flock.Locked(), Equals, false) + c.Check(t.flock.RLocked(), Equals, false) + + var err error + + err = t.flock.RLock() + c.Assert(err, IsNil) + c.Check(t.flock.Locked(), Equals, false) + c.Check(t.flock.RLocked(), Equals, true) + + // test that the short-circuit works + err = t.flock.RLock() + c.Assert(err, IsNil) + + // + // Test that RLock() is a blocking call + // + ch := make(chan error, 2) + gf := flock.New(t.path) + defer gf.Unlock() + + go func(ch chan<- error) { + ch <- nil + ch <- gf.RLock() + close(ch) + }(ch) + + errCh, ok := <-ch + c.Assert(ok, Equals, true) + c.Assert(errCh, IsNil) + + err = t.flock.Unlock() + c.Assert(err, IsNil) + + errCh, ok = <-ch + c.Assert(ok, Equals, true) + c.Assert(errCh, IsNil) + c.Check(t.flock.Locked(), Equals, false) + c.Check(t.flock.RLocked(), Equals, false) + c.Check(gf.Locked(), Equals, false) + c.Check(gf.RLocked(), Equals, true) +} diff --git a/vendor/github.com/gofrs/flock/flock_unix.go b/vendor/github.com/gofrs/flock/flock_unix.go new file mode 100644 index 00000000..45f71a70 --- /dev/null +++ b/vendor/github.com/gofrs/flock/flock_unix.go @@ -0,0 +1,195 @@ +// Copyright 2015 Tim Heckman. All rights reserved. +// Use of this source code is governed by the BSD 3-Clause +// license that can be found in the LICENSE file. + +// +build !windows + +package flock + +import ( + "os" + "syscall" +) + +// Lock is a blocking call to try and take an exclusive file lock. It will wait +// until it is able to obtain the exclusive file lock. It's recommended that +// TryLock() be used over this function. This function may block the ability to +// query the current Locked() or RLocked() status due to a RW-mutex lock. +// +// If we are already exclusive-locked, this function short-circuits and returns +// immediately assuming it can take the mutex lock. +// +// If the *Flock has a shared lock (RLock), this may transparently replace the +// shared lock with an exclusive lock on some UNIX-like operating systems. Be +// careful when using exclusive locks in conjunction with shared locks +// (RLock()), because calling Unlock() may accidentally release the exclusive +// lock that was once a shared lock. +func (f *Flock) Lock() error { + return f.lock(&f.l, syscall.LOCK_EX) +} + +// RLock is a blocking call to try and take a shared file lock. It will wait +// until it is able to obtain the shared file lock. It's recommended that +// TryRLock() be used over this function. This function may block the ability to +// query the current Locked() or RLocked() status due to a RW-mutex lock. +// +// If we are already shared-locked, this function short-circuits and returns +// immediately assuming it can take the mutex lock. +func (f *Flock) RLock() error { + return f.lock(&f.r, syscall.LOCK_SH) +} + +func (f *Flock) lock(locked *bool, flag int) error { + f.m.Lock() + defer f.m.Unlock() + + if *locked { + return nil + } + + if f.fh == nil { + if err := f.setFh(); err != nil { + return err + } + } + + if err := syscall.Flock(int(f.fh.Fd()), flag); err != nil { + shouldRetry, reopenErr := f.reopenFDOnError(err) + if reopenErr != nil { + return reopenErr + } + + if !shouldRetry { + return err + } + + if err = syscall.Flock(int(f.fh.Fd()), flag); err != nil { + return err + } + } + + *locked = true + return nil +} + +// Unlock is a function to unlock the file. This file takes a RW-mutex lock, so +// while it is running the Locked() and RLocked() functions will be blocked. +// +// This function short-circuits if we are unlocked already. If not, it calls +// syscall.LOCK_UN on the file and closes the file descriptor. It does not +// remove the file from disk. It's up to your application to do. +// +// Please note, if your shared lock became an exclusive lock this may +// unintentionally drop the exclusive lock if called by the consumer that +// believes they have a shared lock. Please see Lock() for more details. +func (f *Flock) Unlock() error { + f.m.Lock() + defer f.m.Unlock() + + // if we aren't locked or if the lockfile instance is nil + // just return a nil error because we are unlocked + if (!f.l && !f.r) || f.fh == nil { + return nil + } + + // mark the file as unlocked + if err := syscall.Flock(int(f.fh.Fd()), syscall.LOCK_UN); err != nil { + return err + } + + f.fh.Close() + + f.l = false + f.r = false + f.fh = nil + + return nil +} + +// TryLock is the preferred function for taking an exclusive file lock. This +// function takes an RW-mutex lock before it tries to lock the file, so there is +// the possibility that this function may block for a short time if another +// goroutine is trying to take any action. +// +// The actual file lock is non-blocking. If we are unable to get the exclusive +// file lock, the function will return false instead of waiting for the lock. If +// we get the lock, we also set the *Flock instance as being exclusive-locked. +func (f *Flock) TryLock() (bool, error) { + return f.try(&f.l, syscall.LOCK_EX) +} + +// TryRLock is the preferred function for taking a shared file lock. This +// function takes an RW-mutex lock before it tries to lock the file, so there is +// the possibility that this function may block for a short time if another +// goroutine is trying to take any action. +// +// The actual file lock is non-blocking. If we are unable to get the shared file +// lock, the function will return false instead of waiting for the lock. If we +// get the lock, we also set the *Flock instance as being share-locked. +func (f *Flock) TryRLock() (bool, error) { + return f.try(&f.r, syscall.LOCK_SH) +} + +func (f *Flock) try(locked *bool, flag int) (bool, error) { + f.m.Lock() + defer f.m.Unlock() + + if *locked { + return true, nil + } + + if f.fh == nil { + if err := f.setFh(); err != nil { + return false, err + } + } + + var retried bool +retry: + err := syscall.Flock(int(f.fh.Fd()), flag|syscall.LOCK_NB) + + switch err { + case syscall.EWOULDBLOCK: + return false, nil + case nil: + *locked = true + return true, nil + } + if !retried { + if shouldRetry, reopenErr := f.reopenFDOnError(err); reopenErr != nil { + return false, reopenErr + } else if shouldRetry { + retried = true + goto retry + } + } + + return false, err +} + +// reopenFDOnError determines whether we should reopen the file handle +// in readwrite mode and try again. This comes from util-linux/sys-utils/flock.c: +// Since Linux 3.4 (commit 55725513) +// Probably NFSv4 where flock() is emulated by fcntl(). +func (f *Flock) reopenFDOnError(err error) (bool, error) { + if err != syscall.EIO && err != syscall.EBADF { + return false, nil + } + if st, err := f.fh.Stat(); err == nil { + // if the file is able to be read and written + if st.Mode()&0600 == 0600 { + f.fh.Close() + f.fh = nil + + // reopen in read-write mode and set the filehandle + fh, err := os.OpenFile(f.path, os.O_CREATE|os.O_RDWR, os.FileMode(0600)) + if err != nil { + return false, err + } + f.fh = fh + return true, nil + } + } + + return false, nil +} diff --git a/vendor/github.com/gofrs/flock/flock_winapi.go b/vendor/github.com/gofrs/flock/flock_winapi.go new file mode 100644 index 00000000..fe405a25 --- /dev/null +++ b/vendor/github.com/gofrs/flock/flock_winapi.go @@ -0,0 +1,76 @@ +// Copyright 2015 Tim Heckman. All rights reserved. +// Use of this source code is governed by the BSD 3-Clause +// license that can be found in the LICENSE file. + +// +build windows + +package flock + +import ( + "syscall" + "unsafe" +) + +var ( + kernel32, _ = syscall.LoadLibrary("kernel32.dll") + procLockFileEx, _ = syscall.GetProcAddress(kernel32, "LockFileEx") + procUnlockFileEx, _ = syscall.GetProcAddress(kernel32, "UnlockFileEx") +) + +const ( + winLockfileFailImmediately = 0x00000001 + winLockfileExclusiveLock = 0x00000002 + winLockfileSharedLock = 0x00000000 +) + +// Use of 0x00000000 for the shared lock is a guess based on some the MS Windows +// `LockFileEX` docs, which document the `LOCKFILE_EXCLUSIVE_LOCK` flag as: +// +// > The function requests an exclusive lock. Otherwise, it requests a shared +// > lock. +// +// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365203(v=vs.85).aspx + +func lockFileEx(handle syscall.Handle, flags uint32, reserved uint32, numberOfBytesToLockLow uint32, numberOfBytesToLockHigh uint32, offset *syscall.Overlapped) (bool, syscall.Errno) { + r1, _, errNo := syscall.Syscall6( + uintptr(procLockFileEx), + 6, + uintptr(handle), + uintptr(flags), + uintptr(reserved), + uintptr(numberOfBytesToLockLow), + uintptr(numberOfBytesToLockHigh), + uintptr(unsafe.Pointer(offset))) + + if r1 != 1 { + if errNo == 0 { + return false, syscall.EINVAL + } + + return false, errNo + } + + return true, 0 +} + +func unlockFileEx(handle syscall.Handle, reserved uint32, numberOfBytesToLockLow uint32, numberOfBytesToLockHigh uint32, offset *syscall.Overlapped) (bool, syscall.Errno) { + r1, _, errNo := syscall.Syscall6( + uintptr(procUnlockFileEx), + 5, + uintptr(handle), + uintptr(reserved), + uintptr(numberOfBytesToLockLow), + uintptr(numberOfBytesToLockHigh), + uintptr(unsafe.Pointer(offset)), + 0) + + if r1 != 1 { + if errNo == 0 { + return false, syscall.EINVAL + } + + return false, errNo + } + + return true, 0 +} diff --git a/vendor/github.com/gofrs/flock/flock_windows.go b/vendor/github.com/gofrs/flock/flock_windows.go new file mode 100644 index 00000000..9f4a5f10 --- /dev/null +++ b/vendor/github.com/gofrs/flock/flock_windows.go @@ -0,0 +1,140 @@ +// Copyright 2015 Tim Heckman. All rights reserved. +// Use of this source code is governed by the BSD 3-Clause +// license that can be found in the LICENSE file. + +package flock + +import ( + "syscall" +) + +// ErrorLockViolation is the error code returned from the Windows syscall when a +// lock would block and you ask to fail immediately. +const ErrorLockViolation syscall.Errno = 0x21 // 33 + +// Lock is a blocking call to try and take an exclusive file lock. It will wait +// until it is able to obtain the exclusive file lock. It's recommended that +// TryLock() be used over this function. This function may block the ability to +// query the current Locked() or RLocked() status due to a RW-mutex lock. +// +// If we are already locked, this function short-circuits and returns +// immediately assuming it can take the mutex lock. +func (f *Flock) Lock() error { + return f.lock(&f.l, winLockfileExclusiveLock) +} + +// RLock is a blocking call to try and take a shared file lock. It will wait +// until it is able to obtain the shared file lock. It's recommended that +// TryRLock() be used over this function. This function may block the ability to +// query the current Locked() or RLocked() status due to a RW-mutex lock. +// +// If we are already locked, this function short-circuits and returns +// immediately assuming it can take the mutex lock. +func (f *Flock) RLock() error { + return f.lock(&f.r, winLockfileSharedLock) +} + +func (f *Flock) lock(locked *bool, flag uint32) error { + f.m.Lock() + defer f.m.Unlock() + + if *locked { + return nil + } + + if f.fh == nil { + if err := f.setFh(); err != nil { + return err + } + } + + if _, errNo := lockFileEx(syscall.Handle(f.fh.Fd()), flag, 0, 1, 0, &syscall.Overlapped{}); errNo > 0 { + return errNo + } + + *locked = true + return nil +} + +// Unlock is a function to unlock the file. This file takes a RW-mutex lock, so +// while it is running the Locked() and RLocked() functions will be blocked. +// +// This function short-circuits if we are unlocked already. If not, it calls +// UnlockFileEx() on the file and closes the file descriptor. It does not remove +// the file from disk. It's up to your application to do. +func (f *Flock) Unlock() error { + f.m.Lock() + defer f.m.Unlock() + + // if we aren't locked or if the lockfile instance is nil + // just return a nil error because we are unlocked + if (!f.l && !f.r) || f.fh == nil { + return nil + } + + // mark the file as unlocked + if _, errNo := unlockFileEx(syscall.Handle(f.fh.Fd()), 0, 1, 0, &syscall.Overlapped{}); errNo > 0 { + return errNo + } + + f.fh.Close() + + f.l = false + f.r = false + f.fh = nil + + return nil +} + +// TryLock is the preferred function for taking an exclusive file lock. This +// function does take a RW-mutex lock before it tries to lock the file, so there +// is the possibility that this function may block for a short time if another +// goroutine is trying to take any action. +// +// The actual file lock is non-blocking. If we are unable to get the exclusive +// file lock, the function will return false instead of waiting for the lock. If +// we get the lock, we also set the *Flock instance as being exclusive-locked. +func (f *Flock) TryLock() (bool, error) { + return f.try(&f.l, winLockfileExclusiveLock) +} + +// TryRLock is the preferred function for taking a shared file lock. This +// function does take a RW-mutex lock before it tries to lock the file, so there +// is the possibility that this function may block for a short time if another +// goroutine is trying to take any action. +// +// The actual file lock is non-blocking. If we are unable to get the shared file +// lock, the function will return false instead of waiting for the lock. If we +// get the lock, we also set the *Flock instance as being shared-locked. +func (f *Flock) TryRLock() (bool, error) { + return f.try(&f.r, winLockfileSharedLock) +} + +func (f *Flock) try(locked *bool, flag uint32) (bool, error) { + f.m.Lock() + defer f.m.Unlock() + + if *locked { + return true, nil + } + + if f.fh == nil { + if err := f.setFh(); err != nil { + return false, err + } + } + + _, errNo := lockFileEx(syscall.Handle(f.fh.Fd()), flag|winLockfileFailImmediately, 0, 1, 0, &syscall.Overlapped{}) + + if errNo > 0 { + if errNo == ErrorLockViolation || errNo == syscall.ERROR_IO_PENDING { + return false, nil + } + + return false, errNo + } + + *locked = true + + return true, nil +} |
