Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions doc/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,40 @@ The following variable are available on particular hooks:
### Examples

See [sample hooks](https://github.com/github/gh-ost/tree/master/resources/hooks-sample), as `bash` implementation samples.

### Embedded usage: registering Go callbacks

When `gh-ost` is consumed as a library (importing `github.com/github/gh-ost/go/logic`), callers can register Go functions for any hook event instead of, or in addition to, the on-disk script contract. Implement the `base.Hooks` interface and assign it to `MigrationContext.Hooks` before calling `logic.NewMigrator`:

```go
import (
"github.com/github/gh-ost/go/base"
"github.com/github/gh-ost/go/logic"
)

const version = "1.1.8"

type myHooks struct{}

func (myHooks) OnSuccess(instantDDL bool) error { return nil }
func (myHooks) OnFailure() error { return nil }
// ... implement the remaining base.Hooks methods.

ctx := base.NewMigrationContext()
// ... configure ctx (DatabaseName, OriginalTableName, AlterStatement, etc.)

ctx.Hooks = &myHooks{}
Comment on lines +110 to +114
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0927d5f — snippet now declares version and constructs ctx via base.NewMigrationContext().

m := logic.NewMigrator(ctx, version)
err := m.Migrate()
```

To run shell hooks from `--hooks-path` and Go callbacks together, wrap both in `logic.CompositeHooks`. Each member is invoked in order; the first non-nil error short-circuits, matching the script executor's behavior:

```go
ctx.Hooks = logic.CompositeHooks{
logic.NewHooksExecutor(ctx), // existing scripts under HooksPath
&myHooks{}, // additional Go callbacks
}
```

`MigrationContext.Hooks` is opt-in. When it is nil, `NewMigrator` wires the default script executor and behavior is identical to the CLI. Hooks are read once at `NewMigrator` time, so reassigning the field afterwards has no effect on the running migration.
1 change: 1 addition & 0 deletions go/base/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ type MigrationContext struct {
HooksHintOwner string
HooksHintToken string
HooksStatusIntervalSec int64
Hooks Hooks
PanicOnWarnings bool
Checkpoint bool
CheckpointIntervalSeconds int64
Expand Down
24 changes: 24 additions & 0 deletions go/base/hooks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
Copyright 2026 GitHub Inc.
See https://github.com/github/gh-ost/blob/master/LICENSE
*/

package base

// Hooks is the set of lifecycle callbacks gh-ost invokes during a migration.
type Hooks interface {
OnStartup() error
OnValidated() error
OnRowCountComplete() error
OnBeforeRowCopy() error
OnRowCopyComplete() error
OnBeginPostponed() error
OnBeforeCutOver() error
OnInteractiveCommand(command string) error
OnSuccess(instantDDL bool) error
OnFailure() error
OnBatchCopyRetry(errorMessage string) error
OnStatus(statusMessage string) error
OnStopReplication() error
OnStartReplication() error
}
199 changes: 185 additions & 14 deletions go/logic/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,177 @@ const (
onStartReplication = "gh-ost-on-start-replication"
)

// CompositeHooks invokes each member in order, returning the first non-nil error.
type CompositeHooks []base.Hooks

func (c CompositeHooks) OnStartup() error {
for _, h := range c {
if h == nil {
continue
}
if err := h.OnStartup(); err != nil {
return err
}
}
Comment on lines +40 to +48
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0927d5f — each fan-out method now skips nil members, with a TestCompositeHooks_SkipsNil test.

return nil
}

func (c CompositeHooks) OnValidated() error {
for _, h := range c {
if h == nil {
continue
}
if err := h.OnValidated(); err != nil {
return err
}
}
return nil
}

func (c CompositeHooks) OnRowCountComplete() error {
for _, h := range c {
if h == nil {
continue
}
if err := h.OnRowCountComplete(); err != nil {
return err
}
}
return nil
}

func (c CompositeHooks) OnBeforeRowCopy() error {
for _, h := range c {
if h == nil {
continue
}
if err := h.OnBeforeRowCopy(); err != nil {
return err
}
}
return nil
}

func (c CompositeHooks) OnRowCopyComplete() error {
for _, h := range c {
if h == nil {
continue
}
if err := h.OnRowCopyComplete(); err != nil {
return err
}
}
return nil
}

func (c CompositeHooks) OnBeginPostponed() error {
for _, h := range c {
if h == nil {
continue
}
if err := h.OnBeginPostponed(); err != nil {
return err
}
}
return nil
}

func (c CompositeHooks) OnBeforeCutOver() error {
for _, h := range c {
if h == nil {
continue
}
if err := h.OnBeforeCutOver(); err != nil {
return err
}
}
return nil
}

func (c CompositeHooks) OnInteractiveCommand(command string) error {
for _, h := range c {
if h == nil {
continue
}
if err := h.OnInteractiveCommand(command); err != nil {
return err
}
}
return nil
}

func (c CompositeHooks) OnSuccess(instantDDL bool) error {
for _, h := range c {
if h == nil {
continue
}
if err := h.OnSuccess(instantDDL); err != nil {
return err
}
}
return nil
}

func (c CompositeHooks) OnFailure() error {
for _, h := range c {
if h == nil {
continue
}
if err := h.OnFailure(); err != nil {
return err
}
}
return nil
}

func (c CompositeHooks) OnBatchCopyRetry(errorMessage string) error {
for _, h := range c {
if h == nil {
continue
}
if err := h.OnBatchCopyRetry(errorMessage); err != nil {
return err
}
}
return nil
}

func (c CompositeHooks) OnStatus(statusMessage string) error {
for _, h := range c {
if h == nil {
continue
}
if err := h.OnStatus(statusMessage); err != nil {
return err
}
}
return nil
}

func (c CompositeHooks) OnStopReplication() error {
for _, h := range c {
if h == nil {
continue
}
if err := h.OnStopReplication(); err != nil {
return err
}
}
return nil
}

func (c CompositeHooks) OnStartReplication() error {
for _, h := range c {
if h == nil {
continue
}
if err := h.OnStartReplication(); err != nil {
return err
}
}
return nil
}

type HooksExecutor struct {
migrationContext *base.MigrationContext
writer io.Writer
Expand Down Expand Up @@ -111,61 +282,61 @@ func (he *HooksExecutor) executeHooks(baseName string, extraVariables ...string)
return nil
}

func (he *HooksExecutor) onStartup() error {
func (he *HooksExecutor) OnStartup() error {
return he.executeHooks(onStartup)
}

func (he *HooksExecutor) onValidated() error {
func (he *HooksExecutor) OnValidated() error {
return he.executeHooks(onValidated)
}

func (he *HooksExecutor) onRowCountComplete() error {
func (he *HooksExecutor) OnRowCountComplete() error {
return he.executeHooks(onRowCountComplete)
}
func (he *HooksExecutor) onBeforeRowCopy() error {
func (he *HooksExecutor) OnBeforeRowCopy() error {
return he.executeHooks(onBeforeRowCopy)
}

func (he *HooksExecutor) onBatchCopyRetry(errorMessage string) error {
func (he *HooksExecutor) OnBatchCopyRetry(errorMessage string) error {
v := fmt.Sprintf("GH_OST_LAST_BATCH_COPY_ERROR=%s", errorMessage)
return he.executeHooks(onBatchCopyRetry, v)
}

func (he *HooksExecutor) onRowCopyComplete() error {
func (he *HooksExecutor) OnRowCopyComplete() error {
return he.executeHooks(onRowCopyComplete)
}

func (he *HooksExecutor) onBeginPostponed() error {
func (he *HooksExecutor) OnBeginPostponed() error {
return he.executeHooks(onBeginPostponed)
}

func (he *HooksExecutor) onBeforeCutOver() error {
func (he *HooksExecutor) OnBeforeCutOver() error {
return he.executeHooks(onBeforeCutOver)
}

func (he *HooksExecutor) onInteractiveCommand(command string) error {
func (he *HooksExecutor) OnInteractiveCommand(command string) error {
v := fmt.Sprintf("GH_OST_COMMAND='%s'", command)
return he.executeHooks(onInteractiveCommand, v)
}

func (he *HooksExecutor) onSuccess(instantDDL bool) error {
func (he *HooksExecutor) OnSuccess(instantDDL bool) error {
v := fmt.Sprintf("GH_OST_INSTANT_DDL=%t", instantDDL)
return he.executeHooks(onSuccess, v)
}

func (he *HooksExecutor) onFailure() error {
func (he *HooksExecutor) OnFailure() error {
return he.executeHooks(onFailure)
}

func (he *HooksExecutor) onStatus(statusMessage string) error {
func (he *HooksExecutor) OnStatus(statusMessage string) error {
v := fmt.Sprintf("GH_OST_STATUS='%s'", statusMessage)
return he.executeHooks(onStatus, v)
}

func (he *HooksExecutor) onStopReplication() error {
func (he *HooksExecutor) OnStopReplication() error {
return he.executeHooks(onStopReplication)
}

func (he *HooksExecutor) onStartReplication() error {
func (he *HooksExecutor) OnStartReplication() error {
return he.executeHooks(onStartReplication)
}
Loading
Loading