Skip to content

Add lock_wait_timeout as retryable #3

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
44 changes: 25 additions & 19 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,25 @@
//
// Error and CanRetry are the most important functions in this package. They are used like:
//
// import my "github.com/go-mysql/errors"
// import my "github.com/go-mysql/errors"
//
// SAVE_ITEM_LOOP:
// for tryNo := 1; tryNo <= maxTries; tryNo++ {
// if err := SaveItem(item); err != nil {
// if ok, myerr := my.Error(err); ok { // MySQL error
// if myerr == my.ErrDupeKey {
// return http.StatusConflict
// }
// if my.CanRetry(myerr) {
// time.Sleep(1 * time.Second)
// continue SAVE_ITEM_LOOP // retry
// }
// }
// // Error not handled
// return http.StatusInternalServerError
// }
// break SAVE_ITEM_LOOP // success
// }
Comment on lines -24 to -40
Copy link
Author

Choose a reason for hiding this comment

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

Sorry, this is go 1.19 go fmt related. I would manually unapply, but since most editors do this the next person will just commit this change anyway :(

// SAVE_ITEM_LOOP:
// for tryNo := 1; tryNo <= maxTries; tryNo++ {
// if err := SaveItem(item); err != nil {
// if ok, myerr := my.Error(err); ok { // MySQL error
// if myerr == my.ErrDupeKey {
// return http.StatusConflict
// }
// if my.CanRetry(myerr) {
// time.Sleep(1 * time.Second)
// continue SAVE_ITEM_LOOP // retry
// }
// }
// // Error not handled
// return http.StatusInternalServerError
// }
// break SAVE_ITEM_LOOP // success
// }
//
// The example above tries N-many times to save an item into a database.
// It handles MySQL errors explicitly. On ErrDupeKey it does not retry, it
Expand Down Expand Up @@ -83,6 +83,10 @@ var (
// ErrDupeKey is returned when a unique index prevents a value from being
// inserted or updated. CanRetry returns false on this error.
ErrDupeKey = errors.New("duplicate key value")

// ErrLockWaitTimeout is returned when innodb_lock_wait_timeout and a lock
// could not be acquired. CanRetry returns true on this error.
ErrLockWaitTimeout = errors.New("lock wait timeout")
)

// Error returns an error in this package if possible. The boolean return
Expand Down Expand Up @@ -113,6 +117,8 @@ func Error(err error) (bool, error) {
return true, ErrReadOnly
case 1062: // ER_DUP_ENTRY
return true, ErrDupeKey
case 1205: // ER_LOCK_WAIT_TIMEOUT
return true, ErrLockWaitTimeout
}

// A MySQL error, but not one we handle explicitly
Expand Down Expand Up @@ -160,7 +166,7 @@ func MySQLErrorCode(err error) uint16 {
// It returns false for all other errors, including nil.
func CanRetry(err error) bool {
switch err {
case ErrCannotConnect, ErrConnLost, ErrReadOnly, ErrQueryKilled:
case ErrCannotConnect, ErrConnLost, ErrReadOnly, ErrQueryKilled, ErrLockWaitTimeout:
return true
}
return false
Expand Down
75 changes: 75 additions & 0 deletions errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,4 +503,79 @@ func TestCanRetry(t *testing.T) {
if retry == false {
t.Error("can try = false, expected true")
}
retry = my.CanRetry(my.ErrLockWaitTimeout)
if retry == false {
t.Error("can try = false, expected true")
}
}

func TestLockWaitTimeout(t *testing.T) {
setup(t)

// Make a connection as usual
db := newDB(t, defaultDSN)
ctx := context.TODO()
conn, err := db.Conn(ctx)
if err != nil {
t.Fatal(err)
}

if conn == nil {
t.Fatal("got nil conn, expected it to be set")
}
defer conn.Close()

// Make sure to lower timeouts.
_, err = conn.ExecContext(ctx, "SET GLOBAL innodb_lock_wait_timeout = 1")
if err != nil {
t.Fatal(err)
}

// Create a table with no data.
_, err = conn.ExecContext(ctx, "CREATE TABLE test.lock_wait_timeout (id INT NOT NULL PRIMARY KEY, col1 varchar(255))")
if err != nil {
t.Fatal(err)
}
_, err = conn.ExecContext(ctx, "INSERT INTO test.lock_wait_timeout VALUES (1, 'test')")
if err != nil {
t.Fatal(err)
}
defer func() {
_, err := conn.ExecContext(ctx, "DROP TABLE IF EXISTS test.lock_wait_timeout")
if err != nil {
t.Errorf("cannot drop table test.t: %s", err)
}
}()

// lock row 1
trx, err := conn.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelRepeatableRead,
})
if err != nil {
t.Fatal(err)
}
_, err = trx.Exec("SELECT * FROM test.lock_wait_timeout WHERE id = 1 FOR UPDATE")
if err != nil {
t.Fatal(err)
}
// In another session, try and lock ROW 1
// It should timeout.
conn2, err := db.Conn(ctx)
if err != nil {
t.Fatal(err)
}
defer conn2.Close()
_, err = conn2.ExecContext(ctx, "SELECT * FROM test.lock_wait_timeout WHERE id = 1 FOR UPDATE")
t.Logf("err: %v", err)
ok, myerr := my.Error(err)
if !ok {
t.Error("MySQL error = false, expected true)")
}
if myerr != my.ErrLockWaitTimeout {
t.Errorf("got error '%v', expected ErrLockWaitTimeout", err)
}
// Rollback the transaction
if err := trx.Rollback(); err != nil {
t.Fatal(err)
}
}