Skip to content

fix(postgres): per-connection timeouts so a wedged index can't stall the store#49

Merged
mudler merged 1 commit into
mainfrom
fix/postgres-connection-timeouts
Jun 25, 2026
Merged

fix(postgres): per-connection timeouts so a wedged index can't stall the store#49
mudler merged 1 commit into
mainfrom
fix/postgres-connection-timeouts

Conversation

@localai-bot

Copy link
Copy Markdown
Collaborator

Problem

A corrupt custom-index access method (observed: a BM25 index from pg_textsearch left inconsistent by a past pg_resetwal) can make an INSERT spin indefinitely on a BufferContent LWLock. The wedged backend:

  • holds its relation lock the whole time, so every later statement on that table queues behind it - one stuck insert silently stalls the entire vector store and saturates the connection pool;
  • is uninterruptible - pg_terminate_backend returns true but the backend never dies, so only a Postgres restart clears it (and the next insert re-wedges).

Observed in production: a single 4-day-wedged INSERT with 75 CREATE INDEX IF NOT EXISTS statements queued behind it (LocalRecall re-issues index DDL + inserts on every collection access), connections climbing toward max_connections. pg_buffercache confirmed the only pinned buffer in the instance was in the corrupt bm25 index.

The pool opened connections with no timeouts at all, so nothing bounded the cascade.

Fix

applyConnTimeouts sets safe per-connection timeouts on the pool config before the pool is opened:

Param Default Why
lock_timeout 30s The cascade-killer. Bounds how long a statement waits to acquire a lock, so queued statements fail fast instead of piling up for days. Never interrupts a statement doing real work.
idle_in_transaction_session_timeout 300s Reaps abandoned transactions that would otherwise pin locks and the xmin horizon.
statement_timeout unset (opt-in) Bounds total statement runtime, which also kills the wedged insert itself. Off by default because a large DiskANN/HNSW index build can legitimately exceed any fixed limit.

Index builds run via execNoStatementTimeout (SET LOCAL statement_timeout = 0 in a tx), so operators can safely enable statement_timeout without risking long builds. lock_timeout still applies to builds.

All three are env-overridable; "0"/"off" opts out:

  • POSTGRES_LOCK_TIMEOUT
  • POSTGRES_IDLE_IN_TRANSACTION_TIMEOUT
  • POSTGRES_STATEMENT_TIMEOUT

Tests

New applyConnTimeouts unit specs (Ginkgo, no live DB): defaults, env overrides, 0/off opt-out, nil RuntimeParams. go build ./... and go vet ./rag/engine/ clean.

Follow-ups (separate)

  • Pin pg_textsearch to a tagged release in Dockerfile.pgsql (currently an untracked git clone HEAD → the unreproducible 1.0.0-dev).
  • Bump the localrecall dependency in LocalAI once merged.

…the store

A corrupt custom-index access method (e.g. a BM25 index left inconsistent
by a past pg_resetwal) can make an INSERT spin indefinitely on a
buffer-content lock. The wedged backend holds its relation lock the whole
time, so every later statement on that table queues behind it - one stuck
insert silently stalls the entire vector store, saturates the connection
pool, and is only cleared by a Postgres restart (it is uninterruptible, so
pg_terminate_backend does not help). Observed in production: a single
4-day-wedged INSERT with 75 CREATE INDEX statements queued behind it.

The pool opened connections with no timeouts at all, so nothing bounded
the cascade. Set safe defaults on every pooled connection:

  - lock_timeout=30s        -> queued statements fail fast instead of
                               piling up for days (the cascade-killer;
                               never interrupts real work)
  - idle_in_transaction_session_timeout=300s -> reap abandoned txns
  - statement_timeout       -> opt-in (POSTGRES_STATEMENT_TIMEOUT); off by
                               default because a large DiskANN/HNSW build
                               can legitimately exceed any fixed limit.

Index builds run via execNoStatementTimeout (SET LOCAL statement_timeout=0
in a tx) so operators can safely enable statement_timeout without risking
long builds. lock_timeout still applies to builds.

All three timeouts are env-overridable; "0"/"off" opts out.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]
@mudler mudler merged commit 1c6a03d into main Jun 25, 2026
6 checks passed
mudler added a commit to mudler/LocalAI that referenced this pull request Jun 25, 2026
* chore: bump localrecall for postgres per-connection timeouts

Pulls mudler/LocalRecall#49: sets lock_timeout / idle_in_transaction
(default on) + opt-in statement_timeout on every pooled connection, so a
corrupt/wedged index (e.g. a BM25 insert spinning on a buffer-content lock)
can no longer hold its relation lock forever and head-of-line block the
whole vector store.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]

* docs(agents): document PostgreSQL connection safety timeouts

Note the POSTGRES_LOCK_TIMEOUT / POSTGRES_IDLE_IN_TRANSACTION_TIMEOUT /
POSTGRES_STATEMENT_TIMEOUT env vars read by the embedded vector store, and
that safe defaults are on automatically.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-opus-4-8 [Claude Code]

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants