From 25ae01b0d7f1db4871f88b853db7b9413649ca28 Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Mon, 8 Sep 2025 06:55:04 +0000 Subject: [PATCH] chore(sqlite): skip database GC entirely when it's provably unnecessary (#10379) Store the sequence number of the last GC sweep in a KV. Next time, if it matches we can just skip GC because nothing has been added or removed. Signed-off-by: Jakob Borg --- internal/db/sqlite/db_service.go | 45 +++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/internal/db/sqlite/db_service.go b/internal/db/sqlite/db_service.go index b9b6953be..47b5d30fa 100644 --- a/internal/db/sqlite/db_service.go +++ b/internal/db/sqlite/db_service.go @@ -18,12 +18,14 @@ import ( "github.com/jmoiron/sqlx" "github.com/syncthing/syncthing/internal/db" "github.com/syncthing/syncthing/internal/slogutil" + "github.com/syncthing/syncthing/lib/protocol" "github.com/thejerf/suture/v4" ) const ( - internalMetaPrefix = "dbsvc" - lastMaintKey = "lastMaint" + internalMetaPrefix = "dbsvc" + lastMaintKey = "lastMaint" + lastSuccessfulGCSeqKey = "lastSuccessfulGCSeq" gcMinChunks = 5 gcChunkSize = 100_000 // approximate number of rows to process in a single gc query @@ -98,16 +100,41 @@ func (s *Service) periodic(ctx context.Context) error { } return wrap(s.sdb.forEachFolder(func(fdb *folderDB) error { - fdb.updateLock.Lock() - defer fdb.updateLock.Unlock() + // Get the current device sequence, for comparison in the next step. + seq, err := fdb.GetDeviceSequence(protocol.LocalDeviceID) + if err != nil { + return wrap(err) + } + // Get the last successful GC sequence. If it's the same as the + // current sequence, nothing has changed and we can skip the GC + // entirely. + meta := db.NewTyped(fdb, internalMetaPrefix) + if prev, _, err := meta.Int64(lastSuccessfulGCSeqKey); err != nil { + return wrap(err) + } else if seq == prev { + slog.DebugContext(ctx, "Skipping unnecessary GC", "folder", fdb.folderID, "fdb", fdb.baseName) + return nil + } - if err := garbageCollectOldDeletedLocked(ctx, fdb); err != nil { + // Run the GC steps, in a function to be able to use a deferred + // unlock. + if err := func() error { + fdb.updateLock.Lock() + defer fdb.updateLock.Unlock() + + if err := garbageCollectOldDeletedLocked(ctx, fdb); err != nil { + return wrap(err) + } + if err := garbageCollectBlocklistsAndBlocksLocked(ctx, fdb); err != nil { + return wrap(err) + } + return tidy(ctx, fdb.sql) + }(); err != nil { return wrap(err) } - if err := garbageCollectBlocklistsAndBlocksLocked(ctx, fdb); err != nil { - return wrap(err) - } - return tidy(ctx, fdb.sql) + + // Update the successful GC sequence. + return wrap(meta.PutInt64(lastSuccessfulGCSeqKey, seq)) })) }