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) } 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()) } 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 two committed transactions", callbackCalls.Load()) }