Add support for timeouts for locks (#4377)

This commit is contained in:
Frank Wessels
2017-08-31 11:29:22 -07:00
committed by deekoder
parent 93f126364e
commit 61e0b1454a
32 changed files with 1347 additions and 357 deletions

View File

@@ -41,6 +41,7 @@ func log(msg ...interface{}) {
// DRWMutexAcquireTimeout - tolerance limit to wait for lock acquisition before.
const DRWMutexAcquireTimeout = 1 * time.Second // 1 second.
const drwMutexInfinite = time.Duration(1<<63 - 1)
// A DRWMutex is a distributed mutual exclusion lock.
type DRWMutex struct {
@@ -79,29 +80,52 @@ func NewDRWMutex(name string) *DRWMutex {
func (dm *DRWMutex) Lock() {
isReadLock := false
dm.lockBlocking(isReadLock)
dm.lockBlocking(drwMutexInfinite, isReadLock)
}
// GetLock tries to get a write lock on dm before the timeout elapses.
//
// If the lock is already in use, the calling go routine
// blocks until either the mutex becomes available and return success or
// more time has passed than the timeout value and return false.
func (dm *DRWMutex) GetLock(timeout time.Duration) (locked bool) {
isReadLock := false
return dm.lockBlocking(timeout, isReadLock)
}
// RLock holds a read lock on dm.
//
// If one or more read lock are already in use, it will grant another lock.
// If one or more read locks are already in use, it will grant another lock.
// Otherwise the calling go routine blocks until the mutex is available.
func (dm *DRWMutex) RLock() {
isReadLock := true
dm.lockBlocking(isReadLock)
dm.lockBlocking(drwMutexInfinite, isReadLock)
}
// lockBlocking will acquire either a read or a write lock
// GetRLock tries to get a read lock on dm before the timeout elapses.
//
// The call will block until the lock is granted using a built-in
// timing randomized back-off algorithm to try again until successful
func (dm *DRWMutex) lockBlocking(isReadLock bool) {
doneCh := make(chan struct{})
// If one or more read locks are already in use, it will grant another lock.
// Otherwise the calling go routine blocks until either the mutex becomes
// available and return success or more time has passed than the timeout
// value and return false.
func (dm *DRWMutex) GetRLock(timeout time.Duration) (locked bool) {
isReadLock := true
return dm.lockBlocking(timeout, isReadLock)
}
// lockBlocking will try to acquire either a read or a write lock
//
// The function will loop using a built-in timing randomized back-off
// algorithm until either the lock is acquired successfully or more
// time has elapsed than the timeout value.
func (dm *DRWMutex) lockBlocking(timeout time.Duration, isReadLock bool) (locked bool) {
doneCh, start := make(chan struct{}), time.Now().UTC()
defer close(doneCh)
// We timed out on the previous lock, incrementally wait
// for a longer back-off time and try again afterwards.
// Use incremental back-off algorithm for repeated attempts to acquire the lock
for range newRetryTimerSimple(doneCh) {
// Create temp array on stack.
locks := make([]string, dnodeCount)
@@ -122,11 +146,15 @@ func (dm *DRWMutex) lockBlocking(isReadLock bool) {
copy(dm.writeLocks, locks[:])
}
return
return true
}
// We timed out on the previous lock, incrementally wait
if time.Now().UTC().Sub(start) >= timeout { // Are we past the timeout?
break
}
// Failed to acquire the lock on this attempt, incrementally wait
// for a longer back-off time and try again afterwards.
}
return false
}
// lock tries to acquire the distributed lock, returning true or false.

121
vendor/github.com/minio/lsync/README.md generated vendored Normal file
View File

@@ -0,0 +1,121 @@
# lsync
Local syncing package with support for timeouts. This package offers both a `sync.Mutex` and `sync.RWMutex` compatible interface.
Additionally it provides `lsync.LFrequentAccess` which uses an atomic load and store of a consistently typed value. This can be usefull for shared data structures that are frequently read but infrequently updated (using an copy-on-write mechanism) without the need for protection with a regular mutex.
### Example of LRWMutex
```go
// Create RWMutex compatible mutex
lrwm := NewLRWMutex()
// Try to get lock within timeout
if !lrwm.GetLock(1000 * time.Millisecond) {
fmt.Println("Timeout occured")
return
}
// Acquired lock, do your stuff ...
lrwm.Unlock() // Release lock
```
### Example of LFrequentAccess
````go
type Map map[string]string
// Create new LFrequentAccess for type Map
freqaccess := NewLFrequentAccess(make(Map))
cur := freqaccess.LockBeforeSet().(Map) // Lock in order to update
mp := make(Map) // Create new Map
for k, v := range cur { // Copy over old contents
mp[k] = v
}
mp[key] = val // Add new value
freqaccess.SetNewCopyAndUnlock(mp) // Exchange old version of map with new version
mpReadOnly := freqaccess.ReadOnlyAccess().(Map) // Get read only access to Map
fmt.Println(mpReadOnly[key]) // Safe access with no further synchronization
````
## Design
The design is pretty straightforward in the sense that `lsync` tries to get a lock in a loop with an exponential [backoff](https://www.awsarchitectureblog.com/2015/03/backoff.html) algorithm. The algorithm is configurable in terms of initial delay and jitter.
If the lock is acquired before the timeout has occured, it will return success to the caller and the caller can proceed as intended. The caller must call `unlock` after the operation that is to be protected has completed in order to release the lock.
When more time has elapsed than the timeout value the lock loop will cancel out and signal back to the caller that the lock has not been acquired. In this case the caller must _not_ call `unlock` since no lock was obtained. Typically it should signal an error back up the call stack so that errors can be dealt with appropriately at the correct level.
Note that this algorithm is not 'real-time' in the sense that it will time out exactly at the timeout value, but instead a (short) while after the timeout has lapsed. It is even possible that (in edge cases) a succesful lock can be returned a very short time after the timeout has lapsed.
## API
#### LMutex
```go
func (lm *LMutex) Lock()
func (lm *LMutex) GetLock(timeout time.Duration) (locked bool)
func (lm *LMutex) Unlock()
```
#### LRWMutex
```go
func (lm *LRWMutex) Lock()
func (lm *LRWMutex) GetLock(timeout time.Duration) (locked bool)
func (lm *LRWMutex) RLock()
func (lm *LRWMutex) GetRLock(timeout time.Duration) (locked bool)
func (lm *LRWMutex) Unlock()
func (lm *LRWMutex) RUnlock()
```
#### LFrequentAccess
```go
func (lm *LFrequentAccess) ReadOnlyAccess() (constReadOnly interface{})
func (lm *LFrequentAccess) LockBeforeSet() (constCurVersion interface{})
func (lm *LFrequentAccess) SetNewCopyAndUnlock(newCopy interface{})
```
## Benchmarks
### sync.Mutex vs lsync.LMutex
(with `defaultRetryUnit` and `defaultRetryCap` at 10 microsec)
```
BenchmarkMutex-8 111 1579 +1322.52%
BenchmarkMutexSlack-8 120 1033 +760.83%
BenchmarkMutexWork-8 133 1604 +1106.02%
BenchmarkMutexWorkSlack-8 137 1038 +657.66%
```
(with `defaultRetryUnit` and `defaultRetryCap` at 1 millisec)
```
benchmark old ns/op new ns/op delta
BenchmarkMutex-8 111 2649 +2286.49%
BenchmarkMutexSlack-8 120 1719 +1332.50%
BenchmarkMutexWork-8 133 2637 +1882.71%
BenchmarkMutexWorkSlack-8 137 1729 +1162.04%
```
(with `defaultRetryUnit` and `defaultRetryCap` at 100 millisec)
```
benchmark old ns/op new ns/op delta
BenchmarkMutex-8 111 2649 +2286.49%
BenchmarkMutexSlack-8 120 2478 +1965.00%
BenchmarkMutexWork-8 133 2547 +1815.04%
BenchmarkMutexWorkSlack-8 137 2683 +1858.39%
```
### LFrequentAccess
An `lsync.LFrequentAccess` provides an atomic load and store of a consistently typed value.
```
benchmark old ns/op new ns/op delta
BenchmarkLFrequentAccessMap-8 114 4.67 -95.90%
BenchmarkLFrequentAccessSlice-8 109 5.95 -94.54%
```

64
vendor/github.com/minio/lsync/lfrequentaccess.go generated vendored Normal file
View File

@@ -0,0 +1,64 @@
/*
* Minio Cloud Storage, (C) 2017 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lsync
import (
"sync"
"sync/atomic"
)
// LFrequentAccess is a synchronization mechanism for frequently read yet
// infrequently updated data structures. It uses a copy-on-write paradigm
// for updates to the data.
type LFrequentAccess struct {
state atomic.Value
writeLock sync.Mutex
locked bool
}
// NewLFrequentAccess - initializes a new LFrequentAccess.
func NewLFrequentAccess(x interface{}) *LFrequentAccess {
lm := &LFrequentAccess{}
lm.state.Store(x)
return lm
}
// ReadOnlyAccess returns the data intented for reads without further synchronization
func (lm *LFrequentAccess) ReadOnlyAccess() (constReadOnly interface{}) {
return lm.state.Load()
}
// LockBeforeSet must be called before updates of the data in order to synchronize
// with other potential writers. It returns the current version of the data that
// needs to be copied over into a new version.
func (lm *LFrequentAccess) LockBeforeSet() (constCurVersion interface{}) {
lm.writeLock.Lock()
lm.locked = true
return lm.state.Load()
}
// SetNewCopyAndUnlock updates the data with a new modified copy and unlocks
// simultaneously. Make sure to call LockBeforeSet beforehand to synchronize
// between potential parallel writers (and not lose any updated information).
func (lm *LFrequentAccess) SetNewCopyAndUnlock(newCopy interface{}) {
if !lm.locked {
panic("SetNewCopyAndUnlock: locked state is false (did you call LockBeforeSet?)")
}
lm.state.Store(newCopy)
lm.locked = false
lm.writeLock.Unlock()
}

78
vendor/github.com/minio/lsync/lmutex.go generated vendored Normal file
View File

@@ -0,0 +1,78 @@
/*
* Minio Cloud Storage, (C) 2017 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lsync
import (
"sync/atomic"
"time"
)
// A LMutex is a mutual exclusion lock with timeouts.
type LMutex struct {
state int64
}
// NewLMutex - initializes a new lsync mutex.
func NewLMutex() *LMutex {
return &LMutex{}
}
// Lock holds a lock on lm.
//
// If the lock is already in use, the calling go routine
// blocks until the mutex is available.
func (lm *LMutex) Lock() {
lm.lockLoop(time.Duration(1<<63 - 1))
}
// GetLock tries to get a write lock on lm before the timeout occurs.
func (lm *LMutex) GetLock(timeout time.Duration) (locked bool) {
return lm.lockLoop(timeout)
}
// lockLoop will acquire either a read or a write lock
//
// The call will block until the lock is granted using a built-in
// timing randomized back-off algorithm to try again until successful
func (lm *LMutex) lockLoop(timeout time.Duration) bool {
doneCh, start := make(chan struct{}), time.Now().UTC()
defer close(doneCh)
// We timed out on the previous lock, incrementally wait
// for a longer back-off time and try again afterwards.
for range newRetryTimerSimple(doneCh) {
// Try to acquire the lock.
if atomic.CompareAndSwapInt64(&lm.state, NOLOCKS, WRITELOCK) {
return true
} else if time.Now().UTC().Sub(start) >= timeout { // Are we past the timeout?
break
}
// We timed out on the previous lock, incrementally wait
// for a longer back-off time and try again afterwards.
}
return false
}
// Unlock unlocks the lock.
//
// It is a run-time error if lm is not locked on entry to Unlock.
func (lm *LMutex) Unlock() {
if !atomic.CompareAndSwapInt64(&lm.state, WRITELOCK, NOLOCKS) {
panic("Trying to Unlock() while no Lock() is active")
}
}

180
vendor/github.com/minio/lsync/lrwmutex.go generated vendored Normal file
View File

@@ -0,0 +1,180 @@
/*
* Minio Cloud Storage, (C) 2017 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lsync
import (
"sync"
"time"
)
const (
WRITELOCK = -1 + iota
NOLOCKS
READLOCKS
)
// A LRWMutex is a mutual exclusion lock with timeouts.
type LRWMutex struct {
state int64
m sync.Mutex // Mutex to prevent multiple simultaneous locks
}
// NewLRWMutex - initializes a new lsync RW mutex.
func NewLRWMutex() *LRWMutex {
return &LRWMutex{}
}
// Lock holds a write lock on lm.
//
// If the lock is already in use, the calling go routine
// blocks until the mutex is available.
func (lm *LRWMutex) Lock() {
isWriteLock := true
lm.lockLoop(time.Duration(1<<63-1), isWriteLock)
}
// GetLock tries to get a write lock on lm before the timeout occurs.
func (lm *LRWMutex) GetLock(timeout time.Duration) (locked bool) {
isWriteLock := true
return lm.lockLoop(timeout, isWriteLock)
}
// RLock holds a read lock on lm.
//
// If one or more read lock are already in use, it will grant another lock.
// Otherwise the calling go routine blocks until the mutex is available.
func (lm *LRWMutex) RLock() {
isWriteLock := false
lm.lockLoop(time.Duration(1<<63-1), isWriteLock)
}
// GetRLock tries to get a read lock on lm before the timeout occurs.
func (lm *LRWMutex) GetRLock(timeout time.Duration) (locked bool) {
isWriteLock := false
return lm.lockLoop(timeout, isWriteLock)
}
// lockLoop will acquire either a read or a write lock
//
// The call will block until the lock is granted using a built-in
// timing randomized back-off algorithm to try again until successful
func (lm *LRWMutex) lockLoop(timeout time.Duration, isWriteLock bool) bool {
doneCh, start := make(chan struct{}), time.Now().UTC()
defer close(doneCh)
// We timed out on the previous lock, incrementally wait
// for a longer back-off time and try again afterwards.
for range newRetryTimerSimple(doneCh) {
// Try to acquire the lock.
var success bool
{
lm.m.Lock()
if isWriteLock {
if lm.state == NOLOCKS {
lm.state = WRITELOCK
success = true
}
} else {
if lm.state != WRITELOCK {
lm.state += 1
success = true
}
}
lm.m.Unlock()
}
if success {
return true
}
if time.Now().UTC().Sub(start) >= timeout { // Are we past the timeout?
break
}
// We timed out on the previous lock, incrementally wait
// for a longer back-off time and try again afterwards.
}
return false
}
// Unlock unlocks the write lock.
//
// It is a run-time error if lm is not locked on entry to Unlock.
func (lm *LRWMutex) Unlock() {
isWriteLock := true
success := lm.unlock(isWriteLock)
if !success {
panic("Trying to Unlock() while no Lock() is active")
}
}
// RUnlock releases a read lock held on lm.
//
// It is a run-time error if lm is not locked on entry to RUnlock.
func (lm *LRWMutex) RUnlock() {
isWriteLock := false
success := lm.unlock(isWriteLock)
if !success {
panic("Trying to RUnlock() while no RLock() is active")
}
}
func (lm *LRWMutex) unlock(isWriteLock bool) (unlocked bool) {
lm.m.Lock()
// Try to release lock.
if isWriteLock {
if lm.state == WRITELOCK {
lm.state = NOLOCKS
unlocked = true
}
} else {
if lm.state == WRITELOCK || lm.state == NOLOCKS {
unlocked = false // unlocked called without any active read locks
} else {
lm.state -= 1
unlocked = true
}
}
lm.m.Unlock()
return unlocked
}
// ForceUnlock will forcefully clear a write or read lock.
func (lm *LRWMutex) ForceUnlock() {
lm.m.Lock()
lm.state = NOLOCKS
lm.m.Unlock()
}
// DRLocker returns a sync.Locker interface that implements
// the Lock and Unlock methods by calling drw.RLock and drw.RUnlock.
func (dm *LRWMutex) DRLocker() sync.Locker {
return (*drlocker)(dm)
}
type drlocker LRWMutex
func (dr *drlocker) Lock() { (*LRWMutex)(dr).RLock() }
func (dr *drlocker) Unlock() { (*LRWMutex)(dr).RUnlock() }

142
vendor/github.com/minio/lsync/retry.go generated vendored Normal file
View File

@@ -0,0 +1,142 @@
/*
* Minio Cloud Storage, (C) 2017 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lsync
import (
"math/rand"
"sync"
"time"
)
// lockedRandSource provides protected rand source, implements rand.Source interface.
type lockedRandSource struct {
lk sync.Mutex
src rand.Source
}
// Int63 returns a non-negative pseudo-random 63-bit integer as an
// int64.
func (r *lockedRandSource) Int63() (n int64) {
r.lk.Lock()
n = r.src.Int63()
r.lk.Unlock()
return
}
// Seed uses the provided seed value to initialize the generator to a
// deterministic state.
func (r *lockedRandSource) Seed(seed int64) {
r.lk.Lock()
r.src.Seed(seed)
r.lk.Unlock()
}
// MaxJitter will randomize over the full exponential backoff time
const MaxJitter = 1.0
// NoJitter disables the use of jitter for randomizing the
// exponential backoff time
const NoJitter = 0.0
// Global random source for fetching random values.
var globalRandomSource = rand.New(&lockedRandSource{
src: rand.NewSource(time.Now().UTC().UnixNano()),
})
// newRetryTimerJitter creates a timer with exponentially increasing delays
// until the maximum retry attempts are reached. - this function is a fully
// configurable version, meant for only advanced use cases. For the most part
// one should use newRetryTimerSimple and newRetryTimer.
func newRetryTimerWithJitter(unit time.Duration, cap time.Duration, jitter float64, doneCh chan struct{}) <-chan int {
attemptCh := make(chan int)
// normalize jitter to the range [0, 1.0]
if jitter < NoJitter {
jitter = NoJitter
}
if jitter > MaxJitter {
jitter = MaxJitter
}
// computes the exponential backoff duration according to
// https://www.awsarchitectureblog.com/2015/03/backoff.html
exponentialBackoffWait := func(attempt int) time.Duration {
// 1<<uint(attempt) below could overflow, so limit the value of attempt
maxAttempt := 30
if attempt > maxAttempt {
attempt = maxAttempt
}
//sleep = random_between(0, min(cap, base * 2 ** attempt))
sleep := unit * time.Duration(1<<uint(attempt))
if sleep > cap {
sleep = cap
}
if jitter != NoJitter {
sleep -= time.Duration(globalRandomSource.Float64() * float64(sleep) * jitter)
}
return sleep
}
go func() {
defer close(attemptCh)
nextBackoff := 0
// Channel used to signal after the expiry of backoff wait seconds.
var timer *time.Timer
for {
select { // Attempts starts.
case attemptCh <- nextBackoff:
nextBackoff++
case <-doneCh:
// Stop the routine.
return
}
timer = time.NewTimer(exponentialBackoffWait(nextBackoff))
// wait till next backoff time or till doneCh gets a message.
select {
case <-timer.C:
case <-doneCh:
// stop the timer and return.
timer.Stop()
return
}
}
}()
// Start reading..
return attemptCh
}
// Default retry constants.
const (
defaultRetryUnit = 10 * time.Millisecond // 10 millisecond.
defaultRetryCap = 10 * time.Millisecond // 10 millisecond.
)
// newRetryTimer creates a timer with exponentially increasing delays
// until the maximum retry attempts are reached. - this function provides
// resulting retry values to be of maximum jitter.
func newRetryTimer(unit time.Duration, cap time.Duration, doneCh chan struct{}) <-chan int {
return newRetryTimerWithJitter(unit, cap, MaxJitter, doneCh)
}
// newRetryTimerSimple creates a timer with exponentially increasing delays
// until the maximum retry attempts are reached. - this function is a
// simpler version with all default values.
func newRetryTimerSimple(doneCh chan struct{}) <-chan int {
return newRetryTimerWithJitter(defaultRetryUnit, defaultRetryCap, MaxJitter, doneCh)
}

12
vendor/vendor.json vendored
View File

@@ -290,16 +290,22 @@
"revisionTime": "2017-02-27T07:32:28Z"
},
{
"checksumSHA1": "vrIbl0L+RLwyPRCxMss5+eZtADE=",
"checksumSHA1": "hQ8i4UPTbFW68oPJP3uFxYTLfxk=",
"path": "github.com/minio/dsync",
"revision": "535db94aebce49cacce4de9c6f5f5821601281cd",
"revisionTime": "2017-04-19T20:41:15Z"
"revision": "a26b9de6c8006208d10a9517720d3212b42c374e",
"revisionTime": "2017-05-25T17:53:53Z"
},
{
"path": "github.com/minio/go-homedir",
"revision": "0b1069c753c94b3633cc06a1995252dbcc27c7a6",
"revisionTime": "2016-02-15T17:25:11+05:30"
},
{
"checksumSHA1": "7/Hdd23/j4/yt4BXa+h0kqz1yjw=",
"path": "github.com/minio/lsync",
"revision": "2d7c40f41402df6f0713a749a011cddc12d1b2f3",
"revisionTime": "2017-08-09T21:08:26Z"
},
{
"path": "github.com/minio/mc/pkg/console",
"revision": "db6b4f13442b26995f04b3b2b31b006cae7786e6",