Repository: benbjohnson/litestream

Issue:
Replica.monitor logs context.Canceled
`Replica.monitor` is `context.Context` aware, and passes that into `Replica.Sync` calls within. unfortunately, if the context is cancelled during the call to `Replica.Sync`, because all `err != nil` `errors` are logged, we get a spurious `log.Printf`. this is also non-fatal, since we only log these `error`s from `Replica.Sync`.

spit balling solutions, we could ignore `context.Canceled` (and possibly `context.DeadlineExceeded`):
```diff
diff --git a/replica.go b/replica.go
index 0cfc21d..aa48091 100644
--- a/replica.go
+++ b/replica.go
@@ -3,6 +3,7 @@ package litestream
 import (
        "context"
        "encoding/binary"
+       "errors"
        "fmt"
        "hash/crc64"
        "io"
@@ -627,7 +628,7 @@ func (r *Replica) monitor(ctx context.Context) {
                notify = r.db.Notify()

                // Synchronize the shadow wal into the replication directory.
-               if err := r.Sync(ctx); err != nil {
+               if err := r.Sync(ctx); err != nil && !errors.Is(err, context.Canceled) {
                        log.Printf("%s(%s): monitor error: %s", r.db.Path(), r.Name(), err)
                        continue
                }
```
but it may also be better to ignore any errors if `ctx.Done()` is closed, or punt the issue upstream, since I do not know the canonical solution to this problem.

Discussion:
@hifi: This would indeed be nice. We get a lot of these logs during clean shutdown. There's more places where Litestream logs context canceled. 

Solution patch:
diff --git a/db.go b/db.go
index 84c82dc..8fce816 100644
--- a/db.go
+++ b/db.go
@@ -20,6 +20,7 @@ import (
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/promauto"
 	"github.com/superfly/ltx"
+	"modernc.org/sqlite"
 
 	"github.com/benbjohnson/litestream/internal"
 )
@@ -324,6 +325,30 @@ func (db *DB) Close(ctx context.Context) (err error) {
 	return err
 }
 
+// setPersistWAL sets the PERSIST_WAL file control on the database connection.
+// This prevents SQLite from removing the WAL file when connections close.
+func (db *DB) setPersistWAL(ctx context.Context) error {
+	conn, err := db.db.Conn(ctx)
+	if err != nil {
+		return fmt.Errorf("get connection: %w", err)
+	}
+	defer conn.Close()
+
+	return conn.Raw(func(driverConn interface{}) error {
+		fc, ok := driverConn.(sqlite.FileControl)
+		if !ok {
+			return fmt.Errorf("driver does not implement FileControl")
+		}
+
+		_, err := fc.FileControlPersistWAL("main", 1)
+		if err != nil {
+			return fmt.Errorf("FileControlPersistWAL: %w", err)
+		}
+
+		return nil
+	})
+}
+
 // init initializes the connection to the database.
 // Skipped if already initialized or if the database file does not exist.
 func (db *DB) init(ctx context.Context) (err error) {
@@ -350,12 +375,15 @@ func (db *DB) init(ctx context.Context) (err error) {
 	dsn := db.path
 	dsn += fmt.Sprintf("?_busy_timeout=%d", db.BusyTimeout.Milliseconds())
 
-	// 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 {
+	if db.db, err = sql.Open("sqlite", dsn); err != nil {
 		return err
 	}
 
+	// Set PERSIST_WAL to prevent WAL file removal when database connections close.
+	if err := db.setPersistWAL(ctx); err != nil {
+		return fmt.Errorf("set PERSIST_WAL: %w", err)
+	}
+
 	// Open long-running database file descriptor. Required for non-OFD locks.
 	if db.f, err = os.Open(db.path); err != nil {
 		return fmt.Errorf("open db file descriptor: %w", err)
@@ -1284,7 +1312,10 @@ func (db *DB) Compact(ctx context.Context, dstLevel int) (*ltx.FileInfo, error)
 	// If this is L1, clean up L0 files that are below the minTXID.
 	if dstLevel == 1 {
 		if err := db.EnforceRetentionByTXID(ctx, 0, maxTXID); err != nil {
-			db.Logger.Error("enforce L0 retention", "error", err)
+			// Don't log context cancellation errors during shutdown
+			if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
+				db.Logger.Error("enforce L0 retention", "error", err)
+			}
 		}
 	}
 
diff --git a/go.mod b/go.mod
index 6886633..3c203aa 100644
--- a/go.mod
+++ b/go.mod
@@ -28,9 +28,19 @@ require (
 	golang.org/x/sys v0.35.0
 	google.golang.org/api v0.154.0
 	gopkg.in/yaml.v2 v2.4.0
+	modernc.org/sqlite v1.38.2
 )
 
-require golang.org/x/sync v0.16.0 // indirect
+require (
+	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/ncruces/go-strftime v0.1.9 // indirect
+	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+	golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
+	golang.org/x/sync v0.16.0 // indirect
+	modernc.org/libc v1.66.3 // indirect
+	modernc.org/mathutil v1.7.1 // indirect
+	modernc.org/memory v1.11.0 // indirect
+)
 
 require (
 	cloud.google.com/go v0.111.0 // indirect
@@ -70,7 +80,7 @@ require (
 	github.com/kr/fs v0.1.0 // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
-	github.com/mattn/go-isatty v0.0.17 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
 	github.com/nats-io/nkeys v0.4.11 // indirect
 	github.com/nats-io/nuid v1.0.1 // indirect
diff --git a/go.sum b/go.sum
index 2eb7547..4f7a1f5 100644
--- a/go.sum
+++ b/go.sum
@@ -82,6 +82,8 @@ github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWH
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -131,6 +133,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
 github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
+github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
+github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
 github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
 github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -162,8 +166,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
-github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
 github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
 github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
@@ -177,6 +181,8 @@ github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
 github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
 github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
 github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
+github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
+github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
 github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
 github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
@@ -196,6 +202,8 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k
 github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
 github.com/psanford/sqlite3vfs v0.0.0-20240315230605-24e1d98cf361 h1:vAKifIJuYY306ZJSrwDgKonWcJGELijdaenABqbV03E=
 github.com/psanford/sqlite3vfs v0.0.0-20240315230605-24e1d98cf361/go.mod h1:iW4cSew5PAb1sMZiTEkVJAIBNrepaB6jTYjeP47WtI0=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
 github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
 github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
@@ -234,10 +242,14 @@ golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw
 golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
 golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
+golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
+golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -272,6 +284,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
 golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -295,6 +308,8 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
+golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
@@ -344,3 +359,29 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
+modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
+modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
+modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
+modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
+modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
+modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
+modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
+modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
+modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
+modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
+modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
+modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
+modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
+modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
+modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
+modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
+modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
+modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
+modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
+modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
+modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
+modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
+modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
diff --git a/replica.go b/replica.go
index 4b308fd..e7b6a5f 100644
--- a/replica.go
+++ b/replica.go
@@ -2,6 +2,7 @@ package litestream
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"log/slog"
@@ -325,7 +326,10 @@ func (r *Replica) monitor(ctx context.Context) {
 
 		// Synchronize the shadow wal into the replication directory.
 		if err := r.Sync(ctx); err != nil {
-			r.Logger().Error("monitor error", "error", err)
+			// Don't log context cancellation errors during shutdown
+			if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
+				r.Logger().Error("monitor error", "error", err)
+			}
 			continue
 		}
 	}
diff --git a/store.go b/store.go
index 5fb58d1..ab330c7 100644
--- a/store.go
+++ b/store.go
@@ -162,14 +162,20 @@ LOOP:
 					slog.Debug("recently compacted, skipping", "level", lvl.Level, "path", db.Path())
 					continue
 				} else if err != nil {
-					slog.Error("compaction failed", "level", lvl.Level, "error", err)
+					// Don't log context cancellation errors during shutdown
+					if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
+						slog.Error("compaction failed", "level", lvl.Level, "error", err)
+					}
 					time.Sleep(1 * time.Second) // wait so we don't rack up S3 charges
 				}
 
 				// Each time we snapshot, clean up everything before the oldest snapshot.
 				if lvl.Level == SnapshotLevel {
 					if err := s.EnforceSnapshotRetention(ctx, db); err != nil {
-						slog.Error("retention enforcement failed", "error", err)
+						// Don't log context cancellation errors during shutdown
+						if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
+							slog.Error("retention enforcement failed", "error", err)
+						}
 						time.Sleep(1 * time.Second) // wait so we don't rack up S3 charges
 					}
 				}
diff --git a/vfs.go b/vfs.go
index 571d79b..6e3936d 100644
--- a/vfs.go
+++ b/vfs.go
@@ -1,7 +1,11 @@
+//go:build vfs
+// +build vfs
+
 package litestream
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"log/slog"
 	"strings"
@@ -258,7 +262,10 @@ func (f *VFSFile) monitorReplicaClient(ctx context.Context) {
 			return
 		case <-ticker.C:
 			if err := f.pollReplicaClient(ctx); err != nil {
-				f.logger.Error("cannot fetch new ltx files", "error", err)
+				// Don't log context cancellation errors during shutdown
+				if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
+					f.logger.Error("cannot fetch new ltx files", "error", err)
+				}
 			}
 		}
 	}



Test patch:
diff --git a/cmd/litestream-vfs/main.go b/cmd/litestream-vfs/main.go
deleted file mode 100644
index 06ab7d0..0000000
--- a/cmd/litestream-vfs/main.go
+++ /dev/null
@@ -1 +0,0 @@
-package main
diff --git a/cmd/litestream-vfs/main_test.go b/cmd/litestream-vfs/main_test.go
index 547e816..ca96fee 100644
--- a/cmd/litestream-vfs/main_test.go
+++ b/cmd/litestream-vfs/main_test.go
@@ -1,3 +1,6 @@
+//go:build vfs
+// +build vfs
+
 package main_test
 
 import (
diff --git a/cmd/litestream/main.go b/cmd/litestream/main.go
index 39b1875..0121edf 100644
--- a/cmd/litestream/main.go
+++ b/cmd/litestream/main.go
@@ -17,9 +17,9 @@ import (
 	"time"
 
 	"filippo.io/age"
-	_ "github.com/mattn/go-sqlite3"
 	"github.com/superfly/ltx"
 	"gopkg.in/yaml.v2"
+	_ "modernc.org/sqlite"
 
 	"github.com/benbjohnson/litestream"
 	"github.com/benbjohnson/litestream/abs"
diff --git a/db_test.go b/db_test.go
index d4302dd..89348ae 100644
--- a/db_test.go
+++ b/db_test.go
@@ -200,7 +200,7 @@ func TestDB_Sync(t *testing.T) {
 		}
 
 		// Remove WAL file.
-		if err := os.Remove(db.WALPath()); err != nil {
+		if err := os.Remove(db.WALPath()); err != nil && !os.IsNotExist(err) {
 			t.Fatal(err)
 		}
 
diff --git a/internal/testingutil/testingutil.go b/internal/testingutil/testingutil.go
index c459f2f..897fd20 100644
--- a/internal/testingutil/testingutil.go
+++ b/internal/testingutil/testingutil.go
@@ -150,7 +150,7 @@ func MustCloseDB(tb testing.TB, db *litestream.DB) {
 // MustOpenSQLDB returns a database/sql DB.
 func MustOpenSQLDB(tb testing.TB, path string) *sql.DB {
 	tb.Helper()
-	d, err := sql.Open("sqlite3", path)
+	d, err := sql.Open("sqlite", path)
 	if err != nil {
 		tb.Fatal(err)
 	} else if _, err := d.ExecContext(context.Background(), `PRAGMA journal_mode = wal;`); err != nil {
diff --git a/litestream.go b/litestream.go
index 27c9a11..b27de3c 100644
--- a/litestream.go
+++ b/litestream.go
@@ -4,7 +4,6 @@ import (
 	"database/sql"
 	"encoding/binary"
 	"errors"
-	"fmt"
 	"io"
 	"os"
 	"path"
@@ -12,8 +11,8 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/mattn/go-sqlite3"
 	"github.com/superfly/ltx"
+	_ "modernc.org/sqlite"
 )
 
 // Naming constants.
@@ -49,17 +48,6 @@ var (
 	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
-		},
-	})
-}
-
 // Checksum computes a running SQLite checksum over a byte slice.
 func Checksum(bo binary.ByteOrder, s0, s1 uint32, b []byte) (uint32, uint32) {
 	assert(len(b)%8 == 0, "misaligned checksum byte slice")
diff --git a/litestream_test.go b/litestream_test.go
index 45598bc..fe6d089 100644
--- a/litestream_test.go
+++ b/litestream_test.go
@@ -5,8 +5,8 @@ import (
 	"encoding/hex"
 	"testing"
 
-	_ "github.com/mattn/go-sqlite3"
 	"github.com/superfly/ltx"
+	_ "modernc.org/sqlite"
 
 	"github.com/benbjohnson/litestream"
 )
diff --git a/replica_test.go b/replica_test.go
index 400c35f..8bb656a 100644
--- a/replica_test.go
+++ b/replica_test.go
@@ -1,8 +1,12 @@
 package litestream_test
 
 import (
+	"bytes"
 	"context"
 	"errors"
+	"io"
+	"log/slog"
+	"strings"
 	"testing"
 	"time"
 
@@ -226,3 +230,86 @@ func TestReplica_CalcRestorePlan(t *testing.T) {
 		}
 	})
 }
+
+func TestReplica_ContextCancellationNoLogs(t *testing.T) {
+	// This test verifies that context cancellation errors are not logged during shutdown.
+	// The fix for issue #235 ensures that context.Canceled and context.DeadlineExceeded
+	// errors are filtered out in monitor functions to avoid spurious log messages.
+
+	db, sqldb := testingutil.MustOpenDBs(t)
+	defer testingutil.MustCloseDBs(t, db, sqldb)
+
+	// Create a buffer to capture log output
+	var logBuffer bytes.Buffer
+
+	// Create a custom logger that writes to our buffer
+	db.Logger = slog.New(slog.NewTextHandler(&logBuffer, &slog.HandlerOptions{
+		Level: slog.LevelDebug,
+	}))
+
+	// First, let's trigger a normal sync to ensure the DB is initialized
+	if err := db.Sync(context.Background()); err != nil {
+		t.Fatal(err)
+	}
+
+	// Create a replica with a mock client that simulates context cancellation during Sync
+	syncCount := 0
+	mockClient := &mock.ReplicaClient{
+		LTXFilesFunc: func(ctx context.Context, level int, seek ltx.TXID) (ltx.FileIterator, error) {
+			syncCount++
+			// First few calls succeed, then return context.Canceled
+			if syncCount <= 2 {
+				// Return an empty iterator
+				return ltx.NewFileInfoSliceIterator(nil), nil
+			}
+			// After initial syncs, return context.Canceled to simulate shutdown
+			return nil, context.Canceled
+		},
+		WriteLTXFileFunc: func(ctx context.Context, level int, minTXID, maxTXID ltx.TXID, r io.Reader) (*ltx.FileInfo, error) {
+			// Always succeed for writes to allow normal operation
+			return &ltx.FileInfo{
+				Level:     level,
+				MinTXID:   minTXID,
+				MaxTXID:   maxTXID,
+				CreatedAt: time.Now(),
+			}, nil
+		},
+	}
+
+	r := litestream.NewReplicaWithClient(db, mockClient)
+	r.SyncInterval = 50 * time.Millisecond // Short interval for testing
+
+	// Start the replica monitoring in a goroutine
+	ctx, cancel := context.WithCancel(context.Background())
+
+	if err := r.Start(ctx); err != nil {
+		t.Fatalf("failed to start replica: %v", err)
+	}
+
+	// Give the monitor time to run several sync cycles
+	// This ensures we get both successful syncs and context cancellation errors
+	time.Sleep(200 * time.Millisecond)
+
+	// Cancel the context to trigger shutdown
+	cancel()
+
+	// Stop the replica and wait for it to finish
+	if err := r.Stop(true); err != nil {
+		t.Fatalf("failed to stop replica: %v", err)
+	}
+
+	// Check the logs
+	logs := logBuffer.String()
+
+	// We should have some debug logs from successful operations
+	if !strings.Contains(logs, "replica sync") {
+		t.Errorf("expected 'replica sync' in logs but didn't find it; logs:\n%s", logs)
+	}
+
+	// But we should NOT have "monitor error" with "context canceled"
+	if strings.Contains(logs, "monitor error") && strings.Contains(logs, "context canceled") {
+		t.Errorf("found 'monitor error' with 'context canceled' in logs when it should be filtered:\n%s", logs)
+	}
+
+	// The test passes if context.Canceled errors were properly filtered
+}