From 5bf27a432c08fc02ee1beffe43da5b7a9b820cd3 Mon Sep 17 00:00:00 2001 From: Tommy van der Vorst Date: Wed, 14 Jan 2026 22:10:54 +0100 Subject: [PATCH] chore(sqlite): allow periodic database maintenance to be disabled (#10441) This change allows the periodic database maintenance to be disabled, while providing a way to programmatically start maintenance at a convenient moment. Signed-off-by: Tommy van der Vorst --- cmd/syncthing/main.go | 2 +- internal/db/interface.go | 11 ++++++++++- internal/db/sqlite/db_service.go | 27 +++++++++++++++++++++------ lib/syncthing/syncthing.go | 9 ++++++++- 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index 27844d546..b4e048b04 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -155,7 +155,7 @@ type serveCmd struct { AllowNewerConfig bool `help:"Allow loading newer than current config version" env:"STALLOWNEWERCONFIG"` Audit bool `help:"Write events to audit file" env:"STAUDIT"` AuditFile string `name:"auditfile" help:"Specify audit file (use \"-\" for stdout, \"--\" for stderr)" placeholder:"PATH" env:"STAUDITFILE"` - DBMaintenanceInterval time.Duration `help:"Database maintenance interval" default:"8h" env:"STDBMAINTENANCEINTERVAL"` + DBMaintenanceInterval time.Duration `help:"Database maintenance interval; set to zero to disable periodic maintenance" default:"8h" env:"STDBMAINTENANCEINTERVAL"` DBDeleteRetentionInterval time.Duration `help:"Database deleted item retention interval" default:"10920h" env:"STDBDELETERETENTIONINTERVAL"` GUIAddress string `name:"gui-address" help:"Override GUI address (e.g. \"http://192.0.2.42:8443\")" placeholder:"URL" env:"STGUIADDRESS"` GUIAPIKey string `name:"gui-apikey" help:"Override GUI API key" placeholder:"API-KEY" env:"STGUIAPIKEY"` diff --git a/internal/db/interface.go b/internal/db/interface.go index 014c036a6..447f64cba 100644 --- a/internal/db/interface.go +++ b/internal/db/interface.go @@ -15,8 +15,17 @@ import ( "github.com/thejerf/suture/v4" ) +type DBService interface { + suture.Service + + // Starts maintenance asynchronously, if not already running + StartMaintenance() +} + type DB interface { - Service(maintenanceInterval time.Duration) suture.Service + // Create a service that performs database maintenance periodically (no + // more often than the requested interval) + Service(maintenanceInterval time.Duration) DBService // Basics Update(folder string, device protocol.DeviceID, fs []protocol.FileInfo) error diff --git a/internal/db/sqlite/db_service.go b/internal/db/sqlite/db_service.go index 792a1eca3..448b084cd 100644 --- a/internal/db/sqlite/db_service.go +++ b/internal/db/sqlite/db_service.go @@ -19,7 +19,6 @@ import ( "github.com/syncthing/syncthing/internal/db" "github.com/syncthing/syncthing/internal/slogutil" "github.com/syncthing/syncthing/lib/protocol" - "github.com/thejerf/suture/v4" ) const ( @@ -32,7 +31,7 @@ const ( gcMaxRuntime = 5 * time.Minute // max time to spend on gc, per table, per run ) -func (s *DB) Service(maintenanceInterval time.Duration) suture.Service { +func (s *DB) Service(maintenanceInterval time.Duration) db.DBService { return newService(s, maintenanceInterval) } @@ -40,6 +39,7 @@ type Service struct { sdb *DB maintenanceInterval time.Duration internalMeta *db.Typed + start chan struct{} } func (s *Service) String() string { @@ -51,12 +51,19 @@ func newService(sdb *DB, maintenanceInterval time.Duration) *Service { sdb: sdb, maintenanceInterval: maintenanceInterval, internalMeta: db.NewTyped(sdb, internalMetaPrefix), + start: make(chan struct{}), + } +} + +func (s *Service) StartMaintenance() { + select { + case s.start <- struct{}{}: + default: } } func (s *Service) Serve(ctx context.Context) error { // Run periodic maintenance - // Figure out when we last ran maintenance and schedule accordingly. If // it was never, do it now. lastMaint, _, _ := s.internalMeta.Time(lastMaintKey) @@ -66,21 +73,29 @@ func (s *Service) Serve(ctx context.Context) error { wait = time.Minute } slog.DebugContext(ctx, "Next periodic run due", "after", wait) - timer := time.NewTimer(wait) + + if s.maintenanceInterval == 0 { + timer.Stop() + } + for { select { case <-ctx.Done(): return ctx.Err() case <-timer.C: + case <-s.start: } if err := s.periodic(ctx); err != nil { return wrap(err) } - timer.Reset(s.maintenanceInterval) - slog.DebugContext(ctx, "Next periodic run due", "after", s.maintenanceInterval) + if s.maintenanceInterval != 0 { + timer.Reset(s.maintenanceInterval) + slog.DebugContext(ctx, "Next periodic run due", "after", s.maintenanceInterval) + } + _ = s.internalMeta.PutTime(lastMaintKey, time.Now()) } } diff --git a/lib/syncthing/syncthing.go b/lib/syncthing/syncthing.go index f3aaba8ec..21b186c2f 100644 --- a/lib/syncthing/syncthing.go +++ b/lib/syncthing/syncthing.go @@ -72,6 +72,7 @@ type App struct { stopOnce sync.Once mainServiceCancel context.CancelFunc stopped chan struct{} + dbService db.DBService // Access to internals for direct users of this package. Note that the interface in Internals is unstable! Internals *Internals @@ -114,10 +115,16 @@ func (a *App) Start() error { return nil } +// StartMaintenance asynchronously triggers database maintenance to start. +func (a *App) StartMaintenance() { + a.dbService.StartMaintenance() +} + func (a *App) startup() error { a.mainService.Add(ur.NewFailureHandler(a.cfg, a.evLogger)) - a.mainService.Add(a.sdb.Service(a.opts.DBMaintenanceInterval)) + a.dbService = a.sdb.Service(a.opts.DBMaintenanceInterval) + a.mainService.Add(a.dbService) if a.opts.AuditWriter != nil { a.mainService.Add(newAuditService(a.opts.AuditWriter, a.evLogger))