minio/pkg/lsync/lrwmutex.go
Harshavardhana febe9cc26a
fix: avoid timer leaks in dsync/lsync (#9781)
At a customer setup with lots of concurrent calls
it can be observed that in newRetryTimer there
were lots of tiny alloations which are not
relinquished upon retries, in this codepath
we were only interested in re-using the timer
and use it wisely for each locker.

```
(pprof) top
Showing nodes accounting for 8.68TB, 97.02% of 8.95TB total
Dropped 1198 nodes (cum <= 0.04TB)
Showing top 10 nodes out of 79
      flat  flat%   sum%        cum   cum%
    5.95TB 66.50% 66.50%     5.95TB 66.50%  time.NewTimer
    1.16TB 13.02% 79.51%     1.16TB 13.02%  github.com/ncw/directio.AlignedBlock
    0.67TB  7.53% 87.04%     0.70TB  7.78%  github.com/minio/minio/cmd.xlObjects.putObject
    0.21TB  2.36% 89.40%     0.21TB  2.36%  github.com/minio/minio/cmd.(*posix).Walk
    0.19TB  2.08% 91.49%     0.27TB  2.99%  os.statNolog
    0.14TB  1.59% 93.08%     0.14TB  1.60%  os.(*File).readdirnames
    0.10TB  1.09% 94.17%     0.11TB  1.25%  github.com/minio/minio/cmd.readDirN
    0.10TB  1.07% 95.23%     0.10TB  1.07%  syscall.ByteSliceFromString
    0.09TB  1.03% 96.27%     0.09TB  1.03%  strings.(*Builder).grow
    0.07TB  0.75% 97.02%     0.07TB  0.75%  path.(*lazybuf).append
```
2020-06-08 11:28:40 -07:00

189 lines
4.5 KiB
Go

/*
* 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 (
"context"
"math"
"sync"
"time"
"github.com/minio/minio/pkg/retry"
)
// A LRWMutex is a mutual exclusion lock with timeouts.
type LRWMutex struct {
id string
source string
isWriteLock bool
ref int
m sync.Mutex // Mutex to prevent multiple simultaneous locks
ctx context.Context
}
// NewLRWMutex - initializes a new lsync RW mutex.
func NewLRWMutex(ctx context.Context) *LRWMutex {
return &LRWMutex{ctx: ctx}
}
// 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() {
const isWriteLock = true
lm.lockLoop(lm.id, lm.source, time.Duration(math.MaxInt64), isWriteLock)
}
// GetLock tries to get a write lock on lm before the timeout occurs.
func (lm *LRWMutex) GetLock(id string, source string, timeout time.Duration) (locked bool) {
const isWriteLock = true
return lm.lockLoop(id, source, 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() {
const isWriteLock = false
lm.lockLoop(lm.id, lm.source, time.Duration(1<<63-1), isWriteLock)
}
// GetRLock tries to get a read lock on lm before the timeout occurs.
func (lm *LRWMutex) GetRLock(id string, source string, timeout time.Duration) (locked bool) {
const isWriteLock = false
return lm.lockLoop(id, source, 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(id, source string, timeout time.Duration, isWriteLock bool) bool {
retryCtx, cancel := context.WithTimeout(lm.ctx, timeout)
defer cancel()
// We timed out on the previous lock, incrementally wait
// for a longer back-off time and try again afterwards.
for range retry.NewTimer(retryCtx) {
// Try to acquire the lock.
var success bool
{
lm.m.Lock()
lm.id = id
lm.source = source
if isWriteLock {
if lm.ref == 0 && !lm.isWriteLock {
lm.ref = 1
lm.isWriteLock = true
success = true
}
} else {
if !lm.isWriteLock {
lm.ref++
success = true
}
}
lm.m.Unlock()
}
if success {
return true
}
}
// 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.isWriteLock && lm.ref == 1 {
lm.ref = 0
lm.isWriteLock = false
unlocked = true
}
} else {
if !lm.isWriteLock {
if lm.ref > 0 {
lm.ref--
unlocked = true
}
}
}
lm.m.Unlock()
return unlocked
}
// ForceUnlock will forcefully clear a write or read lock.
func (lm *LRWMutex) ForceUnlock() {
lm.m.Lock()
lm.ref = 0
lm.isWriteLock = false
lm.m.Unlock()
}
// DRLocker returns a sync.Locker interface that implements
// the Lock and Unlock methods by calling drw.RLock and drw.RUnlock.
func (lm *LRWMutex) DRLocker() sync.Locker {
return (*drlocker)(lm)
}
type drlocker LRWMutex
func (dr *drlocker) Lock() { (*LRWMutex)(dr).RLock() }
func (dr *drlocker) Unlock() { (*LRWMutex)(dr).RUnlock() }