Files
Crussell/backend/handlers/payments/locks_test.go
T
popertots e02567564e Add bounded advisory-lock helper; convert loyalty redemption to try-lock
Introduce acquireAdvisoryLock (pg_try_advisory_lock with a ~3s bounded retry) and acquireAdvisoryXactLockBlocking (deliberately blocking for the cancellation-refund path where silently dropping a refund is worse than waiting). Convert ApplyLoyaltyRedemption to the bounded variant: concurrent redemptions during an in-flight payment return 409 instead of pinning a pool connection. Add uncontended + contended-timeout unit tests and a lock-contended 409 redemption test.
2026-08-22 00:34:49 +01:00

87 lines
2.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//go:build test && dev
package payments
import (
"context"
"testing"
"time"
"crussell/db"
)
// TestAdvisoryLock_Uncontended_Acquires verifies the happy path: an unlocked
// key is acquired immediately with a bounded try-lock.
func TestAdvisoryLock_Uncontended_Acquires(t *testing.T) {
t.Parallel()
ctx := context.Background()
conn, err := db.Conn.Acquire(ctx)
if err != nil {
t.Fatalf("failed to acquire pool connection: %v", err)
}
defer conn.Release()
key := "crussell:payment:locktest-uncontended"
acquired, err := acquireAdvisoryLock(ctx, conn, key)
if err != nil {
t.Fatalf("acquireAdvisoryLock returned error: %v", err)
}
if !acquired {
t.Fatal("expected uncontended lock to be acquired")
}
if _, err := conn.Exec(ctx, `SELECT pg_advisory_unlock(hashtext($1))`, key); err != nil {
t.Fatalf("failed to release lock: %v", err)
}
}
// TestAdvisoryLock_Contended_TimesOutWithinBound verifies the bounded try-lock
// timeout branch: when another connection holds the same key, acquireAdvisoryLock
// returns (false, nil) after the ~3s retry bound instead of blocking forever
// (the pool-exhaustion defence).
func TestAdvisoryLock_Contended_TimesOutWithinBound(t *testing.T) {
t.Parallel()
ctx := context.Background()
holder, err := db.Conn.Acquire(ctx)
if err != nil {
t.Fatalf("failed to acquire holder connection: %v", err)
}
defer holder.Release()
key := "crussell:payment:locktest-contended"
// Hold the advisory lock on a dedicated connection so every try-lock
// attempt from the second connection fails.
if _, err := holder.Exec(ctx, `SELECT pg_advisory_lock(hashtext($1))`, key); err != nil {
t.Fatalf("failed to acquire holder lock: %v", err)
}
defer func() {
_, _ = holder.Exec(context.Background(), `SELECT pg_advisory_unlock(hashtext($1))`, key)
}()
waiter, err := db.Conn.Acquire(ctx)
if err != nil {
t.Fatalf("failed to acquire waiter connection: %v", err)
}
defer waiter.Release()
start := time.Now()
acquired, err := acquireAdvisoryLock(ctx, waiter, key)
elapsed := time.Since(start)
if err != nil {
t.Fatalf("acquireAdvisoryLock returned error (expected false, nil): %v", err)
}
if acquired {
t.Fatal("expected contended lock NOT to be acquired")
}
// The bound is 30 attempts × 100ms ≈ 3s. Assert it gave up within a sane
// window (did not hang) and did not return prematurely.
if elapsed < 2*time.Second {
t.Errorf("expected the timeout bound (~3s) to elapse before giving up, returned after %v", elapsed)
}
if elapsed > 10*time.Second {
t.Errorf("expected to give up within the ~3s bound, took %v", elapsed)
}
}