Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement traditional auto-increment lock mode hold the lock for the duration of the insert iter. #7704

Merged
merged 17 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
121 changes: 121 additions & 0 deletions go/libraries/doltcore/sqle/enginetest/dolt_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ package enginetest
import (
"context"
"fmt"
"github.com/dolthub/go-mysql-server/sql/transform"
"io"
"os"
"runtime"
"sync"
Expand Down Expand Up @@ -195,6 +197,125 @@ func newUpdateResult(matched, updated int) gmstypes.OkResult {
}
}

func TestAutoIncrementTrackerLockMode(t *testing.T) {
for _, lockMode := range []int64{0, 1, 2} {
t.Run(fmt.Sprintf("lock mode %d", lockMode), func(t *testing.T) {
testAutoIncrementTrackerWithLockMode(t, lockMode)
})
}
}

// testAutoIncrementTrackerWithLockMode tests that interleaved inserts don't cause deadlocks, regardless of the value of innodb_autoinc_lock_mode.
// In a real use case, these interleaved operations would be happening in different sessions on different threads.
// In order to make the test behave predictably, we manually interleave the two iterators.
func testAutoIncrementTrackerWithLockMode(t *testing.T, lockMode int64) {

err := sql.SystemVariables.AssignValues(map[string]interface{}{"innodb_autoinc_lock_mode": lockMode})
require.NoError(t, err)

setupScripts := []setup.SetupScript{[]string{
"CREATE TABLE test1 (pk int NOT NULL PRIMARY KEY AUTO_INCREMENT,c0 int,index t1_c_index (c0));",
"CREATE TABLE test2 (pk int NOT NULL PRIMARY KEY AUTO_INCREMENT,c0 int,index t2_c_index (c0));",
"CREATE TABLE timestamps (pk int NOT NULL PRIMARY KEY AUTO_INCREMENT, t int);",
"CREATE TRIGGER t1 AFTER INSERT ON test1 FOR EACH ROW INSERT INTO timestamps VALUES (0, 1);",
"CREATE TRIGGER t2 AFTER INSERT ON test2 FOR EACH ROW INSERT INTO timestamps VALUES (0, 2);",
"CREATE VIEW bin AS SELECT 0 AS v UNION ALL SELECT 1;",
"CREATE VIEW sequence5bit AS SELECT b1.v + 2*b2.v + 4*b3.v + 8*b4.v + 16*b5.v AS v from bin b1, bin b2, bin b3, bin b4, bin b5;",
}}

harness := newDoltHarness(t)
defer harness.Close()
harness.Setup(setup.MydbData, setupScripts)
e := mustNewEngine(t, harness)

defer e.Close()
ctx := enginetest.NewContext(harness)

// Confirm that the system variable was correctly set.
_, iter, err := e.Query(ctx, "select @@innodb_autoinc_lock_mode")
require.NoError(t, err)
rows, err := sql.RowIterToRows(ctx, iter)
require.NoError(t, err)
assert.Equal(t, rows, []sql.Row{{lockMode}})

// Ordinarily QueryEngine.query manages transactions.
// Since we can't use that for this test, we manually start a new transaction.
ts := ctx.Session.(sql.TransactionSession)
tx, err := ts.StartTransaction(ctx, sql.ReadWrite)
require.NoError(t, err)
ctx.SetTransaction(tx)

getTriggerIter := func(query string) sql.RowIter {
root, err := e.AnalyzeQuery(ctx, query)
require.NoError(t, err)

var triggerNode *plan.TriggerExecutor
transform.Node(root, func(n sql.Node) (sql.Node, transform.TreeIdentity, error) {
if triggerNode != nil {
return n, transform.SameTree, nil
}
if t, ok := n.(*plan.TriggerExecutor); ok {
triggerNode = t
}
return n, transform.NewTree, nil
})
iter, err := e.EngineAnalyzer().ExecBuilder.Build(ctx, triggerNode, nil)
require.NoError(t, err)
return iter
}

iter1 := getTriggerIter("INSERT INTO test1 (c0) select v from sequence5bit;")
iter2 := getTriggerIter("INSERT INTO test2 (c0) select v from sequence5bit;")

// Alternate the iterators until they're exhausted.
var err1 error
var err2 error
for err1 != io.EOF || err2 != io.EOF {
if err1 != io.EOF {
var row1 sql.Row
require.NoError(t, err1)
row1, err1 = iter1.Next(ctx)
_ = row1
Comment on lines +275 to +278
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(minor) Could you get rid of the var row1 completely by saying: _, err1 = iter1.Next(ctx) like you're doing for iter2?

}
if err2 != io.EOF {
require.NoError(t, err2)
_, err2 = iter2.Next(ctx)
}
}
err = iter1.Close(ctx)
require.NoError(t, err)
err = iter2.Close(ctx)
require.NoError(t, err)

dsess.DSessFromSess(ctx.Session).CommitTransaction(ctx, ctx.GetTransaction())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want these to be executed in the same session?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a real environment they wouldn't be, but I don't think it matters here. The engine tests have a single session baked in, and figuring out how to disentangle that didn't seem worth it for a single test.


// Verify that the inserts are seen by the engine.
{
_, iter, err := e.Query(ctx, "select count(*) from timestamps")
require.NoError(t, err)
rows, err := sql.RowIterToRows(ctx, iter)
require.NoError(t, err)
assert.Equal(t, rows, []sql.Row{{int64(64)}})
}

// Verify that the insert operations are actually interleaved by inspecting the order that values were added to `timestamps`
{
_, iter, err := e.Query(ctx, "select (select min(pk) from timestamps where t = 1) < (select max(pk) from timestamps where t = 2)")
require.NoError(t, err)
rows, err := sql.RowIterToRows(ctx, iter)
require.NoError(t, err)
assert.Equal(t, rows, []sql.Row{{true}})
}

{
_, iter, err := e.Query(ctx, "select (select min(pk) from timestamps where t = 2) < (select max(pk) from timestamps where t = 1)")
require.NoError(t, err)
rows, err := sql.RowIterToRows(ctx, iter)
require.NoError(t, err)
assert.Equal(t, rows, []sql.Row{{true}})
}
}

// Convenience test for debugging a single query. Unskip and set to the desired query.
func TestSingleMergeScript(t *testing.T) {
t.Skip()
Expand Down
23 changes: 0 additions & 23 deletions integration-tests/bats/auto_increment_lock_modes.bats
Original file line number Diff line number Diff line change
Expand Up @@ -77,26 +77,3 @@ EOF
[[ "$output" =~ "0,true,true,true" ]] || false
[[ "$output" =~ "1,true,true,true" ]] || false
}


@test "auto_increment_lock_modes: multiple tables can have interleaved inserts without blocking each other." {
cat > config.yml <<EOF
system_variables:
innodb_autoinc_lock_mode: 0
EOF
start_sql_server_with_config "" config.yml
# We use run here so that we don't terminate if the command fails, since terminating without stopping the server will cause the test to hang.
# We also use a small sequence table because larger tables increase the risk of triggering https://github.com/dolthub/dolt/issues/7702
# If we detect interleaved writes to `timestamps` with a smaller sequence table, then we would see it on the larger table anyway.
run dolt sql -q "INSERT INTO test1 (c0) select v from sequence5bit; SELECT * from timestamps; COMMIT;" &
run dolt sql -q "INSERT INTO test2 (c0) select v from sequence5bit; SELECT * from timestamps; COMMIT;"
wait $!
stop_sql_server
# We confirm that the two inserts are interleaved by comparing the min and max timestamps from both tables.
run dolt sql -q "select (select min(pk) from timestamps where t = 1) < (select max(pk) from timestamps where t = 2)"
[ "$status" -eq 0 ]
[[ "$output" =~ "true" ]] || false
run dolt sql -q "select (select min(pk) from timestamps where t = 2) < (select max(pk) from timestamps where t = 1)"
[ "$status" -eq 0 ]
[[ "$output" =~ "true" ]] || false
}