Skip to content

Commit 7ee3ded

Browse files
John Howardxiang90
John Howard
authored andcommitted
Fix Windows flock/funlock race (etcd-io#122)
Signed-off-by: John Howard <jhoward@microsoft.com>
1 parent 8987c97 commit 7ee3ded

File tree

5 files changed

+46
-26
lines changed

5 files changed

+46
-26
lines changed

bolt_unix.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@ package bbolt
44

55
import (
66
"fmt"
7-
"os"
87
"syscall"
98
"time"
109
"unsafe"
1110
)
1211

1312
// flock acquires an advisory lock on a file descriptor.
14-
func flock(db *DB, mode os.FileMode, exclusive bool, timeout time.Duration) error {
13+
func flock(db *DB, exclusive bool, timeout time.Duration) error {
1514
var t time.Time
1615
if timeout != 0 {
1716
t = time.Now()

bolt_unix_solaris.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package bbolt
22

33
import (
44
"fmt"
5-
"os"
65
"syscall"
76
"time"
87
"unsafe"
@@ -11,7 +10,7 @@ import (
1110
)
1211

1312
// flock acquires an advisory lock on a file descriptor.
14-
func flock(db *DB, mode os.FileMode, exclusive bool, timeout time.Duration) error {
13+
func flock(db *DB, exclusive bool, timeout time.Duration) error {
1514
var t time.Time
1615
if timeout != 0 {
1716
t = time.Now()

bolt_windows.go

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ var (
1616
)
1717

1818
const (
19-
lockExt = ".lock"
20-
2119
// see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365203(v=vs.85).aspx
2220
flagLockExclusive = 2
2321
flagLockFailImmediately = 1
@@ -48,28 +46,24 @@ func fdatasync(db *DB) error {
4846
}
4947

5048
// flock acquires an advisory lock on a file descriptor.
51-
func flock(db *DB, mode os.FileMode, exclusive bool, timeout time.Duration) error {
52-
// Create a separate lock file on windows because a process
53-
// cannot share an exclusive lock on the same file. This is
54-
// needed during Tx.WriteTo().
55-
f, err := os.OpenFile(db.path+lockExt, os.O_CREATE, mode)
56-
if err != nil {
57-
return err
58-
}
59-
db.lockfile = f
60-
49+
func flock(db *DB, exclusive bool, timeout time.Duration) error {
6150
var t time.Time
6251
if timeout != 0 {
6352
t = time.Now()
6453
}
65-
fd := f.Fd()
6654
var flag uint32 = flagLockFailImmediately
6755
if exclusive {
6856
flag |= flagLockExclusive
6957
}
7058
for {
71-
// Attempt to obtain an exclusive lock.
72-
err := lockFileEx(syscall.Handle(fd), flag, 0, 1, 0, &syscall.Overlapped{})
59+
// Fix for https://github.com/etcd-io/bbolt/issues/121. Use byte-range
60+
// -1..0 as the lock on the database file.
61+
var m1 uint32 = (1 << 32) - 1 // -1 in a uint32
62+
err := lockFileEx(syscall.Handle(db.file.Fd()), flag, 0, 1, 0, &syscall.Overlapped{
63+
Offset: m1,
64+
OffsetHigh: m1,
65+
})
66+
7367
if err == nil {
7468
return nil
7569
} else if err != errLockViolation {
@@ -88,9 +82,11 @@ func flock(db *DB, mode os.FileMode, exclusive bool, timeout time.Duration) erro
8882

8983
// funlock releases an advisory lock on a file descriptor.
9084
func funlock(db *DB) error {
91-
err := unlockFileEx(syscall.Handle(db.lockfile.Fd()), 0, 1, 0, &syscall.Overlapped{})
92-
db.lockfile.Close()
93-
os.Remove(db.path + lockExt)
85+
var m1 uint32 = (1 << 32) - 1 // -1 in a uint32
86+
err := unlockFileEx(syscall.Handle(db.file.Fd()), 0, 1, 0, &syscall.Overlapped{
87+
Offset: m1,
88+
OffsetHigh: m1,
89+
})
9490
return err
9591
}
9692

db.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,7 @@ type DB struct {
105105

106106
path string
107107
file *os.File
108-
lockfile *os.File // windows only
109-
dataref []byte // mmap'ed readonly, write throws SEGV
108+
dataref []byte // mmap'ed readonly, write throws SEGV
110109
data *[maxMapSize]byte
111110
datasz int
112111
filesz int // current on disk file size
@@ -197,8 +196,7 @@ func Open(path string, mode os.FileMode, options *Options) (*DB, error) {
197196
// if !options.ReadOnly.
198197
// The database file is locked using the shared lock (more than one process may
199198
// hold a lock at the same time) otherwise (options.ReadOnly is set).
200-
if err := flock(db, mode, !db.readOnly, options.Timeout); err != nil {
201-
db.lockfile = nil // make 'unused' happy. TODO: rework locks
199+
if err := flock(db, !db.readOnly, options.Timeout); err != nil {
202200
_ = db.close()
203201
return nil, err
204202
}

db_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,34 @@ func TestOpen(t *testing.T) {
6363
}
6464
}
6565

66+
// Regression validation for https://github.com/etcd-io/bbolt/pull/122.
67+
// Tests multiple goroutines simultaneously opening a database.
68+
func TestOpen_MultipleGoroutines(t *testing.T) {
69+
const (
70+
instances = 30
71+
iterations = 30
72+
)
73+
path := tempfile()
74+
defer os.RemoveAll(path)
75+
var wg sync.WaitGroup
76+
for iteration := 0; iteration < iterations; iteration++ {
77+
for instance := 0; instance < instances; instance++ {
78+
wg.Add(1)
79+
go func() {
80+
defer wg.Done()
81+
db, err := bolt.Open(path, 0600, nil)
82+
if err != nil {
83+
t.Fatal(err)
84+
}
85+
if err := db.Close(); err != nil {
86+
t.Fatal(err)
87+
}
88+
}()
89+
}
90+
wg.Wait()
91+
}
92+
}
93+
6694
// Ensure that opening a database with a blank path returns an error.
6795
func TestOpen_ErrPathRequired(t *testing.T) {
6896
_, err := bolt.Open("", 0666, nil)

0 commit comments

Comments
 (0)