Skip to content

Commit

Permalink
Merge pull request #15 from theory/readLock
Browse files Browse the repository at this point in the history
Add shared lock support

Signed-off-by: Tim Heckman <t@heckman.io>
  • Loading branch information
theckman committed Jan 24, 2018
2 parents 22bdc08 + ffd5ea4 commit 31b67ec
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 49 deletions.
31 changes: 25 additions & 6 deletions flock.go
Expand Up @@ -24,6 +24,7 @@ type Flock struct {
m sync.RWMutex
fh *os.File
l bool
r bool
}

// NewFlock is a function to return a new instance of *Flock. The only parameter
Expand All @@ -44,18 +45,37 @@ func (f *Flock) Locked() bool {
return f.l
}

// RLocked is a function to return the current read lock state (locked: true, unlocked: false).
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 locking until one of the conditions is met:
// TryLock succeeds, TryLock fails with error, or Context Done channel is closed.
// 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(f.TryLock, ctx, 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(f.TryRLock, ctx, retryDelay)
}

func tryCtx(fn func() (bool, error), ctx context.Context, retryDelay time.Duration) (bool, error) {
if ctx.Err() != nil {
return false, ctx.Err()
}
for {
if ok, err := f.TryLock(); ok || err != nil {
if ok, err := fn(); ok || err != nil {
return ok, err
}
select {
Expand All @@ -69,9 +89,8 @@ func (f *Flock) TryLockContext(ctx context.Context, retryDelay time.Duration) (b

func (f *Flock) setFh() error {
// open a new os.File instance
// create it if it doesn't exist, truncate it if it does exist, open the file read-write
fh, err := os.OpenFile(f.path, os.O_CREATE|os.O_TRUNC|os.O_RDWR, os.FileMode(0600))

// 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
}
Expand Down
115 changes: 115 additions & 0 deletions flock_test.go
Expand Up @@ -50,6 +50,7 @@ func (t *TestSuite) TestNewFlock(c *C) {
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) {
Expand All @@ -64,6 +65,12 @@ func (t *TestSuite) TestFlock_Locked(c *C) {
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()
Expand All @@ -72,6 +79,7 @@ func (t *TestSuite) TestFlock_String(c *C) {

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
Expand All @@ -80,6 +88,7 @@ func (t *TestSuite) TestFlock_TryLock(c *C) {
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)
Expand All @@ -92,6 +101,39 @@ func (t *TestSuite) TestFlock_TryLock(c *C) {
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.NewFlock(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.NewFlock(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())
Expand All @@ -113,6 +155,29 @@ func (t *TestSuite) TestFlock_TryLockContext(c *C) {
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.NewFlock(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.NewFlock(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

Expand All @@ -124,23 +189,27 @@ func (t *TestSuite) TestFlock_Unlock(c *C) {
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()
Expand Down Expand Up @@ -170,5 +239,51 @@ func (t *TestSuite) TestFlock_Lock(c *C) {
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.NewFlock(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)
}
76 changes: 54 additions & 22 deletions flock_unix.go
Expand Up @@ -10,18 +10,33 @@ import (
"syscall"
)

// Lock is a blocking call to try and take the 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() status due to a RW-mutex lock.
// 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.
// If we are already exclusive-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, syscall.LOCK_EX)
}

// RLock is a blocking call to try and take a ahred 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 f.l {
if *locked {
return nil
}

Expand All @@ -31,27 +46,27 @@ func (f *Flock) Lock() error {
}
}

if err := syscall.Flock(int(f.fh.Fd()), syscall.LOCK_EX); err != nil {
if err := syscall.Flock(int(f.fh.Fd()), flag); err != nil {
return err
}

f.l = true
*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() function will be blocked.
// 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.
// 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.
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.fh == nil {
if (!f.l && !f.r) || f.fh == nil {
return nil
}

Expand All @@ -63,24 +78,41 @@ func (f *Flock) Unlock() error {
f.fh.Close()

f.l = false
f.r = false
f.fh = nil

return nil
}

// TryLock is the preferred function for taking a 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.
// 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 locked.
// 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 f.l {
if *locked {
return true, nil
}

Expand All @@ -90,13 +122,13 @@ func (f *Flock) TryLock() (bool, error) {
}
}

err := syscall.Flock(int(f.fh.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
err := syscall.Flock(int(f.fh.Fd()), flag|syscall.LOCK_NB)

switch err {
case syscall.EWOULDBLOCK:
return false, nil
case nil:
f.l = true
*locked = true
return true, nil
}

Expand Down

0 comments on commit 31b67ec

Please sign in to comment.