Skip to content

Commit 3e11e0e

Browse files
committed
fix(storage/sql): retry refresh token update on serialization failures
UpdateRefreshToken runs a read-modify-write inside a SERIALIZABLE transaction. Under concurrent refresh-token rotation of the same token (e.g. multiple kube clients refreshing at once), Postgres aborts the conflicting transaction with SQLSTATE 40001 and the error surfaced to clients as HTTP 500. Add a bounded, jittered retry around the refresh-token update that re-runs the transaction on transient serialization/deadlock failures, detected per-driver (Postgres 40001/40P01, MySQL 1213/1205). Error wraps in the SQL storage now use %w so the driver error can be matched with errors.As. SQLite serializes writes and opts out (nil check). Enables the previously-disabled refresh-token concurrency conformance tests for Postgres and MySQL. Refs: CPEC-711 Signed-off-by: Ronan <ronan.souza@wildlifestudios.com>
1 parent 1018112 commit 3e11e0e

7 files changed

Lines changed: 235 additions & 71 deletions

File tree

storage/sql/config.go

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"crypto/tls"
55
"crypto/x509"
66
"database/sql"
7+
"errors"
78
"fmt"
89
"log/slog"
910
"net"
@@ -21,14 +22,18 @@ import (
2122

2223
const (
2324
// postgres error codes
24-
pgErrUniqueViolation = "23505" // unique_violation
25+
pgErrUniqueViolation = "23505" // unique_violation
26+
pgErrSerializationFailure = "40001" // serialization_failure
27+
pgErrDeadlockDetected = "40P01" // deadlock_detected
2528
)
2629

2730
const (
2831
// MySQL error codes
2932
mysqlErrDupEntry = 1062
3033
mysqlErrDupEntryWithKeyName = 1586
3134
mysqlErrUnknownSysVar = 1193
35+
mysqlErrLockDeadlock = 1213 // ER_LOCK_DEADLOCK
36+
mysqlErrLockWaitTimeout = 1205 // ER_LOCK_WAIT_TIMEOUT
3237
)
3338

3439
const (
@@ -195,7 +200,15 @@ func (p *Postgres) open(logger *slog.Logger) (*conn, error) {
195200
return sqlErr.Code == pgErrUniqueViolation
196201
}
197202

198-
c := &conn{db, &flavorPostgres, logger, errCheck}
203+
retryCheck := func(err error) bool {
204+
var sqlErr *pq.Error
205+
if !errors.As(err, &sqlErr) {
206+
return false
207+
}
208+
return sqlErr.Code == pgErrSerializationFailure || sqlErr.Code == pgErrDeadlockDetected
209+
}
210+
211+
c := &conn{db, &flavorPostgres, logger, errCheck, retryCheck}
199212
if _, err := c.migrate(); err != nil {
200213
return nil, fmt.Errorf("failed to perform migrations: %v", err)
201214
}
@@ -307,7 +320,16 @@ func (s *MySQL) open(logger *slog.Logger) (*conn, error) {
307320
sqlErr.Number == mysqlErrDupEntryWithKeyName
308321
}
309322

310-
c := &conn{db, &flavorMySQL, logger, errCheck}
323+
retryCheck := func(err error) bool {
324+
var sqlErr *mysql.MySQLError
325+
if !errors.As(err, &sqlErr) {
326+
return false
327+
}
328+
return sqlErr.Number == mysqlErrLockDeadlock ||
329+
sqlErr.Number == mysqlErrLockWaitTimeout
330+
}
331+
332+
c := &conn{db, &flavorMySQL, logger, errCheck, retryCheck}
311333
if _, err := c.migrate(); err != nil {
312334
return nil, fmt.Errorf("failed to perform migrations: %v", err)
313335
}

storage/sql/config_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ func TestPostgres(t *testing.T) {
243243
Mode: pgSSLDisable, // Postgres container doesn't support SSL.
244244
},
245245
}
246-
testDB(t, p, true, false)
246+
testDB(t, p, true, true)
247247
}
248248

249249
const testMySQLEnv = "DEX_MYSQL_HOST"
@@ -280,7 +280,7 @@ func TestMySQL(t *testing.T) {
280280
"innodb_lock_wait_timeout": "3",
281281
},
282282
}
283-
testDB(t, s, true, false)
283+
testDB(t, s, true, true)
284284
}
285285

286286
const testMySQL8Env = "DEX_MYSQL8_HOST"
@@ -317,5 +317,5 @@ func TestMySQL8(t *testing.T) {
317317
"innodb_lock_wait_timeout": "3",
318318
},
319319
}
320-
testDB(t, s, true, false)
320+
testDB(t, s, true, true)
321321
}

0 commit comments

Comments
 (0)