Skip to content

Commit bdb3a55

Browse files
authored
feat(db): configurable Postgres statement_timeout on pool connections (#1200)
* feat(db): apply configurable Postgres statement_timeout on pool connections Adds HINDSIGHT_API_DB_STATEMENT_TIMEOUT (default 600s, set 0 to disable). Applied via the asyncpg pool init hook, so it only affects runtime queries — Alembic migrations run on a separate psycopg2 engine and are untouched. Also fixes the ANN chunk path in the retain orchestrator to restore the pool's configured statement_timeout rather than RESET, which would fall back to the server default and silently drop the safety net on that pooled connection. * refactor(ann): drop fixed per-query timeout on compute_semantic_links_ann Now that the asyncpg pool applies a Postgres statement_timeout to every connection (HINDSIGHT_API_DB_STATEMENT_TIMEOUT, default 600s), the ANN path can be treated like any other query — no need for the 300s asyncpg per-query timeout or the orchestrator's SET/restore dance around the pool's default. * chore: regenerate hindsight-docs skill to pick up configuration doc change Re-runs scripts/generate-docs-skill.sh so the skill reference mirror matches the HINDSIGHT_API_DB_STATEMENT_TIMEOUT row added in hindsight-docs/docs/developer/configuration.md. Also picks up unrelated drift (openapi.json version bump, changelog index) that had accumulated on main.
1 parent f1700af commit bdb3a55

6 files changed

Lines changed: 17 additions & 5 deletions

File tree

hindsight-api-slim/hindsight_api/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,7 @@ def normalize_config_dict(config: dict[str, Any]) -> dict[str, Any]:
375375
ENV_DB_POOL_MAX_SIZE = "HINDSIGHT_API_DB_POOL_MAX_SIZE"
376376
ENV_DB_COMMAND_TIMEOUT = "HINDSIGHT_API_DB_COMMAND_TIMEOUT"
377377
ENV_DB_ACQUIRE_TIMEOUT = "HINDSIGHT_API_DB_ACQUIRE_TIMEOUT"
378+
ENV_DB_STATEMENT_TIMEOUT = "HINDSIGHT_API_DB_STATEMENT_TIMEOUT"
378379

379380
# Worker configuration (distributed task processing)
380381
ENV_WORKER_ENABLED = "HINDSIGHT_API_WORKER_ENABLED"
@@ -607,6 +608,7 @@ def normalize_config_dict(config: dict[str, Any]) -> dict[str, Any]:
607608
DEFAULT_DB_POOL_MAX_SIZE = 100
608609
DEFAULT_DB_COMMAND_TIMEOUT = 60 # seconds
609610
DEFAULT_DB_ACQUIRE_TIMEOUT = 30 # seconds
611+
DEFAULT_DB_STATEMENT_TIMEOUT = 600 # seconds (Postgres statement_timeout applied on every pool connection; 0 disables)
610612

611613
# Worker configuration (distributed task processing)
612614
DEFAULT_WORKER_ENABLED = True # API runs worker by default (standalone mode)
@@ -1064,6 +1066,7 @@ class HindsightConfig:
10641066
db_pool_max_size: int
10651067
db_command_timeout: int
10661068
db_acquire_timeout: int
1069+
db_statement_timeout: int
10671070

10681071
# Worker configuration (distributed task processing)
10691072
worker_enabled: bool
@@ -1671,6 +1674,7 @@ def from_env(cls) -> "HindsightConfig":
16711674
db_pool_max_size=int(os.getenv(ENV_DB_POOL_MAX_SIZE, str(DEFAULT_DB_POOL_MAX_SIZE))),
16721675
db_command_timeout=int(os.getenv(ENV_DB_COMMAND_TIMEOUT, str(DEFAULT_DB_COMMAND_TIMEOUT))),
16731676
db_acquire_timeout=int(os.getenv(ENV_DB_ACQUIRE_TIMEOUT, str(DEFAULT_DB_ACQUIRE_TIMEOUT))),
1677+
db_statement_timeout=int(os.getenv(ENV_DB_STATEMENT_TIMEOUT, str(DEFAULT_DB_STATEMENT_TIMEOUT))),
16741678
# Worker configuration
16751679
worker_enabled=os.getenv(ENV_WORKER_ENABLED, str(DEFAULT_WORKER_ENABLED)).lower() == "true",
16761680
worker_id=os.getenv(ENV_WORKER_ID) or DEFAULT_WORKER_ID,

hindsight-api-slim/hindsight_api/engine/memory_engine.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@ def __init__(
482482
self._pool_max_size = pool_max_size if pool_max_size is not None else config.db_pool_max_size
483483
self._db_command_timeout = db_command_timeout if db_command_timeout is not None else config.db_command_timeout
484484
self._db_acquire_timeout = db_acquire_timeout if db_acquire_timeout is not None else config.db_acquire_timeout
485+
self._db_statement_timeout = config.db_statement_timeout
485486
self._run_migrations = run_migrations
486487
self._retain_entity_lookup = config.retain_entity_lookup
487488

@@ -1848,6 +1849,8 @@ async def verify_llm():
18481849
# Create connection pool
18491850
# For read-heavy workloads with many parallel think/search operations,
18501851
# we need a larger pool. Read operations don't need strong isolation.
1852+
stmt_timeout_s = self._db_statement_timeout
1853+
18511854
async def _init_connection(conn: asyncpg.Connection) -> None:
18521855
# SET (not SET LOCAL) so it persists for the connection lifetime.
18531856
# ef_search=200 improves HNSW recall quality for the per-fact_type
@@ -1857,6 +1860,12 @@ async def _init_connection(conn: asyncpg.Connection) -> None:
18571860
except Exception:
18581861
logger.debug("Could not set hnsw.ef_search — extension may not support it")
18591862

1863+
# Server-side safety net for runaway queries. Migrations use a
1864+
# separate SQLAlchemy/psycopg2 engine, so long-running DDL is
1865+
# unaffected. 0 disables.
1866+
if stmt_timeout_s > 0:
1867+
await conn.execute(f"SET statement_timeout = '{stmt_timeout_s}s'")
1868+
18601869
self._pool = await asyncpg.create_pool(
18611870
self.db_url,
18621871
min_size=self._pool_min_size,

hindsight-api-slim/hindsight_api/engine/retain/link_utils.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -812,7 +812,6 @@ async def compute_semantic_links_ann(
812812
bank_id,
813813
fact_type,
814814
top_k,
815-
timeout=300, # ANN on large banks can take minutes
816815
)
817816
logger.debug(f"[ANN] fact_type={fact_type}: {len(ft_rows)} rows in {time_mod.time() - t_query:.3f}s")
818817
rows.extend(ft_rows)

hindsight-api-slim/hindsight_api/engine/retain/orchestrator.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,6 @@ async def _process_ann_chunk(chunk_idx: int) -> None:
753753
async with ann_semaphore:
754754
t0 = time.time()
755755
async with acquire_with_retry(pool) as conn:
756-
await conn.execute("SET statement_timeout = '300s'")
757756
ann_links = await compute_semantic_links_ann(
758757
conn,
759758
bank_id,
@@ -766,7 +765,6 @@ async def _process_ann_chunk(chunk_idx: int) -> None:
766765
if ann_links:
767766
await _bulk_insert_links(conn, ann_links, bank_id=bank_id)
768767
chunk_link_counts[chunk_idx] = len(ann_links)
769-
await conn.execute("RESET statement_timeout")
770768
logger.info(
771769
f"[streaming] Final ANN chunk {chunk_idx + 1}/{num_chunks}: "
772770
f"{len(ann_links)} links in {time.time() - t0:.3f}s"

hindsight-docs/docs/developer/configuration.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ Migrations will automatically create the schema if it doesn't exist and create a
4545
|----------|-------------|---------|
4646
| `HINDSIGHT_API_DB_POOL_MIN_SIZE` | Minimum connections in the pool | `5` |
4747
| `HINDSIGHT_API_DB_POOL_MAX_SIZE` | Maximum connections in the pool | `100` |
48-
| `HINDSIGHT_API_DB_COMMAND_TIMEOUT` | PostgreSQL command timeout in seconds | `60` |
48+
| `HINDSIGHT_API_DB_COMMAND_TIMEOUT` | PostgreSQL command timeout in seconds (asyncpg client-side) | `60` |
4949
| `HINDSIGHT_API_DB_ACQUIRE_TIMEOUT` | Connection acquisition timeout in seconds | `30` |
50+
| `HINDSIGHT_API_DB_STATEMENT_TIMEOUT` | Postgres `statement_timeout` applied to every pool connection, in seconds. Server-side safety net for runaway queries. Does **not** apply to Alembic migrations (which run on a separate psycopg2 engine). Set to `0` to disable. | `600` |
5051

5152
For high-concurrency workloads, increase `DB_POOL_MAX_SIZE`. Each concurrent recall/think operation can use 2-4 connections.
5253

skills/hindsight-docs/references/developer/configuration.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ Migrations will automatically create the schema if it doesn't exist and create a
4545
|----------|-------------|---------|
4646
| `HINDSIGHT_API_DB_POOL_MIN_SIZE` | Minimum connections in the pool | `5` |
4747
| `HINDSIGHT_API_DB_POOL_MAX_SIZE` | Maximum connections in the pool | `100` |
48-
| `HINDSIGHT_API_DB_COMMAND_TIMEOUT` | PostgreSQL command timeout in seconds | `60` |
48+
| `HINDSIGHT_API_DB_COMMAND_TIMEOUT` | PostgreSQL command timeout in seconds (asyncpg client-side) | `60` |
4949
| `HINDSIGHT_API_DB_ACQUIRE_TIMEOUT` | Connection acquisition timeout in seconds | `30` |
50+
| `HINDSIGHT_API_DB_STATEMENT_TIMEOUT` | Postgres `statement_timeout` applied to every pool connection, in seconds. Server-side safety net for runaway queries. Does **not** apply to Alembic migrations (which run on a separate psycopg2 engine). Set to `0` to disable. | `600` |
5051

5152
For high-concurrency workloads, increase `DB_POOL_MAX_SIZE`. Each concurrent recall/think operation can use 2-4 connections.
5253

0 commit comments

Comments
 (0)