diff --git a/cmd/litestream/replicate.go b/cmd/litestream/replicate.go index 7c0403b..1973183 100644 --- a/cmd/litestream/replicate.go +++ b/cmd/litestream/replicate.go @@ -178,7 +178,7 @@ func (c *ReplicateCommand) Run(ctx context.Context) (err error) { // Close closes all open databases. func (c *ReplicateCommand) Close() (err error) { for _, db := range c.DBs { - if e := db.SoftClose(); e != nil { + if e := db.Close(); e != nil { log.Printf("error closing db: path=%s err=%s", db.Path(), e) if err == nil { err = e diff --git a/db.go b/db.go index dd33d7e..b7a31eb 100644 --- a/db.go +++ b/db.go @@ -291,20 +291,9 @@ func (db *DB) Open() (err error) { return nil } -// Close releases the read lock & closes the database. This method should only -// be called by tests as it causes the underlying database to be checkpointed. +// Close flushes outstanding WAL writes to replicas, releases the read lock, +// and closes the database. func (db *DB) Close() (err error) { - return db.close(false) -} - -// SoftClose closes everything but the underlying db connection. This method -// is available because the binary needs to avoid closing the database on exit -// to prevent autocheckpointing. -func (db *DB) SoftClose() (err error) { - return db.close(true) -} - -func (db *DB) close(soft bool) (err error) { db.cancel() db.wg.Wait() @@ -325,7 +314,7 @@ func (db *DB) close(soft bool) (err error) { err = e } } - r.Stop(!soft) + r.Stop(true) } // Release the read lock to allow other applications to handle checkpointing. @@ -335,9 +324,7 @@ func (db *DB) close(soft bool) (err error) { } } - // Only perform full close if this is not a soft close. - // This closes the underlying database connection which can clean up the WAL. - if !soft && db.db != nil { + if db.db != nil { if e := db.db.Close(); e != nil && err == nil { err = e } @@ -392,8 +379,9 @@ func (db *DB) init() (err error) { dsn := db.path dsn += fmt.Sprintf("?_busy_timeout=%d", BusyTimeout.Milliseconds()) - // Connect to SQLite database. - if db.db, err = sql.Open("sqlite3", dsn); err != nil { + // Connect to SQLite database. Use the driver registered with a hook to + // prevent WAL files from being removed. + if db.db, err = sql.Open("litestream-sqlite3", dsn); err != nil { return err } @@ -1419,7 +1407,7 @@ func applyWAL(ctx context.Context, index int, dbPath string) error { } // Open SQLite database and force a truncating checkpoint. - d, err := sql.Open("sqlite3", dbPath) + d, err := sql.Open("litestream-sqlite3", dbPath) if err != nil { return err } diff --git a/db_test.go b/db_test.go index b7eb54b..ef82987 100644 --- a/db_test.go +++ b/db_test.go @@ -270,8 +270,8 @@ func TestDB_Sync(t *testing.T) { t.Fatal(err) } - // Verify WAL does not exist. - if _, err := os.Stat(db.WALPath()); !os.IsNotExist(err) { + // Remove WAL file. + if err := os.Remove(db.WALPath()); err != nil { t.Fatal(err) } diff --git a/litestream.go b/litestream.go index c895ac3..3fa3474 100644 --- a/litestream.go +++ b/litestream.go @@ -13,6 +13,8 @@ import ( "strconv" "strings" "time" + + "github.com/mattn/go-sqlite3" ) // Naming constants. @@ -42,6 +44,25 @@ var ( ErrChecksumMismatch = errors.New("invalid replica, checksum mismatch") ) +var ( + // LogWriter is the destination writer for all logging. + LogWriter = os.Stdout + + // LogFlags are the flags passed to log.New(). + LogFlags = 0 +) + +func init() { + sql.Register("litestream-sqlite3", &sqlite3.SQLiteDriver{ + ConnectHook: func(conn *sqlite3.SQLiteConn) error { + if err := conn.SetFileControlInt("main", sqlite3.SQLITE_FCNTL_PERSIST_WAL, 1); err != nil { + return fmt.Errorf("cannot set file control: %w", err) + } + return nil + }, + }) +} + // SnapshotIterator represents an iterator over a collection of snapshot metadata. type SnapshotIterator interface { io.Closer