Skip to content

Commit 30f28c2

Browse files
Merge pull request #1455 from github/arthur/dont-lock-rows
Avoid causing deadlocks when copying rows on busy tables
2 parents 9af3a07 + 5ddeb21 commit 30f28c2

File tree

4 files changed

+190
-33
lines changed

4 files changed

+190
-33
lines changed

go/logic/applier.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,8 @@ func (this *Applier) ApplyIterationInsertQuery() (chunkSize int64, rowsAffected
631631
this.migrationContext.MigrationIterationRangeMaxValues.AbstractValues(),
632632
this.migrationContext.GetIteration() == 0,
633633
this.migrationContext.IsTransactionalTable(),
634+
// TODO: Don't hardcode this
635+
strings.HasPrefix(this.migrationContext.ApplierMySQLVersion, "8."),
634636
)
635637
if err != nil {
636638
return chunkSize, rowsAffected, duration, err

go/logic/applier_test.go

Lines changed: 167 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
gosql "database/sql"
1111
"strings"
1212
"testing"
13+
"time"
1314

1415
"github.com/stretchr/testify/require"
1516
"github.com/stretchr/testify/suite"
@@ -199,6 +200,30 @@ type ApplierTestSuite struct {
199200
mysqlContainer testcontainers.Container
200201
}
201202

203+
func (suite *ApplierTestSuite) getConnectionConfig(ctx context.Context) (*mysql.ConnectionConfig, error) {
204+
host, err := suite.mysqlContainer.ContainerIP(ctx)
205+
if err != nil {
206+
return nil, err
207+
}
208+
209+
config := mysql.NewConnectionConfig()
210+
config.Key.Hostname = host
211+
config.Key.Port = 3306
212+
config.User = "root"
213+
config.Password = "root-password"
214+
215+
return config, nil
216+
}
217+
218+
func (suite *ApplierTestSuite) getDb(ctx context.Context) (*gosql.DB, error) {
219+
host, err := suite.mysqlContainer.ContainerIP(ctx)
220+
if err != nil {
221+
return nil, err
222+
}
223+
224+
return gosql.Open("mysql", "root:root-password@tcp("+host+":3306)/test")
225+
}
226+
202227
func (suite *ApplierTestSuite) SetupSuite() {
203228
ctx := context.Background()
204229
req := testcontainers.ContainerRequest{
@@ -229,7 +254,7 @@ func (suite *ApplierTestSuite) SetupTest() {
229254
suite.Require().NoError(err)
230255
suite.Require().Equalf(0, rc, "failed to created database: expected exit code 0, got %d", rc)
231256

232-
rc, _, err = suite.mysqlContainer.Exec(ctx, []string{"mysql", "-uroot", "-proot-password", "-e", "CREATE TABLE test.testing (id INT, item_id INT);"})
257+
rc, _, err = suite.mysqlContainer.Exec(ctx, []string{"mysql", "-uroot", "-proot-password", "-e", "CREATE TABLE test.testing (id INT, item_id INT, PRIMARY KEY (id));"})
233258
suite.Require().NoError(err)
234259
suite.Require().Equalf(0, rc, "failed to created table: expected exit code 0, got %d", rc)
235260
}
@@ -245,15 +270,11 @@ func (suite *ApplierTestSuite) TearDownTest() {
245270
func (suite *ApplierTestSuite) TestInitDBConnections() {
246271
ctx := context.Background()
247272

248-
host, err := suite.mysqlContainer.ContainerIP(ctx)
273+
connectionConfig, err := suite.getConnectionConfig(ctx)
249274
suite.Require().NoError(err)
250275

251276
migrationContext := base.NewMigrationContext()
252-
migrationContext.ApplierConnectionConfig = mysql.NewConnectionConfig()
253-
migrationContext.ApplierConnectionConfig.Key.Hostname = host
254-
migrationContext.ApplierConnectionConfig.Key.Port = 3306
255-
migrationContext.ApplierConnectionConfig.User = "root"
256-
migrationContext.ApplierConnectionConfig.Password = "root-password"
277+
migrationContext.ApplierConnectionConfig = connectionConfig
257278
migrationContext.DatabaseName = "test"
258279
migrationContext.OriginalTableName = "testing"
259280
migrationContext.SetConnectionConfig("innodb")
@@ -274,15 +295,11 @@ func (suite *ApplierTestSuite) TestInitDBConnections() {
274295
func (suite *ApplierTestSuite) TestApplyDMLEventQueries() {
275296
ctx := context.Background()
276297

277-
host, err := suite.mysqlContainer.ContainerIP(ctx)
298+
connectionConfig, err := suite.getConnectionConfig(ctx)
278299
suite.Require().NoError(err)
279300

280301
migrationContext := base.NewMigrationContext()
281-
migrationContext.ApplierConnectionConfig = mysql.NewConnectionConfig()
282-
migrationContext.ApplierConnectionConfig.Key.Hostname = host
283-
migrationContext.ApplierConnectionConfig.Key.Port = 3306
284-
migrationContext.ApplierConnectionConfig.User = "root"
285-
migrationContext.ApplierConnectionConfig.Password = "root-password"
302+
migrationContext.ApplierConnectionConfig = connectionConfig
286303
migrationContext.DatabaseName = "test"
287304
migrationContext.OriginalTableName = "testing"
288305
migrationContext.SetConnectionConfig("innodb")
@@ -313,7 +330,7 @@ func (suite *ApplierTestSuite) TestApplyDMLEventQueries() {
313330
suite.Require().NoError(err)
314331

315332
// Check that the row was inserted
316-
db, err := gosql.Open("mysql", "root:root-password@tcp("+host+":3306)/test")
333+
db, err := suite.getDb(ctx)
317334
suite.Require().NoError(err)
318335
defer db.Close()
319336

@@ -340,15 +357,11 @@ func (suite *ApplierTestSuite) TestApplyDMLEventQueries() {
340357
func (suite *ApplierTestSuite) TestValidateOrDropExistingTables() {
341358
ctx := context.Background()
342359

343-
host, err := suite.mysqlContainer.ContainerIP(ctx)
360+
connectionConfig, err := suite.getConnectionConfig(ctx)
344361
suite.Require().NoError(err)
345362

346363
migrationContext := base.NewMigrationContext()
347-
migrationContext.ApplierConnectionConfig = mysql.NewConnectionConfig()
348-
migrationContext.ApplierConnectionConfig.Key.Hostname = host
349-
migrationContext.ApplierConnectionConfig.Key.Port = 3306
350-
migrationContext.ApplierConnectionConfig.User = "root"
351-
migrationContext.ApplierConnectionConfig.Password = "root-password"
364+
migrationContext.ApplierConnectionConfig = connectionConfig
352365
migrationContext.DatabaseName = "test"
353366
migrationContext.OriginalTableName = "testing"
354367
migrationContext.SetConnectionConfig("innodb")
@@ -367,6 +380,140 @@ func (suite *ApplierTestSuite) TestValidateOrDropExistingTables() {
367380
suite.Require().NoError(err)
368381
}
369382

383+
func (suite *ApplierTestSuite) TestApplyIterationInsertQuery() {
384+
ctx := context.Background()
385+
386+
connectionConfig, err := suite.getConnectionConfig(ctx)
387+
suite.Require().NoError(err)
388+
389+
migrationContext := base.NewMigrationContext()
390+
migrationContext.ApplierConnectionConfig = connectionConfig
391+
migrationContext.DatabaseName = "test"
392+
migrationContext.OriginalTableName = "testing"
393+
migrationContext.ChunkSize = 10
394+
migrationContext.SetConnectionConfig("innodb")
395+
396+
db, err := suite.getDb(ctx)
397+
suite.Require().NoError(err)
398+
defer db.Close()
399+
400+
_, err = db.Exec("CREATE TABLE test._testing_gho (id INT, item_id INT, PRIMARY KEY (id))")
401+
suite.Require().NoError(err)
402+
403+
// Insert some test values
404+
for i := 1; i <= 10; i++ {
405+
_, err = db.Exec("INSERT INTO test.testing (id, item_id) VALUES (?, ?)", i, i)
406+
suite.Require().NoError(err)
407+
}
408+
409+
migrationContext.SharedColumns = sql.NewColumnList([]string{"id", "item_id"})
410+
migrationContext.MappedSharedColumns = sql.NewColumnList([]string{"id", "item_id"})
411+
migrationContext.UniqueKey = &sql.UniqueKey{
412+
Name: "PRIMARY",
413+
Columns: *sql.NewColumnList([]string{"id"}),
414+
}
415+
416+
migrationContext.MigrationIterationRangeMinValues = sql.ToColumnValues([]interface{}{1})
417+
migrationContext.MigrationIterationRangeMaxValues = sql.ToColumnValues([]interface{}{10})
418+
419+
applier := NewApplier(migrationContext)
420+
defer applier.Teardown()
421+
422+
err = applier.InitDBConnections()
423+
suite.Require().NoError(err)
424+
425+
chunkSize, rowsAffected, duration, err := applier.ApplyIterationInsertQuery()
426+
suite.Require().NoError(err)
427+
428+
suite.Require().Equal(migrationContext.ChunkSize, chunkSize)
429+
suite.Require().Equal(int64(10), rowsAffected)
430+
suite.Require().Greater(duration, time.Duration(0))
431+
432+
// Check that the rows were inserted
433+
rows, err := db.Query("SELECT * FROM test._testing_gho")
434+
suite.Require().NoError(err)
435+
defer rows.Close()
436+
437+
var count, id, item_id int
438+
for rows.Next() {
439+
err = rows.Scan(&id, &item_id)
440+
suite.Require().NoError(err)
441+
count += 1
442+
}
443+
suite.Require().NoError(rows.Err())
444+
445+
suite.Require().Equal(10, count)
446+
}
447+
448+
func (suite *ApplierTestSuite) TestApplyIterationInsertQueryFailsFastWhenSelectingLockedRows() {
449+
ctx := context.Background()
450+
451+
connectionConfig, err := suite.getConnectionConfig(ctx)
452+
suite.Require().NoError(err)
453+
454+
migrationContext := base.NewMigrationContext()
455+
migrationContext.ApplierConnectionConfig = connectionConfig
456+
migrationContext.DatabaseName = "test"
457+
migrationContext.OriginalTableName = "testing"
458+
migrationContext.ChunkSize = 10
459+
migrationContext.TableEngine = "innodb"
460+
migrationContext.SetConnectionConfig("innodb")
461+
462+
db, err := suite.getDb(ctx)
463+
suite.Require().NoError(err)
464+
defer db.Close()
465+
466+
_, err = db.Exec("CREATE TABLE test._testing_gho (id INT, item_id INT, PRIMARY KEY (id))")
467+
suite.Require().NoError(err)
468+
469+
// Insert some test values
470+
for i := 1; i <= 10; i++ {
471+
_, err = db.Exec("INSERT INTO test.testing (id, item_id) VALUES (?, ?)", i, i)
472+
suite.Require().NoError(err)
473+
}
474+
475+
migrationContext.SharedColumns = sql.NewColumnList([]string{"id", "item_id"})
476+
migrationContext.MappedSharedColumns = sql.NewColumnList([]string{"id", "item_id"})
477+
migrationContext.UniqueKey = &sql.UniqueKey{
478+
Name: "PRIMARY",
479+
Columns: *sql.NewColumnList([]string{"id"}),
480+
}
481+
482+
migrationContext.MigrationIterationRangeMinValues = sql.ToColumnValues([]interface{}{1})
483+
migrationContext.MigrationIterationRangeMaxValues = sql.ToColumnValues([]interface{}{10})
484+
485+
applier := NewApplier(migrationContext)
486+
defer applier.Teardown()
487+
488+
err = applier.InitDBConnections()
489+
suite.Require().NoError(err)
490+
491+
// Lock one of the rows
492+
tx, err := db.Begin()
493+
suite.Require().NoError(err)
494+
defer func() {
495+
suite.Require().NoError(tx.Rollback())
496+
}()
497+
498+
_, err = tx.Exec("SELECT * FROM test.testing WHERE id = 5 FOR UPDATE")
499+
suite.Require().NoError(err)
500+
501+
chunkSize, rowsAffected, duration, err := applier.ApplyIterationInsertQuery()
502+
suite.Require().Error(err)
503+
suite.Require().EqualError(err, "Error 3572 (HY000): Statement aborted because lock(s) could not be acquired immediately and NOWAIT is set.")
504+
505+
suite.Require().Equal(migrationContext.ChunkSize, chunkSize)
506+
suite.Require().Equal(int64(0), rowsAffected)
507+
suite.Require().Equal(time.Duration(0), duration)
508+
509+
// Check that the no rows were inserted
510+
var count int
511+
err = db.QueryRow("SELECT COUNT(*) FROM test._testing_gho").Scan(&count)
512+
suite.Require().NoError(err)
513+
514+
suite.Require().Equal(0, count)
515+
}
516+
370517
func TestApplier(t *testing.T) {
371518
suite.Run(t, new(ApplierTestSuite))
372519
}

go/sql/builder.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ func BuildRangePreparedComparison(columns *ColumnList, args []interface{}, compa
182182
return BuildRangeComparison(columns.Names(), values, args, comparisonSign)
183183
}
184184

185-
func BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName string, sharedColumns []string, mappedSharedColumns []string, uniqueKey string, uniqueKeyColumns *ColumnList, rangeStartValues, rangeEndValues []string, rangeStartArgs, rangeEndArgs []interface{}, includeRangeStartValues bool, transactionalTable bool) (result string, explodedArgs []interface{}, err error) {
185+
func BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName string, sharedColumns []string, mappedSharedColumns []string, uniqueKey string, uniqueKeyColumns *ColumnList, rangeStartValues, rangeEndValues []string, rangeStartArgs, rangeEndArgs []interface{}, includeRangeStartValues bool, transactionalTable bool, noWait bool) (result string, explodedArgs []interface{}, err error) {
186186
if len(sharedColumns) == 0 {
187187
return "", explodedArgs, fmt.Errorf("Got 0 shared columns in BuildRangeInsertQuery")
188188
}
@@ -212,15 +212,19 @@ func BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName strin
212212
return "", explodedArgs, err
213213
}
214214
explodedArgs = append(explodedArgs, rangeExplodedArgs...)
215+
transactionalClause := ""
216+
if transactionalTable {
217+
if noWait {
218+
transactionalClause = "for share nowait"
219+
} else {
220+
transactionalClause = "lock in share mode"
221+
}
222+
}
215223
rangeEndComparison, rangeExplodedArgs, err := BuildRangeComparison(uniqueKeyColumns.Names(), rangeEndValues, rangeEndArgs, LessThanOrEqualsComparisonSign)
216224
if err != nil {
217225
return "", explodedArgs, err
218226
}
219227
explodedArgs = append(explodedArgs, rangeExplodedArgs...)
220-
transactionalClause := ""
221-
if transactionalTable {
222-
transactionalClause = "lock in share mode"
223-
}
224228
result = fmt.Sprintf(`
225229
insert /* gh-ost %s.%s */ ignore
226230
into
@@ -241,10 +245,10 @@ func BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName strin
241245
return result, explodedArgs, nil
242246
}
243247

244-
func BuildRangeInsertPreparedQuery(databaseName, originalTableName, ghostTableName string, sharedColumns []string, mappedSharedColumns []string, uniqueKey string, uniqueKeyColumns *ColumnList, rangeStartArgs, rangeEndArgs []interface{}, includeRangeStartValues bool, transactionalTable bool) (result string, explodedArgs []interface{}, err error) {
248+
func BuildRangeInsertPreparedQuery(databaseName, originalTableName, ghostTableName string, sharedColumns []string, mappedSharedColumns []string, uniqueKey string, uniqueKeyColumns *ColumnList, rangeStartArgs, rangeEndArgs []interface{}, includeRangeStartValues bool, transactionalTable bool, noWait bool) (result string, explodedArgs []interface{}, err error) {
245249
rangeStartValues := buildColumnsPreparedValues(uniqueKeyColumns)
246250
rangeEndValues := buildColumnsPreparedValues(uniqueKeyColumns)
247-
return BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, mappedSharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, includeRangeStartValues, transactionalTable)
251+
return BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, mappedSharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, includeRangeStartValues, transactionalTable, noWait)
248252
}
249253

250254
func BuildUniqueKeyRangeEndPreparedQueryViaOffset(databaseName, tableName string, uniqueKeyColumns *ColumnList, rangeStartArgs, rangeEndArgs []interface{}, chunkSize int64, includeRangeStartValues bool, hint string) (result string, explodedArgs []interface{}, err error) {

go/sql/builder_test.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ func TestBuildRangeInsertQuery(t *testing.T) {
171171
rangeStartArgs := []interface{}{3}
172172
rangeEndArgs := []interface{}{103}
173173

174-
query, explodedArgs, err := BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, sharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, true, false)
174+
query, explodedArgs, err := BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, sharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, true, true, true)
175175
require.NoError(t, err)
176176
expected := `
177177
insert /* gh-ost mydb.tbl */ ignore
@@ -186,6 +186,7 @@ func TestBuildRangeInsertQuery(t *testing.T) {
186186
where
187187
(((id > @v1s) or ((id = @v1s)))
188188
and ((id < @v1e) or ((id = @v1e))))
189+
for share nowait
189190
)`
190191
require.Equal(t, normalizeQuery(expected), normalizeQuery(query))
191192
require.Equal(t, []interface{}{3, 3, 103, 103}, explodedArgs)
@@ -198,7 +199,7 @@ func TestBuildRangeInsertQuery(t *testing.T) {
198199
rangeStartArgs := []interface{}{3, 17}
199200
rangeEndArgs := []interface{}{103, 117}
200201

201-
query, explodedArgs, err := BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, sharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, true, false)
202+
query, explodedArgs, err := BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, sharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, true, true, true)
202203
require.NoError(t, err)
203204
expected := `
204205
insert /* gh-ost mydb.tbl */ ignore
@@ -219,6 +220,7 @@ func TestBuildRangeInsertQuery(t *testing.T) {
219220
or (((name = @v1e))
220221
AND (position < @v2e))
221222
or ((name = @v1e) and (position = @v2e))))
223+
for share nowait
222224
)`
223225
require.Equal(t, normalizeQuery(expected), normalizeQuery(query))
224226
require.Equal(t, []interface{}{3, 3, 17, 3, 17, 103, 103, 117, 103, 117}, explodedArgs)
@@ -239,7 +241,7 @@ func TestBuildRangeInsertQueryRenameMap(t *testing.T) {
239241
rangeStartArgs := []interface{}{3}
240242
rangeEndArgs := []interface{}{103}
241243

242-
query, explodedArgs, err := BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, mappedSharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, true, false)
244+
query, explodedArgs, err := BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, mappedSharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, true, true, true)
243245
require.NoError(t, err)
244246
expected := `
245247
insert /* gh-ost mydb.tbl */ ignore
@@ -255,6 +257,7 @@ func TestBuildRangeInsertQueryRenameMap(t *testing.T) {
255257
(((id > @v1s) or ((id = @v1s)))
256258
and
257259
((id < @v1e) or ((id = @v1e))))
260+
for share nowait
258261
)`
259262
require.Equal(t, normalizeQuery(expected), normalizeQuery(query))
260263
require.Equal(t, []interface{}{3, 3, 103, 103}, explodedArgs)
@@ -267,7 +270,7 @@ func TestBuildRangeInsertQueryRenameMap(t *testing.T) {
267270
rangeStartArgs := []interface{}{3, 17}
268271
rangeEndArgs := []interface{}{103, 117}
269272

270-
query, explodedArgs, err := BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, mappedSharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, true, false)
273+
query, explodedArgs, err := BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, mappedSharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, true, true, true)
271274
require.NoError(t, err)
272275
expected := `
273276
insert /* gh-ost mydb.tbl */ ignore
@@ -284,6 +287,7 @@ func TestBuildRangeInsertQueryRenameMap(t *testing.T) {
284287
AND (position > @v2s)) or ((name = @v1s) and (position = @v2s)))
285288
and ((name < @v1e) or (((name = @v1e)) AND (position < @v2e))
286289
or ((name = @v1e) and (position = @v2e))))
290+
for share nowait
287291
)`
288292
require.Equal(t, normalizeQuery(expected), normalizeQuery(query))
289293
require.Equal(t, []interface{}{3, 3, 17, 3, 17, 103, 103, 117, 103, 117}, explodedArgs)
@@ -301,7 +305,7 @@ func TestBuildRangeInsertPreparedQuery(t *testing.T) {
301305
rangeStartArgs := []interface{}{3, 17}
302306
rangeEndArgs := []interface{}{103, 117}
303307

304-
query, explodedArgs, err := BuildRangeInsertPreparedQuery(databaseName, originalTableName, ghostTableName, sharedColumns, sharedColumns, uniqueKey, uniqueKeyColumns, rangeStartArgs, rangeEndArgs, true, true)
308+
query, explodedArgs, err := BuildRangeInsertPreparedQuery(databaseName, originalTableName, ghostTableName, sharedColumns, sharedColumns, uniqueKey, uniqueKeyColumns, rangeStartArgs, rangeEndArgs, true, true, true)
305309
require.NoError(t, err)
306310
expected := `
307311
insert /* gh-ost mydb.tbl */ ignore
@@ -314,7 +318,7 @@ func TestBuildRangeInsertPreparedQuery(t *testing.T) {
314318
mydb.tbl
315319
force index (name_position_uidx)
316320
where (((name > ?) or (((name = ?)) AND (position > ?)) or ((name = ?) and (position = ?))) and ((name < ?) or (((name = ?)) AND (position < ?)) or ((name = ?) and (position = ?))))
317-
lock in share mode
321+
for share nowait
318322
)`
319323
require.Equal(t, normalizeQuery(expected), normalizeQuery(query))
320324
require.Equal(t, []interface{}{3, 3, 17, 3, 17, 103, 103, 117, 103, 117}, explodedArgs)

0 commit comments

Comments
 (0)