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-09-04 12:43:37 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/rqlite (Old) and /work/SRC/openSUSE:Factory/.rqlite.new.1265 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "rqlite" Fri Sep 4 12:43:37 2026 rev:54 rq:1375706 version:10.3.1 Changes: -------- --- /work/SRC/openSUSE:Factory/rqlite/rqlite.changes 2026-09-02 16:59:47.023179740 +0200 +++ /work/SRC/openSUSE:Factory/.rqlite.new.1265/rqlite.changes 2026-09-04 12:43:38.829405305 +0200 @@ -1,0 +2,8 @@ +Fri Sep 04 05:17:34 UTC 2026 - Andreas Stieger <[email protected]> + +- Update to version 10.3.1: + * Consolidate SQLite driver construction + * Plumb Term and Index through during Snapshot Restore + * Hard exit if FSM Restore fails post database-swap + +------------------------------------------------------------------- Old: ---- rqlite-10.3.0.tar.xz New: ---- rqlite-10.3.1.tar.xz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ rqlite.spec ++++++ --- /var/tmp/diff_new_pack.XUWVbL/_old 2026-09-04 12:43:39.884442356 +0200 +++ /var/tmp/diff_new_pack.XUWVbL/_new 2026-09-04 12:43:39.886442427 +0200 @@ -17,7 +17,7 @@ Name: rqlite -Version: 10.3.0 +Version: 10.3.1 Release: 0 Summary: Distributed relational database built on SQLite License: MIT ++++++ _service ++++++ --- /var/tmp/diff_new_pack.XUWVbL/_old 2026-09-04 12:43:39.918443550 +0200 +++ /var/tmp/diff_new_pack.XUWVbL/_new 2026-09-04 12:43:39.921443656 +0200 @@ -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">v10.3.0</param> + <param name="revision">v10.3.1</param> <param name="versionformat">@PARENT_TAG@</param> <param name="changesgenerate">enable</param> <param name="versionrewrite-pattern">v(.*)</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.XUWVbL/_old 2026-09-04 12:43:39.941444358 +0200 +++ /var/tmp/diff_new_pack.XUWVbL/_new 2026-09-04 12:43:39.944444463 +0200 @@ -1,7 +1,7 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/rqlite/rqlite.git</param> - <param name="changesrevision">6144b4c536861b6be7ded7ae75b8bbce1a9dba82</param> + <param name="changesrevision">c1a3b427827b3babc8c5fa7a7ad35ab2f9d598b0</param> </service> </servicedata> (No newline at EOF) ++++++ rqlite-10.3.0.tar.xz -> rqlite-10.3.1.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-10.3.0/CHANGELOG.md new/rqlite-10.3.1/CHANGELOG.md --- old/rqlite-10.3.0/CHANGELOG.md 2026-09-01 17:26:59.000000000 +0200 +++ new/rqlite-10.3.1/CHANGELOG.md 2026-09-04 04:11:36.000000000 +0200 @@ -1,3 +1,9 @@ +## v10.3.1 (September 3rd 2026) +### Implementation changes and bug fixes +- [PR #2758](https://github.com/rqlite/rqlite/pull/2758): Consolidate SQLite driver construction. Thanks @karangupta982 +- [PR #2760](https://github.com/rqlite/rqlite/pull/2760): Plumb Term and Index through during Snapshot Restore. Fixes issue [#2759](https://github.com/rqlite/rqlite/issues/2759). Thanks @rohanpadhye +- [PR #2761](https://github.com/rqlite/rqlite/pull/2761): Hard exit if FSM Restore fails post database-swap. + ## v10.3.0 (September 1st 2026) ### New features - [PR #2728](https://github.com/rqlite/rqlite/pull/2728): Console app supports quickly querying for first 100 rows of a table. Thanks @rodionlim diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-10.3.0/db/driver.go new/rqlite-10.3.1/db/driver.go --- old/rqlite-10.3.0/db/driver.go 2026-09-01 17:26:59.000000000 +0200 +++ new/rqlite-10.3.1/db/driver.go 2026-09-04 04:11:36.000000000 +0200 @@ -26,6 +26,18 @@ CnkOnCloseModeEnabled ) +// DriverConfig holds the configuration for a composable SQLite driver. +type DriverConfig struct { + // Extensions is the list of paths to SQLite extension shared objects. + Extensions []string + + // ChkOnClose controls whether SQLite checkpoints the WAL on connection close. + ChkOnClose CnkOnCloseMode + + // QueryLogger, if non-nil, installs query tracing on every new connection. + QueryLogger *QueryLogger +} + // Driver is a Database driver. type Driver struct { name string @@ -33,6 +45,24 @@ chkOnClose CnkOnCloseMode } +// NewDriverFromConfig registers a new SQLite driver under name using cfg to +// compose the ConnectHook. Every feature in cfg is applied to each new +// connection, so extensions, checkpoint behavior, and query logging can all +// coexist. +// If a driver with name is already registered, a panic will occur. Callers +// that need a singleton driver (fixed names) should guard this with sync.Once. +func NewDriverFromConfig(name string, cfg DriverConfig) *Driver { + sql.Register(name, &sqlite3.SQLiteDriver{ + Extensions: cfg.Extensions, + ConnectHook: buildConnectHook(cfg), + }) + return &Driver{ + name: name, + extensions: cfg.Extensions, + chkOnClose: cfg.ChkOnClose, + } +} + var defRegisterOnce sync.Once // DefaultDriver returns the default driver. It registers the SQLite3 driver @@ -41,8 +71,8 @@ // for any database in WAL mode. func DefaultDriver() *Driver { defRegisterOnce.Do(func() { - sql.Register(defaultDriverName, &sqlite3.SQLiteDriver{ - ConnectHook: makeConnectHookFn(CnkOnCloseModeDisabled), + NewDriverFromConfig(defaultDriverName, DriverConfig{ + ChkOnClose: CnkOnCloseModeDisabled, }) }) return &Driver{ @@ -59,8 +89,8 @@ // on close for any database in WAL mode. func CheckpointDriver() *Driver { chkRegisterOnce.Do(func() { - sql.Register(chkDriverName, &sqlite3.SQLiteDriver{ - ConnectHook: makeConnectHookFn(CnkOnCloseModeEnabled), + NewDriverFromConfig(chkDriverName, DriverConfig{ + ChkOnClose: CnkOnCloseModeEnabled, }) }) return &Driver{ @@ -98,15 +128,10 @@ // // If a driver with the given name already exists, a panic will occur. func NewDriver(name string, extensions []string, chkpt CnkOnCloseMode) *Driver { - sql.Register(name, &sqlite3.SQLiteDriver{ - Extensions: extensions, - ConnectHook: makeConnectHookFn(chkpt), + return NewDriverFromConfig(name, DriverConfig{ + Extensions: extensions, + ChkOnClose: chkpt, }) - return &Driver{ - name: name, - extensions: extensions, - chkOnClose: chkpt, - } } // Name returns the driver name. @@ -134,13 +159,28 @@ return d.chkOnClose } -func makeConnectHookFn(chkpt CnkOnCloseMode) func(conn *sqlite3.SQLiteConn) error { +// buildConnectHook composes a ConnectHook from cfg, chaining all requested +// connection-level behaviors in order: checkpoint config, then query tracing. +func buildConnectHook(cfg DriverConfig) func(conn *sqlite3.SQLiteConn) error { return func(conn *sqlite3.SQLiteConn) error { - if chkpt == CnkOnCloseModeDisabled { + // Checkpoint-on-close configuration. + if cfg.ChkOnClose == CnkOnCloseModeDisabled { if err := conn.DBConfigNoCkptOnClose(); err != nil { return fmt.Errorf("cannot disable checkpoint on close: %w", err) } } + + // Query tracing. + if cfg.QueryLogger != nil { + if err := conn.SetTrace(&sqlite3.TraceConfig{ + Callback: cfg.QueryLogger.TraceHook, + EventMask: sqlite3.TraceStmt | sqlite3.TraceProfile, + WantExpandedSQL: true, + }); err != nil { + return err + } + } + return nil } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-10.3.0/db/driver_test.go new/rqlite-10.3.1/db/driver_test.go --- old/rqlite-10.3.0/db/driver_test.go 2026-09-01 17:26:59.000000000 +0200 +++ new/rqlite-10.3.1/db/driver_test.go 2026-09-04 04:11:36.000000000 +0200 @@ -1,7 +1,12 @@ package db import ( + "bytes" + "fmt" + "log" "os" + "strings" + "sync/atomic" "testing" "github.com/rqlite/rqlite/v10/internal/fsutil" @@ -120,3 +125,81 @@ t.Fatalf("NewDriver returned incorrect checkpoint mode: %v", d.CheckpointOnCloseMode()) } } + +// A local counter for generating unique driver names. +var driverTestSeq atomic.Int64 + +func testDriverConfigName() string { + return fmt.Sprintf("test-driver-config-%d", driverTestSeq.Add(1)) +} + +// Verifies that a DriverConfig with a QueryLogger +// produces log output for every executed statement. +func Test_NewDriverFromConfig_QueryLogOnly(t *testing.T) { + var buf bytes.Buffer + logger := log.New(&buf, "", 0) + ql := NewQueryLogger(QueryLogConfig{Logger: logger}) + + d := NewDriverFromConfig(testDriverConfigName(), DriverConfig{ + ChkOnClose: CnkOnCloseModeDisabled, + QueryLogger: ql, + }) + + path := mustTempPath() + defer os.RemoveAll(path) + db, err := OpenWithDriver(d, path, false, true) + if err != nil { + t.Fatalf("OpenWithDriver failed: %s", err) + } + defer db.Close() + + mustExecute(db, "CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT)") + mustExecute(db, "INSERT INTO t VALUES (1, 'hello')") + + output := buf.String() + if !strings.Contains(output, "CREATE TABLE t") { + t.Fatalf("expected CREATE TABLE in query log, got:\n%s", output) + } + if !strings.Contains(output, "INSERT INTO t") { + t.Fatalf("expected INSERT in query log, got:\n%s", output) + } +} + +// Verifies that a DriverConfig with nil +// QueryLogger opens and operates normally without tracing. +func Test_NewDriverFromConfig_NoQueryLog(t *testing.T) { + d := NewDriverFromConfig(testDriverConfigName(), DriverConfig{ + ChkOnClose: CnkOnCloseModeDisabled, + QueryLogger: nil, + }) + if d.CheckpointOnCloseMode() != CnkOnCloseModeDisabled { + t.Fatalf("expected CnkOnCloseModeDisabled, got %v", d.CheckpointOnCloseMode()) + } + + path := mustTempPath() + defer os.RemoveAll(path) + db, err := OpenWithDriver(d, path, false, true) + if err != nil { + t.Fatalf("OpenWithDriver failed: %s", err) + } + defer db.Close() + + mustExecute(db, "CREATE TABLE t (id INTEGER PRIMARY KEY)") +} + +// Verifies that extension paths set in +// DriverConfig are reflected on the returned Driver struct. +func Test_DriverConfig_ExtensionsFields(t *testing.T) { + exts := []string{"/tmp/ext1.so", "/tmp/ext2.so"} + d := NewDriverFromConfig(testDriverConfigName(), DriverConfig{ + Extensions: exts, + ChkOnClose: CnkOnCloseModeDisabled, + }) + if len(d.Extensions()) != 2 { + t.Fatalf("expected 2 extensions, got %d", len(d.Extensions())) + } + names := d.ExtensionNames() + if names[0] != "ext1.so" || names[1] != "ext2.so" { + t.Fatalf("unexpected extension names: %v", names) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-10.3.0/db/query_log_driver.go new/rqlite-10.3.1/db/query_log_driver.go --- old/rqlite-10.3.0/db/query_log_driver.go 2026-09-01 17:26:59.000000000 +0200 +++ new/rqlite-10.3.1/db/query_log_driver.go 1970-01-01 01:00:00.000000000 +0100 @@ -1,62 +0,0 @@ -package db - -import ( - "database/sql" - "sync" - - "github.com/mattn/go-sqlite3" -) - -const queryLogDriverName = "rqlite-sqlite3-querylog" - -var queryLogDriverOnce sync.Once - -// QueryLogDriver returns the query-log driver. It registers the SQLite3 -// driver with query logging support. It can be called multiple times but -// only registers the driver once. The driver disables checkpoint-on-close -// and installs the given QueryLogger's TraceHook on every new connection. -func QueryLogDriver(ql *QueryLogger) *Driver { - queryLogDriverOnce.Do(func() { - sql.Register(queryLogDriverName, &sqlite3.SQLiteDriver{ - ConnectHook: makeQueryLogConnectHookFn(ql), - }) - }) - return &Driver{ - name: queryLogDriverName, - chkOnClose: CnkOnCloseModeDisabled, - } -} - -// newTestQueryLogDriver registers a query-log driver with the given name. -// It is used by tests to create isolated drivers. -func newTestQueryLogDriver(name string, ql *QueryLogger) *Driver { - sql.Register(name, &sqlite3.SQLiteDriver{ - ConnectHook: makeQueryLogConnectHookFn(ql), - }) - return &Driver{ - name: name, - chkOnClose: CnkOnCloseModeDisabled, - } -} - -// makeQueryLogConnectHookFn creates a ConnectHook that: -// 1. Disables checkpoint-on-close (same as the default driver). -// 2. Installs the QueryLogger's TraceHook via SetTrace. -func makeQueryLogConnectHookFn(ql *QueryLogger) func(conn *sqlite3.SQLiteConn) error { - return func(conn *sqlite3.SQLiteConn) error { - if err := conn.DBConfigNoCkptOnClose(); err != nil { - return err - } - - if ql != nil { - if err := conn.SetTrace(&sqlite3.TraceConfig{ - Callback: ql.TraceHook, - EventMask: sqlite3.TraceStmt | sqlite3.TraceProfile, - WantExpandedSQL: true, - }); err != nil { - return err - } - } - return nil - } -} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-10.3.0/db/query_log_integration_test.go new/rqlite-10.3.1/db/query_log_integration_test.go --- old/rqlite-10.3.0/db/query_log_integration_test.go 2026-09-01 17:26:59.000000000 +0200 +++ new/rqlite-10.3.1/db/query_log_integration_test.go 2026-09-04 04:11:36.000000000 +0200 @@ -20,7 +20,10 @@ logger := log.New(&buf, "[qlog] ", 0) ql := NewQueryLogger(QueryLogConfig{Logger: logger}) - drv := newTestQueryLogDriver(testDriverName(), ql) + drv := NewDriverFromConfig(testDriverName(), DriverConfig{ + ChkOnClose: CnkOnCloseModeDisabled, + QueryLogger: ql, + }) dbPath := t.TempDir() + "/test.db" db, err := OpenWithDriver(drv, dbPath, false, true) @@ -56,7 +59,10 @@ func Test_QueryLog_Integration_Disabled(t *testing.T) { ql := NewQueryLogger(QueryLogConfig{Logger: nil}) - drv := newTestQueryLogDriver(testDriverName(), ql) + drv := NewDriverFromConfig(testDriverName(), DriverConfig{ + ChkOnClose: CnkOnCloseModeDisabled, + QueryLogger: ql, + }) dbPath := t.TempDir() + "/test.db" db, err := OpenWithDriver(drv, dbPath, false, true) @@ -89,7 +95,10 @@ var buf bytes.Buffer logger := log.New(&buf, "", 0) ql := NewQueryLogger(QueryLogConfig{Logger: logger}) - drv := newTestQueryLogDriver(testDriverName(), ql) + drv := NewDriverFromConfig(testDriverName(), DriverConfig{ + ChkOnClose: CnkOnCloseModeDisabled, + QueryLogger: ql, + }) dbPath := t.TempDir() + "/test.db" db, err := OpenWithDriver(drv, dbPath, false, true) @@ -128,7 +137,10 @@ var buf bytes.Buffer logger := log.New(&buf, "", 0) ql := NewQueryLogger(QueryLogConfig{Logger: logger}) - drv := newTestQueryLogDriver(testDriverName(), ql) + drv := NewDriverFromConfig(testDriverName(), DriverConfig{ + ChkOnClose: CnkOnCloseModeDisabled, + QueryLogger: ql, + }) dbPath := t.TempDir() + "/test.db" db, err := OpenWithDriver(drv, dbPath, false, true) @@ -157,7 +169,10 @@ } func Test_QueryLog_Integration_NilQueryLogger(t *testing.T) { - drv := newTestQueryLogDriver(testDriverName(), nil) + drv := NewDriverFromConfig(testDriverName(), DriverConfig{ + ChkOnClose: CnkOnCloseModeDisabled, + QueryLogger: nil, + }) dbPath := t.TempDir() + "/test.db" db, err := OpenWithDriver(drv, dbPath, false, true) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-10.3.0/snapshot/state.go new/rqlite-10.3.1/snapshot/state.go --- old/rqlite-10.3.0/snapshot/state.go 2026-09-01 17:26:59.000000000 +0200 +++ new/rqlite-10.3.1/snapshot/state.go 2026-09-04 04:11:36.000000000 +0200 @@ -73,6 +73,9 @@ // LatestIndexTerm returns the index and term of the most recent snapshot // in the given directory. If no snapshots are found, it returns 0, 0, nil. +// +// This function does not take any lock on the Snapshot store contained at dir, +// so it not safe to call on an open Snapshot store. func LatestIndexTerm(dir string) (uint64, uint64, error) { cat := &SnapshotCatalog{} sset, err := cat.Scan(dir) @@ -148,3 +151,19 @@ } return it.Index(), it.Term(), nil } + +// StreamerIndexTerm returns the Raft index and term of the snapshot being streamed. +// Raft does not hand the ReadCloser returned by Store.Open to the FSM directly, but +// wraps it first, so any wrappers are unwrapped before the index and term are read. +func StreamerIndexTerm(rc io.ReadCloser) (uint64, uint64, error) { + for src := rc; ; { + if it, ok := src.(IndexTermer); ok { + return it.Index(), it.Term(), nil + } + w, ok := src.(raft.ReadCloserWrapper) + if !ok { + return 0, 0, fmt.Errorf("snapshot streamer of type %T does not know its index and term", rc) + } + src = w.WrappedReadCloser() + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-10.3.0/snapshot/store.go new/rqlite-10.3.1/snapshot/store.go --- old/rqlite-10.3.0/snapshot/store.go 2026-09-01 17:26:59.000000000 +0200 +++ new/rqlite-10.3.1/snapshot/store.go 2026-09-04 04:11:36.000000000 +0200 @@ -111,22 +111,25 @@ mu sync.Mutex timer *time.Timer + + meta *raft.SnapshotMeta } // NewLockingStreamer returns a new LockingStreamer. If timeout > 0, the // streamer will be force-closed after that much time elapses with no Read // activity. A timeout of 0 disables the idle check. -func NewLockingStreamer(rc io.ReadCloser, str *Store, timeout time.Duration) *LockingStreamer { +func NewLockingStreamer(rc io.ReadCloser, str *Store, mt *raft.SnapshotMeta, to time.Duration) *LockingStreamer { l := &LockingStreamer{ ReadCloser: rc, str: str, - timeout: timeout, + timeout: to, timedOut: rsync.NewAtomicBool(), closed: rsync.NewAtomicBool(), + meta: mt, } l.lastRead.Store(time.Now().UnixNano()) - if timeout > 0 { - l.timer = time.AfterFunc(timeout, l.checkIdle) + if to > 0 { + l.timer = time.AfterFunc(to, l.checkIdle) } return l } @@ -146,6 +149,16 @@ return n, err } +// Index returns the Raft Index of the streamed Snapshot +func (l *LockingStreamer) Index() uint64 { + return l.meta.Index +} + +// Term returns the Raft Term of the streamed Snapshot +func (l *LockingStreamer) Term() uint64 { + return l.meta.Term +} + // Close closes the Snapshot and releases the Snapshot Store lock. func (l *LockingStreamer) Close() error { l.mu.Lock() @@ -442,7 +455,7 @@ } meta.Size = sz - return meta, NewLockingStreamer(streamer, s, s.readTimeout), nil + return meta, NewLockingStreamer(streamer, s, meta, s.readTimeout), nil } // RegisterObserver registers an observer to receive observations. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-10.3.0/store/store.go new/rqlite-10.3.1/store/store.go --- old/rqlite-10.3.0/store/store.go 2026-09-01 17:26:59.000000000 +0200 +++ new/rqlite-10.3.1/store/store.go 2026-09-04 04:11:36.000000000 +0200 @@ -782,16 +782,21 @@ stats.Add(numRecoveries, 1) } - // If SQLite extensions are specified, we need a custom driver. - if len(s.dbConf.Extensions) > 0 { - s.dbDrv = sql.NewDriver(random.StringPattern("rqlite-extended-xxxx-xxxx-xxxx"), - s.dbConf.Extensions, sql.CnkOnCloseModeDisabled) - } - - // If query logging is configured, use the query-log driver. - if s.dbConf.QueryLogger != nil { - ql := sql.NewQueryLogger(sql.QueryLogConfig{Logger: s.dbConf.QueryLogger}) - s.dbDrv = sql.QueryLogDriver(ql) + // Build a composed driver if any non-default features are configured. + // This replaces the previous sequential override pattern and allows + // extensions and query logging to coexist in the same driver. + if len(s.dbConf.Extensions) > 0 || s.dbConf.QueryLogger != nil { + cfg := sql.DriverConfig{ + Extensions: s.dbConf.Extensions, + ChkOnClose: sql.CnkOnCloseModeDisabled, + } + if s.dbConf.QueryLogger != nil { + cfg.QueryLogger = sql.NewQueryLogger(sql.QueryLogConfig{Logger: s.dbConf.QueryLogger}) + } + s.dbDrv = sql.NewDriverFromConfig( + random.StringPattern("rqlite-configured-xxxx-xxxx-xxxx"), + cfg, + ) } s.db, err = createDBOnDisk(s.dbPath, s.dbDrv, removeDBFiles, s.dbConf.FKConstraints, s.MaxReadOnlyConns) @@ -2782,9 +2787,14 @@ // will not be called concurrently with Apply(), so synchronization with Execute() // is not necessary. func (s *Store) fsmRestore(rc io.ReadCloser) (retErr error) { + hardExitRequired := false + defer func() { if retErr != nil { stats.Add(numRestoresFailed, 1) + if hardExitRequired { + s.logger.Fatalf("restore failed post database swap, aborting: %s", retErr) + } } }() s.logger.Printf("initiating node restore on node ID %s", s.raftID) @@ -2812,26 +2822,39 @@ s.logger.Printf("error closing snapshot reader after restore: %s", err) } + // Take conservative approach and assume that everything has changed, so update + // the indexes. It is possible that dbAppliedIdx is now ahead of some other nodes' + // same value, since the last index is not necessarily a database-changing index, + // but that is OK. Worse that can happen is that anything paying attention to the + // index might consider the database to be changed when it is not, *logically* speaking. + li, tm, err := snapshot.StreamerIndexTerm(rc) + if err != nil { + return fmt.Errorf("failed to get streamed snapshot index and term: %s", err) + } + // Any existing SQLite file is about to be invalid, so mark that we can't // fast-restart with it. if err := fsutil.RemoveFile(s.cleanSnapshotPath); err != nil { return fmt.Errorf("failed to remove clean snapshot file: %w", err) } + + // We're about to enter a point of no-return. We're going to swap in the incoming + // database with the one on disk. If an error is encountered after swapping the + // and simply returned that error to the Raft subsystem, Raft would consider the + // Restore as FAILED, but the database on disk would be wrong. We could attempt + // to reverse the swap, but a simpler, easier to ensure it's correct, solution + // is to just exit hard and let the node restart. It will then do a full restore + // from Raft. + // + // Doing it like this protects agains future changes in the code by catching the + // error in the defer handler. + hardExitRequired = true + if err := s.db.Swap(tmpPath, s.dbConf.FKConstraints, true); err != nil { return fmt.Errorf("error swapping database file: %v", err) } s.logger.Printf("successfully opened database at %s due to restore", s.db.Path()) - // Take conservative approach and assume that everything has changed, so update - // the indexes. It is possible that dbAppliedIdx is now ahead of some other nodes' - // same value, since the last index is not necessarily a database-changing index, - // but that is OK. Worse that can happen is that anything paying attention to the - // index might consider the database to be changed when it is not, *logically* speaking. - li, tm, err := snapshot.LatestIndexTerm(s.snapshotDir) - if err != nil { - return fmt.Errorf("failed to get latest snapshot index post restore: %s", err) - } - // Installed SQLite database is safe for fast restarts again. It is fingerprinted // against the very index and term the node is adopting here, so that a restart // can tell that the two still correspond to one another. @@ -2844,11 +2867,7 @@ s.fsmTerm.Store(tm) s.dbAppliedIdx.Store(li) s.appliedTarget.Signal(li) - lt, err := s.db.DBLastModified() - if err != nil { - return fmt.Errorf("failed to get last modified time: %s", err) - } - s.dbModifiedTime.Store(lt) + s.dbModifiedTime.Store(time.Now()) // Swapping in a new database deactivates the CDC hooks, so signal that it // needs to be reregistered on the next commit. s.cdcRegistered.Unset() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/rqlite-10.3.0/store/store_snapshot_test.go new/rqlite-10.3.1/store/store_snapshot_test.go --- old/rqlite-10.3.0/store/store_snapshot_test.go 2026-09-01 17:26:59.000000000 +0200 +++ new/rqlite-10.3.1/store/store_snapshot_test.go 2026-09-04 04:11:36.000000000 +0200 @@ -73,7 +73,7 @@ t.Fatalf("failed to open snapshot file: %s", err.Error()) } defer snapFile.Close() - if err := fsm.Restore(snapFile); err != nil { + if err := fsm.Restore(&mockSnapshotStreamer{snapFile}); err != nil { t.Fatalf("failed to restore snapshot from disk: %s", err.Error()) } @@ -885,6 +885,20 @@ return nil } +// mockSnapshotStreamer stands in for the ReadCloser Raft hands to the FSM during +// a restore, which always knows the index and term of the snapshot it streams. +type mockSnapshotStreamer struct { + *os.File +} + +func (m *mockSnapshotStreamer) Index() uint64 { + return 1 +} + +func (m *mockSnapshotStreamer) Term() uint64 { + return 1 +} + func mustExecute(t *testing.T, s *Store, queries []string) []*proto.ExecuteQueryResponse { t.Helper() rows, _, err := s.Execute(context.Background(), executeRequestFromStrings(queries, false, false)) ++++++ vendor.tar.xz ++++++
