diff --git a/doc/interactive-commands.md b/doc/interactive-commands.md index 44413d580..af37935d1 100644 --- a/doc/interactive-commands.md +++ b/doc/interactive-commands.md @@ -41,6 +41,7 @@ Both interfaces may serve at the same time. Both respond to simple text command, - `throttle-control-replicas='replica1,replica2'`: change list of throttle-control replicas, these are replicas `gh-ost` will check. This takes a comma separated list of replica's to check and replaces the previous list. - `throttle`: force migration suspend - `no-throttle`: cancel forced suspension (though other throttling reasons may still apply) +- `postpone-cut-over-flag-file=`: Postpone the [cut-over](cut-over.md) phase, writing a cut over flag file to the given path - `unpostpone`: at a time where `gh-ost` is postponing the [cut-over](cut-over.md) phase, instruct `gh-ost` to stop postponing and proceed immediately to cut-over. - `panic`: immediately panic and abort operation diff --git a/go/logic/server.go b/go/logic/server.go index 4e41fd26b..b5d05b758 100644 --- a/go/logic/server.go +++ b/go/logic/server.go @@ -99,6 +99,16 @@ func (this *Server) runCPUProfile(args string) (io.Reader, error) { return &buf, nil } +func (this *Server) createPostponeCutOverFlagFile(filePath string) (err error) { + if !base.FileExists(filePath) { + if err := base.TouchFile(filePath); err != nil { + return fmt.Errorf("Failed to create postpone cut-over flag file %s: %w", filePath, err) + } + this.migrationContext.Log.Infof("Created postpone-cut-over-flag-file: %s", filePath) + } + return nil +} + func (this *Server) BindSocketFile() (err error) { if this.migrationContext.ServeSocketFile == "" { return nil @@ -222,6 +232,7 @@ throttle-http= # Set a new throttle URL throttle-control-replicas= # Set a new comma delimited list of throttle control replicas throttle # Force throttling no-throttle # End forced throttling (other throttling may still apply) +postpone-cut-over-flag-file= # Postpone the cut-over phase, writing a cut over flag file to the given path unpostpone # Bail out a cut-over postpone; proceed to cut-over panic # panic and quit without cleanup help # This message @@ -395,6 +406,19 @@ help # This message atomic.StoreInt64(&this.migrationContext.ThrottleCommandedByUser, 0) return ForcePrintStatusAndHintRule, nil } + case "postpone-cut-over-flag-file": + { + if arg == "" { + err := fmt.Errorf("User commanded 'postpone-cut-over-flag-file' without specifying file path") + return NoPrintStatusRule, err + } + if err := this.createPostponeCutOverFlagFile(arg); err != nil { + return NoPrintStatusRule, err + } + this.migrationContext.PostponeCutOverFlagFile = arg + fmt.Fprintf(writer, "Postponed\n") + return ForcePrintStatusAndHintRule, nil + } case "unpostpone", "no-postpone", "cut-over": { if arg == "" && this.migrationContext.ForceNamedCutOverCommand { diff --git a/go/logic/server_test.go b/go/logic/server_test.go index 78f0fd6e6..52624a27d 100644 --- a/go/logic/server_test.go +++ b/go/logic/server_test.go @@ -1,6 +1,8 @@ package logic import ( + "os" + "path" "testing" "time" @@ -66,3 +68,38 @@ func TestServerRunCPUProfile(t *testing.T) { require.Equal(t, int64(0), s.isCPUProfiling) }) } + +func TestServerCreatePostponeCutOverFlagFile(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + s := &Server{ + migrationContext: base.NewMigrationContext(), + } + dir, err := os.MkdirTemp("", "gh-ost-test-") + require.NoError(t, err) + defer os.RemoveAll(dir) + + filePath := path.Join(dir, "postpone-cut-over.flag") + + err = s.createPostponeCutOverFlagFile(filePath) + require.NoError(t, err) + require.FileExists(t, filePath) + }) + + t.Run("file already exists", func(t *testing.T) { + s := &Server{ + migrationContext: base.NewMigrationContext(), + } + dir, err := os.MkdirTemp("", "gh-ost-test-") + require.NoError(t, err) + + filePath := path.Join(dir, "postpone-cut-over.flag") + err = base.TouchFile(filePath) + require.NoError(t, err) + + err = s.createPostponeCutOverFlagFile(filePath) + require.NoError(t, err) + require.FileExists(t, filePath) + }) +}