Skip to content

Commit b6cc422

Browse files
Set transaction isolation in connections
1 parent 7d8e4e8 commit b6cc422

File tree

20 files changed

+214
-48
lines changed

20 files changed

+214
-48
lines changed

.github/workflows/golangci-lint.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,5 @@ jobs:
1919
- uses: actions/checkout@v3
2020
- name: golangci-lint
2121
uses: golangci/golangci-lint-action@v3
22+
with:
23+
version: v1.46.2

doc/command-line-flags.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ A more in-depth discussion of various `gh-ost` command line flags: implementatio
66

77
Add this flag when executing on Aliyun RDS.
88

9+
### allow-zero-in-date
10+
11+
Allows the user to make schema changes that include a zero date or zero in date (e.g. adding a `datetime default '0000-00-00 00:00:00'` column), even if global `sql_mode` on MySQL has `NO_ZERO_IN_DATE,NO_ZERO_DATE`.
12+
913
### azure
1014

1115
Add this flag when executing on Azure Database for MySQL.

doc/requirements-and-limitations.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ The `SUPER` privilege is required for `STOP SLAVE`, `START SLAVE` operations. Th
2020
- Switching your `binlog_format` to `ROW`, in the case where it is _not_ `ROW` and you explicitly specified `--switch-to-rbr`
2121
- If your replication is already in RBR (`binlog_format=ROW`) you can specify `--assume-rbr` to avoid the `STOP SLAVE/START SLAVE` operations, hence no need for `SUPER`.
2222

23+
- `gh-ost` uses the `REPEATABLE_READ` transaction isolation level for all MySQL connections, regardless of the server default.
24+
2325
- Running `--test-on-replica`: before the cut-over phase, `gh-ost` stops replication so that you can compare the two tables and satisfy that the migration is sound.
2426

2527
### Limitations

go/base/context.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ type MigrationContext struct {
9292
AssumeRBR bool
9393
SkipForeignKeyChecks bool
9494
SkipStrictMode bool
95+
AllowZeroInDate bool
9596
NullableUniqueKeyAllowed bool
9697
ApproveRenamedColumns bool
9798
SkipRenamedColumns bool

go/base/load_map.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func NewLoadMap() LoadMap {
2323

2424
// NewLoadMap parses a `--*-load` flag (e.g. `--max-load`), which is in multiple
2525
// key-value format, such as:
26-
// 'Threads_running=100,Threads_connected=500'
26+
// 'Threads_running=100,Threads_connected=500'
2727
func ParseLoadMap(loadList string) (LoadMap, error) {
2828
result := NewLoadMap()
2929
if loadList == "" {

go/cmd/gh-ost/main.go

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ func main() {
7878
flag.BoolVar(&migrationContext.DiscardForeignKeys, "discard-foreign-keys", false, "DANGER! This flag will migrate a table that has foreign keys and will NOT create foreign keys on the ghost table, thus your altered table will have NO foreign keys. This is useful for intentional dropping of foreign keys")
7979
flag.BoolVar(&migrationContext.SkipForeignKeyChecks, "skip-foreign-key-checks", false, "set to 'true' when you know for certain there are no foreign keys on your table, and wish to skip the time it takes for gh-ost to verify that")
8080
flag.BoolVar(&migrationContext.SkipStrictMode, "skip-strict-mode", false, "explicitly tell gh-ost binlog applier not to enforce strict sql mode")
81+
flag.BoolVar(&migrationContext.AllowZeroInDate, "allow-zero-in-date", false, "explicitly tell gh-ost binlog applier to ignore NO_ZERO_IN_DATE,NO_ZERO_DATE in sql_mode")
8182
flag.BoolVar(&migrationContext.AliyunRDS, "aliyun-rds", false, "set to 'true' when you execute on Aliyun RDS.")
8283
flag.BoolVar(&migrationContext.GoogleCloudPlatform, "gcp", false, "set to 'true' when you execute on a 1st generation Google Cloud Platform (GCP).")
8384
flag.BoolVar(&migrationContext.AzureMySQL, "azure", false, "set to 'true' when you execute on Azure Database on MySQL.")
@@ -180,7 +181,7 @@ func main() {
180181
}
181182

182183
if migrationContext.AlterStatement == "" {
183-
log.Fatalf("--alter must be provided and statement must not be empty")
184+
log.Fatal("--alter must be provided and statement must not be empty")
184185
}
185186
parser := sql.NewParserFromAlterStatement(migrationContext.AlterStatement)
186187
migrationContext.AlterStatementOptions = parser.GetAlterStatementOptions()
@@ -189,7 +190,7 @@ func main() {
189190
if parser.HasExplicitSchema() {
190191
migrationContext.DatabaseName = parser.GetExplicitSchema()
191192
} else {
192-
log.Fatalf("--database must be provided and database name must not be empty, or --alter must specify database name")
193+
log.Fatal("--database must be provided and database name must not be empty, or --alter must specify database name")
193194
}
194195
}
195196

@@ -201,48 +202,48 @@ func main() {
201202
if parser.HasExplicitTable() {
202203
migrationContext.OriginalTableName = parser.GetExplicitTable()
203204
} else {
204-
log.Fatalf("--table must be provided and table name must not be empty, or --alter must specify table name")
205+
log.Fatal("--table must be provided and table name must not be empty, or --alter must specify table name")
205206
}
206207
}
207208
migrationContext.Noop = !(*executeFlag)
208209
if migrationContext.AllowedRunningOnMaster && migrationContext.TestOnReplica {
209-
migrationContext.Log.Fatalf("--allow-on-master and --test-on-replica are mutually exclusive")
210+
migrationContext.Log.Fatal("--allow-on-master and --test-on-replica are mutually exclusive")
210211
}
211212
if migrationContext.AllowedRunningOnMaster && migrationContext.MigrateOnReplica {
212-
migrationContext.Log.Fatalf("--allow-on-master and --migrate-on-replica are mutually exclusive")
213+
migrationContext.Log.Fatal("--allow-on-master and --migrate-on-replica are mutually exclusive")
213214
}
214215
if migrationContext.MigrateOnReplica && migrationContext.TestOnReplica {
215-
migrationContext.Log.Fatalf("--migrate-on-replica and --test-on-replica are mutually exclusive")
216+
migrationContext.Log.Fatal("--migrate-on-replica and --test-on-replica are mutually exclusive")
216217
}
217218
if migrationContext.SwitchToRowBinlogFormat && migrationContext.AssumeRBR {
218-
migrationContext.Log.Fatalf("--switch-to-rbr and --assume-rbr are mutually exclusive")
219+
migrationContext.Log.Fatal("--switch-to-rbr and --assume-rbr are mutually exclusive")
219220
}
220221
if migrationContext.TestOnReplicaSkipReplicaStop {
221222
if !migrationContext.TestOnReplica {
222-
migrationContext.Log.Fatalf("--test-on-replica-skip-replica-stop requires --test-on-replica to be enabled")
223+
migrationContext.Log.Fatal("--test-on-replica-skip-replica-stop requires --test-on-replica to be enabled")
223224
}
224225
migrationContext.Log.Warning("--test-on-replica-skip-replica-stop enabled. We will not stop replication before cut-over. Ensure you have a plugin that does this.")
225226
}
226227
if migrationContext.CliMasterUser != "" && migrationContext.AssumeMasterHostname == "" {
227-
migrationContext.Log.Fatalf("--master-user requires --assume-master-host")
228+
migrationContext.Log.Fatal("--master-user requires --assume-master-host")
228229
}
229230
if migrationContext.CliMasterPassword != "" && migrationContext.AssumeMasterHostname == "" {
230-
migrationContext.Log.Fatalf("--master-password requires --assume-master-host")
231+
migrationContext.Log.Fatal("--master-password requires --assume-master-host")
231232
}
232233
if migrationContext.TLSCACertificate != "" && !migrationContext.UseTLS {
233-
migrationContext.Log.Fatalf("--ssl-ca requires --ssl")
234+
migrationContext.Log.Fatal("--ssl-ca requires --ssl")
234235
}
235236
if migrationContext.TLSCertificate != "" && !migrationContext.UseTLS {
236-
migrationContext.Log.Fatalf("--ssl-cert requires --ssl")
237+
migrationContext.Log.Fatal("--ssl-cert requires --ssl")
237238
}
238239
if migrationContext.TLSKey != "" && !migrationContext.UseTLS {
239-
migrationContext.Log.Fatalf("--ssl-key requires --ssl")
240+
migrationContext.Log.Fatal("--ssl-key requires --ssl")
240241
}
241242
if migrationContext.TLSAllowInsecure && !migrationContext.UseTLS {
242-
migrationContext.Log.Fatalf("--ssl-allow-insecure requires --ssl")
243+
migrationContext.Log.Fatal("--ssl-allow-insecure requires --ssl")
243244
}
244245
if *replicationLagQuery != "" {
245-
migrationContext.Log.Warningf("--replication-lag-query is deprecated")
246+
migrationContext.Log.Warning("--replication-lag-query is deprecated")
246247
}
247248

248249
switch *cutOver {

go/logic/applier.go

Lines changed: 75 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,24 @@ func (this *Applier) validateAndReadTimeZone() error {
117117
return nil
118118
}
119119

120+
// generateSqlModeQuery return a `sql_mode = ...` query, to be wrapped with a `set session` or `set global`,
121+
// based on gh-ost configuration:
122+
// - User may skip strict mode
123+
// - User may allow zero dats or zero in dates
124+
func (this *Applier) generateSqlModeQuery() string {
125+
sqlModeAddendum := `,NO_AUTO_VALUE_ON_ZERO`
126+
if !this.migrationContext.SkipStrictMode {
127+
sqlModeAddendum = fmt.Sprintf("%s,STRICT_ALL_TABLES", sqlModeAddendum)
128+
}
129+
sqlModeQuery := fmt.Sprintf("CONCAT(@@session.sql_mode, ',%s')", sqlModeAddendum)
130+
if this.migrationContext.AllowZeroInDate {
131+
sqlModeQuery = fmt.Sprintf("REPLACE(REPLACE(%s, 'NO_ZERO_IN_DATE', ''), 'NO_ZERO_DATE', '')", sqlModeQuery)
132+
}
133+
sqlModeQuery = fmt.Sprintf("sql_mode = %s", sqlModeQuery)
134+
135+
return sqlModeQuery
136+
}
137+
120138
// readTableColumns reads table columns on applier
121139
func (this *Applier) readTableColumns() (err error) {
122140
this.migrationContext.Log.Infof("Examining table structure on applier")
@@ -182,11 +200,33 @@ func (this *Applier) CreateGhostTable() error {
182200
sql.EscapeName(this.migrationContext.DatabaseName),
183201
sql.EscapeName(this.migrationContext.GetGhostTableName()),
184202
)
185-
if _, err := sqlutils.ExecNoPrepare(this.db, query); err != nil {
186-
return err
187-
}
188-
this.migrationContext.Log.Infof("Ghost table created")
189-
return nil
203+
204+
err := func() error {
205+
tx, err := this.db.Begin()
206+
if err != nil {
207+
return err
208+
}
209+
defer tx.Rollback()
210+
211+
sessionQuery := fmt.Sprintf(`SET SESSION time_zone = '%s'`, this.migrationContext.ApplierTimeZone)
212+
sessionQuery = fmt.Sprintf("%s, %s", sessionQuery, this.generateSqlModeQuery())
213+
214+
if _, err := tx.Exec(sessionQuery); err != nil {
215+
return err
216+
}
217+
if _, err := tx.Exec(query); err != nil {
218+
return err
219+
}
220+
this.migrationContext.Log.Infof("Ghost table created")
221+
if err := tx.Commit(); err != nil {
222+
// Neither SET SESSION nor ALTER are really transactional, so strictly speaking
223+
// there's no need to commit; but let's do this the legit way anyway.
224+
return err
225+
}
226+
return nil
227+
}()
228+
229+
return err
190230
}
191231

192232
// AlterGhost applies `alter` statement on ghost table
@@ -201,11 +241,33 @@ func (this *Applier) AlterGhost() error {
201241
sql.EscapeName(this.migrationContext.GetGhostTableName()),
202242
)
203243
this.migrationContext.Log.Debugf("ALTER statement: %s", query)
204-
if _, err := sqlutils.ExecNoPrepare(this.db, query); err != nil {
205-
return err
206-
}
207-
this.migrationContext.Log.Infof("Ghost table altered")
208-
return nil
244+
245+
err := func() error {
246+
tx, err := this.db.Begin()
247+
if err != nil {
248+
return err
249+
}
250+
defer tx.Rollback()
251+
252+
sessionQuery := fmt.Sprintf(`SET SESSION time_zone = '%s'`, this.migrationContext.ApplierTimeZone)
253+
sessionQuery = fmt.Sprintf("%s, %s", sessionQuery, this.generateSqlModeQuery())
254+
255+
if _, err := tx.Exec(sessionQuery); err != nil {
256+
return err
257+
}
258+
if _, err := tx.Exec(query); err != nil {
259+
return err
260+
}
261+
this.migrationContext.Log.Infof("Ghost table altered")
262+
if err := tx.Commit(); err != nil {
263+
// Neither SET SESSION nor ALTER are really transactional, so strictly speaking
264+
// there's no need to commit; but let's do this the legit way anyway.
265+
return err
266+
}
267+
return nil
268+
}()
269+
270+
return err
209271
}
210272

211273
// AlterGhost applies `alter` statement on ghost table
@@ -539,12 +601,9 @@ func (this *Applier) ApplyIterationInsertQuery() (chunkSize int64, rowsAffected
539601
return nil, err
540602
}
541603
defer tx.Rollback()
604+
542605
sessionQuery := fmt.Sprintf(`SET SESSION time_zone = '%s'`, this.migrationContext.ApplierTimeZone)
543-
sqlModeAddendum := `,NO_AUTO_VALUE_ON_ZERO`
544-
if !this.migrationContext.SkipStrictMode {
545-
sqlModeAddendum = fmt.Sprintf("%s,STRICT_ALL_TABLES", sqlModeAddendum)
546-
}
547-
sessionQuery = fmt.Sprintf("%s, sql_mode = CONCAT(@@session.sql_mode, ',%s')", sessionQuery, sqlModeAddendum)
606+
sessionQuery = fmt.Sprintf("%s, %s", sessionQuery, this.generateSqlModeQuery())
548607

549608
if _, err := tx.Exec(sessionQuery); err != nil {
550609
return nil, err
@@ -1056,12 +1115,7 @@ func (this *Applier) ApplyDMLEventQueries(dmlEvents [](*binlog.BinlogDMLEvent))
10561115
}
10571116

10581117
sessionQuery := "SET SESSION time_zone = '+00:00'"
1059-
1060-
sqlModeAddendum := `,NO_AUTO_VALUE_ON_ZERO`
1061-
if !this.migrationContext.SkipStrictMode {
1062-
sqlModeAddendum = fmt.Sprintf("%s,STRICT_ALL_TABLES", sqlModeAddendum)
1063-
}
1064-
sessionQuery = fmt.Sprintf("%s, sql_mode = CONCAT(@@session.sql_mode, ',%s')", sessionQuery, sqlModeAddendum)
1118+
sessionQuery = fmt.Sprintf("%s, %s", sessionQuery, this.generateSqlModeQuery())
10651119

10661120
if _, err := tx.Exec(sessionQuery); err != nil {
10671121
return rollback(err)

go/mysql/connection.go

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2016 GitHub Inc.
2+
Copyright 2022 GitHub Inc.
33
See https://github.com/github/gh-ost/blob/master/LICENSE
44
*/
55

@@ -12,12 +12,14 @@ import (
1212
"fmt"
1313
"io/ioutil"
1414
"net"
15+
"strings"
1516

1617
"github.com/go-sql-driver/mysql"
1718
)
1819

1920
const (
20-
TLS_CONFIG_KEY = "ghost"
21+
transactionIsolation = "REPEATABLE-READ"
22+
TLS_CONFIG_KEY = "ghost"
2123
)
2224

2325
// ConnectionConfig is the minimal configuration required to connect to a MySQL server
@@ -112,12 +114,21 @@ func (this *ConnectionConfig) GetDBUri(databaseName string) string {
112114
// Wrap IPv6 literals in square brackets
113115
hostname = fmt.Sprintf("[%s]", hostname)
114116
}
115-
interpolateParams := true
116-
// go-mysql-driver defaults to false if tls param is not provided; explicitly setting here to
117-
// simplify construction of the DSN below.
118-
tlsOption := "false"
117+
118+
tls := "false"
119119
if this.tlsConfig != nil {
120-
tlsOption = TLS_CONFIG_KEY
120+
tls = TLS_CONFIG_KEY
121+
}
122+
connectionParams := []string{
123+
"autocommit=true",
124+
"charset=utf8mb4,utf8,latin1",
125+
"interpolateParams=true",
126+
fmt.Sprintf("tls=%s", tls),
127+
fmt.Sprintf("transaction_isolation=%q", transactionIsolation),
128+
fmt.Sprintf("timeout=%fs", this.Timeout),
129+
fmt.Sprintf("readTimeout=%fs", this.Timeout),
130+
fmt.Sprintf("writeTimeout=%fs", this.Timeout),
121131
}
122-
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?timeout=%fs&readTimeout=%fs&writeTimeout=%fs&interpolateParams=%t&autocommit=true&charset=utf8mb4,utf8,latin1&tls=%s", this.User, this.Password, hostname, this.Key.Port, databaseName, this.Timeout, this.Timeout, this.Timeout, interpolateParams, tlsOption)
132+
133+
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?%s", this.User, this.Password, hostname, this.Key.Port, databaseName, strings.Join(connectionParams, "&"))
123134
}

go/mysql/connection_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2016 GitHub Inc.
2+
Copyright 2022 GitHub Inc.
33
See https://github.com/github/gh-ost/blob/master/LICENSE
44
*/
55

@@ -67,18 +67,20 @@ func TestGetDBUri(t *testing.T) {
6767
c.Key = InstanceKey{Hostname: "myhost", Port: 3306}
6868
c.User = "gromit"
6969
c.Password = "penguin"
70+
c.Timeout = 1.2345
7071

7172
uri := c.GetDBUri("test")
72-
test.S(t).ExpectEquals(uri, "gromit:penguin@tcp(myhost:3306)/test?timeout=0.000000s&readTimeout=0.000000s&writeTimeout=0.000000s&interpolateParams=true&autocommit=true&charset=utf8mb4,utf8,latin1&tls=false")
73+
test.S(t).ExpectEquals(uri, `gromit:penguin@tcp(myhost:3306)/test?autocommit=true&charset=utf8mb4,utf8,latin1&interpolateParams=true&tls=false&transaction_isolation="REPEATABLE-READ"&timeout=1.234500s&readTimeout=1.234500s&writeTimeout=1.234500s`)
7374
}
7475

7576
func TestGetDBUriWithTLSSetup(t *testing.T) {
7677
c := NewConnectionConfig()
7778
c.Key = InstanceKey{Hostname: "myhost", Port: 3306}
7879
c.User = "gromit"
7980
c.Password = "penguin"
81+
c.Timeout = 1.2345
8082
c.tlsConfig = &tls.Config{}
8183

8284
uri := c.GetDBUri("test")
83-
test.S(t).ExpectEquals(uri, "gromit:penguin@tcp(myhost:3306)/test?timeout=0.000000s&readTimeout=0.000000s&writeTimeout=0.000000s&interpolateParams=true&autocommit=true&charset=utf8mb4,utf8,latin1&tls=ghost")
85+
test.S(t).ExpectEquals(uri, `gromit:penguin@tcp(myhost:3306)/test?autocommit=true&charset=utf8mb4,utf8,latin1&interpolateParams=true&tls=ghost&transaction_isolation="REPEATABLE-READ"&timeout=1.234500s&readTimeout=1.234500s&writeTimeout=1.234500s`)
8486
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
drop table if exists gh_ost_test;
2+
create table gh_ost_test (
3+
id int unsigned auto_increment,
4+
i int not null,
5+
dt datetime,
6+
primary key(id)
7+
) auto_increment=1;
8+
9+
drop event if exists gh_ost_test;
10+
delimiter ;;
11+
create event gh_ost_test
12+
on schedule every 1 second
13+
starts current_timestamp
14+
ends current_timestamp + interval 60 second
15+
on completion not preserve
16+
enable
17+
do
18+
begin
19+
insert into gh_ost_test values (null, 7, '2010-10-20 10:20:30');
20+
end ;;

0 commit comments

Comments
 (0)