Browse Source

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.
Jakob Borg 2 months ago
parent
commit
d776657b52
2 changed files with 34 additions and 2 deletions
  1. 1 1
      cmd/syncthing/main.go
  2. 33 1
      lib/syncthing/utils.go

+ 1 - 1
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)
 	}

+ 33 - 1
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()
+}