88 lines
2.5 KiB
Go
88 lines
2.5 KiB
Go
package mysql
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"os"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestIntegrationWithTxRetriesPreCommitDeadlock(t *testing.T) {
|
|
dsn := os.Getenv("ORBIT_TEST_MYSQL_DSN")
|
|
if dsn == "" {
|
|
t.Skip("requires disposable MySQL")
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
db, err := Open(ctx, dsn, 8, 4)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
if _, err := db.ExecContext(ctx, `CREATE TABLE orbit_test_tx_retry (
|
|
id INT NOT NULL COMMENT '测试锁顺序行标识',
|
|
value INT NOT NULL COMMENT '测试事务累加值',
|
|
PRIMARY KEY (id)
|
|
) ENGINE=InnoDB COMMENT='测试提交前死锁重试表'`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.ExecContext(context.Background(), "DROP TABLE orbit_test_tx_retry")
|
|
if _, err := db.ExecContext(ctx, "INSERT INTO orbit_test_tx_retry(id,value) VALUES (1,0),(2,0)"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
beforeStats := ReadTransactionStats()
|
|
|
|
var barrier sync.WaitGroup
|
|
barrier.Add(2)
|
|
var callbackCalls atomic.Int32
|
|
run := func(first, second int) error {
|
|
attempt := 0
|
|
return WithTx(ctx, db, func(tx *sql.Tx) error {
|
|
attempt++
|
|
callbackCalls.Add(1)
|
|
order := []int{first, second}
|
|
if attempt > 1 {
|
|
order = []int{1, 2}
|
|
}
|
|
for index, id := range order {
|
|
if _, err := tx.ExecContext(ctx, "SELECT id FROM orbit_test_tx_retry WHERE id=? FOR UPDATE", id); err != nil {
|
|
return err
|
|
}
|
|
if attempt == 1 && index == 0 {
|
|
barrier.Done()
|
|
barrier.Wait()
|
|
}
|
|
}
|
|
_, err := tx.ExecContext(ctx, "UPDATE orbit_test_tx_retry SET value=value+1 WHERE id IN (1,2)")
|
|
return err
|
|
})
|
|
}
|
|
|
|
results := make(chan error, 2)
|
|
go func() { results <- run(1, 2) }()
|
|
go func() { results <- run(2, 1) }()
|
|
for i := 0; i < 2; i++ {
|
|
if err := <-results; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if callbackCalls.Load() < 3 {
|
|
t.Fatalf("deadlock did not cause a bounded pre-commit retry; callback calls=%d", callbackCalls.Load())
|
|
}
|
|
afterStats := ReadTransactionStats()
|
|
if afterStats.Retries <= beforeStats.Retries {
|
|
t.Fatalf("transaction retry metric did not advance: before=%+v after=%+v", beforeStats, afterStats)
|
|
}
|
|
var value int
|
|
if err := db.QueryRowContext(ctx, "SELECT COALESCE(MIN(value),0) FROM orbit_test_tx_retry").Scan(&value); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if value != 2 {
|
|
t.Fatalf("committed value=%d want 2", value)
|
|
}
|
|
t.Logf("pre-commit deadlock recovered with %d callback attempts and transaction retry metric %+v", callbackCalls.Load(), afterStats)
|
|
}
|