Skip to content

Commit 7573c12

Browse files
author
Shlomi Noach
authored
Merge pull request #53 from github/listen-socket
adding interactive user commands
2 parents c9d02b9 + fc00cb2 commit 7573c12

File tree

7 files changed

+260
-17
lines changed

7 files changed

+260
-17
lines changed

build.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/bin/bash
22
#
33
#
4-
RELEASE_VERSION="0.8.3"
4+
RELEASE_VERSION="0.8.4"
55

66
buildpath=/tmp/gh-ost
77
target=gh-ost

doc/interactive-commands.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Interactive commands
2+
3+
`gh-ost` is designed to be operations friendly. To that effect, it allows the user to control its behavior even while it is running.
4+
5+
### Interactive interfaces
6+
7+
`gh-ost` listens on:
8+
9+
- Unix socket file: either provided via `--serve-socket-file` or determined by `gh-ost`, this interface is always up.
10+
When self-determined, `gh-ost` will advertise the identify of socket file upon start up and throughout the migration.
11+
- TCP: if `--serve-tcp-port` is provided
12+
13+
Both interfaces may serve at the same time. Both respond to simple text command, which makes it easy to interact via shell.
14+
15+
### Known commands
16+
17+
- `help`: shows a brief list of available commands
18+
- `status`: returns a status summary of migration progress and configuration
19+
- `throttle`: force migration suspend
20+
- `no-throttle`: cancel forced suspension (though other throttling reasons may still apply)
21+
- `chunk-size=<newsize>`: modify the `chunk-size`; applies on next running copy-iteration
22+
23+
### Examples
24+
25+
While migration is running:
26+
27+
```shell
28+
$ echo status | nc -U /tmp/gh-ost.test.sample_data_0.sock
29+
# Migrating `test`.`sample_data_0`; Ghost table is `test`.`_sample_data_0_gst`
30+
# Migration started at Tue Jun 07 11:45:16 +0200 2016
31+
# chunk-size: 200; max lag: 1500ms; max-load: map[Threads_connected:20]
32+
# Throttle additional flag file: /tmp/gh-ost.throttle
33+
# Serving on unix socket: /tmp/gh-ost.test.sample_data_0.sock
34+
# Serving on TCP port: 10001
35+
Copy: 0/2915 0.0%; Applied: 0; Backlog: 0/100; Elapsed: 40s(copy), 41s(total); streamer: mysql-bin.000550:49942; ETA: throttled, flag-file
36+
```
37+
38+
```shell
39+
$ echo "chunk-size=250" | nc -U /tmp/gh-ost.test.sample_data_0.sock
40+
# Migrating `test`.`sample_data_0`; Ghost table is `test`.`_sample_data_0_gst`
41+
# Migration started at Tue Jun 07 11:56:03 +0200 2016
42+
# chunk-size: 250; max lag: 1500ms; max-load: map[Threads_connected:20]
43+
# Throttle additional flag file: /tmp/gh-ost.throttle
44+
# Serving on unix socket: /tmp/gh-ost.test.sample_data_0.sock
45+
# Serving on TCP port: 10001
46+
```
47+
48+
```shell
49+
$ echo throttle | nc -U /tmp/gh-ost.test.sample_data_0.sock
50+
51+
$ echo status | nc -U /tmp/gh-ost.test.sample_data_0.sock
52+
# Migrating `test`.`sample_data_0`; Ghost table is `test`.`_sample_data_0_gst`
53+
# Migration started at Tue Jun 07 11:56:03 +0200 2016
54+
# chunk-size: 250; max lag: 1500ms; max-load: map[Threads_connected:20]
55+
# Throttle additional flag file: /tmp/gh-ost.throttle
56+
# Serving on unix socket: /tmp/gh-ost.test.sample_data_0.sock
57+
# Serving on TCP port: 10001
58+
Copy: 0/2915 0.0%; Applied: 0; Backlog: 0/100; Elapsed: 59s(copy), 59s(total); streamer: mysql-bin.000551:68067; ETA: throttled, commanded by user
59+
```

go/base/context.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,14 @@ type MigrationContext struct {
6464
ThrottleControlReplicaKeys *mysql.InstanceKeyMap
6565
ThrottleFlagFile string
6666
ThrottleAdditionalFlagFile string
67+
ThrottleCommandedByUser int64
6768
MaxLoad map[string]int64
6869
PostponeSwapTablesFlagFile string
6970
SwapTablesTimeoutSeconds int64
7071

72+
ServeSocketFile string
73+
ServeTCPPort int64
74+
7175
Noop bool
7276
TestOnReplica bool
7377
OkToDropTable bool
@@ -242,6 +246,16 @@ func (this *MigrationContext) TimeSincePointOfInterest() time.Duration {
242246
return time.Now().Sub(this.pointOfInterestTime)
243247
}
244248

249+
func (this *MigrationContext) SetChunkSize(chunkSize int64) {
250+
if chunkSize < 100 {
251+
chunkSize = 100
252+
}
253+
if chunkSize > 100000 {
254+
chunkSize = 100000
255+
}
256+
atomic.StoreInt64(&this.ChunkSize, chunkSize)
257+
}
258+
245259
func (this *MigrationContext) SetThrottled(throttle bool, reason string) {
246260
this.throttleMutex.Lock()
247261
defer this.throttleMutex.Unlock()

go/cmd/gh-ost/main.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,20 +64,18 @@ func main() {
6464
cutOver := flag.String("cut-over", "", "(mandatory) choose cut-over type (two-step, voluntary-lock)")
6565

6666
flag.BoolVar(&migrationContext.SwitchToRowBinlogFormat, "switch-to-rbr", false, "let this tool automatically switch binary log format to 'ROW' on the replica, if needed. The format will NOT be switched back. I'm too scared to do that, and wish to protect you if you happen to execute another migration while this one is running")
67-
flag.Int64Var(&migrationContext.ChunkSize, "chunk-size", 1000, "amount of rows to handle in each iteration (allowed range: 100-100,000)")
68-
if migrationContext.ChunkSize < 100 {
69-
migrationContext.ChunkSize = 100
70-
}
71-
if migrationContext.ChunkSize > 100000 {
72-
migrationContext.ChunkSize = 100000
73-
}
67+
chunkSize := flag.Int64("chunk-size", 1000, "amount of rows to handle in each iteration (allowed range: 100-100,000)")
68+
7469
flag.Int64Var(&migrationContext.MaxLagMillisecondsThrottleThreshold, "max-lag-millis", 1500, "replication lag at which to throttle operation")
7570
flag.StringVar(&migrationContext.ReplictionLagQuery, "replication-lag-query", "", "Query that detects replication lag in seconds. Result can be a floating point (by default gh-ost issues SHOW SLAVE STATUS and reads Seconds_behind_master). If you're using pt-heartbeat, query would be something like: SELECT ROUND(UNIX_TIMESTAMP() - MAX(UNIX_TIMESTAMP(ts))) AS delay FROM my_schema.heartbeat")
7671
throttleControlReplicas := flag.String("throttle-control-replicas", "", "List of replicas on which to check for lag; comma delimited. Example: myhost1.com:3306,myhost2.com,myhost3.com:3307")
7772
flag.StringVar(&migrationContext.ThrottleFlagFile, "throttle-flag-file", "", "operation pauses when this file exists; hint: use a file that is specific to the table being altered")
7873
flag.StringVar(&migrationContext.ThrottleAdditionalFlagFile, "throttle-additional-flag-file", "/tmp/gh-ost.throttle", "operation pauses when this file exists; hint: keep default, use for throttling multiple gh-ost operations")
7974
flag.StringVar(&migrationContext.PostponeSwapTablesFlagFile, "postpone-swap-tables-flag-file", "", "while this file exists, migration will postpone the final stage of swapping tables, and will keep on syncing the ghost table. Swapping would be ready to perform the moment the file is deleted.")
8075

76+
flag.StringVar(&migrationContext.ServeSocketFile, "serve-socket-file", "", "Unix socket file to serve on. Default: auto-determined and advertised upon startup")
77+
flag.Int64Var(&migrationContext.ServeTCPPort, "serve-tcp-port", 0, "TCP port to serve on. Default: disabled")
78+
8179
maxLoad := flag.String("max-load", "", "Comma delimited status-name=threshold. e.g: 'Threads_running=100,Threads_connected=500'")
8280
quiet := flag.Bool("quiet", false, "quiet")
8381
verbose := flag.Bool("verbose", false, "verbose")
@@ -148,6 +146,10 @@ func main() {
148146
if err := migrationContext.ReadMaxLoad(*maxLoad); err != nil {
149147
log.Fatale(err)
150148
}
149+
if migrationContext.ServeSocketFile == "" {
150+
migrationContext.ServeSocketFile = fmt.Sprintf("/tmp/gh-ost.%s.%s.sock", migrationContext.DatabaseName, migrationContext.OriginalTableName)
151+
}
152+
migrationContext.SetChunkSize(*chunkSize)
151153
migrationContext.ApplyCredentials()
152154

153155
log.Infof("starting gh-ost %+v", AppVersion)

go/logic/applier.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ func (this *Applier) CalculateNextIterationRangeEndValues() (hasFurtherRange boo
373373
this.migrationContext.UniqueKey.Columns.Names,
374374
this.migrationContext.MigrationIterationRangeMinValues.AbstractValues(),
375375
this.migrationContext.MigrationRangeMaxValues.AbstractValues(),
376-
this.migrationContext.ChunkSize,
376+
atomic.LoadInt64(&this.migrationContext.ChunkSize),
377377
this.migrationContext.GetIteration() == 0,
378378
fmt.Sprintf("iteration:%d", this.migrationContext.GetIteration()),
379379
)

go/logic/migrator.go

Lines changed: 87 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@
66
package logic
77

88
import (
9+
"bufio"
910
"fmt"
11+
"io"
1012
"math"
1113
"os"
1214
"os/signal"
15+
"strconv"
16+
"strings"
1317
"sync/atomic"
1418
"syscall"
1519
"time"
@@ -41,6 +45,7 @@ type Migrator struct {
4145
inspector *Inspector
4246
applier *Applier
4347
eventsStreamer *EventsStreamer
48+
server *Server
4449
migrationContext *base.MigrationContext
4550

4651
tablesInPlace chan bool
@@ -95,6 +100,9 @@ func (this *Migrator) acceptSignals() {
95100

96101
func (this *Migrator) shouldThrottle() (result bool, reason string) {
97102
// User-based throttle
103+
if atomic.LoadInt64(&this.migrationContext.ThrottleCommandedByUser) > 0 {
104+
return true, "commanded by user"
105+
}
98106
if this.migrationContext.ThrottleFlagFile != "" {
99107
if base.FileExists(this.migrationContext.ThrottleFlagFile) {
100108
// Throttle file defined and exists!
@@ -321,6 +329,9 @@ func (this *Migrator) Migrate() (err error) {
321329
}
322330
}
323331

332+
if err := this.initiateServer(); err != nil {
333+
return err
334+
}
324335
if err := this.addDMLEventsListener(); err != nil {
325336
return err
326337
}
@@ -518,6 +529,64 @@ func (this *Migrator) stopWritesAndCompleteMigrationOnReplica() (err error) {
518529
return nil
519530
}
520531

532+
func (this *Migrator) onServerCommand(command string, writer *bufio.Writer) (err error) {
533+
tokens := strings.Split(command, "=")
534+
command = strings.TrimSpace(tokens[0])
535+
arg := ""
536+
if len(tokens) > 1 {
537+
arg = strings.TrimSpace(tokens[1])
538+
}
539+
switch command {
540+
case "help":
541+
{
542+
fmt.Fprintln(writer, `available commands:
543+
status # Print a status message
544+
chunk-size=<newsize> # Set a new chunk-size
545+
throttle # Force throttling
546+
no-throttle # End forced throttling (other throttling may still apply)
547+
help # This message
548+
`)
549+
}
550+
case "info", "status":
551+
this.printMigrationStatusHint(writer)
552+
this.printStatus(writer)
553+
case "chunk-size":
554+
{
555+
if chunkSize, err := strconv.Atoi(arg); err != nil {
556+
return log.Errore(err)
557+
} else {
558+
this.migrationContext.SetChunkSize(int64(chunkSize))
559+
this.printMigrationStatusHint(writer)
560+
}
561+
}
562+
case "throttle", "pause", "suspend":
563+
{
564+
atomic.StoreInt64(&this.migrationContext.ThrottleCommandedByUser, 1)
565+
}
566+
case "no-throttle", "unthrottle", "resume", "continue":
567+
{
568+
atomic.StoreInt64(&this.migrationContext.ThrottleCommandedByUser, 0)
569+
}
570+
default:
571+
return fmt.Errorf("Unknown command: %s", command)
572+
}
573+
writer.Flush()
574+
return nil
575+
}
576+
577+
func (this *Migrator) initiateServer() (err error) {
578+
this.server = NewServer(this.onServerCommand)
579+
if err := this.server.BindSocketFile(); err != nil {
580+
return err
581+
}
582+
if err := this.server.BindTCPPort(); err != nil {
583+
return err
584+
}
585+
586+
go this.server.Serve()
587+
return nil
588+
}
589+
521590
func (this *Migrator) initiateInspector() (err error) {
522591
this.inspector = NewInspector()
523592
if err := this.inspector.InitDBConnections(); err != nil {
@@ -563,34 +632,42 @@ func (this *Migrator) initiateStatus() error {
563632
return nil
564633
}
565634

566-
func (this *Migrator) printMigrationStatusHint() {
567-
fmt.Println(fmt.Sprintf("# Migrating %s.%s; Ghost table is %s.%s",
635+
func (this *Migrator) printMigrationStatusHint(writers ...io.Writer) {
636+
writers = append(writers, os.Stdout)
637+
w := io.MultiWriter(writers...)
638+
fmt.Fprintln(w, fmt.Sprintf("# Migrating %s.%s; Ghost table is %s.%s",
568639
sql.EscapeName(this.migrationContext.DatabaseName),
569640
sql.EscapeName(this.migrationContext.OriginalTableName),
570641
sql.EscapeName(this.migrationContext.DatabaseName),
571642
sql.EscapeName(this.migrationContext.GetGhostTableName()),
572643
))
573-
fmt.Println(fmt.Sprintf("# Migration started at %+v",
644+
fmt.Fprintln(w, fmt.Sprintf("# Migration started at %+v",
574645
this.migrationContext.StartTime.Format(time.RubyDate),
575646
))
576-
fmt.Println(fmt.Sprintf("# chunk-size: %+v; max lag: %+vms; max-load: %+v",
647+
fmt.Fprintln(w, fmt.Sprintf("# chunk-size: %+v; max lag: %+vms; max-load: %+v",
577648
atomic.LoadInt64(&this.migrationContext.ChunkSize),
578649
atomic.LoadInt64(&this.migrationContext.MaxLagMillisecondsThrottleThreshold),
579650
this.migrationContext.MaxLoad,
580651
))
581652
if this.migrationContext.ThrottleFlagFile != "" {
582-
fmt.Println(fmt.Sprintf("# Throttle flag file: %+v",
653+
fmt.Fprintln(w, fmt.Sprintf("# Throttle flag file: %+v",
583654
this.migrationContext.ThrottleFlagFile,
584655
))
585656
}
586657
if this.migrationContext.ThrottleAdditionalFlagFile != "" {
587-
fmt.Println(fmt.Sprintf("# Throttle additional flag file: %+v",
658+
fmt.Fprintln(w, fmt.Sprintf("# Throttle additional flag file: %+v",
588659
this.migrationContext.ThrottleAdditionalFlagFile,
589660
))
590661
}
662+
fmt.Fprintln(w, fmt.Sprintf("# Serving on unix socket: %+v",
663+
this.migrationContext.ServeSocketFile,
664+
))
665+
if this.migrationContext.ServeTCPPort != 0 {
666+
fmt.Fprintln(w, fmt.Sprintf("# Serving on TCP port: %+v", this.migrationContext.ServeTCPPort))
667+
}
591668
}
592669

593-
func (this *Migrator) printStatus() {
670+
func (this *Migrator) printStatus(writers ...io.Writer) {
594671
elapsedTime := this.migrationContext.ElapsedTime()
595672
elapsedSeconds := int64(elapsedTime.Seconds())
596673
totalRowsCopied := this.migrationContext.GetTotalRowsCopied()
@@ -656,7 +733,9 @@ func (this *Migrator) printStatus() {
656733
fmt.Sprintf("copy iteration %d at %d", this.migrationContext.GetIteration(), time.Now().Unix()),
657734
status,
658735
)
659-
fmt.Println(status)
736+
writers = append(writers, os.Stdout)
737+
w := io.MultiWriter(writers...)
738+
fmt.Fprintln(w, status)
660739
}
661740

662741
func (this *Migrator) initiateHeartbeatListener() {

0 commit comments

Comments
 (0)