Improve Wal locking on BSD (#204)

This commit is contained in:
Nuno Cruces
2024-12-16 13:15:00 +00:00
committed by GitHub
parent 503db60927
commit e32d8401fb
4 changed files with 94 additions and 30 deletions

View File

@@ -74,7 +74,7 @@ This project aims for [high test coverage](https://github.com/ncruces/go-sqlite3
It also benefits greatly from [SQLite's](https://sqlite.org/testing.html) and It also benefits greatly from [SQLite's](https://sqlite.org/testing.html) and
[wazero's](https://tetrate.io/blog/introducing-wazero-from-tetrate/#:~:text=Rock%2Dsolid%20test%20approach) thorough testing. [wazero's](https://tetrate.io/blog/introducing-wazero-from-tetrate/#:~:text=Rock%2Dsolid%20test%20approach) thorough testing.
Every commit is [tested](https://github.com/ncruces/go-sqlite3/wiki/Test-matrix) on Every commit is [tested](https://github.com/ncruces/go-sqlite3/wiki/Support-matrix) on
Linux (amd64/arm64/386/riscv64/ppc64le/s390x), macOS (amd64/arm64), Linux (amd64/arm64/386/riscv64/ppc64le/s390x), macOS (amd64/arm64),
Windows (amd64), FreeBSD (amd64), OpenBSD (amd64), NetBSD (amd64), Windows (amd64), FreeBSD (amd64), OpenBSD (amd64), NetBSD (amd64),
DragonFly BSD (amd64), illumos (amd64), and Solaris (amd64). DragonFly BSD (amd64), illumos (amd64), and Solaris (amd64).

View File

@@ -48,11 +48,6 @@ On Unix, this package may use `mmap` to implement
[shared-memory for the WAL-index](https://sqlite.org/wal.html#implementation_of_shared_memory_for_the_wal_index), [shared-memory for the WAL-index](https://sqlite.org/wal.html#implementation_of_shared_memory_for_the_wal_index),
like SQLite. like SQLite.
With [BSD locks](https://man.freebsd.org/cgi/man.cgi?query=flock&sektion=2)
a WAL database can only be accessed by a single proccess.
Other processes that attempt to access a database locked with BSD locks,
will fail with the [`SQLITE_PROTOCOL`](https://sqlite.org/rescode.html#protocol) error code.
On Windows, this package may use `MapViewOfFile`, like SQLite. On Windows, this package may use `MapViewOfFile`, like SQLite.
You can also opt into a cross-platform, in-process, memory sharing implementation You can also opt into a cross-platform, in-process, memory sharing implementation

View File

@@ -9,11 +9,11 @@ import (
) )
func osGetSharedLock(file *os.File) _ErrorCode { func osGetSharedLock(file *os.File) _ErrorCode {
return osLock(file, unix.LOCK_SH|unix.LOCK_NB, _IOERR_RDLOCK) return osFlock(file, unix.LOCK_SH|unix.LOCK_NB, _IOERR_RDLOCK)
} }
func osGetReservedLock(file *os.File) _ErrorCode { func osGetReservedLock(file *os.File) _ErrorCode {
rc := osLock(file, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK) rc := osFlock(file, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK)
if rc == _BUSY { if rc == _BUSY {
// The documentation states that a lock is upgraded by // The documentation states that a lock is upgraded by
// releasing the previous lock, then acquiring the new lock. // releasing the previous lock, then acquiring the new lock.
@@ -37,7 +37,7 @@ func osGetExclusiveLock(file *os.File, state *LockLevel) _ErrorCode {
} }
func osDowngradeLock(file *os.File, _ LockLevel) _ErrorCode { func osDowngradeLock(file *os.File, _ LockLevel) _ErrorCode {
rc := osLock(file, unix.LOCK_SH|unix.LOCK_NB, _IOERR_RDLOCK) rc := osFlock(file, unix.LOCK_SH|unix.LOCK_NB, _IOERR_RDLOCK)
if rc == _BUSY { if rc == _BUSY {
// The documentation states that a lock is downgraded by // The documentation states that a lock is downgraded by
// releasing the previous lock then acquiring the new lock. // releasing the previous lock then acquiring the new lock.
@@ -66,7 +66,36 @@ func osCheckReservedLock(file *os.File) (bool, _ErrorCode) {
return lock == unix.F_WRLCK, rc return lock == unix.F_WRLCK, rc
} }
func osLock(file *os.File, how int, def _ErrorCode) _ErrorCode { func osFlock(file *os.File, how int, def _ErrorCode) _ErrorCode {
err := unix.Flock(int(file.Fd()), how) err := unix.Flock(int(file.Fd()), how)
return osLockErrorCode(err, def) return osLockErrorCode(err, def)
} }
func osReadLock(file *os.File, start, len int64) _ErrorCode {
return osLock(file, unix.F_RDLCK, start, len, _IOERR_RDLOCK)
}
func osWriteLock(file *os.File, start, len int64) _ErrorCode {
return osLock(file, unix.F_WRLCK, start, len, _IOERR_LOCK)
}
func osLock(file *os.File, typ int16, start, len int64, def _ErrorCode) _ErrorCode {
err := unix.FcntlFlock(file.Fd(), unix.F_SETLK, &unix.Flock_t{
Type: typ,
Start: start,
Len: len,
})
return osLockErrorCode(err, def)
}
func osUnlock(file *os.File, start, len int64) _ErrorCode {
err := unix.FcntlFlock(file.Fd(), unix.F_SETLK, &unix.Flock_t{
Type: unix.F_UNLCK,
Start: start,
Len: len,
})
if err != nil {
return _IOERR_UNLOCK
}
return _OK
}

View File

@@ -4,7 +4,9 @@ package vfs
import ( import (
"context" "context"
"errors"
"io" "io"
"io/fs"
"os" "os"
"sync" "sync"
@@ -71,23 +73,16 @@ func (s *vfsShm) shmOpen() _ErrorCode {
return _OK return _OK
} }
// Always open file read-write, as it will be shared.
f, err := os.OpenFile(s.path,
os.O_RDWR|os.O_CREATE|_O_NOFOLLOW, 0666)
if err != nil {
return _CANTOPEN
}
// Closes file if it's not nil.
defer func() { f.Close() }()
fi, err := f.Stat()
if err != nil {
return _IOERR_FSTAT
}
vfsShmListMtx.Lock() vfsShmListMtx.Lock()
defer vfsShmListMtx.Unlock() defer vfsShmListMtx.Unlock()
// Stat file without opening it.
// Closing it would release all POSIX locks on it.
fi, err := os.Stat(s.path)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return _IOERR_FSTAT
}
// Find a shared file, increase the reference count. // Find a shared file, increase the reference count.
for _, g := range vfsShmList { for _, g := range vfsShmList {
if g != nil && os.SameFile(fi, g.info) { if g != nil && os.SameFile(fi, g.info) {
@@ -97,13 +92,35 @@ func (s *vfsShm) shmOpen() _ErrorCode {
} }
} }
// Lock and truncate the file. // Always open file read-write, as it will be shared.
// The lock is only released by closing the file. f, err := os.OpenFile(s.path,
if rc := osLock(f, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK); rc != _OK { os.O_RDWR|os.O_CREATE|_O_NOFOLLOW, 0666)
if err != nil {
return _CANTOPEN
}
// Closes file if it's not nil.
defer func() { f.Close() }()
// Dead man's switch.
if lock, rc := osTestLock(f, _SHM_DMS, 1); rc != _OK {
return _IOERR_LOCK
} else if lock == unix.F_WRLCK {
return _BUSY
} else if lock == unix.F_UNLCK {
if rc := osWriteLock(f, _SHM_DMS, 1); rc != _OK {
return rc
}
if err := f.Truncate(0); err != nil {
return _IOERR_SHMOPEN
}
}
if rc := osReadLock(f, _SHM_DMS, 1); rc != _OK {
return rc return rc
} }
if err := f.Truncate(0); err != nil {
return _IOERR_SHMOPEN fi, err = f.Stat()
if err != nil {
return _IOERR_FSTAT
} }
// Add the new shared file. // Add the new shared file.
@@ -157,7 +174,30 @@ func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, ext
func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) _ErrorCode { func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) _ErrorCode {
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
return s.shmMemLock(offset, n, flags)
// Check if we could obtain/release the lock locally.
rc := s.shmMemLock(offset, n, flags)
if rc != _OK {
return rc
}
// Obtain/release the appropriate file lock.
switch {
case flags&_SHM_UNLOCK != 0:
return osUnlock(s.File, _SHM_BASE+int64(offset), int64(n))
case flags&_SHM_SHARED != 0:
rc = osReadLock(s.File, _SHM_BASE+int64(offset), int64(n))
case flags&_SHM_EXCLUSIVE != 0:
rc = osWriteLock(s.File, _SHM_BASE+int64(offset), int64(n))
default:
panic(util.AssertErr())
}
// Release the local lock.
if rc != _OK {
s.shmMemLock(offset, n, flags^(_SHM_UNLOCK|_SHM_LOCK))
}
return rc
} }
func (s *vfsShm) shmUnmap(delete bool) { func (s *vfsShm) shmUnmap(delete bool) {