mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(backend): add timeout guard for locked_transaction used for credit transactions (#10528)
## Summary This PR adds a timeout guard to the `locked_transaction` function used for credit transactions to prevent indefinite blocking and improve reliability. ## Changes - Modified `locked_transaction` in `/backend/backend/data/db.py` to add proper timeout handling - Set `lock_timeout` and `statement_timeout` to prevent indefinite blocking - Updated function signature to use default timeout parameter - Added comprehensive docstring explaining the locking mechanism ## Motivation The previous implementation could potentially block indefinitely if a lock couldn't be acquired, which could cause issues in production environments, especially for critical credit transactions. ## Testing - Existing tests pass - The timeout mechanism ensures transactions won't hang indefinitely - Advisory locks are properly released on commit/rollback 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import logging
|
||||
import os
|
||||
import zlib
|
||||
from contextlib import asynccontextmanager
|
||||
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
|
||||
from uuid import uuid4
|
||||
@@ -88,35 +87,50 @@ TRANSACTION_TIMEOUT = 15000 # 15 seconds - Increased from 5s to prevent timeout
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def transaction(timeout: int | None = None):
|
||||
async def transaction(timeout: int = TRANSACTION_TIMEOUT):
|
||||
"""
|
||||
Create a database transaction with optional timeout.
|
||||
|
||||
Args:
|
||||
timeout: Transaction timeout in milliseconds. If None, uses TRANSACTION_TIMEOUT (15s).
|
||||
"""
|
||||
if timeout is None:
|
||||
timeout = TRANSACTION_TIMEOUT
|
||||
|
||||
async with prisma.tx(timeout=timeout) as tx:
|
||||
yield tx
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def locked_transaction(key: str, timeout: int | None = None):
|
||||
async def locked_transaction(key: str, timeout: int = TRANSACTION_TIMEOUT):
|
||||
"""
|
||||
Create a database transaction with advisory lock.
|
||||
Create a transaction and take a per-key advisory *transaction* lock.
|
||||
|
||||
- Uses a 64-bit lock id via hashtextextended(key, 0) to avoid 32-bit collisions.
|
||||
- Bound by lock_timeout and statement_timeout so it won't block indefinitely.
|
||||
- Lock is held for the duration of the transaction and auto-released on commit/rollback.
|
||||
|
||||
Args:
|
||||
key: Lock key for advisory lock
|
||||
timeout: Transaction timeout in milliseconds. If None, uses TRANSACTION_TIMEOUT (15s).
|
||||
key: String lock key (e.g., "usr_trx_<uuid>").
|
||||
timeout: Transaction/lock/statement timeout in milliseconds.
|
||||
"""
|
||||
if timeout is None:
|
||||
timeout = TRANSACTION_TIMEOUT
|
||||
|
||||
lock_key = zlib.crc32(key.encode("utf-8"))
|
||||
async with transaction(timeout=timeout) as tx:
|
||||
await tx.execute_raw("SELECT pg_advisory_xact_lock($1)", lock_key)
|
||||
# Ensure we don't wait longer than desired
|
||||
# Note: SET LOCAL doesn't support parameterized queries, must use string interpolation
|
||||
await tx.execute_raw(f"SET LOCAL statement_timeout = '{int(timeout)}ms'") # type: ignore[arg-type]
|
||||
await tx.execute_raw(f"SET LOCAL lock_timeout = '{int(timeout)}ms'") # type: ignore[arg-type]
|
||||
|
||||
# Block until acquired or lock_timeout hits
|
||||
try:
|
||||
await tx.execute_raw(
|
||||
"SELECT pg_advisory_xact_lock(hashtextextended($1, 0))",
|
||||
key,
|
||||
)
|
||||
except Exception as e:
|
||||
# Normalize PG's lock timeout error to TimeoutError for callers
|
||||
if "lock timeout" in str(e).lower():
|
||||
raise TimeoutError(
|
||||
f"Could not acquire lock for key={key!r} within {timeout}ms"
|
||||
) from e
|
||||
raise
|
||||
|
||||
yield tx
|
||||
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ def conn_retry(
|
||||
|
||||
|
||||
# Preconfigured retry decorator for general functions
|
||||
func_retry = create_retry_decorator(max_attempts=5, reraise=False)
|
||||
func_retry = create_retry_decorator(max_attempts=5)
|
||||
|
||||
|
||||
def continuous_retry(*, retry_delay: float = 1.0):
|
||||
|
||||
Reference in New Issue
Block a user