From d776657b523169ece0d4982e6766ca35a6c1d7cf Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Fri, 22 Aug 2025 08:35:42 +0200 Subject: [PATCH] fix(cmd): provide temporary GUI/API server during database migration (#10279) This adds a temporary GUI/API server during the database migration. It responds with 200 OK and some log output for every request. This serves two purposes: - Primarily, for deployments that use the API as a health check, it gives them something positive to accept during the migration, reducing the risk of the migration getting killed halfway through and restarted, thus never completing. - Secondarily, it gives humans who happen to try to load the GUI some sort of indication of what's going on. Obviously, anything that expects a well-formed API response at this stage is still going to fail. They were already failing though, as we didn't even listen at this point before. --- cmd/syncthing/main.go | 2 +- lib/syncthing/utils.go | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index d7ff88a07..57d64d3df 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -479,7 +479,7 @@ func (c *serveCmd) syncthingMain() { }) } - if err := syncthing.TryMigrateDatabase(c.DBDeleteRetentionInterval); err != nil { + if err := syncthing.TryMigrateDatabase(ctx, c.DBDeleteRetentionInterval, cfgWrapper.GUI().Address()); err != nil { slog.Error("Failed to migrate old-style database", slogutil.Error(err)) os.Exit(1) } diff --git a/lib/syncthing/utils.go b/lib/syncthing/utils.go index 5e5cb6937..1fd646a9e 100644 --- a/lib/syncthing/utils.go +++ b/lib/syncthing/utils.go @@ -7,11 +7,13 @@ package syncthing import ( + "context" "crypto/tls" "errors" "fmt" "io" "log/slog" + "net/http" "os" "sync" "time" @@ -156,7 +158,7 @@ func OpenDatabase(path string, deleteRetention time.Duration) (db.DB, error) { } // Attempts migration of the old (LevelDB-based) database type to the new (SQLite-based) type -func TryMigrateDatabase(deleteRetention time.Duration) error { +func TryMigrateDatabase(ctx context.Context, deleteRetention time.Duration, apiAddr string) error { oldDBDir := locations.Get(locations.LegacyDatabase) if _, err := os.Lstat(oldDBDir); err != nil { // No old database @@ -170,6 +172,12 @@ func TryMigrateDatabase(deleteRetention time.Duration) error { } defer be.Close() + // Start a temporary API server during the migration + api := migratingAPI{addr: apiAddr} + apiCtx, cancel := context.WithCancel(ctx) + defer cancel() + go api.Serve(apiCtx) + sdb, err := sqlite.OpenForMigration(locations.Get(locations.Database)) if err != nil { return err @@ -284,3 +292,27 @@ func TryMigrateDatabase(deleteRetention time.Duration) error { slog.Info("Migration complete", "files", totFiles, "blocks", totBlocks/1000, "duration", time.Since(t0).Truncate(time.Second)) return nil } + +type migratingAPI struct { + addr string +} + +func (m migratingAPI) Serve(ctx context.Context) error { + srv := &http.Server{ + Addr: m.addr, + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.Write([]byte("*** Database migration in progress ***\n\n")) + for _, line := range slogutil.GlobalRecorder.Since(time.Time{}) { + line.WriteTo(w) + } + }), + } + go func() { + slog.InfoContext(ctx, "Starting temporary GUI/API during migration", slogutil.Address(m.addr)) + err := srv.ListenAndServe() + slog.InfoContext(ctx, "Temporary GUI/API closed", slogutil.Address(m.addr), slogutil.Error(err)) + }() + <-ctx.Done() + return srv.Close() +}