Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
1a8c372
Using golang 1.14
shlomi-noach Jun 28, 2020
fb4aca1
checksums
shlomi-noach Jun 28, 2020
2b71b73
Actions/workflows: upload binary artifact
shlomi-noach Jun 28, 2020
b60b12d
Support --checksum-data flag, on-the-fly checksum verification
shlomi-noach Jun 29, 2020
5c0d9ab
extra table timeout when checksum-data is enabled
shlomi-noach Jun 29, 2020
4be4cb9
builder tests
shlomi-noach Jun 29, 2020
ed7aa85
builder tests
shlomi-noach Jun 29, 2020
edc1053
builder tests
shlomi-noach Jun 29, 2020
8eb300b
expect 1.14 and above in build scripts; update to readme.md
shlomi-noach Jun 29, 2020
b774dc1
better iteration on checksum comparison
shlomi-noach Jun 29, 2020
aa33f10
iteration on string representation
shlomi-noach Jun 29, 2020
f430ba4
Visibility into pending/successful cehcksum comparisons
shlomi-noach Jul 1, 2020
1f47f52
better synchronization logic
shlomi-noach Jul 5, 2020
3907a13
GhostUniqueKey
shlomi-noach Jul 6, 2020
1182ad0
Using GhostUniqueKey for building checksum query on ghost table
shlomi-noach Jul 6, 2020
8e43847
stricter checksum with IFNULL
shlomi-noach Jul 7, 2020
6c7b473
Support a complete ALTER TABLE statement in --alter
shlomi-noach Jul 22, 2020
f482356
Merge branch 'master' into parse-alter-statement
shlomi-noach Jul 22, 2020
c9249f2
Updating and using AlterTableOptions
shlomi-noach Jul 23, 2020
87595b1
Merge pull request #6 from github/master
shlomi-noach Jul 23, 2020
88c73c0
Merge branch 'master' into parse-alter-statement
shlomi-noach Jul 23, 2020
731df3c
comments
shlomi-noach Jul 23, 2020
d1fcef4
Merge branch 'master' into golang1.14
shlomi-noach Jul 27, 2020
b9d400a
Merge branch 'master' into workflow-upload-artifact
shlomi-noach Jul 27, 2020
34d1624
Merge pull request #1 from openark/golang1.14
shlomi-noach Jul 28, 2020
b54d256
Merge branch 'master' into workflow-upload-artifact
shlomi-noach Jul 28, 2020
1083109
Merge branch 'master' into rowcopy-checksum
shlomi-noach Jul 28, 2020
317c807
removed debug messages
shlomi-noach Jul 28, 2020
1de2b5d
fix log call
shlomi-noach Jul 28, 2020
9b2a04d
Merge pull request #2 from openark/workflow-upload-artifact
shlomi-noach Jul 28, 2020
ae4dd18
extra unit test checks
shlomi-noach Jul 29, 2020
9ccde4f
Merge pull request #5 from openark/parse-alter-statement
shlomi-noach Jul 29, 2020
e9fbd4e
Merge branch 'master' into rowcopy-checksum
shlomi-noach Jul 29, 2020
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
13 changes: 9 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@master
- uses: actions/checkout@v2

- name: Set up Go 1.12
- name: Set up Go 1.14
uses: actions/setup-go@v1
with:
version: 1.12
id: go
go-version: 1.14

- name: Build
run: script/cibuild

- name: Upload gh-ost binary artifact
uses: actions/upload-artifact@v1
with:
name: gh-ost
path: bin/gh-ost
7 changes: 3 additions & 4 deletions .github/workflows/replica-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@master
- uses: actions/checkout@v2

- name: Set up Go 1.12
- name: Set up Go 1.14
uses: actions/setup-go@v1
with:
version: 1.12
id: go
go-version: 1.14

- name: migration tests
run: script/cibuild-gh-ost-replica-tests
2 changes: 1 addition & 1 deletion Dockerfile.packaging
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#

FROM golang:1.12.6
FROM golang:1.14.4

RUN apt-get update
RUN apt-get install -y ruby ruby-dev rubygems build-essential
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.test
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.12.1
FROM golang:1.14.4
LABEL maintainer="github@github.com"

RUN apt-get update
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Please see [Coding gh-ost](doc/coding-ghost.md) for a guide to getting started d

[Download latest release here](https://github.com/github/gh-ost/releases/latest)

`gh-ost` is a Go project; it is built with Go `1.12` and above. To build on your own, use either:
`gh-ost` is a Go project; it is built with Go `1.14` and above. To build on your own, use either:
- [script/build](https://github.com/github/gh-ost/blob/master/script/build) - this is the same build script used by CI hence the authoritative; artifact is `./bin/gh-ost` binary.
- [build.sh](https://github.com/github/gh-ost/blob/master/build.sh) for building `tar.gz` artifacts in `/tmp/gh-ost`

Expand Down
4 changes: 2 additions & 2 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ function build {
GOOS=$3
GOARCH=$4

if ! go version | egrep -q 'go(1\.1[234])' ; then
echo "go version must be 1.12 or above"
if ! go version | egrep -q 'go(1\.1[456])' ; then
echo "go version must be 1.14 or above"
exit 1
fi

Expand Down
49 changes: 49 additions & 0 deletions go/base/checksum_comparison.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
Copyright 2015 Shlomi Noach, courtesy Booking.com
See https://github.com/github/gh-ost/blob/master/LICENSE
*/

package base

import (
"fmt"

"github.com/github/gh-ost/go/sql"
)

type ChecksumFunc func() (checksum string, err error)

// BinlogCoordinates described binary log coordinates in the form of log file & log position.
type ChecksumComparison struct {
Iteration int64
OriginalTableChecksumFunc ChecksumFunc
GhostTableChecksumFunc ChecksumFunc
MigrationIterationRangeMinValues *sql.ColumnValues
MigrationIterationRangeMaxValues *sql.ColumnValues
Attempts int
}

func NewChecksumComparison(
iteration int64,
originalTableChecksumFunc, ghostTableChecksumFunc ChecksumFunc,
rangeMinValues, rangeMaxValues *sql.ColumnValues,
) *ChecksumComparison {
return &ChecksumComparison{
Iteration: iteration,
OriginalTableChecksumFunc: originalTableChecksumFunc,
GhostTableChecksumFunc: ghostTableChecksumFunc,
MigrationIterationRangeMinValues: rangeMinValues,
MigrationIterationRangeMaxValues: rangeMaxValues,
Attempts: 0,
}
}

func (this *ChecksumComparison) IncrementAttempts() {
this.Attempts = this.Attempts + 1
}

func (this *ChecksumComparison) String() string {
return fmt.Sprintf("iteration: %d, range: [%s]..[%s], attempts: %d",
this.Iteration, this.MigrationIterationRangeMinValues, this.MigrationIterationRangeMaxValues, this.Attempts,
)
}
12 changes: 9 additions & 3 deletions go/base/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,14 @@ func NewThrottleCheckResult(throttle bool, reason string, reasonHint ThrottleRea
type MigrationContext struct {
Uuid string

DatabaseName string
OriginalTableName string
AlterStatement string
DatabaseName string
OriginalTableName string
AlterStatement string
AlterStatementOptions string // anything following the 'ALTER TABLE [schema.]table' from AlterStatement

CountTableRows bool
ConcurrentCountTableRows bool
ChecksumData bool
AllowedRunningOnMaster bool
AllowedMasterMaster bool
SwitchToRowBinlogFormat bool
Expand Down Expand Up @@ -178,6 +180,9 @@ type MigrationContext struct {
pointOfInterestTimeMutex *sync.Mutex
CurrentLag int64
currentProgress uint64
PendingChecksumComparisons int64
SuccessfulChecksumComparisons int64
SubmittedChecksumComparisons int64
ThrottleHTTPStatusCode int64
controlReplicasLagResult mysql.ReplicationLagResult
TotalRowsCopied int64
Expand Down Expand Up @@ -206,6 +211,7 @@ type MigrationContext struct {
GhostTableVirtualColumns *sql.ColumnList
GhostTableUniqueKeys [](*sql.UniqueKey)
UniqueKey *sql.UniqueKey
GhostUniqueKey *sql.UniqueKey
SharedColumns *sql.ColumnList
ColumnRenameMap map[string]string
DroppedColumnsMap map[string]bool
Expand Down
23 changes: 18 additions & 5 deletions go/cmd/gh-ost/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/github/gh-ost/go/base"
"github.com/github/gh-ost/go/logic"
"github.com/github/gh-ost/go/sql"
_ "github.com/go-sql-driver/mysql"
"github.com/outbrain/golib/log"

Expand Down Expand Up @@ -66,6 +67,7 @@ func main() {
flag.StringVar(&migrationContext.AlterStatement, "alter", "", "alter statement (mandatory)")
flag.BoolVar(&migrationContext.CountTableRows, "exact-rowcount", false, "actually count table rows as opposed to estimate them (results in more accurate progress estimation)")
flag.BoolVar(&migrationContext.ConcurrentCountTableRows, "concurrent-rowcount", true, "(with --exact-rowcount), when true (default): count rows after row-copy begins, concurrently, and adjust row estimate later on; when false: first count rows, then start row copy")
flag.BoolVar(&migrationContext.ChecksumData, "checksum-data", false, "if true, checksum original and ghost table shared data on the fly, fail migration if checksum mismatches. Checksum queries run on applier node (the master unless testing on replica)")
flag.BoolVar(&migrationContext.AllowedRunningOnMaster, "allow-on-master", false, "allow this migration to run directly on master. Preferably it would run on a replica")
flag.BoolVar(&migrationContext.AllowedMasterMaster, "allow-master-master", false, "explicitly allow running in a master-master setup")
flag.BoolVar(&migrationContext.NullableUniqueKeyAllowed, "allow-nullable-unique-key", false, "allow gh-ost to migrate based on a unique key with nullable columns. As long as no NULL values exist, this should be OK. If NULL values exist in chosen key, data may be corrupted. Use at your own risk!")
Expand Down Expand Up @@ -172,14 +174,25 @@ func main() {
migrationContext.Log.SetLevel(log.ERROR)
}

if migrationContext.AlterStatement == "" {
log.Fatalf("--alter must be provided and statement must not be empty")
}
parser := sql.NewParserFromAlterStatement(migrationContext.AlterStatement)
migrationContext.AlterStatementOptions = parser.GetAlterStatementOptions()

if migrationContext.DatabaseName == "" {
migrationContext.Log.Fatalf("--database must be provided and database name must not be empty")
if parser.HasExplicitSchema() {
migrationContext.DatabaseName = parser.GetExplicitSchema()
} else {
log.Fatalf("--database must be provided and database name must not be empty, or --alter must specify database name")
}
}
if migrationContext.OriginalTableName == "" {
migrationContext.Log.Fatalf("--table must be provided and table name must not be empty")
}
if migrationContext.AlterStatement == "" {
migrationContext.Log.Fatalf("--alter must be provided and statement must not be empty")
if parser.HasExplicitTable() {
migrationContext.OriginalTableName = parser.GetExplicitTable()
} else {
log.Fatalf("--table must be provided and table name must not be empty, or --alter must specify table name")
}
}
migrationContext.Noop = !(*executeFlag)
if migrationContext.AllowedRunningOnMaster && migrationContext.TestOnReplica {
Expand Down
56 changes: 46 additions & 10 deletions go/logic/applier.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (

const (
atomicCutOverMagicHint = "ghost-cut-over-sentry"
groupConcatMaxLength = 1024 * 1024
)

type dmlBuildResult struct {
Expand Down Expand Up @@ -68,7 +69,7 @@ func NewApplier(migrationContext *base.MigrationContext) *Applier {

func (this *Applier) InitDBConnections() (err error) {

applierUri := this.connectionConfig.GetDBUri(this.migrationContext.DatabaseName)
applierUri := this.connectionConfig.GetDBUri(this.migrationContext.DatabaseName, fmt.Sprintf("group_concat_max_len=%d", groupConcatMaxLength))
if this.db, _, err = mysql.GetDB(this.migrationContext.Uuid, applierUri); err != nil {
return err
}
Expand Down Expand Up @@ -190,7 +191,7 @@ func (this *Applier) AlterGhost() error {
query := fmt.Sprintf(`alter /* gh-ost */ table %s.%s %s`,
sql.EscapeName(this.migrationContext.DatabaseName),
sql.EscapeName(this.migrationContext.GetGhostTableName()),
this.migrationContext.AlterStatement,
this.migrationContext.AlterStatementOptions,
)
this.migrationContext.Log.Infof("Altering ghost table %s.%s",
sql.EscapeName(this.migrationContext.DatabaseName),
Expand Down Expand Up @@ -454,25 +455,25 @@ func (this *Applier) CalculateNextIterationRangeEndValues() (hasFurtherRange boo

// ApplyIterationInsertQuery issues a chunk-INSERT query on the ghost table. It is where
// data actually gets copied from original table.
func (this *Applier) ApplyIterationInsertQuery() (chunkSize int64, rowsAffected int64, duration time.Duration, err error) {
func (this *Applier) ApplyIterationInsertQuery() (chunkSize int64, rowsAffected int64, duration time.Duration, checksumComparison *base.ChecksumComparison, err error) {
startTime := time.Now()
chunkSize = atomic.LoadInt64(&this.migrationContext.ChunkSize)

query, explodedArgs, err := sql.BuildRangeInsertPreparedQuery(
insertQuery, originalChecksumQuery, ghostChecksumQuery, explodedArgs, err := sql.BuildRangeInsertPreparedQuery(
this.migrationContext.DatabaseName,
this.migrationContext.OriginalTableName,
this.migrationContext.GetGhostTableName(),
this.migrationContext.SharedColumns.Names(),
this.migrationContext.MappedSharedColumns.Names(),
this.migrationContext.UniqueKey.Name,
&this.migrationContext.UniqueKey.Columns,
this.migrationContext.UniqueKey,
this.migrationContext.GhostUniqueKey,
this.migrationContext.MigrationIterationRangeMinValues.AbstractValues(),
this.migrationContext.MigrationIterationRangeMaxValues.AbstractValues(),
this.migrationContext.GetIteration() == 0,
this.migrationContext.IsTransactionalTable(),
)
if err != nil {
return chunkSize, rowsAffected, duration, err
return chunkSize, rowsAffected, duration, checksumComparison, err
}

sqlResult, err := func() (gosql.Result, error) {
Expand All @@ -491,7 +492,7 @@ func (this *Applier) ApplyIterationInsertQuery() (chunkSize int64, rowsAffected
if _, err := tx.Exec(sessionQuery); err != nil {
return nil, err
}
result, err := tx.Exec(query, explodedArgs...)
result, err := tx.Exec(insertQuery, explodedArgs...)
if err != nil {
return nil, err
}
Expand All @@ -502,7 +503,7 @@ func (this *Applier) ApplyIterationInsertQuery() (chunkSize int64, rowsAffected
}()

if err != nil {
return chunkSize, rowsAffected, duration, err
return chunkSize, rowsAffected, duration, checksumComparison, err
}
rowsAffected, _ = sqlResult.RowsAffected()
duration = time.Since(startTime)
Expand All @@ -512,7 +513,38 @@ func (this *Applier) ApplyIterationInsertQuery() (chunkSize int64, rowsAffected
this.migrationContext.MigrationIterationRangeMaxValues,
this.migrationContext.GetIteration(),
chunkSize)
return chunkSize, rowsAffected, duration, nil

var originalTableChecksumFunc base.ChecksumFunc = func() (checksum string, err error) {
err = this.db.QueryRow(originalChecksumQuery, explodedArgs...).Scan(&checksum)
return checksum, err
}
var ghostTableChecksumFunc base.ChecksumFunc = func() (checksum string, err error) {
err = this.db.QueryRow(ghostChecksumQuery, explodedArgs...).Scan(&checksum)
return checksum, err
}
checksumComparison = base.NewChecksumComparison(
this.migrationContext.GetIteration(),
originalTableChecksumFunc, ghostTableChecksumFunc,
this.migrationContext.MigrationIterationRangeMinValues,
this.migrationContext.MigrationIterationRangeMaxValues,
)

return chunkSize, rowsAffected, duration, checksumComparison, nil
}

func (this *Applier) CompareChecksum(checksumComparison *base.ChecksumComparison) error {
originalChecksum, err := checksumComparison.OriginalTableChecksumFunc()
if err != nil {
return err
}
ghostChecksum, err := checksumComparison.GhostTableChecksumFunc()
if err != nil {
return err
}
if originalChecksum != ghostChecksum {
return fmt.Errorf("Checksum failure. Iteration: %d", checksumComparison.Iteration)
}
return nil
}

// LockOriginalTable places a write lock on the original table
Expand Down Expand Up @@ -811,6 +843,10 @@ func (this *Applier) AtomicCutOverMagicLock(sessionIdChan chan int64, tableLocke
}

tableLockTimeoutSeconds := this.migrationContext.CutOverLockTimeoutSeconds * 2
if this.migrationContext.ChecksumData {
// Allow extra time for checksum to evaluate
tableLockTimeoutSeconds += this.migrationContext.CutOverLockTimeoutSeconds
}
this.migrationContext.Log.Infof("Setting LOCK timeout as %d seconds", tableLockTimeoutSeconds)
query = fmt.Sprintf(`set session lock_wait_timeout:=%d`, tableLockTimeoutSeconds)
if _, err := tx.Exec(query); err != nil {
Expand Down
13 changes: 8 additions & 5 deletions go/logic/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func (this *Inspector) inspectOriginalAndGhostTables() (err error) {
if err != nil {
return err
}
sharedUniqueKeys, err := this.getSharedUniqueKeys(this.migrationContext.OriginalTableUniqueKeys, this.migrationContext.GhostTableUniqueKeys)
sharedUniqueKeys, ghostSharedUniqueKeys, err := this.getSharedUniqueKeys(this.migrationContext.OriginalTableUniqueKeys, this.migrationContext.GhostTableUniqueKeys)
if err != nil {
return err
}
Expand All @@ -150,13 +150,15 @@ func (this *Inspector) inspectOriginalAndGhostTables() (err error) {
}
if uniqueKeyIsValid {
this.migrationContext.UniqueKey = sharedUniqueKeys[i]
this.migrationContext.GhostUniqueKey = ghostSharedUniqueKeys[i]
break
}
}
if this.migrationContext.UniqueKey == nil {
return fmt.Errorf("No shared unique key can be found after ALTER! Bailing out")
}
this.migrationContext.Log.Infof("Chosen shared unique key is %s", this.migrationContext.UniqueKey.Name)
this.migrationContext.Log.Infof("Chosen shared unique key is %s. ghost unique key is %s", this.migrationContext.UniqueKey.Name, this.migrationContext.GhostUniqueKey.Name)
this.migrationContext.Log.Infof("Chosen shared unique key columns %+v. ghost unique key columns: %+v", this.migrationContext.UniqueKey.Columns.Names(), this.migrationContext.GhostUniqueKey.Columns.Names())
if this.migrationContext.UniqueKey.HasNullable {
if this.migrationContext.NullableUniqueKeyAllowed {
this.migrationContext.Log.Warningf("Chosen key (%s) has nullable columns. You have supplied with --allow-nullable-unique-key and so this migration proceeds. As long as there aren't NULL values in this key's column, migration should be fine. NULL values will corrupt migration's data", this.migrationContext.UniqueKey)
Expand Down Expand Up @@ -668,17 +670,18 @@ func (this *Inspector) getCandidateUniqueKeys(tableName string) (uniqueKeys [](*

// getSharedUniqueKeys returns the intersection of two given unique keys,
// testing by list of columns
func (this *Inspector) getSharedUniqueKeys(originalUniqueKeys, ghostUniqueKeys [](*sql.UniqueKey)) (uniqueKeys [](*sql.UniqueKey), err error) {
func (this *Inspector) getSharedUniqueKeys(originalUniqueKeys, ghostUniqueKeys [](*sql.UniqueKey)) (sharedUniqueKeys [](*sql.UniqueKey), ghostSharedUniqueKeys [](*sql.UniqueKey), err error) {
// We actually do NOT rely on key name, just on the set of columns. This is because maybe
// the ALTER is on the name itself...
for _, originalUniqueKey := range originalUniqueKeys {
for _, ghostUniqueKey := range ghostUniqueKeys {
if originalUniqueKey.Columns.EqualsByNames(&ghostUniqueKey.Columns) {
uniqueKeys = append(uniqueKeys, originalUniqueKey)
sharedUniqueKeys = append(sharedUniqueKeys, originalUniqueKey)
ghostSharedUniqueKeys = append(ghostSharedUniqueKeys, ghostUniqueKey)
}
}
}
return uniqueKeys, nil
return sharedUniqueKeys, ghostSharedUniqueKeys, nil
}

// getSharedColumns returns the intersection of two lists of columns in same order as the first list
Expand Down
Loading