Skip to content

Transaction isolation levels #619

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

Merged
merged 6 commits into from
Jun 16, 2017
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
Icon?
ehthumbs.db
Thumbs.db
.idea
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Xiangyu Hu <xiangyu.hu at outlook.com>
Xiaobing Jiang <s7v7nislands at gmail.com>
Xiuming Chen <cc at cxm.cc>
Zhenye Xie <xiezhenye at gmail.com>
Maciej Zimnoch <maciej.zimnoch@codilime.com>
Copy link
Member

Choose a reason for hiding this comment

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

Please insert your name to alphabetical order


# Organizations

Expand Down
16 changes: 12 additions & 4 deletions connection_go18.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,23 @@ func (mc *mysqlConn) Ping(ctx context.Context) error {

// BeginTx implements driver.ConnBeginTx interface
func (mc *mysqlConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
if sql.IsolationLevel(opts.Isolation) != sql.LevelDefault {
// TODO: support isolation levels
return nil, errors.New("mysql: isolation levels not supported")
}
if opts.ReadOnly {
// TODO: support read-only transactions
return nil, errors.New("mysql: read-only transactions not supported")
}

if sql.IsolationLevel(opts.Isolation) != sql.LevelDefault {
level, err := mapIsolationLevel(opts.Isolation)
if err != nil {
return nil, err
}
err = mc.exec("SET TRANSACTION ISOLATION LEVEL " + level)
if err != nil {
return nil, err
}
mc.finish()
Copy link
Member

Choose a reason for hiding this comment

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

You shouldn't use mc.finish() here. It should be paried with mc.watchCancel().
Move whole this if statement after watchCancel(), and remove mc.finish().

Copy link
Contributor

Choose a reason for hiding this comment

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

Move whole this if statement after watchCancel(), and remove mc.finish().

In addition, please don't forget to call mc.finish() before return nil, err.
mc.finish() must called before all return.

}

if err := mc.watchCancel(ctx); err != nil {
return nil, err
}
Expand Down
98 changes: 98 additions & 0 deletions driver_go18_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"database/sql/driver"
"fmt"
"reflect"
"sync"
"testing"
"time"
)
Expand Down Expand Up @@ -468,3 +469,100 @@ func TestContextCancelBegin(t *testing.T) {
}
})
}

func TestContextBeginIsolationLevel(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nice test👍

but, this test can be written more concisely, can't it?
for example

tx1, err := dbt.db.BeginTx(ctx, &sql.TxOptions{
    Isolation: sql.LevelRepeatableRead,
})
tx2, err := dbt.db.BeginTx(ctx, &sql.TxOptions{
    Isolation: sql.LevelReadCommitted,
})

row := tx2.QueryRowContext(ctx, "SELECT COUNT(*) FROM test")
// check row.

_, err = tx1.ExecContext(ctx, "INSERT INTO test VALUES (1)")

row = tx2.QueryRowContext(ctx, "SELECT COUNT(*) FROM test")
// check row.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, thanks for advice.

runTests(t, dsn, func(dbt *DBTest) {
dbt.mustExec("CREATE TABLE test (v INTEGER)")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Waitgroup syncing BeginTx
beginWg := sync.WaitGroup{}
beginWg.Add(2)

// Waitgroup syncing insert in writer transaction
insertWg := sync.WaitGroup{}
insertWg.Add(1)

// Waitgroup syncing writer transaction commit before reader reads
readWg := sync.WaitGroup{}
readWg.Add(1)

// Waitgroup syncing commit in writer transaction
commitWg := sync.WaitGroup{}
commitWg.Add(1)

// Waitgroup syncing end of both goroutines
testDoneWg := sync.WaitGroup{}
testDoneWg.Add(2)

repeatableReadGoroutine := func() {
tx, err := dbt.db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelRepeatableRead,
})
if err != nil {
dbt.Fatal(err)
}
beginWg.Done()
// Wait until other session will begin it's transaction
beginWg.Wait()

_, err = tx.ExecContext(ctx, "INSERT INTO test VALUES (1)")
if err != nil {
dbt.Fatal(err)
}
insertWg.Done()

// Wait until reader transaction finish reading
readWg.Wait()

err = tx.Commit()
if err != nil {
dbt.Fatal(err)
}
commitWg.Done()

testDoneWg.Done()
}

readCommitedGoroutine := func() {
tx, err := dbt.db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelReadCommitted,
})
if err != nil {
dbt.Fatal(err)
}
beginWg.Done()
// Wait until writer transaction will begin
beginWg.Wait()
// Wait until writer transaction will insert value
insertWg.Wait()
var v int
row := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM test")
if err := row.Scan(&v); err != nil {
dbt.Fatal(err)
}
// Because writer transaction wasn't commited yet, it should be available
if v != 0 {
dbt.Errorf("expected val to be 0, got %d", v)
}
readWg.Done()
// Wait until writer transaction will commit
commitWg.Wait()
row = tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM test")
if err := row.Scan(&v); err != nil {
dbt.Fatal(err)
}
// Data written by writer transaction is already commited, it should be selectable
if v != 1 {
dbt.Errorf("expected val to be 1, got %d", v)
}
tx.Commit()
testDoneWg.Done()
}

go repeatableReadGoroutine()
go readCommitedGoroutine()
testDoneWg.Wait()
})
}
16 changes: 16 additions & 0 deletions utils_go18.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ package mysql

import (
"crypto/tls"
"database/sql"
"database/sql/driver"
"errors"
)
Expand All @@ -31,3 +32,18 @@ func namedValueToValue(named []driver.NamedValue) ([]driver.Value, error) {
}
return dargs, nil
}

func mapIsolationLevel(level driver.IsolationLevel) (string, error) {
switch sql.IsolationLevel(level) {
case sql.LevelRepeatableRead:
return "REPEATABLE READ", nil
case sql.LevelReadCommitted:
return "READ COMMITTED", nil
case sql.LevelReadUncommitted:
return "READ UNCOMMITTED", nil
case sql.LevelSerializable:
return "SERIALIZABLE", nil
default:
return "", errors.New("mysql: unsupported isolation level: " + string(level))
}
}
54 changes: 54 additions & 0 deletions utils_go18_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2017 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.

// +build go1.8

package mysql

import (
"database/sql"
"database/sql/driver"
"testing"
)

func TestIsolationLevelMapping(t *testing.T) {

data := []struct {
level driver.IsolationLevel
expected string
}{
{
level: driver.IsolationLevel(sql.LevelReadCommitted),
expected: "READ COMMITTED",
},
{
level: driver.IsolationLevel(sql.LevelRepeatableRead),
expected: "REPEATABLE READ",
},
{
level: driver.IsolationLevel(sql.LevelReadUncommitted),
expected: "READ UNCOMMITTED",
},
{
level: driver.IsolationLevel(sql.LevelSerializable),
expected: "SERIALIZABLE",
},
}

for i, td := range data {
if actual, err := mapIsolationLevel(td.level); actual != td.expected || err != nil {
t.Fatal(i, td.expected, actual, err)
}
}

// check unsupported mapping
if actual, err := mapIsolationLevel(driver.IsolationLevel(sql.LevelLinearizable)); actual != "" || err == nil {
t.Fatal("Expected error on unsupported isolation level")
}

}