2016-04-04 13:30:49 +00:00
/ *
Copyright 2016 GitHub Inc .
See https : //github.com/github/gh-osc/blob/master/LICENSE
* /
package logic
import (
gosql "database/sql"
"fmt"
2016-04-08 08:34:44 +00:00
"sync/atomic"
2016-04-07 13:57:12 +00:00
"time"
2016-04-04 13:30:49 +00:00
"github.com/github/gh-osc/go/base"
2016-04-11 15:27:16 +00:00
"github.com/github/gh-osc/go/binlog"
2016-04-04 13:30:49 +00:00
"github.com/github/gh-osc/go/mysql"
"github.com/github/gh-osc/go/sql"
"github.com/outbrain/golib/log"
"github.com/outbrain/golib/sqlutils"
)
// Applier reads data from the read-MySQL-server (typically a replica, but can be the master)
// It is used for gaining initial status and structure, and later also follow up on progress and changelog
type Applier struct {
connectionConfig * mysql . ConnectionConfig
db * gosql . DB
2016-04-18 17:57:18 +00:00
singletonDB * gosql . DB
2016-04-04 13:30:49 +00:00
migrationContext * base . MigrationContext
}
func NewApplier ( ) * Applier {
return & Applier {
2016-04-14 11:37:56 +00:00
connectionConfig : base . GetMigrationContext ( ) . ApplierConnectionConfig ,
2016-04-04 13:30:49 +00:00
migrationContext : base . GetMigrationContext ( ) ,
}
}
func ( this * Applier ) InitDBConnections ( ) ( err error ) {
2016-04-18 17:57:18 +00:00
applierUri := this . connectionConfig . GetDBUri ( this . migrationContext . DatabaseName )
if this . db , _ , err = sqlutils . GetDB ( applierUri ) ; err != nil {
2016-04-04 13:30:49 +00:00
return err
}
2016-04-18 17:57:18 +00:00
singletonApplierUri := fmt . Sprintf ( "%s?timeout=0" , applierUri )
if this . singletonDB , _ , err = sqlutils . GetDB ( singletonApplierUri ) ; err != nil {
return err
}
this . singletonDB . SetMaxOpenConns ( 1 )
if err := this . validateConnection ( this . db ) ; err != nil {
return err
}
if err := this . validateConnection ( this . singletonDB ) ; err != nil {
2016-04-04 13:30:49 +00:00
return err
}
return nil
}
// validateConnection issues a simple can-connect to MySQL
2016-04-18 17:57:18 +00:00
func ( this * Applier ) validateConnection ( db * gosql . DB ) error {
2016-04-04 13:30:49 +00:00
query := ` select @@global.port `
var port int
2016-04-18 17:57:18 +00:00
if err := db . QueryRow ( query ) . Scan ( & port ) ; err != nil {
2016-04-04 13:30:49 +00:00
return err
}
if port != this . connectionConfig . Key . Port {
return fmt . Errorf ( "Unexpected database port reported: %+v" , port )
}
log . Infof ( "connection validated on %+v" , this . connectionConfig . Key )
return nil
}
2016-05-03 09:55:17 +00:00
func ( this * Applier ) tableExists ( tableName string ) ( tableFound bool ) {
query := fmt . Sprintf ( ` show /* gh-osc */ table status from %s like '%s' ` , sql . EscapeName ( this . migrationContext . DatabaseName ) , tableName )
sqlutils . QueryRowsMap ( this . db , query , func ( m sqlutils . RowMap ) error {
tableFound = true
return nil
} )
return tableFound
}
func ( this * Applier ) ValidateOrDropExistingTables ( ) error {
if this . migrationContext . InitiallyDropGhostTable {
if err := this . DropGhostTable ( ) ; err != nil {
return err
}
}
if this . tableExists ( this . migrationContext . GetGhostTableName ( ) ) {
return fmt . Errorf ( "Table %s already exists. Panicking. Use --initially-drop-ghost-table to force dropping it" , sql . EscapeName ( this . migrationContext . GetGhostTableName ( ) ) )
}
if this . migrationContext . InitiallyDropOldTable {
if err := this . DropOldTable ( ) ; err != nil {
return err
}
}
if this . tableExists ( this . migrationContext . GetOldTableName ( ) ) {
return fmt . Errorf ( "Table %s already exists. Panicking. Use --initially-drop-old-table to force dropping it" , sql . EscapeName ( this . migrationContext . GetOldTableName ( ) ) )
}
return nil
}
2016-04-11 15:27:16 +00:00
// CreateGhostTable creates the ghost table on the applier host
2016-04-04 13:30:49 +00:00
func ( this * Applier ) CreateGhostTable ( ) error {
query := fmt . Sprintf ( ` create /* gh-osc */ table %s.%s like %s.%s ` ,
sql . EscapeName ( this . migrationContext . DatabaseName ) ,
sql . EscapeName ( this . migrationContext . GetGhostTableName ( ) ) ,
sql . EscapeName ( this . migrationContext . DatabaseName ) ,
sql . EscapeName ( this . migrationContext . OriginalTableName ) ,
)
log . Infof ( "Creating ghost table %s.%s" ,
sql . EscapeName ( this . migrationContext . DatabaseName ) ,
sql . EscapeName ( this . migrationContext . GetGhostTableName ( ) ) ,
)
if _ , err := sqlutils . ExecNoPrepare ( this . db , query ) ; err != nil {
return err
}
2016-04-07 13:57:12 +00:00
log . Infof ( "Ghost table created" )
2016-04-04 13:30:49 +00:00
return nil
}
2016-04-11 15:27:16 +00:00
// AlterGhost applies `alter` statement on ghost table
2016-04-04 13:30:49 +00:00
func ( this * Applier ) AlterGhost ( ) error {
query := fmt . Sprintf ( ` alter /* gh-osc */ table %s.%s %s ` ,
sql . EscapeName ( this . migrationContext . DatabaseName ) ,
sql . EscapeName ( this . migrationContext . GetGhostTableName ( ) ) ,
this . migrationContext . AlterStatement ,
)
log . Infof ( "Altering ghost table %s.%s" ,
sql . EscapeName ( this . migrationContext . DatabaseName ) ,
sql . EscapeName ( this . migrationContext . GetGhostTableName ( ) ) ,
)
log . Debugf ( "ALTER statement: %s" , query )
if _ , err := sqlutils . ExecNoPrepare ( this . db , query ) ; err != nil {
return err
}
2016-04-07 13:57:12 +00:00
log . Infof ( "Ghost table altered" )
return nil
}
2016-04-11 15:27:16 +00:00
// CreateChangelogTable creates the changelog table on the applier host
2016-04-07 13:57:12 +00:00
func ( this * Applier ) CreateChangelogTable ( ) error {
2016-05-03 09:55:17 +00:00
if err := this . DropChangelogTable ( ) ; err != nil {
return err
}
2016-04-07 13:57:12 +00:00
query := fmt . Sprintf ( ` create /* gh-osc */ table % s . % s (
2016-04-08 12:35:06 +00:00
id bigint auto_increment ,
2016-04-07 13:57:12 +00:00
last_update timestamp not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ,
hint varchar ( 64 ) charset ascii not null ,
2016-04-08 12:35:06 +00:00
value varchar ( 255 ) charset ascii not null ,
2016-04-07 13:57:12 +00:00
primary key ( id ) ,
unique key hint_uidx ( hint )
2016-04-11 15:27:16 +00:00
) auto_increment = 256
2016-04-07 13:57:12 +00:00
` ,
sql . EscapeName ( this . migrationContext . DatabaseName ) ,
sql . EscapeName ( this . migrationContext . GetChangelogTableName ( ) ) ,
)
log . Infof ( "Creating changelog table %s.%s" ,
sql . EscapeName ( this . migrationContext . DatabaseName ) ,
sql . EscapeName ( this . migrationContext . GetChangelogTableName ( ) ) ,
)
if _ , err := sqlutils . ExecNoPrepare ( this . db , query ) ; err != nil {
return err
}
log . Infof ( "Changelog table created" )
return nil
}
2016-04-11 15:27:16 +00:00
// dropTable drops a given table on the applied host
func ( this * Applier ) dropTable ( tableName string ) error {
2016-04-07 13:57:12 +00:00
query := fmt . Sprintf ( ` drop /* gh-osc */ table if exists %s.%s ` ,
sql . EscapeName ( this . migrationContext . DatabaseName ) ,
2016-04-11 15:27:16 +00:00
sql . EscapeName ( tableName ) ,
2016-04-07 13:57:12 +00:00
)
2016-04-11 15:27:16 +00:00
log . Infof ( "Droppping table %s.%s" ,
2016-04-07 13:57:12 +00:00
sql . EscapeName ( this . migrationContext . DatabaseName ) ,
2016-04-11 15:27:16 +00:00
sql . EscapeName ( tableName ) ,
2016-04-07 13:57:12 +00:00
)
if _ , err := sqlutils . ExecNoPrepare ( this . db , query ) ; err != nil {
return err
}
2016-04-11 15:27:16 +00:00
log . Infof ( "Table dropped" )
2016-04-04 13:30:49 +00:00
return nil
}
2016-04-04 16:19:46 +00:00
2016-04-11 15:27:16 +00:00
// DropChangelogTable drops the changelog table on the applier host
func ( this * Applier ) DropChangelogTable ( ) error {
return this . dropTable ( this . migrationContext . GetChangelogTableName ( ) )
}
2016-05-03 09:55:17 +00:00
// DropOldTable drops the _Old table on the applier host
func ( this * Applier ) DropOldTable ( ) error {
return this . dropTable ( this . migrationContext . GetOldTableName ( ) )
}
2016-04-11 15:27:16 +00:00
// DropGhostTable drops the ghost table on the applier host
func ( this * Applier ) DropGhostTable ( ) error {
return this . dropTable ( this . migrationContext . GetGhostTableName ( ) )
}
2016-04-07 13:57:12 +00:00
// WriteChangelog writes a value to the changelog table.
// It returns the hint as given, for convenience
func ( this * Applier ) WriteChangelog ( hint , value string ) ( string , error ) {
2016-04-14 11:37:56 +00:00
explicitId := 0
switch hint {
case "heartbeat" :
explicitId = 1
case "state" :
explicitId = 2
case "throttle" :
explicitId = 3
}
2016-04-07 13:57:12 +00:00
query := fmt . Sprintf ( `
insert /* gh-osc */ into % s . % s
( id , hint , value )
values
2016-04-14 11:37:56 +00:00
( NULLIF ( ? , 0 ) , ? , ? )
2016-04-07 13:57:12 +00:00
on duplicate key update
last_update = NOW ( ) ,
value = VALUES ( value )
` ,
sql . EscapeName ( this . migrationContext . DatabaseName ) ,
sql . EscapeName ( this . migrationContext . GetChangelogTableName ( ) ) ,
)
2016-04-14 11:37:56 +00:00
_ , err := sqlutils . Exec ( this . db , query , explicitId , hint , value )
2016-04-07 13:57:12 +00:00
return hint , err
}
2016-04-11 15:27:16 +00:00
func ( this * Applier ) WriteAndLogChangelog ( hint , value string ) ( string , error ) {
2016-04-08 12:35:06 +00:00
this . WriteChangelog ( hint , value )
return this . WriteChangelog ( fmt . Sprintf ( "%s at %d" , hint , time . Now ( ) . UnixNano ( ) ) , value )
}
2016-04-11 15:27:16 +00:00
func ( this * Applier ) WriteChangelogState ( value string ) ( string , error ) {
return this . WriteAndLogChangelog ( "state" , value )
}
2016-04-07 13:57:12 +00:00
// InitiateHeartbeat creates a heartbeat cycle, writing to the changelog table.
// This is done asynchronously
2016-04-14 11:37:56 +00:00
func ( this * Applier ) InitiateHeartbeat ( heartbeatIntervalMilliseconds int64 ) {
numSuccessiveFailures := 0
injectHeartbeat := func ( ) error {
2016-04-18 17:57:18 +00:00
if _ , err := this . WriteChangelog ( "heartbeat" , time . Now ( ) . Format ( time . RFC3339Nano ) ) ; err != nil {
2016-04-14 11:37:56 +00:00
numSuccessiveFailures ++
if numSuccessiveFailures > this . migrationContext . MaxRetries ( ) {
return log . Errore ( err )
2016-04-07 13:57:12 +00:00
}
2016-04-14 11:37:56 +00:00
} else {
numSuccessiveFailures = 0
2016-04-07 13:57:12 +00:00
}
2016-04-14 11:37:56 +00:00
return nil
}
injectHeartbeat ( )
heartbeatTick := time . Tick ( time . Duration ( heartbeatIntervalMilliseconds ) * time . Millisecond )
for range heartbeatTick {
// Generally speaking, we would issue a goroutine, but I'd actually rather
// have this blocked rather than spam the master in the event something
// goes wrong
if err := injectHeartbeat ( ) ; err != nil {
return
2016-04-07 13:57:12 +00:00
}
2016-04-14 11:37:56 +00:00
}
2016-04-07 13:57:12 +00:00
}
2016-04-04 16:19:46 +00:00
// ReadMigrationMinValues
func ( this * Applier ) ReadMigrationMinValues ( uniqueKey * sql . UniqueKey ) error {
log . Debugf ( "Reading migration range according to key: %s" , uniqueKey . Name )
2016-04-11 15:27:16 +00:00
query , err := sql . BuildUniqueKeyMinValuesPreparedQuery ( this . migrationContext . DatabaseName , this . migrationContext . OriginalTableName , uniqueKey . Columns . Names )
2016-04-04 16:19:46 +00:00
if err != nil {
return err
}
rows , err := this . db . Query ( query )
if err != nil {
return err
}
for rows . Next ( ) {
2016-04-11 15:27:16 +00:00
this . migrationContext . MigrationRangeMinValues = sql . NewColumnValues ( uniqueKey . Len ( ) )
2016-04-04 16:19:46 +00:00
if err = rows . Scan ( this . migrationContext . MigrationRangeMinValues . ValuesPointers ... ) ; err != nil {
return err
}
}
log . Infof ( "Migration min values: [%s]" , this . migrationContext . MigrationRangeMinValues )
return err
}
// ReadMigrationMinValues
func ( this * Applier ) ReadMigrationMaxValues ( uniqueKey * sql . UniqueKey ) error {
log . Debugf ( "Reading migration range according to key: %s" , uniqueKey . Name )
2016-04-11 15:27:16 +00:00
query , err := sql . BuildUniqueKeyMaxValuesPreparedQuery ( this . migrationContext . DatabaseName , this . migrationContext . OriginalTableName , uniqueKey . Columns . Names )
2016-04-04 16:19:46 +00:00
if err != nil {
return err
}
rows , err := this . db . Query ( query )
if err != nil {
return err
}
for rows . Next ( ) {
2016-04-11 15:27:16 +00:00
this . migrationContext . MigrationRangeMaxValues = sql . NewColumnValues ( uniqueKey . Len ( ) )
2016-04-04 16:19:46 +00:00
if err = rows . Scan ( this . migrationContext . MigrationRangeMaxValues . ValuesPointers ... ) ; err != nil {
return err
}
}
log . Infof ( "Migration max values: [%s]" , this . migrationContext . MigrationRangeMaxValues )
return err
}
2016-04-05 07:14:22 +00:00
func ( this * Applier ) ReadMigrationRangeValues ( ) error {
if err := this . ReadMigrationMinValues ( this . migrationContext . UniqueKey ) ; err != nil {
2016-04-04 16:19:46 +00:00
return err
}
2016-04-05 07:14:22 +00:00
if err := this . ReadMigrationMaxValues ( this . migrationContext . UniqueKey ) ; err != nil {
2016-04-04 16:19:46 +00:00
return err
}
return nil
}
2016-04-08 08:34:44 +00:00
// __unused_IterationIsComplete lets us know when the copy-iteration phase is complete, i.e.
2016-04-05 07:14:22 +00:00
// we've exhausted all rows
2016-04-08 08:34:44 +00:00
func ( this * Applier ) __unused_IterationIsComplete ( ) ( bool , error ) {
2016-04-05 07:14:22 +00:00
if ! this . migrationContext . HasMigrationRange ( ) {
return false , nil
}
if this . migrationContext . MigrationIterationRangeMinValues == nil {
return false , nil
}
2016-04-05 17:50:49 +00:00
args := sqlutils . Args ( )
2016-04-11 15:27:16 +00:00
compareWithIterationRangeStart , explodedArgs , err := sql . BuildRangePreparedComparison ( this . migrationContext . UniqueKey . Columns . Names , this . migrationContext . MigrationIterationRangeMinValues . AbstractValues ( ) , sql . GreaterThanOrEqualsComparisonSign )
2016-04-05 07:14:22 +00:00
if err != nil {
return false , err
}
2016-04-05 17:50:49 +00:00
args = append ( args , explodedArgs ... )
2016-04-11 15:27:16 +00:00
compareWithRangeEnd , explodedArgs , err := sql . BuildRangePreparedComparison ( this . migrationContext . UniqueKey . Columns . Names , this . migrationContext . MigrationRangeMaxValues . AbstractValues ( ) , sql . LessThanComparisonSign )
2016-04-05 07:14:22 +00:00
if err != nil {
return false , err
}
2016-04-05 17:50:49 +00:00
args = append ( args , explodedArgs ... )
2016-04-05 07:14:22 +00:00
query := fmt . Sprintf ( `
select /* gh-osc IterationIsComplete */ 1
from % s . % s
where ( % s ) and ( % s )
limit 1
` ,
sql . EscapeName ( this . migrationContext . DatabaseName ) ,
sql . EscapeName ( this . migrationContext . OriginalTableName ) ,
compareWithIterationRangeStart ,
compareWithRangeEnd ,
)
moreRowsFound := false
err = sqlutils . QueryRowsMap ( this . db , query , func ( rowMap sqlutils . RowMap ) error {
moreRowsFound = true
return nil
2016-04-05 17:50:49 +00:00
} , args ... )
2016-04-05 07:14:22 +00:00
if err != nil {
return false , err
}
return ! moreRowsFound , nil
}
2016-04-08 08:34:44 +00:00
// CalculateNextIterationRangeEndValues reads the next-iteration-range-end unique key values,
// which will be used for copying the next chunk of rows. Ir returns "false" if there is
// no further chunk to work through, i.e. we're past the last chunk and are done with
// itrating the range (and this done with copying row chunks)
func ( this * Applier ) CalculateNextIterationRangeEndValues ( ) ( hasFurtherRange bool , err error ) {
2016-04-05 07:14:22 +00:00
this . migrationContext . MigrationIterationRangeMinValues = this . migrationContext . MigrationIterationRangeMaxValues
2016-04-06 11:05:58 +00:00
if this . migrationContext . MigrationIterationRangeMinValues == nil {
this . migrationContext . MigrationIterationRangeMinValues = this . migrationContext . MigrationRangeMinValues
2016-04-05 07:14:22 +00:00
}
2016-04-05 17:50:49 +00:00
query , explodedArgs , err := sql . BuildUniqueKeyRangeEndPreparedQuery (
this . migrationContext . DatabaseName ,
this . migrationContext . OriginalTableName ,
2016-04-11 15:27:16 +00:00
this . migrationContext . UniqueKey . Columns . Names ,
2016-04-06 11:05:58 +00:00
this . migrationContext . MigrationIterationRangeMinValues . AbstractValues ( ) ,
2016-04-05 17:50:49 +00:00
this . migrationContext . MigrationRangeMaxValues . AbstractValues ( ) ,
this . migrationContext . ChunkSize ,
2016-04-08 12:35:06 +00:00
fmt . Sprintf ( "iteration:%d" , this . migrationContext . GetIteration ( ) ) ,
2016-04-05 17:50:49 +00:00
)
if err != nil {
2016-04-08 08:34:44 +00:00
return hasFurtherRange , err
2016-04-05 17:50:49 +00:00
}
rows , err := this . db . Query ( query , explodedArgs ... )
2016-04-05 07:14:22 +00:00
if err != nil {
2016-04-08 08:34:44 +00:00
return hasFurtherRange , err
2016-04-05 07:14:22 +00:00
}
2016-04-11 15:27:16 +00:00
iterationRangeMaxValues := sql . NewColumnValues ( this . migrationContext . UniqueKey . Len ( ) )
2016-04-05 07:14:22 +00:00
for rows . Next ( ) {
if err = rows . Scan ( iterationRangeMaxValues . ValuesPointers ... ) ; err != nil {
2016-04-08 08:34:44 +00:00
return hasFurtherRange , err
2016-04-05 07:14:22 +00:00
}
2016-04-08 08:34:44 +00:00
hasFurtherRange = true
2016-04-05 07:14:22 +00:00
}
2016-04-08 08:34:44 +00:00
if ! hasFurtherRange {
2016-04-14 11:37:56 +00:00
log . Debugf ( "Iteration complete: no further range to iterate" )
2016-04-08 08:34:44 +00:00
return hasFurtherRange , nil
2016-04-05 07:14:22 +00:00
}
this . migrationContext . MigrationIterationRangeMaxValues = iterationRangeMaxValues
2016-04-08 08:34:44 +00:00
return hasFurtherRange , nil
2016-04-06 11:05:58 +00:00
}
2016-04-08 08:34:44 +00:00
func ( this * Applier ) ApplyIterationInsertQuery ( ) ( chunkSize int64 , rowsAffected int64 , duration time . Duration , err error ) {
startTime := time . Now ( )
chunkSize = atomic . LoadInt64 ( & this . migrationContext . ChunkSize )
2016-04-06 11:05:58 +00:00
query , explodedArgs , err := sql . BuildRangeInsertPreparedQuery (
this . migrationContext . DatabaseName ,
this . migrationContext . OriginalTableName ,
this . migrationContext . GetGhostTableName ( ) ,
2016-04-11 15:27:16 +00:00
this . migrationContext . SharedColumns . Names ,
2016-04-06 11:05:58 +00:00
this . migrationContext . UniqueKey . Name ,
2016-04-11 15:27:16 +00:00
this . migrationContext . UniqueKey . Columns . Names ,
2016-04-06 11:05:58 +00:00
this . migrationContext . MigrationIterationRangeMinValues . AbstractValues ( ) ,
this . migrationContext . MigrationIterationRangeMaxValues . AbstractValues ( ) ,
2016-04-08 12:35:06 +00:00
this . migrationContext . GetIteration ( ) == 0 ,
2016-04-08 08:34:44 +00:00
this . migrationContext . IsTransactionalTable ( ) ,
2016-04-06 11:05:58 +00:00
)
if err != nil {
2016-04-08 08:34:44 +00:00
return chunkSize , rowsAffected , duration , err
2016-04-06 11:05:58 +00:00
}
2016-04-08 08:34:44 +00:00
sqlResult , err := sqlutils . Exec ( this . db , query , explodedArgs ... )
if err != nil {
return chunkSize , rowsAffected , duration , err
2016-04-06 11:05:58 +00:00
}
2016-04-08 08:34:44 +00:00
rowsAffected , _ = sqlResult . RowsAffected ( )
duration = time . Now ( ) . Sub ( startTime )
2016-04-06 11:05:58 +00:00
log . Debugf (
"Issued INSERT on range: [%s]..[%s]; iteration: %d; chunk-size: %d" ,
this . migrationContext . MigrationIterationRangeMinValues ,
this . migrationContext . MigrationIterationRangeMaxValues ,
2016-04-08 12:35:06 +00:00
this . migrationContext . GetIteration ( ) ,
2016-04-08 08:34:44 +00:00
chunkSize )
return chunkSize , rowsAffected , duration , nil
2016-04-05 07:14:22 +00:00
}
2016-04-08 08:34:44 +00:00
// LockTables
func ( this * Applier ) LockTables ( ) error {
2016-04-18 17:57:18 +00:00
// query := fmt.Sprintf(`lock /* gh-osc */ tables %s.%s write, %s.%s write, %s.%s write`,
// sql.EscapeName(this.migrationContext.DatabaseName),
// sql.EscapeName(this.migrationContext.OriginalTableName),
// sql.EscapeName(this.migrationContext.DatabaseName),
// sql.EscapeName(this.migrationContext.GetGhostTableName()),
// sql.EscapeName(this.migrationContext.DatabaseName),
// sql.EscapeName(this.migrationContext.GetChangelogTableName()),
// )
query := fmt . Sprintf ( ` lock /* gh-osc */ tables %s.%s write ` ,
2016-04-08 08:34:44 +00:00
sql . EscapeName ( this . migrationContext . DatabaseName ) ,
sql . EscapeName ( this . migrationContext . OriginalTableName ) ,
)
log . Infof ( "Locking tables" )
2016-04-18 17:57:18 +00:00
this . migrationContext . LockTablesStartTime = time . Now ( )
if _ , err := sqlutils . ExecNoPrepare ( this . singletonDB , query ) ; err != nil {
2016-04-04 16:19:46 +00:00
return err
}
2016-04-08 08:34:44 +00:00
log . Infof ( "Tables locked" )
return nil
}
2016-04-04 16:19:46 +00:00
2016-04-08 08:34:44 +00:00
// UnlockTables
func ( this * Applier ) UnlockTables ( ) error {
query := ` unlock /* gh-osc */ tables `
log . Infof ( "Unlocking tables" )
2016-04-18 17:57:18 +00:00
if _ , err := sqlutils . ExecNoPrepare ( this . singletonDB , query ) ; err != nil {
2016-04-04 16:19:46 +00:00
return err
}
2016-04-08 08:34:44 +00:00
log . Infof ( "Tables unlocked" )
2016-04-04 16:19:46 +00:00
return nil
}
2016-04-08 12:35:06 +00:00
2016-04-22 20:41:20 +00:00
// SwapTablesQuickAndBumpy
func ( this * Applier ) SwapTablesQuickAndBumpy ( ) error {
2016-04-18 17:57:18 +00:00
query := fmt . Sprintf ( ` alter /* gh-osc */ table %s.%s rename %s ` ,
sql . EscapeName ( this . migrationContext . DatabaseName ) ,
sql . EscapeName ( this . migrationContext . OriginalTableName ) ,
sql . EscapeName ( this . migrationContext . GetOldTableName ( ) ) ,
)
log . Infof ( "Renaming original table" )
this . migrationContext . RenameTablesStartTime = time . Now ( )
if _ , err := sqlutils . ExecNoPrepare ( this . singletonDB , query ) ; err != nil {
return err
}
query = fmt . Sprintf ( ` alter /* gh-osc */ table %s.%s rename %s ` ,
sql . EscapeName ( this . migrationContext . DatabaseName ) ,
sql . EscapeName ( this . migrationContext . GetGhostTableName ( ) ) ,
sql . EscapeName ( this . migrationContext . OriginalTableName ) ,
)
log . Infof ( "Renaming ghost table" )
if _ , err := sqlutils . ExecNoPrepare ( this . db , query ) ; err != nil {
return err
}
this . migrationContext . RenameTablesEndTime = time . Now ( )
log . Infof ( "Tables renamed" )
return nil
}
2016-04-23 02:46:34 +00:00
// SwapTablesAtomic issues a single two-table RENAME statement to swap ghost table
// into original's place
func ( this * Applier ) SwapTablesAtomic ( sessionIdChan chan int64 ) error {
tx , err := this . db . Begin ( )
if err != nil {
return err
}
log . Infof ( "Setting timeout for RENAME for %d seconds" , this . migrationContext . SwapTablesTimeoutSeconds )
query := fmt . Sprintf ( ` set session lock_wait_timeout:=%d ` , this . migrationContext . SwapTablesTimeoutSeconds )
if _ , err := tx . Exec ( query ) ; err != nil {
return err
}
var sessionId int64
if err := tx . QueryRow ( ` select connection_id() ` ) . Scan ( & sessionId ) ; err != nil {
return err
}
sessionIdChan <- sessionId
query = fmt . Sprintf ( ` rename /* gh-osc */ table %s.%s to %s.%s, %s.%s to %s.%s ` ,
2016-04-22 20:41:20 +00:00
sql . EscapeName ( this . migrationContext . DatabaseName ) ,
sql . EscapeName ( this . migrationContext . OriginalTableName ) ,
sql . EscapeName ( this . migrationContext . DatabaseName ) ,
sql . EscapeName ( this . migrationContext . GetOldTableName ( ) ) ,
sql . EscapeName ( this . migrationContext . DatabaseName ) ,
sql . EscapeName ( this . migrationContext . GetGhostTableName ( ) ) ,
sql . EscapeName ( this . migrationContext . DatabaseName ) ,
sql . EscapeName ( this . migrationContext . OriginalTableName ) ,
)
log . Infof ( "Renaming tables" )
2016-04-23 02:46:34 +00:00
this . migrationContext . RenameTablesStartTime = time . Now ( )
if _ , err := tx . Exec ( query ) ; err != nil {
2016-04-22 20:41:20 +00:00
return err
}
2016-04-23 02:46:34 +00:00
this . migrationContext . RenameTablesEndTime = time . Now ( )
tx . Commit ( )
2016-04-22 20:41:20 +00:00
log . Infof ( "Tables renamed" )
return nil
}
2016-04-23 02:46:34 +00:00
// StopSlaveIOThread is applicable with --test-on-replica; it stops the IO thread, duh.
// We need to keep the SQL thread active so as to complete processing received events,
// and have them written to the binary log, so that we can then read them via streamer
2016-04-18 17:57:18 +00:00
func ( this * Applier ) StopSlaveIOThread ( ) error {
query := ` stop /* gh-osc */ slave io_thread `
log . Infof ( "Stopping replication" )
if _ , err := sqlutils . ExecNoPrepare ( this . db , query ) ; err != nil {
return err
}
log . Infof ( "Replication stopped" )
return nil
}
2016-05-16 09:03:15 +00:00
// MasterPosWait is applicable with --test-on-replica
func ( this * Applier ) MasterPosWait ( binlogCoordinates * mysql . BinlogCoordinates ) error {
var appliedRows int64
if err := this . db . QueryRow ( ` select master_pos_wait(?, ?, ?) ` , binlogCoordinates . LogFile , binlogCoordinates . LogPos , 1 ) . Scan ( & appliedRows ) ; err != nil {
return err
}
if appliedRows < 0 {
return fmt . Errorf ( "Timeout waiting on master_pos_wait()" )
}
return nil
}
func ( this * Applier ) StopSlaveNicely ( ) error {
if err := this . StopSlaveIOThread ( ) ; err != nil {
return err
}
binlogCoordinates , err := mysql . GetReadBinlogCoordinates ( this . db )
if err != nil {
return err
}
log . Debugf ( "Replication stopped at %+v. Will wait for SQL thread to apply" , * binlogCoordinates )
if err := this . MasterPosWait ( binlogCoordinates ) ; err != nil {
return err
}
log . Debugf ( "Replication SQL thread applied all events" )
if selfBinlogCoordinates , err := mysql . GetSelfBinlogCoordinates ( this . db ) ; err != nil {
return err
} else {
log . Debugf ( "Self binlog coordinates: %+v" , * selfBinlogCoordinates )
}
return nil
}
2016-04-23 02:46:34 +00:00
// GrabVoluntaryLock gets a named lock (`GET_LOCK`) and listens
// on a okToRelease in order to release it
func ( this * Applier ) GrabVoluntaryLock ( lockGrabbed chan <- error , okToRelease <- chan bool ) error {
lockName := this . migrationContext . GetVoluntaryLockName ( )
tx , err := this . db . Begin ( )
if err != nil {
lockGrabbed <- err
return err
}
// Grab
query := ` select get_lock(?, 0) `
lockResult := 0
log . Infof ( "Grabbing voluntary lock: %s" , lockName )
if err := tx . QueryRow ( query , lockName ) . Scan ( & lockResult ) ; err != nil {
lockGrabbed <- err
return err
}
if lockResult != 1 {
err := fmt . Errorf ( "Lock was not acquired" )
lockGrabbed <- err
return err
}
log . Infof ( "Voluntary lock grabbed" )
lockGrabbed <- nil // No error.
// Listeners on the above will proceed to submit the "all queries till lock have been found"
// We will wait here till we're told to. This will happen once all DML events up till lock
// have been appleid on the ghost table
<- okToRelease
// Release
query = ` select ifnull(release_lock(?),0) `
log . Infof ( "Releasing voluntary lock" )
if err := tx . QueryRow ( query , lockName ) . Scan ( & lockResult ) ; err != nil {
return log . Errore ( err )
}
if lockResult != 1 {
// Generally speaking we should never get this.
return log . Errorf ( "release_lock result was %+v" , lockResult )
}
tx . Rollback ( )
log . Infof ( "Voluntary lock released" )
return nil
}
// IssueBlockingQueryOnVoluntaryLock will SELECT on the original table using a
// conditional on a known to be occupied lock. This query is expected to block,
// and will further block the followup RENAME statement
func ( this * Applier ) IssueBlockingQueryOnVoluntaryLock ( sessionIdChan chan int64 ) error {
lockName := this . migrationContext . GetVoluntaryLockName ( )
tx , err := this . db . Begin ( )
if err != nil {
return err
}
var sessionId int64
if err := tx . QueryRow ( ` select connection_id() ` ) . Scan ( & sessionId ) ; err != nil {
return err
}
sessionIdChan <- sessionId
// Grab
query := fmt . Sprintf ( `
select /* gh-osc blocking-query-%s */
release_lock ( ? )
from % s . % s
where
get_lock ( ? , 86400 ) >= 0
limit 1
` ,
lockName ,
sql . EscapeName ( this . migrationContext . DatabaseName ) ,
sql . EscapeName ( this . migrationContext . OriginalTableName ) ,
)
dummyResult := 0
log . Infof ( "Issuing blocking query" )
this . migrationContext . LockTablesStartTime = time . Now ( )
tx . QueryRow ( query , lockName , lockName ) . Scan ( & dummyResult )
tx . Rollback ( )
log . Infof ( "Blocking query released" )
return nil
}
func ( this * Applier ) ExpectProcess ( sessionId int64 , stateHint , infoHint string ) error {
found := false
query := `
select id
from information_schema . processlist
where
id != connection_id ( )
and ? in ( 0 , id )
and state like concat ( '%' , ? , '%' )
and info like concat ( '%' , ? , '%' )
`
err := sqlutils . QueryRowsMap ( this . db , query , func ( m sqlutils . RowMap ) error {
found = true
return nil
} , sessionId , stateHint , infoHint )
if err != nil {
return err
}
if ! found {
return fmt . Errorf ( "Cannot find process. Hints: %s, %s" , stateHint , infoHint )
}
return nil
}
2016-04-08 12:35:06 +00:00
func ( this * Applier ) ShowStatusVariable ( variableName string ) ( result int64 , err error ) {
query := fmt . Sprintf ( ` show global status like '%s' ` , variableName )
if err := this . db . QueryRow ( query ) . Scan ( & variableName , & result ) ; err != nil {
return 0 , err
}
return result , nil
}
2016-04-11 15:27:16 +00:00
2016-05-04 05:23:34 +00:00
func ( this * Applier ) buildDMLEventQuery ( dmlEvent * binlog . BinlogDMLEvent ) ( query string , args [ ] interface { } , rowsDelta int64 , err error ) {
2016-04-11 15:27:16 +00:00
switch dmlEvent . DML {
case binlog . DeleteDML :
{
2016-04-14 11:37:56 +00:00
query , uniqueKeyArgs , err := sql . BuildDMLDeleteQuery ( dmlEvent . DatabaseName , this . migrationContext . GetGhostTableName ( ) , this . migrationContext . OriginalTableColumns , & this . migrationContext . UniqueKey . Columns , dmlEvent . WhereColumnValues . AbstractValues ( ) )
2016-05-04 05:23:34 +00:00
return query , uniqueKeyArgs , - 1 , err
2016-04-14 11:37:56 +00:00
}
case binlog . InsertDML :
{
query , sharedArgs , err := sql . BuildDMLInsertQuery ( dmlEvent . DatabaseName , this . migrationContext . GetGhostTableName ( ) , this . migrationContext . OriginalTableColumns , this . migrationContext . SharedColumns , dmlEvent . NewColumnValues . AbstractValues ( ) )
2016-05-04 05:23:34 +00:00
return query , sharedArgs , 1 , err
2016-04-14 11:37:56 +00:00
}
case binlog . UpdateDML :
{
query , sharedArgs , uniqueKeyArgs , err := sql . BuildDMLUpdateQuery ( dmlEvent . DatabaseName , this . migrationContext . GetGhostTableName ( ) , this . migrationContext . OriginalTableColumns , this . migrationContext . SharedColumns , & this . migrationContext . UniqueKey . Columns , dmlEvent . NewColumnValues . AbstractValues ( ) , dmlEvent . WhereColumnValues . AbstractValues ( ) )
args = append ( args , sharedArgs ... )
args = append ( args , uniqueKeyArgs ... )
2016-05-04 05:23:34 +00:00
return query , args , 0 , err
2016-04-11 15:27:16 +00:00
}
}
2016-05-04 05:23:34 +00:00
return "" , args , 0 , fmt . Errorf ( "Unknown dml event type: %+v" , dmlEvent . DML )
2016-04-14 11:37:56 +00:00
}
func ( this * Applier ) ApplyDMLEventQuery ( dmlEvent * binlog . BinlogDMLEvent ) error {
2016-05-04 05:23:34 +00:00
query , args , rowDelta , err := this . buildDMLEventQuery ( dmlEvent )
2016-04-14 11:37:56 +00:00
if err != nil {
return err
}
_ , err = sqlutils . Exec ( this . db , query , args ... )
2016-04-19 11:25:32 +00:00
if err == nil {
atomic . AddInt64 ( & this . migrationContext . TotalDMLEventsApplied , 1 )
}
2016-05-04 05:23:34 +00:00
if this . migrationContext . CountTableRows {
atomic . AddInt64 ( & this . migrationContext . RowsEstimate , rowDelta )
}
2016-04-14 11:37:56 +00:00
return err
2016-04-11 15:27:16 +00:00
}