Skip to content

Commit 528efd8

Browse files
authored
Merge pull request #33 from ecordell/win-pager
Add a pager for windows
2 parents 16dcf4b + 8532e88 commit 528efd8

File tree

3 files changed

+154
-12
lines changed

3 files changed

+154
-12
lines changed

db/pager.go

+7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
package db
22

3+
const (
4+
sqlitePendingByte = 0x40000000
5+
sqliteReservedByte = sqlitePendingByte + 1
6+
sqliteSharedFirst = sqlitePendingByte + 2
7+
sqliteSharedSize = 510
8+
)
9+
310
type pager interface {
411
// load a page from storage.
512
page(n int, pagesize int) ([]byte, error)

db/pager_unix.go

+10-12
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// +build !windows
2+
13
// unix implementation of the `pager` interface (the file reader) with POSIX
24
// advisory locking
35

@@ -12,11 +14,7 @@ import (
1214
)
1315

1416
const (
15-
seek_set = 0 // should be defined in syscall
16-
sqlite_pending_byte = 0x40000000
17-
sqlite_reserved_byte = sqlite_pending_byte + 1
18-
sqlite_shared_first = sqlite_pending_byte + 2
19-
sqlite_shared_size = 510
17+
seekSet = 0 // should be defined in syscall
2018
)
2119

2220
type filePager struct {
@@ -62,8 +60,8 @@ func (f *filePager) RLock() error {
6260
// - get PENDING lock
6361
pending := &unix.Flock_t{
6462
Type: unix.F_RDLCK,
65-
Whence: seek_set,
66-
Start: sqlite_pending_byte,
63+
Whence: seekSet,
64+
Start: sqlitePendingByte,
6765
Len: 1,
6866
}
6967
if err := f.lock(pending); err != nil {
@@ -79,9 +77,9 @@ func (f *filePager) RLock() error {
7977
// Get the read-lock
8078
read := &unix.Flock_t{
8179
Type: unix.F_RDLCK,
82-
Whence: seek_set,
83-
Start: sqlite_shared_first,
84-
Len: sqlite_shared_size,
80+
Whence: seekSet,
81+
Start: sqliteSharedFirst,
82+
Len: sqliteSharedSize,
8583
}
8684
if err := f.lock(read); err != nil {
8785
return err
@@ -105,8 +103,8 @@ func (f *filePager) CheckReservedLock() (bool, error) {
105103
// per SQLite's unixCheckReservedLock()
106104
lock := &unix.Flock_t{
107105
Type: unix.F_WRLCK,
108-
Whence: seek_set,
109-
Start: sqlite_shared_first,
106+
Whence: seekSet,
107+
Start: sqliteReservedByte,
110108
Len: 1,
111109
}
112110
err := unix.FcntlFlock(f.f.Fd(), unix.F_GETLK, lock)

db/pager_windows.go

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// windows implementation of the `pager` interface (the file reader) with LockFileEx
2+
// locking
3+
4+
// references:
5+
// winapi docs: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex
6+
// sqlite3 winlock: https://github.com/sqlite/sqlite/blob/c398c65bee850b6b8f24a44852872a27f114535d/src/os_win.c#L3236
7+
8+
package db
9+
10+
import (
11+
"errors"
12+
"fmt"
13+
"os"
14+
"time"
15+
16+
"golang.org/x/exp/mmap"
17+
"golang.org/x/sys/windows"
18+
)
19+
20+
const (
21+
reserved uint32 = 0
22+
)
23+
24+
// LockState is used to represent the program's opinion of whether a file is locked or not
25+
// this does not represent the lock state of the file - we can't distinguish between
26+
// a file that is locked by another process and failing to acquire a lock
27+
type LockState int
28+
29+
const (
30+
Unlocked LockState = iota
31+
Locked
32+
)
33+
34+
type filePager struct {
35+
f *os.File
36+
mm *mmap.ReaderAt
37+
state LockState
38+
}
39+
40+
func newFilePager(file string) (*filePager, error) {
41+
f, err := os.Open(file)
42+
if err != nil {
43+
return nil, err
44+
}
45+
mm, err := mmap.Open(file)
46+
if err != nil {
47+
f.Close()
48+
return nil, err
49+
}
50+
return &filePager{
51+
f: f,
52+
mm: mm,
53+
state: Unlocked,
54+
}, nil
55+
}
56+
57+
// pages start counting at 1
58+
func (f *filePager) page(id int, pagesize int) ([]byte, error) {
59+
buf := make([]byte, pagesize)
60+
_, err := f.mm.ReadAt(buf[:], int64(id-1)*int64(pagesize))
61+
return buf, err
62+
}
63+
64+
func (f *filePager) lock(start, len uint32) error {
65+
ol := new(windows.Overlapped)
66+
ol.Offset = start
67+
return windows.LockFileEx(windows.Handle(f.f.Fd()), windows.LOCKFILE_FAIL_IMMEDIATELY|windows.LOCKFILE_EXCLUSIVE_LOCK, reserved, len, 0, ol)
68+
}
69+
70+
func (f *filePager) unlock(start, len uint32) error {
71+
ol := new(windows.Overlapped)
72+
ol.Offset = start
73+
return windows.UnlockFileEx(windows.Handle(f.f.Fd()), reserved, len, 0, ol)
74+
}
75+
76+
func (f *filePager) RLock() error {
77+
// Set a 'SHARED' lock, following winLock() logic from sqlite3.c
78+
if f.state == Locked {
79+
return errors.New("trying to lock a locked lock") // panic?
80+
}
81+
82+
// Try 3 times to get the pending lock. This is needed to work
83+
// around problems caused by indexing and/or anti-virus software on
84+
// Windows systems.
85+
// If you are using this code as a model for alternative VFSes, do not
86+
// copy this retry logic. It is a hack intended for Windows only.
87+
//
88+
// source: https://github.com/sqlite/sqlite/blob/c398c65bee850b6b8f24a44852872a27f114535d/src/os_win.c#L3280
89+
for i := 0; i < 3; i++ {
90+
err := f.lock(sqlitePendingByte, 1)
91+
if err == nil {
92+
break
93+
}
94+
if errors.Is(err, windows.ERROR_INVALID_HANDLE) {
95+
return fmt.Errorf("failed to get lock, invalid handle")
96+
}
97+
time.Sleep(time.Microsecond)
98+
}
99+
100+
defer func() {
101+
// - drop the pending lock. No idea what to do with the error :/
102+
f.unlock(sqlitePendingByte, 1)
103+
}()
104+
105+
// Get the read-lock
106+
if err := f.lock(sqliteSharedFirst, sqliteSharedSize); err != nil {
107+
return err
108+
}
109+
f.state = Locked
110+
return nil
111+
}
112+
113+
func (f *filePager) RUnlock() error {
114+
if f.state == Unlocked {
115+
return errors.New("trying to unlock an unlocked lock") // panic?
116+
}
117+
if err := f.unlock(sqliteSharedFirst, sqliteSharedSize); err != nil {
118+
return err
119+
}
120+
f.state = Unlocked
121+
return nil
122+
}
123+
124+
// True if there is a 'reserved' lock on the database, by any process.
125+
func (f *filePager) CheckReservedLock() (bool, error) {
126+
// per SQLite's winCheckReservedLock()
127+
err := f.lock(sqliteReservedByte, 1)
128+
if err == nil {
129+
f.unlock(sqliteReservedByte, 1)
130+
}
131+
return err == nil, err
132+
}
133+
134+
func (f *filePager) Close() error {
135+
f.f.Close()
136+
return f.mm.Close()
137+
}

0 commit comments

Comments
 (0)