-
Notifications
You must be signed in to change notification settings - Fork 0
ARGUS A22
| Meta Field | Specification |
|---|---|
| Rule Code | ARGUS-A22 |
| Identifier | SERIALIZATION_FAILURE_RETRY |
| Severity | HIGH |
| Category | Concurrency Resilience, SSI Fault Tolerance & Error Recovery |
| Analysis Layer | Layer 2 - Go-AST Structural & Control Flow Analysis |
| CWE Mapping | CWE-362: Concurrent Execution with Improper Synchronization |
| OWASP ASVS | OWASP ASVS v4.0.3/v5.0 §V5.3.1, §V11.1.4 (Transaction Concurrency & Race Condition Recovery) |
| PostgreSQL Target | PostgreSQL 18.x (Serializable Snapshot Isolation & SQLSTATE 40001/40P01 Contract) |
| Default Status | enabled |
Database transactions initialized with strict isolation levels (pgx.Serializable or pgx.RepeatableRead) must be wrapped in an automated retry loop with exponential backoff and randomized jitter to handle normal engine aborts (SQLSTATE 40001: serialization_failure and SQLSTATE 40P01: deadlock_detected).
┌─────────────────────────────────────────────────────────────────────────────┐
│ ARCHITECTURAL INVARIANT │
│ │
│ Transactions using `pgx.Serializable` or `pgx.RepeatableRead` MUST NOT │
│ be executed as single-shot operations. They MUST be enclosed in automated │
│ retry loops or official transaction retry helpers (CWE-362). │
│ │
│ Standard Solutions: │
│ 1. Wrapped in `for attempt := 1; attempt <= maxRetries; attempt++` loop │
│ 2. Handled via helper functions (`WithRetrySerializable`, `WithRetry`) │
│ 3. Catches and retries on SQLSTATE `40001` and `40P01` │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ POSTGRESQL SSI SERIALIZATION RETRY CONTRACT │
│ │
│ Two Concurrent Transactions Mutate Balance (IsoLevel: pgx.Serializable) │
│ [ Transaction A ] [ Transaction B ] │
│ │ │ │
│ ▼ ▼ │
│ PostgreSQL SSI Engine detects rw-antidependency cycle (SIREAD lock) │
│ ├─► Transaction A: Successfully COMMITS │
│ └─► Transaction B: INTENTIONALLY ABORTED (SQLSTATE 40001) │
│ │
│ Case A: Without Retry Loop (VIOLATION): │
│ └─► Handler immediately throws error -> HTTP 500 Outage to Client! (FAIL) │
│ │
│ Case B: Wrapped in Automated Retry Loop + Jitter (COMPLIANT): │
│ ├─► Catch SQLSTATE 40001 -> Sleep 25ms + random jitter │
│ ├─► Re-run transaction from clean state with new snapshot │
│ └─► Attempt 2: Successfully COMMITS! (Zero Downtime / Seamless Success) │
└─────────────────────────────────────────────────────────────────────────────┘
PostgreSQL Serializable Snapshot Isolation (SSI) does not lock rows for reading; instead, it tracks lock anti-dependencies in virtual memory (SIREAD locks) to detect dangerous structures that could lead to anomalies (like write-skew). When a cycle is detected, PostgreSQL intentionally aborts one of the transactions with:
ERROR: could not serialize access due to read/write dependencies among transactions (SQLSTATE 40001)
In SSI, transaction aborts are an integral part of the consistency contract. Without a retry loop, an application under moderate concurrent load will bubble up 500 Internal Server Errors to users, leading to perceived system instability.
flowchart TD
A["BeginTx Call Expression"] --> B{"Is Isolation Level Serializable or RepeatableRead?"}
B -- "No (ReadCommitted / Default)" --> C["PASS (Standard Isolation)"]
B -- "Yes" --> D{"Is Call Enclosed in For-Loop or Retry Helper?"}
D -- "Yes" --> E["PASS (Automated Retry Guardrail Present)"]
D -- "No" --> F["FAIL: ARGUS-A22 Single-Shot Serializable Transaction Violation"]
Argus AST visitor inspects:
-
BeginTxCall Evaluation: Scans AST forBeginTxinvocations. -
TxOptions.IsoLevelResolution: Evaluates composite literals and variable declarations forpgx.Serializableorpgx.RepeatableRead. -
Control Flow & Enclosing Context: Traverses the AST upwards to verify that the
BeginTxcall is contained within:- A retry loop (
*ast.ForStmtor*ast.RangeStmt), OR - A helper closure function (e.g.
WithRetrySerializable,WithRetry).
- A retry loop (
-
Exemptions:
- Default
Begin(ctx)calls. -
BeginTxwithReadCommittedorReadUncommitted. - Functions decorated with
// argus:ignore ARGUS-A22.
- Default
// VIOLATION: Serialization aborts (40001) immediately fail the request with 500
func TransferFunds(ctx context.Context, pool *pgxpool.Pool, req TransferReq) error {
opts := pgx.TxOptions{IsoLevel: pgx.Serializable}
tx, err := pool.BeginTx(ctx, opts)
if err != nil {
return err
}
defer tx.Rollback(ctx)
// ... Balance Mutation ...
return tx.Commit(ctx) // Incurring 40001 here causes unhandled failure
}// COMPLIANT: Automatic retry loop catching SQLSTATE 40001 and 40P01
func TransferFunds(ctx context.Context, pool *pgxpool.Pool, req TransferReq) error {
const maxRetries = 3
opts := pgx.TxOptions{IsoLevel: pgx.Serializable}
for attempt := 1; attempt <= maxRetries; attempt++ {
err := func() error {
tx, err := pool.BeginTx(ctx, opts)
if err != nil {
return err
}
defer tx.Rollback(ctx)
// ... Idempotent Balance Mutation ...
return tx.Commit(ctx)
}()
if err == nil {
return nil
}
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && (pgErr.Code == "40001" || pgErr.Code == "40P01") {
if attempt == maxRetries {
return fmt.Errorf("transaction aborted after %d retries: %w", maxRetries, err)
}
backoff := time.Duration(attempt*25)*time.Millisecond + time.Duration(rand.Intn(20))*time.Millisecond
select {
case <-time.After(backoff):
continue
case <-ctx.Done():
return ctx.Err()
}
}
return err // Non-retryable error
}
return nil
}// COMPLIANT: Using standard repository retry helper
func TransferFunds(ctx context.Context, pool *pgxpool.Pool, req TransferReq) error {
return WithRetrySerializable(ctx, pool, func(tx pgx.Tx) error {
// Idempotent balance mutation logic
return nil
})
}-
Wrap in Retry Loop: Encapsulate
Serializabletransaction blocks in a loop with at least 3 attempts. -
Check Error Codes: Specifically catch
pgconn.PgErrorcodes40001(serialization failure) and40P01(deadlock detected). - Idempotence: Ensure all operations inside the retryable transaction closure are strictly idempotent.
- Jittered Backoff: Introduce exponential backoff with random jitter before retrying to prevent thundering herd collisions.
rules:
ARGUS-A22:
enabled: true
max_retries: 5// argus:ignore ARGUS-A22 intentional single-shot abort test assertion
tx, err := pool.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.Serializable})
// argus:ignore SERIALIZATION_FAILURE_RETRY offline batch task
tx, err := conn.BeginTx(ctx, opts)