Skip to content

Commit e32d840

Browse files
authored
Improve Wal locking on BSD (#204)
1 parent 503db60 commit e32d840

File tree

4 files changed

+93
-29
lines changed

4 files changed

+93
-29
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ This project aims for [high test coverage](https://github.com/ncruces/go-sqlite3
7474
It also benefits greatly from [SQLite's](https://sqlite.org/testing.html) and
7575
[wazero's](https://tetrate.io/blog/introducing-wazero-from-tetrate/#:~:text=Rock%2Dsolid%20test%20approach) thorough testing.
7676

77-
Every commit is [tested](https://github.com/ncruces/go-sqlite3/wiki/Test-matrix) on
77+
Every commit is [tested](https://github.com/ncruces/go-sqlite3/wiki/Support-matrix) on
7878
Linux (amd64/arm64/386/riscv64/ppc64le/s390x), macOS (amd64/arm64),
7979
Windows (amd64), FreeBSD (amd64), OpenBSD (amd64), NetBSD (amd64),
8080
DragonFly BSD (amd64), illumos (amd64), and Solaris (amd64).

vfs/README.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,6 @@ On Unix, this package may use `mmap` to implement
4848
[shared-memory for the WAL-index](https://sqlite.org/wal.html#implementation_of_shared_memory_for_the_wal_index),
4949
like SQLite.
5050

51-
With [BSD locks](https://man.freebsd.org/cgi/man.cgi?query=flock&sektion=2)
52-
a WAL database can only be accessed by a single proccess.
53-
Other processes that attempt to access a database locked with BSD locks,
54-
will fail with the [`SQLITE_PROTOCOL`](https://sqlite.org/rescode.html#protocol) error code.
55-
5651
On Windows, this package may use `MapViewOfFile`, like SQLite.
5752

5853
You can also opt into a cross-platform, in-process, memory sharing implementation

vfs/os_bsd.go

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import (
99
)
1010

1111
func osGetSharedLock(file *os.File) _ErrorCode {
12-
return osLock(file, unix.LOCK_SH|unix.LOCK_NB, _IOERR_RDLOCK)
12+
return osFlock(file, unix.LOCK_SH|unix.LOCK_NB, _IOERR_RDLOCK)
1313
}
1414

1515
func osGetReservedLock(file *os.File) _ErrorCode {
16-
rc := osLock(file, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK)
16+
rc := osFlock(file, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK)
1717
if rc == _BUSY {
1818
// The documentation states that a lock is upgraded by
1919
// releasing the previous lock, then acquiring the new lock.
@@ -37,7 +37,7 @@ func osGetExclusiveLock(file *os.File, state *LockLevel) _ErrorCode {
3737
}
3838

3939
func osDowngradeLock(file *os.File, _ LockLevel) _ErrorCode {
40-
rc := osLock(file, unix.LOCK_SH|unix.LOCK_NB, _IOERR_RDLOCK)
40+
rc := osFlock(file, unix.LOCK_SH|unix.LOCK_NB, _IOERR_RDLOCK)
4141
if rc == _BUSY {
4242
// The documentation states that a lock is downgraded by
4343
// releasing the previous lock then acquiring the new lock.
@@ -66,7 +66,36 @@ func osCheckReservedLock(file *os.File) (bool, _ErrorCode) {
6666
return lock == unix.F_WRLCK, rc
6767
}
6868

69-
func osLock(file *os.File, how int, def _ErrorCode) _ErrorCode {
69+
func osFlock(file *os.File, how int, def _ErrorCode) _ErrorCode {
7070
err := unix.Flock(int(file.Fd()), how)
7171
return osLockErrorCode(err, def)
7272
}
73+
74+
func osReadLock(file *os.File, start, len int64) _ErrorCode {
75+
return osLock(file, unix.F_RDLCK, start, len, _IOERR_RDLOCK)
76+
}
77+
78+
func osWriteLock(file *os.File, start, len int64) _ErrorCode {
79+
return osLock(file, unix.F_WRLCK, start, len, _IOERR_LOCK)
80+
}
81+
82+
func osLock(file *os.File, typ int16, start, len int64, def _ErrorCode) _ErrorCode {
83+
err := unix.FcntlFlock(file.Fd(), unix.F_SETLK, &unix.Flock_t{
84+
Type: typ,
85+
Start: start,
86+
Len: len,
87+
})
88+
return osLockErrorCode(err, def)
89+
}
90+
91+
func osUnlock(file *os.File, start, len int64) _ErrorCode {
92+
err := unix.FcntlFlock(file.Fd(), unix.F_SETLK, &unix.Flock_t{
93+
Type: unix.F_UNLCK,
94+
Start: start,
95+
Len: len,
96+
})
97+
if err != nil {
98+
return _IOERR_UNLOCK
99+
}
100+
return _OK
101+
}

vfs/shm_bsd.go

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ package vfs
44

55
import (
66
"context"
7+
"errors"
78
"io"
9+
"io/fs"
810
"os"
911
"sync"
1012

@@ -71,23 +73,16 @@ func (s *vfsShm) shmOpen() _ErrorCode {
7173
return _OK
7274
}
7375

74-
// Always open file read-write, as it will be shared.
75-
f, err := os.OpenFile(s.path,
76-
os.O_RDWR|os.O_CREATE|_O_NOFOLLOW, 0666)
77-
if err != nil {
78-
return _CANTOPEN
79-
}
80-
// Closes file if it's not nil.
81-
defer func() { f.Close() }()
76+
vfsShmListMtx.Lock()
77+
defer vfsShmListMtx.Unlock()
8278

83-
fi, err := f.Stat()
84-
if err != nil {
79+
// Stat file without opening it.
80+
// Closing it would release all POSIX locks on it.
81+
fi, err := os.Stat(s.path)
82+
if err != nil && !errors.Is(err, fs.ErrNotExist) {
8583
return _IOERR_FSTAT
8684
}
8785

88-
vfsShmListMtx.Lock()
89-
defer vfsShmListMtx.Unlock()
90-
9186
// Find a shared file, increase the reference count.
9287
for _, g := range vfsShmList {
9388
if g != nil && os.SameFile(fi, g.info) {
@@ -97,13 +92,35 @@ func (s *vfsShm) shmOpen() _ErrorCode {
9792
}
9893
}
9994

100-
// Lock and truncate the file.
101-
// The lock is only released by closing the file.
102-
if rc := osLock(f, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK); rc != _OK {
95+
// Always open file read-write, as it will be shared.
96+
f, err := os.OpenFile(s.path,
97+
os.O_RDWR|os.O_CREATE|_O_NOFOLLOW, 0666)
98+
if err != nil {
99+
return _CANTOPEN
100+
}
101+
// Closes file if it's not nil.
102+
defer func() { f.Close() }()
103+
104+
// Dead man's switch.
105+
if lock, rc := osTestLock(f, _SHM_DMS, 1); rc != _OK {
106+
return _IOERR_LOCK
107+
} else if lock == unix.F_WRLCK {
108+
return _BUSY
109+
} else if lock == unix.F_UNLCK {
110+
if rc := osWriteLock(f, _SHM_DMS, 1); rc != _OK {
111+
return rc
112+
}
113+
if err := f.Truncate(0); err != nil {
114+
return _IOERR_SHMOPEN
115+
}
116+
}
117+
if rc := osReadLock(f, _SHM_DMS, 1); rc != _OK {
103118
return rc
104119
}
105-
if err := f.Truncate(0); err != nil {
106-
return _IOERR_SHMOPEN
120+
121+
fi, err = f.Stat()
122+
if err != nil {
123+
return _IOERR_FSTAT
107124
}
108125

109126
// Add the new shared file.
@@ -157,7 +174,30 @@ func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, ext
157174
func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) _ErrorCode {
158175
s.Lock()
159176
defer s.Unlock()
160-
return s.shmMemLock(offset, n, flags)
177+
178+
// Check if we could obtain/release the lock locally.
179+
rc := s.shmMemLock(offset, n, flags)
180+
if rc != _OK {
181+
return rc
182+
}
183+
184+
// Obtain/release the appropriate file lock.
185+
switch {
186+
case flags&_SHM_UNLOCK != 0:
187+
return osUnlock(s.File, _SHM_BASE+int64(offset), int64(n))
188+
case flags&_SHM_SHARED != 0:
189+
rc = osReadLock(s.File, _SHM_BASE+int64(offset), int64(n))
190+
case flags&_SHM_EXCLUSIVE != 0:
191+
rc = osWriteLock(s.File, _SHM_BASE+int64(offset), int64(n))
192+
default:
193+
panic(util.AssertErr())
194+
}
195+
196+
// Release the local lock.
197+
if rc != _OK {
198+
s.shmMemLock(offset, n, flags^(_SHM_UNLOCK|_SHM_LOCK))
199+
}
200+
return rc
161201
}
162202

163203
func (s *vfsShm) shmUnmap(delete bool) {

0 commit comments

Comments
 (0)