Skip to content

Commit fe66fdd

Browse files
committed
Fix potential races in the volume store
Uses finer grained locking so that each volume name gets its own lock rather than only being protected by the global lock, which itself needs to be unlocked during cetain operations (`create` especially`) Signed-off-by: Brian Goff <cpuguy83@gmail.com>
1 parent 5087e8c commit fe66fdd

File tree

4 files changed

+324
-34
lines changed

4 files changed

+324
-34
lines changed

pkg/locker/README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
Locker
2+
=====
3+
4+
locker provides a mechanism for creating finer-grained locking to help
5+
free up more global locks to handle other tasks.
6+
7+
The implementation looks close to a sync.Mutex, however the user must provide a
8+
reference to use to refer to the underlying lock when locking and unlocking,
9+
and unlock may generate an error.
10+
11+
If a lock with a given name does not exist when `Lock` is called, one is
12+
created.
13+
Lock references are automatically cleaned up on `Unlock` if nothing else is
14+
waiting for the lock.
15+
16+
17+
## Usage
18+
19+
```go
20+
package important
21+
22+
import (
23+
"sync"
24+
"time"
25+
26+
"github.com/docker/docker/pkg/locker"
27+
)
28+
29+
type important struct {
30+
locks *locker.Locker
31+
data map[string]interface{}
32+
mu sync.Mutex
33+
}
34+
35+
func (i *important) Get(name string) interface{} {
36+
i.locks.Lock(name)
37+
defer i.locks.Unlock(name)
38+
return data[name]
39+
}
40+
41+
func (i *important) Create(name string, data interface{}) {
42+
i.locks.Lock(name)
43+
defer i.locks.Unlock(name)
44+
45+
i.createImporatant(data)
46+
47+
s.mu.Lock()
48+
i.data[name] = data
49+
s.mu.Unlock()
50+
}
51+
52+
func (i *important) createImportant(data interface{}) {
53+
time.Sleep(10 * time.Second)
54+
}
55+
```
56+
57+
For functions dealing with a given name, always lock at the beginning of the
58+
function (or before doing anything with the underlying state), this ensures any
59+
other function that is dealing with the same name will block.
60+
61+
When needing to modify the underlying data, use the global lock to ensure nothing
62+
else is modfying it at the same time.
63+
Since name lock is already in place, no reads will occur while the modification
64+
is being performed.
65+

pkg/locker/locker.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
Package locker provides a mechanism for creating finer-grained locking to help
3+
free up more global locks to handle other tasks.
4+
5+
The implementation looks close to a sync.Mutex, however the user must provide a
6+
reference to use to refer to the underlying lock when locking and unlocking,
7+
and unlock may generate an error.
8+
9+
If a lock with a given name does not exist when `Lock` is called, one is
10+
created.
11+
Lock references are automatically cleaned up on `Unlock` if nothing else is
12+
waiting for the lock.
13+
*/
14+
package locker
15+
16+
import (
17+
"errors"
18+
"sync"
19+
"sync/atomic"
20+
)
21+
22+
// ErrNoSuchLock is returned when the requested lock does not exist
23+
var ErrNoSuchLock = errors.New("no such lock")
24+
25+
// Locker provides a locking mechanism based on the passed in reference name
26+
type Locker struct {
27+
mu sync.Mutex
28+
locks map[string]*lockCtr
29+
}
30+
31+
// lockCtr is used by Locker to represent a lock with a given name.
32+
type lockCtr struct {
33+
mu sync.Mutex
34+
// waiters is the number of waiters waiting to acquire the lock
35+
waiters uint32
36+
}
37+
38+
// inc increments the number of waiters waiting for the lock
39+
func (l *lockCtr) inc() {
40+
atomic.AddUint32(&l.waiters, 1)
41+
}
42+
43+
// dec decrements the number of waiters wating on the lock
44+
func (l *lockCtr) dec() {
45+
atomic.AddUint32(&l.waiters, ^uint32(l.waiters-1))
46+
}
47+
48+
// count gets the current number of waiters
49+
func (l *lockCtr) count() uint32 {
50+
return atomic.LoadUint32(&l.waiters)
51+
}
52+
53+
// Lock locks the mutex
54+
func (l *lockCtr) Lock() {
55+
l.mu.Lock()
56+
}
57+
58+
// Unlock unlocks the mutex
59+
func (l *lockCtr) Unlock() {
60+
l.mu.Unlock()
61+
}
62+
63+
// New creates a new Locker
64+
func New() *Locker {
65+
return &Locker{
66+
locks: make(map[string]*lockCtr),
67+
}
68+
}
69+
70+
// Lock locks a mutex with the given name. If it doesn't exist, one is created
71+
func (l *Locker) Lock(name string) {
72+
l.mu.Lock()
73+
if l.locks == nil {
74+
l.locks = make(map[string]*lockCtr)
75+
}
76+
77+
nameLock, exists := l.locks[name]
78+
if !exists {
79+
nameLock = &lockCtr{}
80+
l.locks[name] = nameLock
81+
}
82+
83+
// increment the nameLock waiters while inside the main mutex
84+
// this makes sure that the lock isn't deleted if `Lock` and `Unlock` are called concurrently
85+
nameLock.inc()
86+
l.mu.Unlock()
87+
88+
// Lock the nameLock outside the main mutex so we don't block other operations
89+
// once locked then we can decrement the number of waiters for this lock
90+
nameLock.Lock()
91+
nameLock.dec()
92+
}
93+
94+
// Unlock unlocks the mutex with the given name
95+
// If the given lock is not being waited on by any other callers, it is deleted
96+
func (l *Locker) Unlock(name string) error {
97+
l.mu.Lock()
98+
nameLock, exists := l.locks[name]
99+
if !exists {
100+
l.mu.Unlock()
101+
return ErrNoSuchLock
102+
}
103+
104+
if nameLock.count() == 0 {
105+
delete(l.locks, name)
106+
}
107+
nameLock.Unlock()
108+
109+
l.mu.Unlock()
110+
return nil
111+
}

pkg/locker/locker_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package locker
2+
3+
import (
4+
"runtime"
5+
"testing"
6+
)
7+
8+
func TestLockCounter(t *testing.T) {
9+
l := &lockCtr{}
10+
l.inc()
11+
12+
if l.waiters != 1 {
13+
t.Fatal("counter inc failed")
14+
}
15+
16+
l.dec()
17+
if l.waiters != 0 {
18+
t.Fatal("counter dec failed")
19+
}
20+
}
21+
22+
func TestLockerLock(t *testing.T) {
23+
l := New()
24+
l.Lock("test")
25+
ctr := l.locks["test"]
26+
27+
if ctr.count() != 0 {
28+
t.Fatalf("expected waiters to be 0, got :%d", ctr.waiters)
29+
}
30+
31+
chDone := make(chan struct{})
32+
go func() {
33+
l.Lock("test")
34+
close(chDone)
35+
}()
36+
37+
runtime.Gosched()
38+
39+
select {
40+
case <-chDone:
41+
t.Fatal("lock should not have returned while it was still held")
42+
default:
43+
}
44+
45+
if ctr.count() != 1 {
46+
t.Fatalf("expected waiters to be 1, got: %d", ctr.count())
47+
}
48+
49+
if err := l.Unlock("test"); err != nil {
50+
t.Fatal(err)
51+
}
52+
runtime.Gosched()
53+
54+
select {
55+
case <-chDone:
56+
default:
57+
// one more time just to be sure
58+
runtime.Gosched()
59+
select {
60+
case <-chDone:
61+
default:
62+
t.Fatalf("lock should have completed")
63+
}
64+
}
65+
66+
if ctr.count() != 0 {
67+
t.Fatalf("expected waiters to be 0, got: %d", ctr.count())
68+
}
69+
}
70+
71+
func TestLockerUnlock(t *testing.T) {
72+
l := New()
73+
74+
l.Lock("test")
75+
l.Unlock("test")
76+
77+
chDone := make(chan struct{})
78+
go func() {
79+
l.Lock("test")
80+
close(chDone)
81+
}()
82+
83+
runtime.Gosched()
84+
85+
select {
86+
case <-chDone:
87+
default:
88+
t.Fatalf("lock should not be blocked")
89+
}
90+
}

0 commit comments

Comments
 (0)