Files
replicate/cmd/replicate-vfs/main_test.go
zeekayandGitHub 4b1d504181 fork: switch to hanzoai/lz4 + hanzoai/ltx (#2)
* chore: add .dockerignore

* ci: migrate to canonical hanzoai/.github/docker-build.yml reusable

* fork: switch to hanzoai/lz4 + hanzoai/ltx (pin v4.1.22 lz4 wire format)

pierrec/lz4/v4 v4.1.23+ broke end-of-frame detection causing
superfly/ltx (and replicate's verify path) to fail with 'expected
lz4 end frame'. We now own the lz4 + ltx forks at hanzoai/lz4 and
hanzoai/ltx, pinned to the working v4.1.22 wire format.

All tests passing.
2026-04-27 12:28:56 -07:00

2422 lines
71 KiB
Go

//go:build vfs
// +build vfs
package main_test
import (
"bytes"
"context"
"database/sql"
"errors"
"fmt"
"io"
"log/slog"
"math/rand"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
sqlite3 "github.com/mattn/go-sqlite3"
"github.com/psanford/sqlite3vfs"
"github.com/stretchr/testify/require"
"github.com/hanzoai/ltx"
"github.com/hanzoai/replicate"
"github.com/hanzoai/replicate/file"
"github.com/hanzoai/replicate/internal/testingutil"
)
func TestVFS_Simple(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
vfs := newVFS(t, client)
if err := sqlite3vfs.RegisterVFS("replicate", vfs); err != nil {
t.Fatalf("failed to register replicate vfs: %v", err)
}
db := testingutil.NewDB(t, filepath.Join(t.TempDir(), "db"))
db.MonitorInterval = 100 * time.Millisecond
db.Replica = replicate.NewReplica(db)
db.Replica.Client = client
if err := db.Open(); err != nil {
t.Fatal(err)
}
sqldb0 := testingutil.MustOpenSQLDB(t, db.Path())
defer testingutil.MustCloseSQLDB(t, sqldb0)
if _, err := sqldb0.Exec("CREATE TABLE t (x)"); err != nil {
t.Fatal(err)
}
if _, err := sqldb0.Exec("INSERT INTO t (x) VALUES (100)"); err != nil {
t.Fatal(err)
}
waitForLTXFiles(t, client, 10*time.Second, db.MonitorInterval)
sqldb1, err := sql.Open("sqlite3", "file:/tmp/test.db?vfs=replicate")
if err != nil {
t.Fatalf("failed to open database: %v", err)
}
defer sqldb1.Close()
// Execute query - wait for value to be replicated
waitForReplicaValue(t, sqldb1, "SELECT * FROM t", 100, 10*time.Second, db.MonitorInterval)
}
func TestVFS_Updating(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
vfs := newVFS(t, client)
if err := sqlite3vfs.RegisterVFS("replicate", vfs); err != nil {
t.Fatalf("failed to register replicate vfs: %v", err)
}
db := testingutil.NewDB(t, filepath.Join(t.TempDir(), "db"))
db.MonitorInterval = 100 * time.Millisecond
db.Replica = replicate.NewReplica(db)
db.Replica.Client = client
db.Replica.SyncInterval = 100 * time.Millisecond
if err := db.Open(); err != nil {
t.Fatal(err)
}
sqldb0 := testingutil.MustOpenSQLDB(t, db.Path())
defer testingutil.MustCloseSQLDB(t, sqldb0)
t.Log("creating table")
if _, err := sqldb0.Exec("CREATE TABLE t (x)"); err != nil {
t.Fatal(err)
}
if _, err := sqldb0.Exec("INSERT INTO t (x) VALUES (100)"); err != nil {
t.Fatal(err)
}
waitForLTXFiles(t, client, 10*time.Second, db.MonitorInterval)
t.Log("opening vfs")
sqldb1, err := sql.Open("sqlite3", "file:/tmp/test.db?vfs=replicate")
if err != nil {
t.Fatalf("failed to open database: %v", err)
}
defer sqldb1.Close()
// Wait for initial value to replicate
waitForReplicaValue(t, sqldb1, "SELECT * FROM t", 100, 10*time.Second, db.MonitorInterval)
t.Log("updating source database")
// Update the value from the source database.
if _, err := sqldb0.Exec("UPDATE t SET x = 200"); err != nil {
t.Fatal(err)
}
// Ensure replica has updated itself.
t.Log("ensuring replica has updated")
waitForReplicaValue(t, sqldb1, "SELECT * FROM t", 200, 10*time.Second, db.MonitorInterval)
if err := db.Replica.Stop(false); err != nil {
t.Fatalf("stop replica: %v", err)
}
}
func TestVFS_ActiveReadTransaction(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
vfs := newVFS(t, client)
if err := sqlite3vfs.RegisterVFS("replicate-txn", vfs); err != nil {
t.Fatalf("failed to register replicate vfs: %v", err)
}
db := testingutil.NewDB(t, filepath.Join(t.TempDir(), "db"))
db.MonitorInterval = 100 * time.Millisecond
db.Replica = replicate.NewReplica(db)
db.Replica.Client = client
db.Replica.SyncInterval = 100 * time.Millisecond
if err := db.Open(); err != nil {
t.Fatal(err)
}
sqldb0 := testingutil.MustOpenSQLDB(t, db.Path())
defer testingutil.MustCloseSQLDB(t, sqldb0)
// Create a table with many rows to ensure we span multiple pages
// With 4KB page size, we want to ensure we're using hundreds of pages
t.Log("creating table with many rows")
if _, err := sqldb0.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY, data TEXT)"); err != nil {
t.Fatal(err)
}
// Insert ~10000 rows, each with substantial data to span many pages
// This should occupy at least 200+ pages (assuming ~200 bytes per row, ~20 rows per 4KB page)
if _, err := sqldb0.Exec("BEGIN"); err != nil {
t.Fatal(err)
}
for i := 0; i < 10000; i++ {
data := fmt.Sprintf("initial_data_%d_padding_%s", i, string(make([]byte, 100)))
if _, err := sqldb0.Exec("INSERT INTO t (id, data) VALUES (?, ?)", i, data); err != nil {
t.Fatal(err)
}
}
if _, err := sqldb0.Exec("COMMIT"); err != nil {
t.Fatal(err)
}
// Wait for replication to sync
waitForLTXFiles(t, client, 10*time.Second, db.MonitorInterval)
t.Log("opening vfs replica")
sqldb1, err := sql.Open("sqlite3", "file:/tmp/test-txn.db?vfs=replicate-txn")
if err != nil {
t.Fatalf("failed to open database: %v", err)
}
defer sqldb1.Close()
// Start a read transaction on the replica
t.Log("starting read transaction on replica")
tx, err := sqldb1.Begin()
if err != nil {
t.Fatalf("failed to begin transaction: %v", err)
}
defer tx.Rollback()
// Verify we can read initial data from within the transaction
var initialData string
if err := tx.QueryRow("SELECT data FROM t WHERE id = 5000").Scan(&initialData); err != nil {
t.Fatalf("failed to query initial data: %v", err)
}
if !strings.HasPrefix(initialData, "initial_data_5000") {
t.Fatalf("unexpected initial data: %s", initialData)
}
t.Log("updating source database with many affected pages")
// Update many rows in the source database to affect many pages
if _, err := sqldb0.Exec("BEGIN"); err != nil {
t.Fatal(err)
}
for i := 0; i < 10000; i++ {
data := fmt.Sprintf("updated_data_%d_padding_%s", i, string(make([]byte, 100)))
if _, err := sqldb0.Exec("UPDATE t SET data = ? WHERE id = ?", data, i); err != nil {
t.Fatal(err)
}
}
if _, err := sqldb0.Exec("COMMIT"); err != nil {
t.Fatal(err)
}
// Wait for replication to sync the updates - verify new data is available
// by checking from a fresh connection (not the transaction's snapshot)
t.Log("waiting for replication sync")
require.Eventually(t, func() bool {
var data string
// Use a new query to check if updates have replicated
if err := sqldb1.QueryRow("SELECT data FROM t WHERE id = 5000").Scan(&data); err != nil {
return false
}
return strings.HasPrefix(data, "updated_data_5000")
}, 10*time.Second, db.MonitorInterval, "updates should replicate")
// The active read transaction should still see old data (snapshot isolation)
t.Log("verifying read transaction still sees old data")
var txData string
if err := tx.QueryRow("SELECT data FROM t WHERE id = 5000").Scan(&txData); err != nil {
t.Fatalf("failed to query within transaction: %v", err)
}
if !strings.HasPrefix(txData, "initial_data_5000") {
t.Fatalf("transaction should see old data, got: %s", txData)
}
// Commit the read transaction
t.Log("committing read transaction")
if err := tx.Commit(); err != nil {
t.Fatalf("failed to commit transaction: %v", err)
}
// Verify multiple rows across different pages
t.Log("verifying multiple rows across pages")
for _, id := range []int{0, 2500, 5000, 7500, 9999} {
var data string
if err := sqldb1.QueryRow("SELECT data FROM t WHERE id = ?", id).Scan(&data); err != nil {
t.Fatalf("failed to query id %d: %v", id, err)
}
expected := fmt.Sprintf("updated_data_%d", id)
if !strings.HasPrefix(data, expected) {
t.Fatalf("id %d: expected prefix %s, got: %s", id, expected, data)
}
}
}
func TestVFS_PollsL1Files(t *testing.T) {
ctx := context.Background()
client := file.NewReplicaClient(t.TempDir())
// Create and populate source database
db := testingutil.NewDB(t, filepath.Join(t.TempDir(), "db"))
db.MonitorInterval = 100 * time.Millisecond
db.Replica = replicate.NewReplica(db)
db.Replica.Client = client
db.Replica.SyncInterval = 100 * time.Millisecond
db.Replica.MonitorEnabled = false
// Create a store to handle compaction
levels := replicate.CompactionLevels{
{Level: 0},
{Level: 1, Interval: 1 * time.Second},
}
store := replicate.NewStore([]*replicate.DB{db}, levels)
store.CompactionMonitorEnabled = false
if err := store.Open(ctx); err != nil {
t.Fatal(err)
}
defer store.Close(ctx)
sqldb0 := testingutil.MustOpenSQLDB(t, db.Path())
defer testingutil.MustCloseSQLDB(t, sqldb0)
// Create table and insert data
t.Log("creating table with data")
if _, err := sqldb0.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY, data TEXT)"); err != nil {
t.Fatal(err)
}
// Insert multiple transactions to create several L0 files
for i := 0; i < 5; i++ {
if _, err := sqldb0.Exec("INSERT INTO t (data) VALUES (?)", fmt.Sprintf("value-%d", i)); err != nil {
t.Fatal(err)
}
if err := db.Sync(ctx); err != nil {
t.Fatal(err)
}
if err := db.Replica.Sync(ctx); err != nil {
t.Fatal(err)
}
time.Sleep(50 * time.Millisecond) // Small delay between transactions
}
t.Log("compacting to L1")
// Compact L0 files to L1
if _, err := store.CompactDB(ctx, db, levels[1]); err != nil {
t.Fatalf("failed to compact to L1: %v", err)
}
// Verify L1 files exist
itr, err := client.LTXFiles(ctx, 1, 0, false)
if err != nil {
t.Fatal(err)
}
var l1Count int
for itr.Next() {
l1Count++
}
itr.Close()
if l1Count == 0 {
t.Fatal("expected L1 files to exist after compaction")
}
t.Logf("found %d L1 file(s)", l1Count)
// Register VFS
vfs := newVFS(t, client)
if err := sqlite3vfs.RegisterVFS("replicate-l1", vfs); err != nil {
t.Fatalf("failed to register replicate vfs: %v", err)
}
// Open database through VFS
t.Log("opening vfs")
sqldb1, err := sql.Open("sqlite3", "file:/tmp/test-l1.db?vfs=replicate-l1")
if err != nil {
t.Fatalf("failed to open database: %v", err)
}
defer sqldb1.Close()
// Query to ensure data is readable
var count int
if err := sqldb1.QueryRow("SELECT COUNT(*) FROM t").Scan(&count); err != nil {
t.Fatalf("failed to query database: %v", err)
} else if got, want := count, 5; got != want {
t.Fatalf("got %d rows, want %d", got, want)
}
// Get the VFS file to check maxTXID1
// The VFS creates the file when opened, we need to access it
// Since VFS.Open returns the file, we need to track it
// For now, let's add more data and wait for polling
t.Log("adding more data to source")
// Add more data to L0 to trigger polling
for i := 5; i < 10; i++ {
if _, err := sqldb0.Exec("INSERT INTO t (data) VALUES (?)", fmt.Sprintf("value-%d", i)); err != nil {
t.Fatal(err)
}
if err := db.Sync(ctx); err != nil {
t.Fatal(err)
}
if err := db.Replica.Sync(ctx); err != nil {
t.Fatal(err)
}
}
// Close and reopen the VFS connection to see updates
// (VFS is designed for read replicas where clients open new connections)
sqldb1.Close()
t.Log("reopening vfs to see updates")
sqldb1, err = sql.Open("sqlite3", "file:/tmp/test-l1.db?vfs=replicate-l1")
if err != nil {
t.Fatalf("failed to reopen database: %v", err)
}
defer sqldb1.Close()
// Wait for VFS to poll new files
t.Log("waiting for VFS to poll")
require.Eventually(t, func() bool {
if err := sqldb1.QueryRow("SELECT COUNT(*) FROM t").Scan(&count); err != nil {
return false
}
return count == 10
}, 10*time.Second, vfs.PollInterval, "VFS should poll and see 10 rows")
// Compact the new L0 files to L1
// Use Eventually since compaction has a 1-second interval and first compaction just completed
t.Log("compacting new data to L1")
require.Eventually(t, func() bool {
_, err := store.CompactDB(ctx, db, levels[1])
return err == nil
}, 5*time.Second, 200*time.Millisecond, "second compaction should succeed after interval passes")
// At this point, the VFS should have polled L1 files
// We can't directly access the VFSFile from here without modifying VFS.Open
// But we can verify the data is readable, which proves L1 files are being used
// Query a specific value to ensure L1 data is accessible
var data string
if err := sqldb1.QueryRow("SELECT data FROM t WHERE id = 7").Scan(&data); err != nil {
t.Fatalf("failed to query specific row: %v", err)
} else if got, want := data, "value-6"; got != want {
t.Fatalf("got data %q, want %q", got, want)
}
t.Log("L1 file polling verified successfully")
}
func TestVFS_LongRunningTxnStress(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
vfs := newVFS(t, client)
vfs.PollInterval = 25 * time.Millisecond
vfsName := registerTestVFS(t, vfs)
_, primary := openReplicatedPrimary(t, client, 25*time.Millisecond, 25*time.Millisecond)
defer testingutil.MustCloseSQLDB(t, primary)
if _, err := primary.Exec("CREATE TABLE metrics (id INTEGER PRIMARY KEY, value INTEGER)"); err != nil {
t.Fatalf("create table: %v", err)
}
if _, err := primary.Exec("INSERT INTO metrics (id, value) VALUES (1, 0)"); err != nil {
t.Fatalf("insert row: %v", err)
}
replica := openVFSReplicaDB(t, vfsName)
defer replica.Close()
require.Eventually(t, func() bool {
var tmp int
return replica.QueryRow("SELECT value FROM metrics WHERE id = 1").Scan(&tmp) == nil
}, 30*time.Second, 50*time.Millisecond, "replica should observe metrics row")
tx, err := replica.Begin()
if err != nil {
t.Fatalf("begin replica txn: %v", err)
}
defer tx.Rollback()
var initialValue int
if err := tx.QueryRow("SELECT value FROM metrics WHERE id = 1").Scan(&initialValue); err != nil {
t.Fatalf("initial read: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
writerDone := make(chan error, 1)
go func() {
defer close(writerDone)
value := 0
for {
select {
case <-ctx.Done():
return
default:
}
value++
if _, err := primary.Exec("UPDATE metrics SET value = ? WHERE id = 1", value); err != nil {
writerDone <- err
return
}
time.Sleep(10 * time.Millisecond)
}
}()
for {
select {
case <-ctx.Done():
if err := <-writerDone; err != nil && !errors.Is(err, context.Canceled) {
t.Fatalf("writer error: %v", err)
}
goto done
case <-time.After(50 * time.Millisecond):
var v int
if err := tx.QueryRow("SELECT value FROM metrics WHERE id = 1").Scan(&v); err != nil {
t.Fatalf("read during txn: %v", err)
}
if v != initialValue {
t.Fatalf("long-running txn observed change: got %d want %d", v, initialValue)
}
}
}
done:
if err := tx.Commit(); err != nil {
t.Fatalf("commit: %v", err)
}
var finalValue int
if err := replica.QueryRow("SELECT value FROM metrics WHERE id = 1").Scan(&finalValue); err != nil {
t.Fatalf("post-commit read: %v", err)
}
if finalValue == initialValue {
t.Fatalf("expected updated value after commit")
}
}
func TestVFS_HighLoadConcurrentReads(t *testing.T) {
if testing.Short() {
t.Skip("skipping high-load test in short mode")
}
client := file.NewReplicaClient(t.TempDir())
vfs := newVFS(t, client)
vfs.PollInterval = 50 * time.Millisecond
vfsName := registerTestVFS(t, vfs)
_, primary := openReplicatedPrimary(t, client, 50*time.Millisecond, 50*time.Millisecond)
defer testingutil.MustCloseSQLDB(t, primary)
if _, err := primary.Exec(`CREATE TABLE t (
id INTEGER PRIMARY KEY AUTOINCREMENT,
value TEXT,
updated_at INTEGER
)`); err != nil {
t.Fatalf("create table: %v", err)
}
seedLargeTable(t, primary, 2000)
replica := openVFSReplicaDB(t, vfsName)
defer replica.Close()
if _, err := replica.Exec("PRAGMA temp_store = MEMORY"); err != nil {
t.Fatalf("set temp_store: %v", err)
}
waitForReplicaRowCount(t, primary, replica, 30*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var writerOps atomic.Int64
writerErr := make(chan error, 1)
go func() {
defer close(writerErr)
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
for {
select {
case <-ctx.Done():
writerErr <- nil
return
default:
}
switch rnd.Intn(3) {
case 0:
if _, err := primary.Exec("INSERT INTO t (value, updated_at) VALUES (?, strftime('%s','now'))", fmt.Sprintf("value-%d", rnd.Int())); err != nil {
writerErr <- err
return
}
case 1:
if _, err := primary.Exec("UPDATE t SET value = value || '-u' WHERE id IN (SELECT id FROM t ORDER BY RANDOM() LIMIT 1)"); err != nil {
writerErr <- err
return
}
default:
if _, err := primary.Exec("DELETE FROM t WHERE id IN (SELECT id FROM t ORDER BY RANDOM() LIMIT 1)"); err != nil {
writerErr <- err
return
}
}
writerOps.Add(1)
time.Sleep(time.Duration(rnd.Intn(5)+1) * time.Millisecond)
}
}()
readerErrCh := make(chan error, 1)
var readerWg sync.WaitGroup
for i := 0; i < 8; i++ {
readerWg.Add(1)
go func(id int) {
defer readerWg.Done()
for {
select {
case <-ctx.Done():
return
default:
}
var count int
var totalBytes int
if err := replica.QueryRow("SELECT COUNT(*), IFNULL(SUM(LENGTH(value)), 0) FROM t").Scan(&count, &totalBytes); err != nil {
readerErrCh <- fmt.Errorf("reader %d query: %w", id, err)
return
}
if count < 0 || totalBytes < 0 {
readerErrCh <- fmt.Errorf("reader %d observed invalid stats", id)
return
}
}
}(i)
}
<-ctx.Done()
readerWg.Wait()
if err := <-writerErr; err != nil && !errors.Is(err, context.Canceled) {
t.Fatalf("writer error: %v", err)
}
select {
case err := <-readerErrCh:
if err != nil {
t.Fatalf("reader error: %v", err)
}
default:
}
if ops := writerOps.Load(); ops < 100 {
t.Fatalf("expected high write volume, got %d ops", ops)
}
waitForReplicaRowCount(t, primary, replica, 30*time.Second)
var primaryCount, replicaCount int
if err := primary.QueryRow("SELECT COUNT(*) FROM t").Scan(&primaryCount); err != nil {
t.Fatalf("primary count: %v", err)
}
if err := replica.QueryRow("SELECT COUNT(*) FROM t").Scan(&replicaCount); err != nil {
t.Fatalf("replica count: %v", err)
}
if primaryCount != replicaCount {
t.Fatalf("replica lagging: primary=%d replica=%d", primaryCount, replicaCount)
}
}
// TestVFS_OverlappingTransactionCommitStorm tests that the VFS can handle
// concurrent read operations while writes are happening on the primary.
// The test verifies that the replica eventually catches up with the primary.
func TestVFS_OverlappingTransactionCommitStorm(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
const interval = 25 * time.Millisecond
db, primary := openReplicatedPrimary(t, client, interval, interval)
defer testingutil.MustCloseSQLDB(t, primary)
if _, err := primary.Exec("CREATE TABLE ledger (id INTEGER PRIMARY KEY AUTOINCREMENT, account INTEGER, amount INTEGER, created_at INTEGER)"); err != nil {
t.Fatalf("create table: %v", err)
}
if _, err := primary.Exec("INSERT INTO ledger (account, amount, created_at) VALUES (1, 0, strftime('%s','now'))"); err != nil {
t.Fatalf("seed ledger: %v", err)
}
// Wait for LTX files to be created before opening VFS replica
require.Eventually(t, func() bool {
itr, err := client.LTXFiles(context.Background(), 0, 0, false)
if err != nil {
return false
}
defer itr.Close()
return itr.Next()
}, 5*time.Second, db.MonitorInterval, "LTX files should be created")
forceReplicaSync(t, db)
vfs := newVFS(t, client)
vfs.PollInterval = interval
vfsName := registerTestVFS(t, vfs)
replica := openVFSReplicaDB(t, vfsName)
defer replica.Close()
// Verify initial sync
require.Eventually(t, func() bool {
var primaryCount int
if err := primary.QueryRow("SELECT COUNT(*) FROM ledger").Scan(&primaryCount); err != nil {
return false
}
var replicaCount int
if err := replica.QueryRow("SELECT COUNT(*) FROM ledger").Scan(&replicaCount); err != nil {
return false
}
return primaryCount == replicaCount
}, time.Minute, 25*time.Millisecond, "ledger counts should match initially")
// Run concurrent writers for a short period (reduced from 10s to 3s)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
var writerWG sync.WaitGroup
writer := func(account int) {
defer writerWG.Done()
rnd := rand.New(rand.NewSource(time.Now().UnixNano() + int64(account)))
for {
select {
case <-ctx.Done():
return
default:
}
amount := rnd.Intn(200) - 100
if _, err := primary.Exec("BEGIN IMMEDIATE"); err != nil {
continue
}
if _, err := primary.Exec("INSERT INTO ledger (account, amount, created_at) VALUES (?, ?, strftime('%s','now'))", account, amount); err != nil {
primary.Exec("ROLLBACK")
continue
}
if _, err := primary.Exec("COMMIT"); err != nil {
primary.Exec("ROLLBACK")
continue
}
// Slow down writes to allow background monitor to keep up
time.Sleep(time.Duration(rnd.Intn(20)+10) * time.Millisecond)
}
}
writerWG.Add(2)
go writer(1)
go writer(2)
// Run concurrent reader that verifies count never goes to zero
readerCtx, readerCancel := context.WithCancel(ctx)
readerErr := make(chan error, 1)
var readerWG sync.WaitGroup
readerWG.Add(1)
go func() {
defer readerWG.Done()
for {
select {
case <-readerCtx.Done():
return
default:
}
var count int
if err := replica.QueryRow("SELECT COUNT(*) FROM ledger").Scan(&count); err != nil {
readerErr <- err
return
}
if count == 0 {
readerErr <- fmt.Errorf("ledger count went to zero")
return
}
time.Sleep(25 * time.Millisecond)
}
}()
<-ctx.Done()
readerCancel()
writerWG.Wait()
readerWG.Wait()
// Check for reader errors
select {
case err := <-readerErr:
if err != nil {
t.Fatalf("reader error: %v", err)
}
default:
}
// Force final sync and wait for replica to catch up
forceReplicaSync(t, db)
require.Eventually(t, func() bool {
var primaryCount int
if err := primary.QueryRow("SELECT COUNT(*) FROM ledger").Scan(&primaryCount); err != nil {
return false
}
var replicaCount int
if err := replica.QueryRow("SELECT COUNT(*) FROM ledger").Scan(&replicaCount); err != nil {
return false
}
return primaryCount == replicaCount
}, 30*time.Second, 100*time.Millisecond, "ledger counts should match after writer done")
}
func TestVFS_CacheMissStorm(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
const interval = 20 * time.Millisecond
_, primary := openReplicatedPrimary(t, client, interval, interval)
defer testingutil.MustCloseSQLDB(t, primary)
if _, err := primary.Exec("CREATE TABLE stats (id INTEGER PRIMARY KEY, payload TEXT)"); err != nil {
t.Fatalf("create table: %v", err)
}
for i := 0; i < 1000; i++ {
if _, err := primary.Exec("INSERT INTO stats (payload) VALUES (?)", fmt.Sprintf("row-%d", i)); err != nil {
t.Fatalf("insert payload: %v", err)
}
}
vfs := newVFS(t, client)
vfs.PollInterval = interval
vfsName := registerTestVFS(t, vfs)
replica := openVFSReplicaDB(t, vfsName)
defer replica.Close()
waitForTableRowCount(t, primary, replica, "stats", 30*time.Second)
if _, err := replica.Exec("PRAGMA cache_size = -64"); err != nil {
t.Fatalf("set cache_size: %v", err)
}
if _, err := replica.Exec("PRAGMA cache_spill = ON"); err != nil {
t.Fatalf("enable cache_spill: %v", err)
}
for i := 0; i < 100; i++ {
var maxID int
if err := replica.QueryRow("SELECT MAX(id) FROM stats").Scan(&maxID); err != nil {
t.Fatalf("cache-miss query: %v", err)
}
if maxID == 0 {
t.Fatalf("unexpected empty stats table")
}
}
}
func BenchmarkVFS_LargeDatabase(b *testing.B) {
if testing.Short() {
b.Skip("skipping large benchmark in short mode")
}
client := file.NewReplicaClient(b.TempDir())
db, primary := openReplicatedPrimary(b, client, 25*time.Millisecond, 25*time.Millisecond)
b.Cleanup(func() { testingutil.MustCloseSQLDB(b, primary) })
if _, err := primary.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT, updated_at INTEGER)"); err != nil {
b.Fatalf("create table: %v", err)
}
seedLargeTable(b, primary, 20000)
forceReplicaSync(b, db)
if err := db.Replica.Stop(false); err != nil {
b.Fatalf("stop replica: %v", err)
}
vfs := newVFS(b, client)
vfs.PollInterval = 25 * time.Millisecond
vfsName := registerTestVFS(b, vfs)
replica := openVFSReplicaDB(b, vfsName)
b.Cleanup(func() { replica.Close() })
waitForReplicaRowCount(b, primary, replica, 30*time.Second)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
var count, totalBytes int
if err := replica.QueryRow("SELECT COUNT(*), IFNULL(SUM(LENGTH(value)), 0) FROM t").Scan(&count, &totalBytes); err != nil {
b.Fatalf("benchmark query: %v", err)
}
}
}
func TestVFS_NetworkLatencySensitivity(t *testing.T) {
client := &latencyReplicaClient{ReplicaClient: file.NewReplicaClient(t.TempDir()), delay: 10 * time.Millisecond}
vfs := newVFS(t, client)
vfs.PollInterval = 25 * time.Millisecond
vfsName := registerTestVFS(t, vfs)
_, primary := openReplicatedPrimary(t, client, 25*time.Millisecond, 25*time.Millisecond)
defer testingutil.MustCloseSQLDB(t, primary)
if _, err := primary.Exec("CREATE TABLE logs (id INTEGER PRIMARY KEY, value TEXT)"); err != nil {
t.Fatalf("create table: %v", err)
}
if _, err := primary.Exec("INSERT INTO logs (value) VALUES ('ok')"); err != nil {
t.Fatalf("insert row: %v", err)
}
replica := openVFSReplicaDB(t, vfsName)
defer replica.Close()
require.Eventually(t, func() bool {
var count int
if err := replica.QueryRow("SELECT COUNT(*) FROM logs").Scan(&count); err != nil {
return false
}
return count == 1
}, 10*time.Second, 50*time.Millisecond, "replica should observe log row under injected latency")
}
func TestVFS_ConcurrentConnectionScaling(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
vfs := newVFS(t, client)
vfs.PollInterval = 25 * time.Millisecond
vfsName := registerTestVFS(t, vfs)
db, primary := openReplicatedPrimary(t, client, 25*time.Millisecond, 25*time.Millisecond)
defer testingutil.MustCloseSQLDB(t, primary)
if _, err := primary.Exec("CREATE TABLE metrics (id INTEGER PRIMARY KEY AUTOINCREMENT, value INTEGER)"); err != nil {
t.Fatalf("create table: %v", err)
}
for i := 0; i < 1000; i++ {
if _, err := primary.Exec("INSERT INTO metrics (value) VALUES (?)", i); err != nil {
t.Fatalf("insert row: %v", err)
}
}
// Wait for LTX files to be created before forceReplicaSync
require.Eventually(t, func() bool {
itr, err := client.LTXFiles(context.Background(), 0, 0, false)
if err != nil {
return false
}
defer itr.Close()
return itr.Next()
}, 5*time.Second, db.MonitorInterval, "LTX files should be created")
forceReplicaSync(t, db)
const connCount = 32
conns := make([]*sql.DB, connCount)
for i := 0; i < connCount; i++ {
conns[i] = openVFSReplicaDB(t, vfsName)
}
defer func() {
for _, c := range conns {
c.Close()
}
}()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var wg sync.WaitGroup
for idx := range conns {
wg.Add(1)
go func(id int, dbConn *sql.DB) {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
default:
}
var min, max int
if err := dbConn.QueryRow("SELECT MIN(value), MAX(value) FROM metrics").Scan(&min, &max); err != nil {
t.Errorf("conn %d query: %v", id, err)
return
}
}
}(idx, conns[idx])
}
wg.Wait()
if err := ctx.Err(); err != context.Canceled && err != context.DeadlineExceeded {
t.Fatalf("unexpected context err: %v", err)
}
}
func TestVFS_PRAGMAQueryBehavior(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
vfs := newVFS(t, client)
vfs.PollInterval = 25 * time.Millisecond
vfsName := registerTestVFS(t, vfs)
db, primary := openReplicatedPrimary(t, client, 25*time.Millisecond, 25*time.Millisecond)
defer testingutil.MustCloseSQLDB(t, primary)
if _, err := primary.Exec("CREATE TABLE configs (id INTEGER PRIMARY KEY, name TEXT)"); err != nil {
t.Fatalf("create table: %v", err)
}
if _, err := primary.Exec("INSERT INTO configs (name) VALUES ('ok')"); err != nil {
t.Fatalf("insert row: %v", err)
}
if _, err := primary.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY, value TEXT)"); err != nil {
t.Fatalf("create table t: %v", err)
}
if _, err := primary.Exec("INSERT INTO t (value) VALUES ('seed')"); err != nil {
t.Fatalf("seed t: %v", err)
}
// Wait for LTX files to be created before forceReplicaSync
require.Eventually(t, func() bool {
itr, err := client.LTXFiles(context.Background(), 0, 0, false)
if err != nil {
return false
}
defer itr.Close()
return itr.Next()
}, 5*time.Second, db.MonitorInterval, "LTX files should be created")
forceReplicaSync(t, db)
replica := openVFSReplicaDB(t, vfsName)
defer replica.Close()
waitForReplicaRowCount(t, primary, replica, 30*time.Second)
var journalMode string
if err := replica.QueryRow("PRAGMA journal_mode").Scan(&journalMode); err != nil {
t.Fatalf("read journal_mode: %v", err)
}
if strings.ToLower(journalMode) != "delete" {
t.Fatalf("expected journal_mode delete, got %s", journalMode)
}
if _, err := replica.Exec("PRAGMA cache_size = -2048"); err != nil {
t.Fatalf("set cache_size: %v", err)
}
var cacheSize int
if err := replica.QueryRow("PRAGMA cache_size").Scan(&cacheSize); err != nil {
t.Fatalf("read cache_size: %v", err)
}
if cacheSize != -2048 {
t.Fatalf("unexpected cache_size: %d", cacheSize)
}
var pageSize int
if err := replica.QueryRow("PRAGMA page_size").Scan(&pageSize); err != nil {
t.Fatalf("read page_size: %v", err)
}
if pageSize != 4096 {
t.Fatalf("unexpected page_size: %d", pageSize)
}
}
func TestVFS_SortingLargeResultSet(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
vfs := newVFS(t, client)
vfs.PollInterval = 50 * time.Millisecond
vfsName := registerTestVFS(t, vfs)
db, primary := openReplicatedPrimary(t, client, 50*time.Millisecond, 50*time.Millisecond)
defer testingutil.MustCloseSQLDB(t, primary)
if _, err := primary.Exec(`CREATE TABLE t (
id INTEGER PRIMARY KEY,
payload TEXT NOT NULL,
grp INTEGER NOT NULL
)`); err != nil {
t.Fatalf("create table: %v", err)
}
seedSortedDataset(t, primary, 25000)
if err := db.Replica.Stop(false); err != nil {
t.Fatalf("stop replica: %v", err)
}
replica := openVFSReplicaDB(t, vfsName)
defer replica.Close()
if _, err := replica.Exec("PRAGMA temp_store = FILE"); err != nil {
t.Fatalf("set temp_store: %v", err)
}
if _, err := replica.Exec("PRAGMA cache_size = -2048"); err != nil {
t.Fatalf("set cache_size: %v", err)
}
waitForReplicaRowCount(t, primary, replica, time.Minute)
expected := fetchOrderedPayloads(t, primary, 500, "payload DESC, id DESC")
got := fetchOrderedPayloads(t, replica, 500, "payload DESC, id DESC")
if len(expected) != len(got) {
t.Fatalf("unexpected result size: expected=%d got=%d", len(expected), len(got))
}
for i := range expected {
if expected[i] != got[i] {
t.Fatalf("mismatched payload at %d: expected=%q got=%q", i, expected[i], got[i])
}
}
}
func TestVFS_ConcurrentIndexAccessRaces(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
const monitorInterval = 10 * time.Millisecond
_, primary := openReplicatedPrimary(t, client, monitorInterval, 10*time.Millisecond)
defer testingutil.MustCloseSQLDB(t, primary)
if _, err := primary.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT, updated_at INTEGER)"); err != nil {
t.Fatalf("create table: %v", err)
}
seedLargeTable(t, primary, 10000)
vfs := newVFS(t, client)
vfs.PollInterval = 15 * time.Millisecond
vfsName := registerTestVFS(t, vfs)
dsn := fmt.Sprintf("file:%s?vfs=%s", filepath.ToSlash(filepath.Join(t.TempDir(), "fail.db")), vfsName)
replica, err := sql.Open("sqlite3", dsn)
if err != nil {
t.Fatalf("open replica db: %v", err)
}
defer replica.Close()
replica.SetMaxOpenConns(4)
replica.SetMaxIdleConns(4)
replica.SetConnMaxIdleTime(30 * time.Second)
waitForReplicaRowCount(t, primary, replica, 30*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
readerErrCh := make(chan error, 1)
var readerWG sync.WaitGroup
for i := 0; i < 100; i++ {
readerWG.Add(1)
go func(id int) {
defer readerWG.Done()
rnd := rand.New(rand.NewSource(int64(id) + time.Now().UnixNano()))
for {
select {
case <-ctx.Done():
return
default:
}
var count int
var totalBytes int
if err := replica.QueryRow("SELECT COUNT(*), IFNULL(SUM(LENGTH(value)), 0) FROM t").Scan(&count, &totalBytes); err != nil {
select {
case readerErrCh <- fmt.Errorf("reader %d: %w", id, err):
default:
}
cancel()
return
}
if count < 0 || totalBytes < 0 {
select {
case readerErrCh <- fmt.Errorf("reader %d observed invalid stats", id):
default:
}
cancel()
return
}
_ = rnd.Int() // exercise RNG to vary workload
}
}(i)
}
var writerOps atomic.Int64
writerErrCh := make(chan error, 1)
go func() {
defer close(writerErrCh)
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
for {
select {
case <-ctx.Done():
return
default:
}
switch rnd.Intn(3) {
case 0:
_, err := primary.Exec("INSERT INTO t (value, updated_at) VALUES (?, strftime('%s','now'))", fmt.Sprintf("writer-%d", rnd.Int()))
if err != nil {
if isBusyError(err) {
continue
}
writerErrCh <- err
cancel()
return
}
case 1:
_, err := primary.Exec("UPDATE t SET value = value || '-u', updated_at = strftime('%s','now') WHERE id IN (SELECT id FROM t ORDER BY RANDOM() LIMIT 1)")
if err != nil {
if isBusyError(err) {
continue
}
writerErrCh <- err
cancel()
return
}
default:
_, err := primary.Exec("DELETE FROM t WHERE id IN (SELECT id FROM t ORDER BY RANDOM() LIMIT 1)")
if err != nil {
if isBusyError(err) {
continue
}
writerErrCh <- err
cancel()
return
}
}
writerOps.Add(1)
time.Sleep(time.Duration(rnd.Intn(5)+1) * time.Millisecond)
}
}()
<-ctx.Done()
readerWG.Wait()
if err := <-writerErrCh; err != nil && !errors.Is(err, context.Canceled) {
t.Fatalf("writer error: %v", err)
}
select {
case err := <-readerErrCh:
if err != nil {
t.Fatalf("reader error: %v", err)
}
default:
}
if ops := writerOps.Load(); ops == 0 {
t.Fatalf("writer did not perform any operations")
}
}
func TestVFS_MultiplePageSizes(t *testing.T) {
pageSizes := []int{512, 1024, 2048, 4096, 8192, 16384, 32768, 65536}
for _, pageSize := range pageSizes {
pageSize := pageSize
const monitorInterval = 50 * time.Millisecond
t.Run(fmt.Sprintf("page_%d", pageSize), func(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
_, primary := openReplicatedPrimary(t, client, monitorInterval, 50*time.Millisecond)
defer testingutil.MustCloseSQLDB(t, primary)
if _, err := primary.Exec("PRAGMA journal_mode=DELETE"); err != nil {
t.Fatalf("disable wal: %v", err)
}
if _, err := primary.Exec(fmt.Sprintf("PRAGMA page_size = %d", pageSize)); err != nil {
t.Fatalf("set page size: %v", err)
}
if _, err := primary.Exec("VACUUM"); err != nil {
t.Fatalf("vacuum: %v", err)
}
if _, err := primary.Exec("PRAGMA journal_mode=WAL"); err != nil {
t.Fatalf("enable wal: %v", err)
}
if _, err := primary.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY, payload TEXT)"); err != nil {
t.Fatalf("create table: %v", err)
}
const totalRows = 200
if _, err := primary.Exec("BEGIN"); err != nil {
t.Fatalf("begin tx: %v", err)
}
for i := 0; i < totalRows; i++ {
payload := pageSizedPayload(pageSize, i)
if _, err := primary.Exec("INSERT INTO t (payload) VALUES (?)", payload); err != nil {
primary.Exec("ROLLBACK")
t.Fatalf("insert row %d: %v", i, err)
}
}
if _, err := primary.Exec("COMMIT"); err != nil {
t.Fatalf("commit: %v", err)
}
vfs := newVFS(t, client)
vfs.PollInterval = 50 * time.Millisecond
vfsName := registerTestVFS(t, vfs)
replica := openVFSReplicaDB(t, vfsName)
defer replica.Close()
waitForReplicaRowCount(t, primary, replica, 30*time.Second)
var replicaPageSize int
if err := replica.QueryRow("PRAGMA page_size").Scan(&replicaPageSize); err != nil {
t.Fatalf("read replica page size: %v", err)
}
if replicaPageSize != pageSize {
t.Fatalf("unexpected page size: got %d want %d", replicaPageSize, pageSize)
}
rows, err := replica.Query("SELECT id, payload FROM t ORDER BY id")
if err != nil {
t.Fatalf("select rows: %v", err)
}
defer rows.Close()
count := 0
for rows.Next() {
var id int
var payload string
if err := rows.Scan(&id, &payload); err != nil {
t.Fatalf("scan row: %v", err)
}
expected := pageSizedPayload(pageSize, id-1)
if payload != expected {
t.Fatalf("row %d mismatch: got %q want %q", id, payload, expected)
}
count++
}
if err := rows.Err(); err != nil {
t.Fatalf("rows err: %v", err)
}
if count != totalRows {
t.Fatalf("unexpected row count: got %d want %d", count, totalRows)
}
})
}
}
func TestVFS_WaitsForInitialSnapshot(t *testing.T) {
t.Run("BlocksUntilSnapshot", func(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
vfs := newVFS(t, client)
vfs.PollInterval = 50 * time.Millisecond
vfsName := registerTestVFS(t, vfs)
dsn := fmt.Sprintf("file:%s?vfs=%s", filepath.ToSlash(filepath.Join(t.TempDir(), "wait.db")), vfsName)
errCh := make(chan error, 1)
go func() {
sqldb, err := sql.Open("sqlite3", dsn)
if err != nil {
errCh <- fmt.Errorf("open replica: %w", err)
return
}
defer sqldb.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var count int
if err := sqldb.QueryRowContext(ctx, "SELECT COUNT(*) FROM sqlite_master").Scan(&count); err != nil {
errCh <- err
return
}
errCh <- nil
}()
select {
case err := <-errCh:
t.Fatalf("replica should block until snapshot is available, got %v", err)
case <-time.After(200 * time.Millisecond):
}
_, primary := openReplicatedPrimary(t, client, 25*time.Millisecond, 25*time.Millisecond)
defer testingutil.MustCloseSQLDB(t, primary)
if _, err := primary.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY)"); err != nil {
t.Fatalf("create table: %v", err)
}
if _, err := primary.Exec("INSERT INTO t (id) VALUES (1)"); err != nil {
t.Fatalf("insert row: %v", err)
}
select {
case err := <-errCh:
if err != nil {
t.Fatalf("replica query failed: %v", err)
}
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for replica to observe initial snapshot")
}
})
}
func TestVFS_StorageFailureInjection(t *testing.T) {
tests := []struct {
name string
mode string
}{
{"timeout", "timeout"},
{"server_error", "server"},
{"partial_read", "partial"},
{"corrupt_data", "corrupt"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
db, primary := openReplicatedPrimary(t, client, 50*time.Millisecond, 50*time.Millisecond)
defer testingutil.MustCloseSQLDB(t, primary)
if _, err := primary.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY, value TEXT)"); err != nil {
t.Fatalf("create table: %v", err)
}
if _, err := primary.Exec("INSERT INTO t (value) VALUES ('ok')"); err != nil {
t.Fatalf("insert row: %v", err)
}
// Wait for LTX files to be written by background monitor
require.Eventually(t, func() bool {
itr, err := client.LTXFiles(context.Background(), 0, 0, false)
if err != nil {
return false
}
defer itr.Close()
return itr.Next()
}, 5*time.Second, db.MonitorInterval, "LTX files should be created by background monitor")
forceReplicaSync(t, db)
if err := db.Replica.Stop(false); err != nil {
t.Fatalf("stop replica: %v", err)
}
vfs := newVFS(t, client)
vfs.PollInterval = time.Hour
vfsName := registerTestVFS(t, vfs)
replicaPath := filepath.Join(t.TempDir(), fmt.Sprintf("storage-failure-%s.db", tt.name))
dsn := fmt.Sprintf("file:%s?vfs=%s", filepath.ToSlash(replicaPath), vfsName)
replica, err := sql.Open("sqlite3", dsn)
if err != nil {
t.Fatalf("open replica db: %v", err)
}
defer replica.Close()
replica.SetMaxOpenConns(4)
replica.SetMaxIdleConns(4)
replica.SetConnMaxIdleTime(30 * time.Second)
if _, err := replica.Exec("PRAGMA busy_timeout = 2000"); err != nil {
t.Fatalf("set busy timeout: %v", err)
}
injectFailure := func() {
var err error
switch tt.mode {
case "timeout":
err = context.DeadlineExceeded
case "server":
err = fmt.Errorf("storage error: 500 Internal Server Error")
case "partial":
err = io.ErrUnexpectedEOF
case "corrupt":
err = fmt.Errorf("corrupt data")
default:
err = fmt.Errorf("injected failure")
}
vfs.Inject(replicaPath, err)
}
injectFailure()
var val string
if err := replica.QueryRow("SELECT value FROM t").Scan(&val); err == nil {
t.Fatalf("expected failure due to injected storage error")
}
if err := replica.QueryRow("SELECT value FROM t").Scan(&val); err != nil {
t.Fatalf("second read failed: %v", err)
}
if val != "ok" {
t.Fatalf("unexpected row value: %q", val)
}
})
}
}
func TestVFS_PartialLTXUpload(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
db, primary := openReplicatedPrimary(t, client, 25*time.Millisecond, 25*time.Millisecond)
defer testingutil.MustCloseSQLDB(t, primary)
if _, err := primary.Exec("CREATE TABLE logs (id INTEGER PRIMARY KEY, value TEXT)"); err != nil {
t.Fatalf("create table: %v", err)
}
if _, err := primary.Exec("INSERT INTO logs (value) VALUES ('ok')"); err != nil {
t.Fatalf("insert row: %v", err)
}
// Wait for LTX files to be created before forceReplicaSync
require.Eventually(t, func() bool {
itr, err := client.LTXFiles(context.Background(), 0, 0, false)
if err != nil {
return false
}
defer itr.Close()
return itr.Next()
}, 5*time.Second, db.MonitorInterval, "LTX files should be created")
forceReplicaSync(t, db)
vfs := newVFS(t, client)
vfs.PollInterval = time.Hour
vfsName := registerTestVFS(t, vfs)
replicaPath := filepath.Join(t.TempDir(), "partial.db")
dsn := fmt.Sprintf("file:%s?vfs=%s", filepath.ToSlash(replicaPath), vfsName)
replica, err := sql.Open("sqlite3", dsn)
if err != nil {
t.Fatalf("open replica db: %v", err)
}
defer replica.Close()
replica.SetMaxOpenConns(8)
replica.SetMaxIdleConns(8)
replica.SetConnMaxIdleTime(30 * time.Second)
if _, err := replica.Exec("PRAGMA busy_timeout = 2000"); err != nil {
t.Fatalf("set busy timeout: %v", err)
}
vfs.Inject(replicaPath, io.ErrUnexpectedEOF)
var val string
if err := replica.QueryRow("SELECT value FROM logs").Scan(&val); err == nil {
t.Fatalf("expected failure due to partial upload")
}
if err := replica.QueryRow("SELECT value FROM logs").Scan(&val); err != nil {
t.Fatalf("second attempt should succeed: %v", err)
}
if val != "ok" {
t.Fatalf("unexpected row value: %q", val)
}
}
func TestVFS_S3EventualConsistency(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
_, primary := openReplicatedPrimary(t, client, 25*time.Millisecond, 25*time.Millisecond)
defer testingutil.MustCloseSQLDB(t, primary)
if _, err := primary.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY, value TEXT)"); err != nil {
t.Fatalf("create table: %v", err)
}
if _, err := primary.Exec("INSERT INTO t (value) VALUES ('visible')"); err != nil {
t.Fatalf("insert row: %v", err)
}
eventualClient := &eventualConsistencyClient{ReplicaClient: client}
vfs := newVFS(t, eventualClient)
vfs.PollInterval = 25 * time.Millisecond
vfsName := registerTestVFS(t, vfs)
replica := openVFSReplicaDB(t, vfsName)
defer replica.Close()
waitForReplicaRowCount(t, primary, replica, 5*time.Second)
if calls := eventualClient.calls.Load(); calls < 2 {
t.Fatalf("expected multiple polls under eventual consistency, got %d", calls)
}
}
func TestVFS_FileDescriptorBudget(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
_, primary := openReplicatedPrimary(t, client, 25*time.Millisecond, 25*time.Millisecond)
defer testingutil.MustCloseSQLDB(t, primary)
if _, err := primary.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY, value TEXT)"); err != nil {
t.Fatalf("create table: %v", err)
}
if _, err := primary.Exec("INSERT INTO t (value) VALUES ('seed')"); err != nil {
t.Fatalf("insert seed: %v", err)
}
limited := &fdLimitedReplicaClient{ReplicaClient: client, limit: 64}
vfs := newVFS(t, limited)
vfs.PollInterval = 10 * time.Millisecond
vfsName := registerTestVFS(t, vfs)
replica := openVFSReplicaDB(t, vfsName)
defer replica.Close()
waitForReplicaRowCount(t, primary, replica, 5*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 1500*time.Millisecond)
defer cancel()
writerDone := make(chan error, 1)
go func() {
defer close(writerDone)
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
for {
select {
case <-ctx.Done():
return
default:
}
if _, err := primary.Exec("INSERT INTO t (value) VALUES (?)", fmt.Sprintf("v-%d", rnd.Int())); err != nil {
if isBusyError(err) {
time.Sleep(2 * time.Millisecond)
continue
}
writerDone <- err
return
}
time.Sleep(20 * time.Millisecond)
}
}()
const readers = 8
errs := make(chan error, readers)
for i := 0; i < readers; i++ {
go func() {
for {
select {
case <-ctx.Done():
errs <- nil
return
default:
}
var count int
if err := replica.QueryRow("SELECT COUNT(*) FROM t").Scan(&count); err != nil {
if isBusyError(err) {
time.Sleep(2 * time.Millisecond)
continue
}
errs <- err
return
}
}
}()
}
<-ctx.Done()
for i := 0; i < readers; i++ {
if err := <-errs; err != nil {
t.Fatalf("reader %d error: %T %v", i, err, err)
}
}
if err := <-writerDone; err != nil && !errors.Is(err, context.Canceled) {
t.Fatalf("writer error: %v", err)
}
deadline := time.After(250 * time.Millisecond)
for limited.open.Load() != 0 {
select {
case <-deadline:
t.Fatalf("descriptor leak: %d handles still open", limited.open.Load())
case <-time.After(10 * time.Millisecond):
}
}
}
func TestVFS_PageIndexOOM(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
_, primary := openReplicatedPrimary(t, client, 25*time.Millisecond, 25*time.Millisecond)
defer testingutil.MustCloseSQLDB(t, primary)
if _, err := primary.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY, value TEXT)"); err != nil {
t.Fatalf("create table: %v", err)
}
if _, err := primary.Exec("INSERT INTO t (value) VALUES ('ok')"); err != nil {
t.Fatalf("insert row: %v", err)
}
for i := 0; i < 64; i++ {
payload := strings.Repeat("p", 3500)
if _, err := primary.Exec("INSERT INTO t (value) VALUES (?)", payload); err != nil {
t.Fatalf("bulk insert: %v", err)
}
}
oomClient := &oomPageIndexClient{ReplicaClient: client}
vfs := newVFS(t, oomClient)
vfs.PollInterval = 20 * time.Millisecond
vfsName := registerTestVFS(t, vfs)
dsn := fmt.Sprintf("file:%s?vfs=%s", filepath.ToSlash(filepath.Join(t.TempDir(), "oom.db")), vfsName)
failing, err := sql.Open("sqlite3", dsn)
if err != nil {
t.Fatalf("open replica db: %v", err)
}
defer failing.Close()
failing.SetMaxOpenConns(4)
failing.SetMaxIdleConns(4)
oomClient.failNext.Store(true)
var count int
if err := failing.QueryRow("SELECT COUNT(*) FROM t").Scan(&count); err == nil {
t.Fatalf("expected query to fail due to page index OOM")
}
if !oomClient.triggered.Load() {
t.Fatalf("page index client never triggered")
}
oomClient.failNext.Store(false)
replica := openVFSReplicaDB(t, vfsName)
defer replica.Close()
waitForReplicaRowCount(t, primary, replica, 5*time.Second)
if err := replica.QueryRow("SELECT COUNT(*) FROM t").Scan(&count); err != nil {
t.Fatalf("post-oom read failed: %v", err)
}
var expected int
if err := primary.QueryRow("SELECT COUNT(*) FROM t").Scan(&expected); err != nil {
t.Fatalf("primary count: %v", err)
}
if count != expected {
t.Fatalf("unexpected row count: got %d want %d", count, expected)
}
}
func TestVFS_PageIndexCorruptionRecovery(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
_, primary := openReplicatedPrimary(t, client, 25*time.Millisecond, 25*time.Millisecond)
defer testingutil.MustCloseSQLDB(t, primary)
if _, err := primary.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY, value TEXT)"); err != nil {
t.Fatalf("create table: %v", err)
}
if _, err := primary.Exec("INSERT INTO t (value) VALUES ('ok')"); err != nil {
t.Fatalf("insert row: %v", err)
}
corruptClient := &corruptingPageIndexClient{ReplicaClient: client}
vfs := newVFS(t, corruptClient)
vfs.PollInterval = 20 * time.Millisecond
vfsName := registerTestVFS(t, vfs)
dsn := fmt.Sprintf("file:%s?vfs=%s", filepath.ToSlash(filepath.Join(t.TempDir(), "corrupt.db")), vfsName)
corruptClient.corruptNext.Store(true)
badConn, err := sql.Open("sqlite3", dsn)
if err != nil {
t.Fatalf("open corrupt replica: %v", err)
}
badConn.SetMaxOpenConns(8)
badConn.SetMaxIdleConns(8)
badConn.SetConnMaxIdleTime(30 * time.Second)
var count int
if err := badConn.QueryRow("SELECT COUNT(*) FROM t").Scan(&count); err == nil {
badConn.Close()
t.Fatalf("expected corruption failure")
}
badConn.Close()
if !corruptClient.triggered.Load() {
t.Fatalf("corruption hook never triggered")
}
goodConn := openVFSReplicaDB(t, vfsName)
defer goodConn.Close()
if err := goodConn.QueryRow("SELECT COUNT(*) FROM t").Scan(&count); err != nil {
t.Fatalf("post-corruption read failed: %v", err)
}
if count != 1 {
t.Fatalf("unexpected row count after recovery: %d", count)
}
}
func TestVFS_RapidUpdateCoalescing(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
const interval = 5 * time.Millisecond
_, primary := openReplicatedPrimary(t, client, interval, interval)
defer testingutil.MustCloseSQLDB(t, primary)
if _, err := primary.Exec("CREATE TABLE metrics (id INTEGER PRIMARY KEY, value INTEGER)"); err != nil {
t.Fatalf("create table: %v", err)
}
if _, err := primary.Exec("INSERT INTO metrics (id, value) VALUES (1, 0)"); err != nil {
t.Fatalf("insert row: %v", err)
}
vfs := newVFS(t, client)
vfs.PollInterval = interval
vfsName := registerTestVFS(t, vfs)
replica := openVFSReplicaDB(t, vfsName)
defer replica.Close()
const updates = 200
writerDone := make(chan struct{})
go func() {
defer close(writerDone)
for i := 1; i <= updates; i++ {
if _, err := primary.Exec("UPDATE metrics SET value = ? WHERE id = 1", i); err != nil {
return
}
time.Sleep(time.Millisecond)
}
}()
require.Eventually(t, func() bool {
var value int
if err := replica.QueryRow("SELECT value FROM metrics WHERE id = 1").Scan(&value); err != nil {
return false
}
return value == updates
}, 3*time.Second, 5*time.Millisecond, "replica should observe final value")
<-writerDone
var value int
if err := replica.QueryRow("SELECT value FROM metrics WHERE id = 1").Scan(&value); err != nil {
t.Fatalf("final read: %v", err)
}
if value != updates {
t.Fatalf("unexpected final value: got %d want %d", value, updates)
}
}
func TestVFS_NonContiguousTXIDGapFailsOnOpen(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
for txID := ltx.TXID(1); txID <= 4; txID++ {
writeSinglePageLTXFile(t, client, txID, byte('a'+int(txID)))
}
missing := client.LTXFilePath(0, 2, 2)
if err := os.Remove(missing); err != nil {
t.Fatalf("remove ltx file: %v", err)
}
fileLogger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelError}))
f := replicate.NewVFSFile(client, "gap.db", fileLogger)
f.PollInterval = 25 * time.Millisecond
if err := f.Open(); err == nil {
t.Fatalf("expected open to fail after removing %s", filepath.Base(missing))
} else if errMsg := err.Error(); !strings.Contains(errMsg, "non-contiguous") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestVFS_PollingThreadRecoversFromLTXListFailure(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
flakyClient := &flakyLTXClient{ReplicaClient: client}
const monitorInterval = 25 * time.Millisecond
_, primary := openReplicatedPrimary(t, client, monitorInterval, monitorInterval)
defer testingutil.MustCloseSQLDB(t, primary)
if _, err := primary.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY, value TEXT)"); err != nil {
t.Fatalf("create table: %v", err)
}
if _, err := primary.Exec("INSERT INTO t (value) VALUES ('seed')"); err != nil {
t.Fatalf("insert seed: %v", err)
}
vfs := newVFS(t, flakyClient)
vfs.PollInterval = 25 * time.Millisecond
vfsName := registerTestVFS(t, vfs)
replica := openVFSReplicaDB(t, vfsName)
defer replica.Close()
waitForReplicaRowCount(t, primary, replica, 10*time.Second)
flakyClient.failNext.Store(true)
if _, err := primary.Exec("INSERT INTO t (value) VALUES ('after-failure')"); err != nil {
t.Fatalf("insert post-failure: %v", err)
}
waitForReplicaRowCount(t, primary, replica, 10*time.Second)
if flakyClient.failures.Load() == 0 {
t.Fatalf("expected at least one LTXFiles failure")
}
var primaryCount, replicaCount int
if err := primary.QueryRow("SELECT COUNT(*) FROM t").Scan(&primaryCount); err != nil {
t.Fatalf("primary count: %v", err)
}
if err := replica.QueryRow("SELECT COUNT(*) FROM t").Scan(&replicaCount); err != nil {
t.Fatalf("replica count: %v", err)
}
if primaryCount != replicaCount {
t.Fatalf("replica did not catch up after failure: primary=%d replica=%d", primaryCount, replicaCount)
}
}
func TestVFS_PollIntervalEdgeCases(t *testing.T) {
tests := []struct {
name string
interval time.Duration
minCalls int64
maxCalls int64
}{
{"fast", 5 * time.Millisecond, 10, 500},
{"slow", 200 * time.Millisecond, 1, 10},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
obs := &observingReplicaClient{ReplicaClient: client}
_, primary := openReplicatedPrimary(t, obs, tt.interval, tt.interval)
defer testingutil.MustCloseSQLDB(t, primary)
if _, err := primary.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY, value INTEGER)"); err != nil {
t.Fatalf("create table: %v", err)
}
vfs := newVFS(t, obs)
vfs.PollInterval = tt.interval
vfsName := registerTestVFS(t, vfs)
replica := openVFSReplicaDB(t, vfsName)
defer replica.Close()
start := obs.ltxCalls.Load()
time.Sleep(750 * time.Millisecond)
delta := obs.ltxCalls.Load() - start
if delta < tt.minCalls {
t.Fatalf("expected at least %d polls, got %d", tt.minCalls, delta)
}
if tt.maxCalls > 0 && delta > tt.maxCalls {
t.Fatalf("expected at most %d polls, got %d", tt.maxCalls, delta)
}
})
}
}
func TestVFS_PooledWriteNoFalseConflict(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
db, primary := openReplicatedPrimary(t, client, 25*time.Millisecond, 25*time.Millisecond)
defer testingutil.MustCloseSQLDB(t, primary)
if _, err := primary.Exec("CREATE TABLE seed (id INTEGER PRIMARY KEY)"); err != nil {
t.Fatalf("create seed table: %v", err)
}
if _, err := primary.Exec("INSERT INTO seed (id) VALUES (1)"); err != nil {
t.Fatalf("insert seed: %v", err)
}
require.Eventually(t, func() bool {
itr, err := client.LTXFiles(context.Background(), 0, 0, false)
if err != nil {
return false
}
defer itr.Close()
return itr.Next()
}, 5*time.Second, db.MonitorInterval, "LTX files should be created")
forceReplicaSync(t, db)
vfs := newVFS(t, client)
vfs.WriteEnabled = true
vfs.WriteSyncInterval = 0
vfs.PollInterval = 25 * time.Millisecond
vfsName := registerTestVFS(t, vfs)
dsn := fmt.Sprintf("file:%s?vfs=%s", filepath.ToSlash(filepath.Join(t.TempDir(), "pooled-write.db")), vfsName)
sqldb, err := sql.Open("sqlite3", dsn)
if err != nil {
t.Fatalf("open write db: %v", err)
}
defer sqldb.Close()
sqldb.SetMaxOpenConns(2)
sqldb.SetMaxIdleConns(2)
if _, err := sqldb.Exec("PRAGMA busy_timeout = 5000"); err != nil {
t.Fatalf("set busy timeout: %v", err)
}
if _, err := sqldb.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY, value TEXT)"); err != nil {
t.Fatalf("create table: %v", err)
}
const totalWrites = 20
for i := 1; i <= totalWrites; i++ {
if _, err := sqldb.Exec("INSERT INTO t (id, value) VALUES (?, ?)", i, fmt.Sprintf("row-%d", i)); err != nil {
if strings.Contains(err.Error(), "conflict") || errors.Is(err, replicate.ErrConflict) {
t.Fatalf("false ErrConflict on write %d: %v", i, err)
}
t.Fatalf("write %d failed: %v", i, err)
}
}
var count int
if err := sqldb.QueryRow("SELECT COUNT(*) FROM t").Scan(&count); err != nil {
t.Fatalf("count rows: %v", err)
}
if count != totalWrites {
t.Fatalf("expected %d rows, got %d", totalWrites, count)
}
}
func TestVFS_PooledWriteStress(t *testing.T) {
client := file.NewReplicaClient(t.TempDir())
db, primary := openReplicatedPrimary(t, client, 25*time.Millisecond, 25*time.Millisecond)
defer testingutil.MustCloseSQLDB(t, primary)
if _, err := primary.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT)"); err != nil {
t.Fatalf("create table: %v", err)
}
if _, err := primary.Exec("INSERT INTO t (value) VALUES ('seed')"); err != nil {
t.Fatalf("insert seed: %v", err)
}
require.Eventually(t, func() bool {
itr, err := client.LTXFiles(context.Background(), 0, 0, false)
if err != nil {
return false
}
defer itr.Close()
return itr.Next()
}, 5*time.Second, db.MonitorInterval, "LTX files should be created")
forceReplicaSync(t, db)
vfs := newVFS(t, client)
vfs.WriteEnabled = true
vfs.WriteSyncInterval = 10 * time.Millisecond
vfs.PollInterval = 25 * time.Millisecond
vfsName := registerTestVFS(t, vfs)
dsn := fmt.Sprintf("file:%s?vfs=%s&_busy_timeout=5000", filepath.ToSlash(filepath.Join(t.TempDir(), "stress-write.db")), vfsName)
sqldb, err := sql.Open("sqlite3", dsn)
if err != nil {
t.Fatalf("open write db: %v", err)
}
defer sqldb.Close()
sqldb.SetMaxOpenConns(4)
sqldb.SetMaxIdleConns(4)
const totalWrites = 50
for i := 0; i < totalWrites; i++ {
if _, err := sqldb.Exec("INSERT INTO t (value) VALUES (?)", fmt.Sprintf("row-%d", i)); err != nil {
if strings.Contains(err.Error(), "conflict") || errors.Is(err, replicate.ErrConflict) {
t.Fatalf("false ErrConflict on write %d: %v", i, err)
}
t.Fatalf("write %d failed: %v", i, err)
}
// Brief pause every 5 writes to allow sync ticker to fire,
// which causes TXID advancement and exercises the coordination logic
if (i+1)%5 == 0 {
time.Sleep(25 * time.Millisecond)
}
}
var count int
if err := sqldb.QueryRow("SELECT COUNT(*) FROM t").Scan(&count); err != nil {
t.Fatalf("count rows: %v", err)
}
expected := totalWrites + 1 // +1 for seed row
if count != expected {
t.Fatalf("expected %d rows, got %d", expected, count)
}
}
func newVFS(tb testing.TB, client replicate.ReplicaClient) *testVFS {
tb.Helper()
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
base := replicate.NewVFS(client, logger)
base.PollInterval = 100 * time.Millisecond
return &testVFS{
VFS: base,
failures: make(map[string][]error),
}
}
type testVFS struct {
*replicate.VFS
mu sync.Mutex
failures map[string][]error
}
func (v *testVFS) Open(name string, flags sqlite3vfs.OpenFlag) (sqlite3vfs.File, sqlite3vfs.OpenFlag, error) {
f, flags, err := v.VFS.Open(name, flags)
if err != nil {
return nil, flags, err
}
return &injectingFile{File: f, vfs: v, name: name}, flags, nil
}
func (v *testVFS) Inject(path string, err error) {
v.mu.Lock()
v.failures[path] = append(v.failures[path], err)
v.mu.Unlock()
}
func (v *testVFS) popFailure(path string) error {
v.mu.Lock()
defer v.mu.Unlock()
queue := v.failures[path]
if len(queue) == 0 {
return nil
}
err := queue[0]
if len(queue) == 1 {
delete(v.failures, path)
} else {
v.failures[path] = queue[1:]
}
if err == nil {
return errors.New("vfs page read error")
}
return err
}
type injectingFile struct {
sqlite3vfs.File
vfs *testVFS
name string
}
func (f *injectingFile) ReadAt(p []byte, off int64) (int, error) {
if err := f.vfs.popFailure(f.name); err != nil {
return 0, err
}
return f.File.ReadAt(p, off)
}
func (f *injectingFile) FileControl(op int, pragmaName string, pragmaValue *string) (*string, error) {
if fc, ok := f.File.(sqlite3vfs.FileController); ok {
return fc.FileControl(op, pragmaName, pragmaValue)
}
return nil, sqlite3vfs.NotFoundError
}
func registerTestVFS(tb testing.TB, vfs sqlite3vfs.VFS) string {
tb.Helper()
name := fmt.Sprintf("replicate-%s-%d", strings.ToLower(tb.Name()), time.Now().UnixNano())
if err := sqlite3vfs.RegisterVFS(name, vfs); err != nil {
tb.Fatalf("failed to register replicate vfs %s: %v", name, err)
}
return name
}
func openReplicatedPrimary(tb testing.TB, client replicate.ReplicaClient, monitorInterval, syncInterval time.Duration) (*replicate.DB, *sql.DB) {
tb.Helper()
db := testingutil.NewDB(tb, filepath.Join(tb.TempDir(), "primary.db"))
db.MonitorInterval = monitorInterval
db.Replica = replicate.NewReplica(db)
db.Replica.Client = client
db.Replica.SyncInterval = syncInterval
if err := db.Open(); err != nil {
tb.Fatalf("open db: %v", err)
}
sqldb := testingutil.MustOpenSQLDB(tb, db.Path())
tb.Cleanup(func() { _ = db.Close(context.Background()) })
return db, sqldb
}
func forceReplicaSync(tb testing.TB, db *replicate.DB) {
tb.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := db.Sync(ctx); err != nil {
tb.Fatalf("force sync: %v", err)
}
if db.Replica != nil {
if err := db.Replica.Sync(ctx); err != nil {
tb.Fatalf("replica sync: %v", err)
}
}
}
func openVFSReplicaDB(tb testing.TB, vfsName string) *sql.DB {
tb.Helper()
dsn := fmt.Sprintf("file:%s?vfs=%s", filepath.ToSlash(filepath.Join(tb.TempDir(), vfsName+".db")), vfsName)
sqldb, err := sql.Open("sqlite3", dsn)
if err != nil {
tb.Fatalf("open replica db: %v", err)
}
sqldb.SetMaxOpenConns(32)
sqldb.SetMaxIdleConns(32)
sqldb.SetConnMaxIdleTime(30 * time.Second)
if _, err := sqldb.Exec("PRAGMA busy_timeout = 2000"); err != nil {
tb.Fatalf("set busy timeout: %v", err)
}
return sqldb
}
func waitForReplicaRowCount(tb testing.TB, primary, replica *sql.DB, timeout time.Duration) {
tb.Helper()
require.Eventually(tb, func() bool {
var primaryCount int
if err := primary.QueryRow("SELECT COUNT(*) FROM t").Scan(&primaryCount); err != nil {
return false
}
var replicaCount int
if err := replica.QueryRow("SELECT COUNT(*) FROM t").Scan(&replicaCount); err != nil {
return false
}
return primaryCount == replicaCount
}, timeout, 50*time.Millisecond, "replica row count should match primary")
}
func waitForTableRowCount(tb testing.TB, primary, replica *sql.DB, table string, timeout time.Duration) {
tb.Helper()
query := fmt.Sprintf("SELECT COUNT(*) FROM %s", table)
require.Eventually(tb, func() bool {
var primaryCount int
if err := primary.QueryRow(query).Scan(&primaryCount); err != nil {
return false
}
var replicaCount int
if err := replica.QueryRow(query).Scan(&replicaCount); err != nil {
return false
}
return primaryCount == replicaCount
}, timeout, 50*time.Millisecond, "replica row count for %s should match primary", table)
}
func fetchOrderedPayloads(tb testing.TB, db *sql.DB, limit int, orderBy string) []string {
tb.Helper()
query := fmt.Sprintf("SELECT payload FROM t ORDER BY %s LIMIT %d", orderBy, limit)
rows, err := db.Query(query)
if err != nil {
tb.Fatalf("query payloads: %v", err)
}
defer rows.Close()
var out []string
for rows.Next() {
var payload string
if err := rows.Scan(&payload); err != nil {
tb.Fatalf("scan payload: %v", err)
}
out = append(out, payload)
}
if err := rows.Err(); err != nil {
tb.Fatalf("rows err: %v", err)
}
return out
}
func seedLargeTable(tb testing.TB, db *sql.DB, n int) {
tb.Helper()
trx, err := db.Begin()
if err != nil {
tb.Fatalf("begin seed: %v", err)
}
stmt, err := trx.Prepare("INSERT INTO t (value, updated_at) VALUES (?, strftime('%s','now'))")
if err != nil {
_ = trx.Rollback()
tb.Fatalf("prepare seed: %v", err)
}
defer stmt.Close()
rnd := rand.New(rand.NewSource(42))
for i := 0; i < n; i++ {
if _, err := stmt.Exec(fmt.Sprintf("seed-%d-%d", i, rnd.Int())); err != nil {
_ = trx.Rollback()
tb.Fatalf("seed exec: %v", err)
}
}
if err := trx.Commit(); err != nil {
tb.Fatalf("commit seed: %v", err)
}
}
func seedSortedDataset(tb testing.TB, db *sql.DB, n int) {
tb.Helper()
trx, err := db.Begin()
if err != nil {
tb.Fatalf("begin sorted seed: %v", err)
}
stmt, err := trx.Prepare("INSERT INTO t (id, payload, grp) VALUES (?, ?, ?)")
if err != nil {
_ = trx.Rollback()
tb.Fatalf("prepare sorted seed: %v", err)
}
defer stmt.Close()
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := 0; i < n; i++ {
if _, err := stmt.Exec(i+1, randomPayload(rnd, 256), rnd.Intn(1024)); err != nil {
_ = trx.Rollback()
tb.Fatalf("sorted seed exec: %v", err)
}
}
if err := trx.Commit(); err != nil {
tb.Fatalf("commit sorted seed: %v", err)
}
}
func randomPayload(r *rand.Rand, n int) string {
const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, n)
for i := range b {
b[i] = letters[r.Intn(len(letters))]
}
return string(b)
}
func pageSizedPayload(pageSize int, row int) string {
base := fmt.Sprintf("row_%05d_", row)
maxPayload := pageSize / 4
if maxPayload < len(base)+1 {
maxPayload = len(base) + 1
}
if maxPayload > 4096 {
maxPayload = 4096
}
fillerLen := maxPayload - len(base)
if fillerLen < 0 {
fillerLen = 0
}
return base + strings.Repeat("x", fillerLen)
}
func isBusyError(err error) bool {
if err == nil {
return false
}
if e, ok := err.(sqlite3.Error); ok {
if e.Code == sqlite3.ErrBusy || e.Code == sqlite3.ErrLocked {
return true
}
// Under heavy churn, go-sqlite3 can surface ErrError with the
// generic "SQL logic error" message while the VFS swaps databases.
if e.Code == sqlite3.ErrError && strings.Contains(e.Error(), "SQL logic error") {
return true
}
}
msg := err.Error()
if strings.Contains(msg, "database is locked") || strings.Contains(msg, "database is busy") {
return true
}
return strings.Contains(msg, "converting NULL to int")
}
func writeSinglePageLTXFile(tb testing.TB, client *file.ReplicaClient, txid ltx.TXID, fill byte) {
tb.Helper()
page := bytes.Repeat([]byte{fill}, 4096)
var buf bytes.Buffer
enc, err := ltx.NewEncoder(&buf)
if err != nil {
tb.Fatalf("new encoder: %v", err)
}
hdr := ltx.Header{
Version: ltx.Version,
PageSize: 4096,
Commit: 1,
MinTXID: txid,
MaxTXID: txid,
Timestamp: time.Now().UnixMilli(),
Flags: ltx.HeaderFlagNoChecksum,
}
if err := enc.EncodeHeader(hdr); err != nil {
tb.Fatalf("encode header: %v", err)
}
if err := enc.EncodePage(ltx.PageHeader{Pgno: 1}, page); err != nil {
tb.Fatalf("encode page: %v", err)
}
if err := enc.Close(); err != nil {
tb.Fatalf("close encoder: %v", err)
}
if _, err := client.WriteLTXFile(context.Background(), 0, txid, txid, bytes.NewReader(buf.Bytes())); err != nil {
tb.Fatalf("write ltx file: %v", err)
}
}
type latencyReplicaClient struct {
replicate.ReplicaClient
delay time.Duration
}
func (c *latencyReplicaClient) OpenLTXFile(ctx context.Context, level int, minTXID, maxTXID ltx.TXID, offset, size int64) (io.ReadCloser, error) {
time.Sleep(c.delay)
return c.ReplicaClient.OpenLTXFile(ctx, level, minTXID, maxTXID, offset, size)
}
func (c *latencyReplicaClient) LTXFiles(ctx context.Context, level int, seek ltx.TXID, useMetadata bool) (ltx.FileIterator, error) {
time.Sleep(c.delay)
return c.ReplicaClient.LTXFiles(ctx, level, seek, useMetadata)
}
type eventualConsistencyClient struct {
replicate.ReplicaClient
calls atomic.Int32
}
func (c *eventualConsistencyClient) LTXFiles(ctx context.Context, level int, seek ltx.TXID, useMetadata bool) (ltx.FileIterator, error) {
if c.calls.Add(1) == 1 {
return ltx.NewFileInfoSliceIterator(nil), nil
}
return c.ReplicaClient.LTXFiles(ctx, level, seek, useMetadata)
}
type observingReplicaClient struct {
replicate.ReplicaClient
ltxCalls atomic.Int64
}
type fdLimitedReplicaClient struct {
replicate.ReplicaClient
limit int32
open atomic.Int32
maxOpen atomic.Int32
}
func (c *fdLimitedReplicaClient) OpenLTXFile(ctx context.Context, level int, minTXID, maxTXID ltx.TXID, offset, size int64) (io.ReadCloser, error) {
current := c.open.Add(1)
for {
max := c.maxOpen.Load()
if current <= max || c.maxOpen.CompareAndSwap(max, current) {
break
}
}
if current > c.limit {
c.open.Add(-1)
return nil, fmt.Errorf("fd limit exceeded: %d/%d", current, c.limit)
}
rc, err := c.ReplicaClient.OpenLTXFile(ctx, level, minTXID, maxTXID, offset, size)
if err != nil {
c.open.Add(-1)
return nil, err
}
return &hookedReadCloser{ReadCloser: rc, hook: func() { c.open.Add(-1) }}, nil
}
func (c *observingReplicaClient) LTXFiles(ctx context.Context, level int, seek ltx.TXID, useMetadata bool) (ltx.FileIterator, error) {
c.ltxCalls.Add(1)
return c.ReplicaClient.LTXFiles(ctx, level, seek, useMetadata)
}
type flakyLTXClient struct {
replicate.ReplicaClient
failNext atomic.Bool
failures atomic.Int64
}
func (c *flakyLTXClient) LTXFiles(ctx context.Context, level int, seek ltx.TXID, useMetadata bool) (ltx.FileIterator, error) {
if c.failNext.CompareAndSwap(true, false) {
c.failures.Add(1)
return nil, fmt.Errorf("ltx list unavailable")
}
return c.ReplicaClient.LTXFiles(ctx, level, seek, useMetadata)
}
type oomPageIndexClient struct {
replicate.ReplicaClient
failNext atomic.Bool
triggered atomic.Bool
}
func (c *oomPageIndexClient) OpenLTXFile(ctx context.Context, level int, minTXID, maxTXID ltx.TXID, offset, size int64) (io.ReadCloser, error) {
if offset > 0 && c.failNext.CompareAndSwap(true, false) {
c.triggered.Store(true)
return nil, fmt.Errorf("simulated page index OOM")
}
return c.ReplicaClient.OpenLTXFile(ctx, level, minTXID, maxTXID, offset, size)
}
type corruptingPageIndexClient struct {
replicate.ReplicaClient
corruptNext atomic.Bool
triggered atomic.Bool
}
func (c *corruptingPageIndexClient) OpenLTXFile(ctx context.Context, level int, minTXID, maxTXID ltx.TXID, offset, size int64) (io.ReadCloser, error) {
rc, err := c.ReplicaClient.OpenLTXFile(ctx, level, minTXID, maxTXID, offset, size)
if err != nil {
return nil, err
}
if c.corruptNext.CompareAndSwap(true, false) {
c.triggered.Store(true)
data, readErr := io.ReadAll(rc)
rc.Close()
if readErr != nil {
return nil, readErr
}
if len(data) > 0 {
data[0] ^= 0xFF
}
return io.NopCloser(bytes.NewReader(data)), nil
}
return rc, nil
}
type hookedReadCloser struct {
io.ReadCloser
once sync.Once
hook func()
}
func (h *hookedReadCloser) Close() error {
var err error
h.once.Do(func() {
err = h.ReadCloser.Close()
if h.hook != nil {
h.hook()
}
})
return err
}
// waitForLTXFiles waits until at least one LTX file is available in the replica client.
func waitForLTXFiles(t *testing.T, client replicate.ReplicaClient, timeout, tick time.Duration) {
t.Helper()
require.Eventually(t, func() bool {
itr, err := client.LTXFiles(context.Background(), 0, 0, false)
if err != nil {
return false
}
defer itr.Close()
return itr.Next()
}, timeout, tick, "LTX files should be available")
}
// waitForReplicaValue waits until the replica database returns the expected int value.
func waitForReplicaValue(t *testing.T, db *sql.DB, query string, expected int, timeout, tick time.Duration) {
t.Helper()
require.Eventually(t, func() bool {
var got int
if err := db.QueryRow(query).Scan(&got); err != nil {
return false
}
return got == expected
}, timeout, tick, "replica should return expected value")
}