Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package rqlite for openSUSE:Factory checked in at 2026-03-11 20:52:54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/rqlite (Old) and /work/SRC/openSUSE:Factory/.rqlite.new.8177 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "rqlite" Wed Mar 11 20:52:54 2026 rev:46 rq:1338088 version:9.4.5 Changes: -------- --- /work/SRC/openSUSE:Factory/rqlite/rqlite.changes 2026-03-10 17:53:45.864666586 +0100 +++ /work/SRC/openSUSE:Factory/.rqlite.new.8177/rqlite.changes 2026-03-11 20:53:45.778096284 +0100 @@ -1,0 +2,7 @@ +Tue Mar 10 19:12:22 UTC 2026 - Andreas Stieger <[email protected]> + +- Update to version 9.4.5: + * Rely soley on SQLite busy for WAL truncate + * Prevent deadlock situation during raft snapshots + +------------------------------------------------------------------- Old: ---- rqlite-9.4.3.tar.xz New: ---- rqlite-9.4.5.tar.xz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ rqlite.spec ++++++ --- /var/tmp/diff_new_pack.ACYjUd/_old 2026-03-11 20:53:46.654132086 +0100 +++ /var/tmp/diff_new_pack.ACYjUd/_new 2026-03-11 20:53:46.658132250 +0100 @@ -17,7 +17,7 @@ Name: rqlite -Version: 9.4.3 +Version: 9.4.5 Release: 0 Summary: Distributed relational database built on SQLite License: MIT ++++++ _service ++++++ --- /var/tmp/diff_new_pack.ACYjUd/_old 2026-03-11 20:53:46.694133721 +0100 +++ /var/tmp/diff_new_pack.ACYjUd/_new 2026-03-11 20:53:46.702134048 +0100 @@ -3,7 +3,7 @@ <param name="url">https://github.com/rqlite/rqlite.git</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">v9.4.3</param> + <param name="revision">v9.4.5</param> <param name="versionformat">@PARENT_TAG@</param> <param name="changesgenerate">enable</param> <param name="versionrewrite-pattern">v(.*)</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.ACYjUd/_old 2026-03-11 20:53:46.738135519 +0100 +++ /var/tmp/diff_new_pack.ACYjUd/_new 2026-03-11 20:53:46.742135683 +0100 @@ -1,7 +1,7 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/rqlite/rqlite.git</param> - <param name="changesrevision">379044842cbed8e9ff2b7f3aa69539f2a7494a9b</param> + <param name="changesrevision">41d7a347ab2db0fa494fc9553b9d9467c343a678</param> </service> </servicedata> (No newline at EOF) ++++++ rqlite-9.4.3.tar.xz -> rqlite-9.4.5.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-9.4.3/CHANGELOG.md new/rqlite-9.4.5/CHANGELOG.md --- old/rqlite-9.4.3/CHANGELOG.md 2026-03-09 15:12:14.000000000 +0100 +++ new/rqlite-9.4.5/CHANGELOG.md 2026-03-10 03:03:14.000000000 +0100 @@ -1,3 +1,15 @@ +## v9.4.4 (March 9th 2026) +### Implementation changes and bug fixes +- [PR #2544](https://github.com/rqlite/rqlite/pull/2544): Ensure the checkpoint-truncate runs to completion, or exit. Fixes issue [#2537](https://github.com/rqlite/rqlite/issues/2537). + +## v9.4.3 (March 9th 2026) +### Implementation changes and bug fixes +- [b94b538](https://github.com/rqlite/rqlite/commit/b94b538451622d1f04bebc19aa580297457f439f): Remove `mustTruncate` as it can result in a deadlock. It was introduced in v9.3.14, but has been shown to be flawed. See [issue #2537](https://github.com/rqlite/rqlite/issues/2537). + +## v9.4.2 (March 9th 2026) +### Implementation changes and bug fixes +- [PR #2533](https://github.com/rqlite/rqlite/pull/2533): Fix code in `db` package which hits an error, but was actually returning `nil`. Fixes issue [#2530](https://github.com/rqlite/rqlite/issues/2530). + ## v9.4.1 (February 11th 2026) ### Implementation changes and bug fixes - [PR #2468](https://github.com/rqlite/rqlite/pull/2468): Add _proxy_ layer between HTTP layer and Store. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-9.4.3/db/db.go new/rqlite-9.4.5/db/db.go --- old/rqlite-9.4.3/db/db.go 2026-03-09 15:12:14.000000000 +0100 +++ new/rqlite-9.4.5/db/db.go 2026-03-10 03:03:14.000000000 +0100 @@ -28,6 +28,7 @@ SQLiteHeaderSize = 32 bkDelay = 250 * time.Millisecond checkpointBusyTimeout = 250 * time.Millisecond + checkpointBusyDelay = 10 * time.Millisecond durToOpenLog = 2 * time.Second OptimizeDefault = 0xFFFE OptimizeAll = 0x10002 @@ -37,7 +38,6 @@ openDuration = "open_duration_ms" numCheckpoints = "checkpoints" numCheckpointErrors = "checkpoint_errors" - numCheckpointedPages = "checkpointed_pages" numCheckpointedMoves = "checkpointed_moves" checkpointDuration = "checkpoint_duration_ms" numExecutions = "executions" @@ -105,7 +105,6 @@ stats.Add(openDuration, 0) stats.Add(numCheckpoints, 0) stats.Add(numCheckpointErrors, 0) - stats.Add(numCheckpointedPages, 0) stats.Add(numCheckpointedMoves, 0) stats.Add(checkpointDuration, 0) stats.Add(numExecutions, 0) @@ -664,6 +663,49 @@ return db.CheckpointWithTimeout(mode, checkpointBusyTimeout) } +// CheckpointTruncateWithTimeout performs a WAL checkpoint in TRUNCATE mode. +// The caller must guarantee that no write transactions will commit for the +// duration of the call. If all readers release their locks within dur, +// the WAL is truncated and nil is returned. If readers hold locks for +// the entire duration, an error is returned. +func (db *DB) CheckpointTruncateWithTimeout(dur time.Duration) error { + rwBt, _, err := db.BusyTimeout() + if err != nil { + return fmt.Errorf("failed to get busy_timeout: %s", err.Error()) + } + if err := db.SetBusyTimeout(int(dur.Milliseconds()), -1); err != nil { + return fmt.Errorf("failed to set busy_timeout: %s", err.Error()) + } + defer func() { + if err := db.SetBusyTimeout(rwBt, -1); err != nil { + db.logger.Printf("failed to reset busy_timeout: %s", err.Error()) + } + }() + + currMode, err := db.GetSynchronousMode() + if err != nil { + return fmt.Errorf("failed to get synchronous mode: %s", err.Error()) + } + if err := db.SetSynchronousMode(SynchronousFull); err != nil { + return fmt.Errorf("failed to set synchronous mode to FULL: %s", err.Error()) + } + defer func() { + if err := db.SetSynchronousMode(currMode); err != nil { + db.logger.Fatalf("failed to reset synchronous mode to %s: %s", currMode, err.Error()) + } + }() + + ok, _, nMoved, err := checkpointDB(db.rwDB, CheckpointTruncate) + if err != nil { + return fmt.Errorf("error checkpointing WAL: %s", err.Error()) + } + if ok != 0 { + return fmt.Errorf("checkpoint did not complete within %s", dur) + } + stats.Add(numCheckpointedMoves, int64(nMoved)) + return nil +} + // CheckpointWithTimeout performs a WAL checkpoint. If the checkpoint does not // run to completion within the given duration, an error is returned. If the // duration is 0, the busy timeout is not modified before executing the @@ -716,7 +758,6 @@ if err != nil { return nil, fmt.Errorf("error checkpointing WAL: %s", err.Error()) } - stats.Add(numCheckpointedPages, int64(nPages)) stats.Add(numCheckpointedMoves, int64(nMoved)) return &CheckpointMeta{ Code: ok, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-9.4.3/db/db_checkpoint_test.go new/rqlite-9.4.5/db/db_checkpoint_test.go --- old/rqlite-9.4.3/db/db_checkpoint_test.go 2026-03-09 15:12:14.000000000 +0100 +++ new/rqlite-9.4.5/db/db_checkpoint_test.go 2026-03-10 03:03:14.000000000 +0100 @@ -5,6 +5,7 @@ "context" "io" "os" + "sync" "testing" "time" @@ -518,6 +519,106 @@ } } +func Test_WALDatabaseCheckpointTruncateOK(t *testing.T) { + path := mustTempFile() + defer os.Remove(path) + + db, err := Open(path, false, true) + if err != nil { + t.Fatalf("failed to open database in WAL mode: %s", err.Error()) + } + defer db.Close() + + _, err = db.ExecuteStringStmt(`CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)`) + if err != nil { + t.Fatalf("failed to execute on single node: %s", err.Error()) + } + + err = db.CheckpointTruncateWithTimeout(100 * time.Millisecond) + if err != nil { + t.Fatalf("failed to checkpoint database: %s", err.Error()) + } + + // Confirm that the WAL file is zero bytes long. + sz, err := fileSize(db.WALPath()) + if err != nil { + t.Fatalf("failed to get WAL file size: %s", err.Error()) + } + if sz != 0 { + t.Fatalf("expected WAL file size to be 0 after checkpoint truncate, got %d", sz) + } + + // Ensure idempotency by checkpointing again. + err = db.CheckpointTruncateWithTimeout(100 * time.Millisecond) + if err != nil { + t.Fatalf("failed to checkpoint database: %s", err.Error()) + } + + // Ensure that the database remains in Synchronous=OFF mode. + syncMode, err := db.GetSynchronousMode() + if err != nil { + t.Fatalf("failed to get synchronous mode: %s", err.Error()) + } + if syncMode != SynchronousOff { + t.Fatalf("expected synchronous mode to be OFF, got %s", syncMode) + } +} + +func Test_WALDatabaseCheckpointTruncateFail_Blocked(t *testing.T) { + path := mustTempFile() + defer os.Remove(path) + + db, err := Open(path, false, true) + if err != nil { + t.Fatalf("failed to open database in WAL mode: %s", err.Error()) + } + defer db.Close() + + _, err = db.ExecuteStringStmt(`CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)`) + if err != nil { + t.Fatalf("failed to execute on single node: %s", err.Error()) + } + _, err = db.ExecuteStringStmt(`INSERT INTO foo(name) VALUES("alice")`) + if err != nil { + t.Fatalf("failed to execute INSERT on single node: %s", err.Error()) + } + + // Issue a long-running read that should block the checkpoint. + qr := &command.Request{ + Statements: []*command.Statement{ + { + Sql: "SELECT * FROM foo", + ForceStall: true, + }, + }, + } + ctx, cancelFunc := context.WithCancel(context.Background()) + go func() { + db.QueryWithContext(ctx, qr, false) + }() + time.Sleep(2 * time.Second) + err = db.CheckpointTruncateWithTimeout(100 * time.Millisecond) + if err == nil { + t.Fatalf("expected checkpoint to fail due to blocking read") + } + + // Now, start the checkpoint again, but this time with a longer timeout. + // Kick it off in a goroutine, and then cancel the blocking read after a short delay, + // and confirm that the checkpoint succeeds. + var blockErr error + var wg sync.WaitGroup + wg.Go(func() { + blockErr = db.CheckpointTruncateWithTimeout(5 * time.Second) + }) + + time.Sleep(2 * time.Second) + cancelFunc() + wg.Wait() + if blockErr != nil { + t.Fatalf("expected checkpoint to succeed after blocking read was cancelled, got error: %s", blockErr.Error()) + } +} + func mustReadBytes(path string) []byte { b, err := os.ReadFile(path) if err != nil { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-9.4.3/db/swappable_db.go new/rqlite-9.4.5/db/swappable_db.go --- old/rqlite-9.4.3/db/swappable_db.go 2026-03-09 15:12:14.000000000 +0100 +++ new/rqlite-9.4.5/db/swappable_db.go 2026-03-10 03:03:14.000000000 +0100 @@ -156,11 +156,12 @@ return s.db.StmtReadOnly(sql) } -// Checkpoint calls Checkpoint on the underlying database. -func (s *SwappableDB) Checkpoint(mode CheckpointMode) (*CheckpointMeta, error) { +// CheckpointTruncateWithTimeout calls CheckpointTruncateWithTimeout on the +// underlying database. +func (s *SwappableDB) CheckpointTruncateWithTimeout(timeout time.Duration) error { s.dbMu.RLock() defer s.dbMu.RUnlock() - return s.db.Checkpoint(mode) + return s.db.CheckpointTruncateWithTimeout(timeout) } // Optimize calls Optimize on the underlying database. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-9.4.3/store/state.go new/rqlite-9.4.5/store/state.go --- old/rqlite-9.4.3/store/state.go 2026-03-09 15:12:14.000000000 +0100 +++ new/rqlite-9.4.5/store/state.go 2026-03-10 03:03:14.000000000 +0100 @@ -210,13 +210,10 @@ // Create a new snapshot, placing the configuration in as if it was // committed at index 1. - meta, err := db.Checkpoint(sql.CheckpointTruncate) + err = db.CheckpointTruncateWithTimeout(truncateTimeout) if err != nil { return fmt.Errorf("failed to checkpoint database: %s", err) } - if !meta.Success() { - return fmt.Errorf("database checkpoint was not successful: %s", meta.String()) - } tmpDBFD, err := os.Open(tmpDBPath) if err != nil { return fmt.Errorf("failed to open temporary database file: %s", err) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-9.4.3/store/store.go new/rqlite-9.4.5/store/store.go --- old/rqlite-9.4.3/store/store.go 2026-03-09 15:12:14.000000000 +0100 +++ new/rqlite-9.4.5/store/store.go 2026-03-10 03:03:14.000000000 +0100 @@ -130,6 +130,7 @@ leaderWaitDelay = 100 * time.Millisecond appliedWaitDelay = 100 * time.Millisecond commitEquivalenceDelay = 50 * time.Millisecond + truncateTimeout = 5 * time.Minute backupCASTimeout = 10 * time.Second backupCASRetryDelay = 100 * time.Millisecond connectionPoolCount = 5 @@ -156,11 +157,6 @@ numWALSnapshotsFailed = "num_wal_snapshots_failed" numSnapshotsFull = "num_snapshots_full" numSnapshotsIncremental = "num_snapshots_incremental" - numFullCheckpointFailed = "num_full_checkpoint_failed" - numWALCheckpointTruncateFailed = "num_wal_checkpoint_truncate_failed" - numWALCheckpointAllMovedFailed = "num_wal_checkpoint_all_moved_failed" - numWALCheckpointIncomplete = "num_wal_checkpoint_incomplete" - numWALMustCheckpoint = "num_wal_must_checkpoint" numAutoVacuums = "num_auto_vacuums" numAutoVacuumsFailed = "num_auto_vacuums_failed" autoVacuumDuration = "auto_vacuum_duration" @@ -226,11 +222,6 @@ stats.Add(numWALSnapshotsFailed, 0) stats.Add(numSnapshotsFull, 0) stats.Add(numSnapshotsIncremental, 0) - stats.Add(numFullCheckpointFailed, 0) - stats.Add(numWALCheckpointTruncateFailed, 0) - stats.Add(numWALCheckpointAllMovedFailed, 0) - stats.Add(numWALCheckpointIncomplete, 0) - stats.Add(numWALMustCheckpoint, 0) stats.Add(numAutoVacuums, 0) stats.Add(numAutoVacuumsFailed, 0) stats.Add(autoVacuumDuration, 0) @@ -2576,10 +2567,8 @@ var walTmpFD *os.File if fullNeeded { chkStartTime := time.Now() - if err := s.checkpointWAL(); err != nil { - stats.Add(numFullCheckpointFailed, 1) - return nil, fmt.Errorf("full snapshot can't complete due to WAL checkpoint error (will retry): %s", - err.Error()) + if err := s.db.CheckpointTruncateWithTimeout(truncateTimeout); err != nil { + s.logger.Fatalf("failed to checkpoint and truncate database for full snapshot: %s", err.Error()) } stats.Get(snapshotCreateChkTruncateDuration).(*expvar.Int).Set(time.Since(chkStartTime).Milliseconds()) dbFD, err := os.Open(s.db.Path()) @@ -2642,12 +2631,10 @@ return nil, err } chkTStartTime := time.Now() - if err := s.checkpointWAL(); err != nil { + if err := s.db.CheckpointTruncateWithTimeout(truncateTimeout); err != nil { walTmpFD.Close() os.Remove(walTmpFD.Name()) - stats.Add(numWALCheckpointTruncateFailed, 1) - return nil, fmt.Errorf("incremental snapshot can't complete due to WAL checkpoint error (will retry): %s", - err.Error()) + s.logger.Fatalf("failed to checkpoint and truncate database for incremental snapshot: %s", err.Error()) } stats.Get(snapshotCreateChkTruncateDuration).(*expvar.Int).Set(time.Since(chkTStartTime).Milliseconds()) stats.Get(snapshotPrecompactWALSize).(*expvar.Int).Set(walSzPre) @@ -2951,25 +2938,6 @@ return closeCh, doneCh } -// checkpointWAL performs a checkpoint of the WAL, truncating it. If it returns an error -// the checkpoint operation can be retried at the caller's discretion. -func (s *Store) checkpointWAL() (retErr error) { - meta, err := s.db.Checkpoint(sql.CheckpointTruncate) - if err != nil { - return err - } - if !meta.Success() { - if meta.Pages == meta.Moved { - s.logger.Printf("checkpoint moved all pages (%d/%d), but failed to truncate WAL", - meta.Moved, meta.Pages) - stats.Add(numWALCheckpointAllMovedFailed, 1) - } - stats.Add(numWALCheckpointIncomplete, 1) - return fmt.Errorf("checkpoint incomplete: %s", meta.String()) - } - return nil -} - // selfLeaderChange is called when this node detects that its leadership // status has changed. func (s *Store) selfLeaderChange(leader bool) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-9.4.3/store/store_snapshot_test.go new/rqlite-9.4.5/store/store_snapshot_test.go --- old/rqlite-9.4.3/store/store_snapshot_test.go 2026-03-09 15:12:14.000000000 +0100 +++ new/rqlite-9.4.5/store/store_snapshot_test.go 2026-03-10 03:03:14.000000000 +0100 @@ -11,7 +11,6 @@ "time" "github.com/rqlite/rqlite/v9/command/proto" - "github.com/rqlite/rqlite/v9/db" "github.com/rqlite/rqlite/v9/internal/random" ) @@ -228,69 +227,6 @@ } } -func Test_SingleNode_SnapshotFail_Blocked(t *testing.T) { - s, ln := mustNewStore(t) - defer ln.Close() - - s.SnapshotThreshold = 8192 - s.SnapshotInterval = time.Hour - s.NoSnapshotOnClose = true - if err := s.Open(); err != nil { - t.Fatalf("failed to open single-node store: %s", err.Error()) - } - defer s.Close(true) - if err := s.Bootstrap(NewServer(s.ID(), s.Addr(), true)); err != nil { - t.Fatalf("failed to bootstrap single-node store: %s", err.Error()) - } - if _, err := s.WaitForLeader(10 * time.Second); err != nil { - t.Fatalf("Error waiting for leader: %s", err) - } - er := executeRequestFromString(`CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)`, - false, false) - _, _, err := s.Execute(context.Background(), er) - if err != nil { - t.Fatalf("failed to execute on single node: %s", err.Error()) - } - - er = executeRequestFromString(`INSERT INTO foo(name) VALUES("fiona")`, false, false) - _, _, err = s.Execute(context.Background(), er) - if err != nil { - t.Fatalf("failed to execute on single node: %s", err.Error()) - } - - ctx, cancelFunc := context.WithCancel(context.Background()) - go func() { - qr := queryRequestFromString("SELECT * FROM foo", false, false) - qr.GetRequest().Statements[0].ForceStall = true - - blockingDB, err := db.Open(s.dbPath, false, true) - if err != nil { - t.Errorf("failed to open blocking DB connection: %s", err.Error()) - } - defer blockingDB.Close() - - _, err = blockingDB.QueryWithContext(ctx, qr.GetRequest(), false) - if err != nil { - t.Errorf("failed to execute stalled query on blocking DB connection: %s", err.Error()) - } - }() - time.Sleep(1 * time.Second) - - er = executeRequestFromString(`INSERT INTO foo(name) VALUES("bob")`, false, false) - _, _, err = s.Execute(context.Background(), er) - if err != nil { - t.Fatalf("failed to execute on single node: %s", err.Error()) - } - - if err := s.Snapshot(0); err == nil { - t.Fatalf("expected error snapshotting single-node store with stalled query") - } - - // Shutdown the blocking query so we can clean up. Windows in particular. - cancelFunc() - <-ctx.Done() -} - func Test_SingleNode_SnapshotWithAutoOptimize_Stress(t *testing.T) { s, ln := mustNewStore(t) defer ln.Close() ++++++ vendor.tar.xz ++++++
