//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) } }