fix: retry precommit mysql lock conflicts
This commit is contained in:
@@ -51,16 +51,56 @@ func Open(ctx context.Context, dsn string, maxOpen, maxIdle int) (*sql.DB, error
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// WithTx does not replay callbacks: a failed commit may have succeeded remotely.
|
||||
// Callers reconcile by stable operation identity before retrying an ambiguous write.
|
||||
const transactionRetryLimit = 2
|
||||
|
||||
// WithTx retries only a callback failure caused by a MySQL deadlock or lock wait
|
||||
// timeout before COMMIT. It never retries Begin/Commit errors because a commit
|
||||
// response may be lost after the database has already applied the transaction;
|
||||
// callers must reconcile such an ambiguous write by stable operation identity.
|
||||
func WithTx(ctx context.Context, db *sql.DB, fn func(*sql.Tx) error) error {
|
||||
for attempt := 0; attempt <= transactionRetryLimit; attempt++ {
|
||||
err, retryable := withTxAttempt(ctx, db, fn)
|
||||
if err == nil || !retryable || attempt == transactionRetryLimit {
|
||||
return err
|
||||
}
|
||||
if err := waitTransactionRetry(ctx, attempt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return errors.New("transaction retry loop exhausted")
|
||||
}
|
||||
|
||||
func withTxAttempt(ctx context.Context, db *sql.DB, fn func(*sql.Tx) error) (error, bool) {
|
||||
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
|
||||
if err != nil {
|
||||
return err
|
||||
return err, false
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if err := fn(tx); err != nil {
|
||||
return err
|
||||
return err, retryableTransactionError(err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err, false
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func retryableTransactionError(err error) bool {
|
||||
var mysqlErr *driver.MySQLError
|
||||
if !errors.As(err, &mysqlErr) {
|
||||
return false
|
||||
}
|
||||
return mysqlErr.Number == 1205 || mysqlErr.Number == 1213
|
||||
}
|
||||
|
||||
func waitTransactionRetry(ctx context.Context, attempt int) error {
|
||||
delay := time.Duration(attempt+1) * 25 * time.Millisecond
|
||||
timer := time.NewTimer(delay)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
return nil
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user