Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent WAL writes when using EXCLUSIVE locking mode #426

Merged
merged 1 commit into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions cmd/litefs/mount_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2583,6 +2583,43 @@ func TestEventStream(t *testing.T) {
})
}

// See: https://github.com/superfly/litefs/issues/425
func TestPreventExclusiveLockingModeWithWAL(t *testing.T) {
if !testingutil.IsWALMode() {
t.Skip("test only applies to WAL mode, skipping")
}

// Ensures writes to a new WAL fail if using EXCLUSIVE locking mode.
t.Run("WALHeader", func(t *testing.T) {
cmd0 := runMountCommand(t, newMountCommand(t, t.TempDir(), nil))
db := testingutil.OpenSQLDB(t, filepath.Join(cmd0.Config.FUSE.Dir, "db"))

if _, err := db.Exec("PRAGMA locking_mode = EXCLUSIVE"); err != nil {
t.Fatal(err)
}
if _, err := db.Exec(`CREATE TABLE t (x)`); err == nil || err.Error() != `disk I/O error` {
t.Fatalf("unexpected error: %v", err)
}
})

// Ensures writes to an existing WAL fail if using EXCLUSIVE locking mode.
t.Run("WALFrame", func(t *testing.T) {
cmd0 := runMountCommand(t, newMountCommand(t, t.TempDir(), nil))
db := testingutil.OpenSQLDB(t, filepath.Join(cmd0.Config.FUSE.Dir, "db"))

if _, err := db.Exec(`CREATE TABLE t (x)`); err != nil {
t.Fatal(err)
}

if _, err := db.Exec("PRAGMA locking_mode = EXCLUSIVE"); err != nil {
t.Fatal(err)
}
if _, err := db.Exec(`INSERT INTO t VALUES ('foo')`); err == nil || err.Error() != `disk I/O error` {
t.Fatalf("unexpected error: %v", err)
}
})
}

// Ensure multiple nodes can run in a cluster for an extended period of time.
func TestFunctional_OK(t *testing.T) {
if *funTime <= 0 {
Expand Down
15 changes: 15 additions & 0 deletions db.go
Original file line number Diff line number Diff line change
Expand Up @@ -1379,6 +1379,11 @@ func (db *DB) writeWALHeader(ctx context.Context, f *os.File, data []byte, offse
return fmt.Errorf("WAL header write must be 32 bytes in size, received %d", len(data))
}

// Prevent transactions when the write lock has not been acquired (e.g. EXCLUSIVE lock)
if db.writeLock.State() != RWMutexStateExclusive {
return fmt.Errorf("cannot write to WAL header without WRITE lock, exclusive locking not allowed")
}

// Determine byte order of checksums.
switch magic := binary.BigEndian.Uint32(data[0:]); magic {
case 0x377f0682:
Expand Down Expand Up @@ -1427,6 +1432,11 @@ func (db *DB) writeWALFrameHeader(ctx context.Context, f *os.File, data []byte,
}
}()

// Prevent transactions when the write lock has not been acquired (e.g. EXCLUSIVE lock)
if db.writeLock.State() != RWMutexStateExclusive {
return fmt.Errorf("cannot write to WAL frame header without WRITE lock, exclusive locking not allowed")
}

// Prevent SQLite from writing before the current WAL position.
if offset < db.wal.offset {
return fmt.Errorf("cannot write wal frame header @%d before current WAL position @%d", offset, db.wal.offset)
Expand All @@ -1442,6 +1452,11 @@ func (db *DB) writeWALFrameData(ctx context.Context, f *os.File, data []byte, of
TraceLog.Printf("[WriteWALFrameData(%s)]: offset=%d size=%d owner=%d %s", db.name, offset, len(data), owner, errorKeyValue(err))
}()

// Prevent transactions when the write lock has not been acquired (e.g. EXCLUSIVE lock)
if db.writeLock.State() != RWMutexStateExclusive {
return fmt.Errorf("cannot write to WAL frame data without WRITE lock, exclusive locking not allowed")
}

// Prevent SQLite from writing before the current WAL position.
if offset < db.wal.offset {
return fmt.Errorf("cannot write wal frame data @%d before current WAL position @%d", offset, db.wal.offset)
Expand Down
Loading