Skip to content

Commit 277dfa3

Browse files
authored
Merge pull request #30 from xtrimf/support_for_triggers_no_rollback
Support for triggers no rollback
2 parents ded0f91 + 765a1df commit 277dfa3

File tree

7 files changed

+260
-2
lines changed

7 files changed

+260
-2
lines changed

go/base/context.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"sync"
1515
"sync/atomic"
1616
"time"
17+
"unicode/utf8"
1718

1819
uuid "github.com/satori/go.uuid"
1920

@@ -225,6 +226,11 @@ type MigrationContext struct {
225226
MigrationIterationRangeMaxValues *sql.ColumnValues
226227
ForceTmpTableName string
227228

229+
IncludeTriggers bool
230+
RemoveTriggerSuffix bool
231+
TriggerSuffix string
232+
Triggers []mysql.Trigger
233+
228234
recentBinlogCoordinates mysql.BinlogCoordinates
229235

230236
Log Logger
@@ -854,3 +860,20 @@ func (this *MigrationContext) ReadConfigFile() error {
854860

855861
return nil
856862
}
863+
864+
// getGhostTriggerName generates the name of a ghost trigger, based on original trigger name
865+
// or a given trigger name
866+
func (this *MigrationContext) GetGhostTriggerName(triggerName string) string {
867+
if this.RemoveTriggerSuffix && strings.HasSuffix(triggerName, this.TriggerSuffix) {
868+
return strings.TrimSuffix(triggerName, this.TriggerSuffix)
869+
}
870+
// else
871+
return triggerName + this.TriggerSuffix
872+
}
873+
874+
// validateGhostTriggerLength check if the ghost trigger name length is not more than 64 characters
875+
func (this *MigrationContext) ValidateGhostTriggerLengthBelowMaxLength(triggerName string) bool {
876+
ghostTriggerName := this.GetGhostTriggerName(triggerName)
877+
878+
return utf8.RuneCountInString(ghostTriggerName) <= mysql.MaxTableNameLength
879+
}

go/base/context_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"io/ioutil"
1111
"os"
12+
"strings"
1213
"testing"
1314
"time"
1415

@@ -60,6 +61,68 @@ func TestGetTableNames(t *testing.T) {
6061
}
6162
}
6263

64+
func TestGetTriggerNames(t *testing.T) {
65+
{
66+
context := NewMigrationContext()
67+
context.TriggerSuffix = "_gho"
68+
test.S(t).ExpectEquals(context.GetGhostTriggerName("my_trigger"), "my_trigger"+context.TriggerSuffix)
69+
}
70+
{
71+
context := NewMigrationContext()
72+
context.TriggerSuffix = "_gho"
73+
context.RemoveTriggerSuffix = true
74+
test.S(t).ExpectEquals(context.GetGhostTriggerName("my_trigger"), "my_trigger"+context.TriggerSuffix)
75+
}
76+
{
77+
context := NewMigrationContext()
78+
context.TriggerSuffix = "_gho"
79+
context.RemoveTriggerSuffix = true
80+
test.S(t).ExpectEquals(context.GetGhostTriggerName("my_trigger_gho"), "my_trigger")
81+
}
82+
{
83+
context := NewMigrationContext()
84+
context.TriggerSuffix = "_gho"
85+
context.RemoveTriggerSuffix = false
86+
test.S(t).ExpectEquals(context.GetGhostTriggerName("my_trigger_gho"), "my_trigger_gho_gho")
87+
}
88+
89+
}
90+
func TestValidateGhostTriggerLengthBelowMaxLength(t *testing.T) {
91+
{
92+
context := NewMigrationContext()
93+
context.TriggerSuffix = "_gho"
94+
test.S(t).ExpectEquals(context.ValidateGhostTriggerLengthBelowMaxLength("my_trigger"), true)
95+
}
96+
{
97+
context := NewMigrationContext()
98+
context.TriggerSuffix = "_ghost"
99+
test.S(t).ExpectEquals(context.ValidateGhostTriggerLengthBelowMaxLength(strings.Repeat("my_trigger_ghost", 4)), false) // 64 characters + "_ghost"
100+
}
101+
{
102+
context := NewMigrationContext()
103+
context.TriggerSuffix = "_ghost"
104+
test.S(t).ExpectEquals(context.ValidateGhostTriggerLengthBelowMaxLength(strings.Repeat("my_trigger_ghost", 3)), true) // 48 characters + "_ghost"
105+
}
106+
{
107+
context := NewMigrationContext()
108+
context.TriggerSuffix = "_ghost"
109+
context.RemoveTriggerSuffix = true
110+
test.S(t).ExpectEquals(context.ValidateGhostTriggerLengthBelowMaxLength(strings.Repeat("my_trigger_ghost", 4)), true) // 64 characters + "_ghost" removed
111+
}
112+
{
113+
context := NewMigrationContext()
114+
context.TriggerSuffix = "_ghost"
115+
context.RemoveTriggerSuffix = true
116+
test.S(t).ExpectEquals(context.ValidateGhostTriggerLengthBelowMaxLength(strings.Repeat("my_trigger_ghost", 4)+"X"), false) // 65 characters + "_ghost" not removed
117+
}
118+
{
119+
context := NewMigrationContext()
120+
context.TriggerSuffix = "_ghost"
121+
context.RemoveTriggerSuffix = true
122+
test.S(t).ExpectEquals(context.ValidateGhostTriggerLengthBelowMaxLength(strings.Repeat("my_trigger_ghost", 4)+"_ghost"), true) // 70 characters + last "_ghost" removed
123+
}
124+
}
125+
63126
func TestReadConfigFile(t *testing.T) {
64127
{
65128
context := NewMigrationContext()

go/cmd/gh-ost/main.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"fmt"
1111
"os"
1212
"os/signal"
13+
"regexp"
1314
"syscall"
1415

1516
"github.com/github/gh-ost/go/base"
@@ -128,6 +129,10 @@ func main() {
128129

129130
flag.UintVar(&migrationContext.ReplicaServerId, "replica-server-id", 99999, "server id used by gh-ost process. Default: 99999")
130131

132+
flag.BoolVar(&migrationContext.IncludeTriggers, "include-triggers", false, "When true, the triggers (if exist) will be created on the new table")
133+
flag.StringVar(&migrationContext.TriggerSuffix, "trigger-suffix", "", "Add a suffix to the trigger name (i.e '_v2'). Requires '--include-triggers'")
134+
flag.BoolVar(&migrationContext.RemoveTriggerSuffix, "remove-trigger-suffix-if-exists", false, "Remove given suffix from name of trigger. Requires '--include-triggers' and '--trigger-suffix'")
135+
131136
maxLoad := flag.String("max-load", "", "Comma delimited status-name=threshold. e.g: 'Threads_running=100,Threads_connected=500'. When status exceeds threshold, app throttles writes")
132137
criticalLoad := flag.String("critical-load", "", "Comma delimited status-name=threshold, same format as --max-load. When status exceeds threshold, app panics and quits")
133138
flag.Int64Var(&migrationContext.CriticalLoadIntervalMilliseconds, "critical-load-interval-millis", 0, "When 0, migration immediately bails out upon meeting critical-load. When non-zero, a second check is done after given interval, and migration only bails out if 2nd check still meets critical load")
@@ -236,6 +241,19 @@ func main() {
236241
if *replicationLagQuery != "" {
237242
migrationContext.Log.Warningf("--replication-lag-query is deprecated")
238243
}
244+
if migrationContext.IncludeTriggers && migrationContext.TriggerSuffix == "" {
245+
migrationContext.Log.Fatalf("--trigger-suffix must be used with --include-triggers")
246+
}
247+
if !migrationContext.IncludeTriggers && migrationContext.TriggerSuffix != "" {
248+
migrationContext.Log.Fatalf("--trigger-suffix cannot be be used without --include-triggers")
249+
}
250+
if migrationContext.TriggerSuffix != "" {
251+
regex := regexp.MustCompile(`^[\da-zA-Z_]+$`)
252+
253+
if !regex.Match([]byte(migrationContext.TriggerSuffix)) {
254+
migrationContext.Log.Fatalf("--trigger-suffix must contain only alpha numeric characters and underscore (0-9,a-z,A-Z,_)")
255+
}
256+
}
239257

240258
switch *cutOver {
241259
case "atomic", "default", "":

go/logic/applier.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,56 @@ func (this *Applier) dropTable(tableName string) error {
334334
return nil
335335
}
336336

337+
// dropTriggers drop the triggers on the applied host
338+
func (this *Applier) DropTriggersFromGhost() error {
339+
if len(this.migrationContext.Triggers) > 0 {
340+
for _, trigger := range this.migrationContext.Triggers {
341+
triggerName := this.migrationContext.GetGhostTriggerName(trigger.Name)
342+
query := fmt.Sprintf("drop trigger if exists %s", sql.EscapeName(triggerName))
343+
_, err := sqlutils.ExecNoPrepare(this.db, query)
344+
if err != nil {
345+
return err
346+
}
347+
this.migrationContext.Log.Infof("Trigger '%s' dropped", triggerName)
348+
}
349+
}
350+
return nil
351+
}
352+
353+
// createTriggers creates the triggers on the applied host
354+
func (this *Applier) createTriggers(tableName string) error {
355+
if len(this.migrationContext.Triggers) > 0 {
356+
for _, trigger := range this.migrationContext.Triggers {
357+
triggerName := this.migrationContext.GetGhostTriggerName(trigger.Name)
358+
query := fmt.Sprintf(`create /* gh-ost */ trigger %s %s %s on %s.%s for each row
359+
%s`,
360+
sql.EscapeName(triggerName),
361+
trigger.Timing,
362+
trigger.Event,
363+
sql.EscapeName(this.migrationContext.DatabaseName),
364+
sql.EscapeName(tableName),
365+
trigger.Statement,
366+
)
367+
this.migrationContext.Log.Infof("Createing trigger %s on %s.%s",
368+
sql.EscapeName(triggerName),
369+
sql.EscapeName(this.migrationContext.DatabaseName),
370+
sql.EscapeName(tableName),
371+
)
372+
if _, err := sqlutils.ExecNoPrepare(this.db, query); err != nil {
373+
return err
374+
}
375+
}
376+
this.migrationContext.Log.Infof("Triggers created on %s", tableName)
377+
}
378+
return nil
379+
}
380+
381+
// CreateTriggers creates the original triggers on applier host
382+
func (this *Applier) CreateTriggersOnGhost() error {
383+
err := this.createTriggers(this.migrationContext.GetGhostTableName())
384+
return err
385+
}
386+
337387
// DropChangelogTable drops the changelog table on the applier host
338388
func (this *Applier) DropChangelogTable() error {
339389
return this.dropTable(this.migrationContext.GetChangelogTableName())

go/logic/inspect.go

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ func (this *Inspector) validateTableForeignKeys(allowChildForeignKeys bool) erro
483483
return nil
484484
}
485485

486-
// validateTableTriggers makes sure no triggers exist on the migrated table
486+
// validateTableTriggers makes sure no triggers exist on the migrated table. if --include_triggers is used then it fetches the triggers
487487
func (this *Inspector) validateTableTriggers() error {
488488
query := `
489489
SELECT COUNT(*) AS num_triggers
@@ -505,12 +505,74 @@ func (this *Inspector) validateTableTriggers() error {
505505
return err
506506
}
507507
if numTriggers > 0 {
508-
return this.migrationContext.Log.Errorf("Found triggers on %s.%s. Triggers are not supported at this time. Bailing out", sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName))
508+
if this.migrationContext.IncludeTriggers {
509+
this.migrationContext.Log.Infof("Found %d triggers on %s.%s.", numTriggers, sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName))
510+
this.migrationContext.Triggers, err = mysql.GetTriggers(this.db, this.migrationContext.DatabaseName, this.migrationContext.OriginalTableName)
511+
if err != nil {
512+
return err
513+
}
514+
if err := this.validateGhostTriggersDontExist(); err != nil {
515+
return err
516+
}
517+
if err := this.validateGhostTriggersLength(); err != nil {
518+
return err
519+
}
520+
return nil
521+
}
522+
return this.migrationContext.Log.Errorf("Found triggers on %s.%s. Tables with triggers are supported only when using \"include-triggers\" flag. Bailing out", sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName))
523+
509524
}
510525
this.migrationContext.Log.Debugf("Validated no triggers exist on table")
511526
return nil
512527
}
513528

529+
// verifyTriggersDontExist verifies before createing new triggers we want to make sure these triggers dont exist already in the DB
530+
func (this *Inspector) validateGhostTriggersDontExist() error {
531+
if len(this.migrationContext.Triggers) > 0 {
532+
var foundTriggers []string
533+
for _, trigger := range this.migrationContext.Triggers {
534+
triggerName := this.migrationContext.GetGhostTriggerName(trigger.Name)
535+
query := "select 1 from information_schema.triggers where trigger_name = ? and trigger_schema = ?"
536+
err := sqlutils.QueryRowsMap(this.db, query, func(rowMap sqlutils.RowMap) error {
537+
triggerExists := rowMap.GetInt("1")
538+
if triggerExists == 1 {
539+
foundTriggers = append(foundTriggers, triggerName)
540+
}
541+
return nil
542+
},
543+
triggerName,
544+
this.migrationContext.DatabaseName,
545+
)
546+
if err != nil {
547+
return err
548+
}
549+
}
550+
if len(foundTriggers) > 0 {
551+
return this.migrationContext.Log.Errorf("Found gh-ost triggers (%s). Please use a different suffix or drop them. Bailing out", strings.Join(foundTriggers, ","))
552+
}
553+
}
554+
555+
return nil
556+
}
557+
558+
func (this *Inspector) validateGhostTriggersLength() error {
559+
if len(this.migrationContext.Triggers) > 0 {
560+
var foundTriggers []string
561+
for _, trigger := range this.migrationContext.Triggers {
562+
triggerName := this.migrationContext.GetGhostTriggerName(trigger.Name)
563+
if ok := this.migrationContext.ValidateGhostTriggerLengthBelowMaxLength(triggerName); !ok {
564+
foundTriggers = append(foundTriggers, triggerName)
565+
}
566+
567+
}
568+
if len(foundTriggers) > 0 {
569+
return this.migrationContext.Log.Errorf("Gh-ost triggers (%s) length > %d characters. Bailing out", strings.Join(foundTriggers, ","), mysql.MaxTableNameLength)
570+
}
571+
}
572+
573+
return nil
574+
}
575+
514576
// estimateTableRowsViaExplain estimates number of rows on original table
515577
func (this *Inspector) estimateTableRowsViaExplain() error {
516578
query := fmt.Sprintf(`explain select /* gh-ost */ * from %s.%s where 1=1`, sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName))

go/logic/migrator.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,12 @@ func (this *Migrator) cutOverTwoStep() (err error) {
615615
if err := this.retryOperation(this.waitForEventsUpToLock); err != nil {
616616
return err
617617
}
618+
// If we need to create triggers we need to do it here (only create part)
619+
if this.migrationContext.IncludeTriggers && len(this.migrationContext.Triggers) > 0 {
620+
if this.retryOperation(this.applier.CreateTriggersOnGhost); err != nil {
621+
return err
622+
}
623+
}
618624
if err := this.retryOperation(this.applier.SwapTablesQuickAndBumpy); err != nil {
619625
return err
620626
}
@@ -663,6 +669,13 @@ func (this *Migrator) atomicCutOver() (err error) {
663669
return this.migrationContext.Log.Errore(err)
664670
}
665671

672+
// If we need to create triggers we need to do it here (only create part)
673+
if this.migrationContext.IncludeTriggers && len(this.migrationContext.Triggers) > 0 {
674+
if err := this.applier.CreateTriggersOnGhost(); err != nil {
675+
this.migrationContext.Log.Errore(err)
676+
}
677+
}
678+
666679
// Step 2
667680
// We now attempt an atomic RENAME on original & ghost tables, and expect it to block.
668681
this.migrationContext.RenameTablesStartTime = time.Now()
@@ -720,6 +733,7 @@ func (this *Migrator) atomicCutOver() (err error) {
720733
// ooh nice! We're actually truly and thankfully done
721734
lockAndRenameDuration := this.migrationContext.RenameTablesEndTime.Sub(this.migrationContext.LockTablesStartTime)
722735
this.migrationContext.Log.Infof("Lock & rename duration: %s. During this time, queries on %s were blocked", lockAndRenameDuration, sql.EscapeName(this.migrationContext.OriginalTableName))
736+
723737
return nil
724738
}
725739

go/mysql/utils.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ type ReplicationLagResult struct {
3030
Err error
3131
}
3232

33+
type Trigger struct {
34+
Name string
35+
Event string
36+
Statement string
37+
Timing string
38+
}
39+
3340
func NewNoReplicationLagResult() *ReplicationLagResult {
3441
return &ReplicationLagResult{Lag: 0, Err: nil}
3542
}
@@ -205,3 +212,24 @@ func GetTableColumns(db *gosql.DB, databaseName, tableName string) (*sql.ColumnL
205212
}
206213
return sql.NewColumnList(columnNames), sql.NewColumnList(virtualColumnNames), nil
207214
}
215+
216+
// GetTriggers reads trigger list from given table
217+
func GetTriggers(db *gosql.DB, databaseName, tableName string) (triggers []Trigger, err error) {
218+
query := fmt.Sprintf(`select trigger_name as name, event_manipulation as event, action_statement as statement, action_timing as timing
219+
from information_schema.triggers
220+
where trigger_schema = '%s' and event_object_table = '%s'`, databaseName, tableName)
221+
222+
err = sqlutils.QueryRowsMap(db, query, func(rowMap sqlutils.RowMap) error {
223+
triggers = append(triggers, Trigger{
224+
Name: rowMap.GetString("name"),
225+
Event: rowMap.GetString("event"),
226+
Statement: rowMap.GetString("statement"),
227+
Timing: rowMap.GetString("timing"),
228+
})
229+
return nil
230+
})
231+
if err != nil {
232+
return nil, err
233+
}
234+
return triggers, nil
235+
}

0 commit comments

Comments
 (0)