Ir al contenido

Cómo evitar starvation y deadlocks (Odoo + PgBouncer + PostgreSQL)

27 de noviembre de 2025 por
Cómo evitar starvation y deadlocks (Odoo + PgBouncer + PostgreSQL)
John Wolf
| Todavía no hay comentarios

Cuando subes workers, metes crons pesados y pones PgBouncer en pool_mode=transaction, aparecen dos “enemigos invisibles” de la concurrencia:

  • Starvation (hambre): algunas transacciones “nunca” consiguen avanzar porque otras les ganan el lock una y otra vez.

  • Deadlocks: dos (o más) transacciones se bloquean mutuamente en círculo; PostgreSQL detecta el ciclo y mata a una con error.

La diferencia importa porque se solucionan con tácticas distintas.


1) Qué pasa “por debajo” con PgBouncer (por qué amplifica el problema)

En transaction pooling, PgBouncer asigna una conexión a un cliente solo mientras dura la transacción; cuando termina, la conexión vuelve al pool. PgBouncer

Consecuencia directa:

  • una transacción larga o con locks esperando puede “secuestrar” conexiones server y generar colas (starvation a nivel pool),

  • aunque Odoo tenga workers disponibles.


2) Starvation vs Deadlock (para reconocerlos en logs)

Starvation (síntomas típicos)

  • La app se pone lenta “por oleadas”.

  • Ves sesiones “waiting for lock” que duran mucho.

  • PgBouncer muestra clientes esperando (ej. cl_waiting subiendo).

  • No siempre hay error: solo espera.

Deadlock (síntoma típico)

  • Errores explícitos en Postgres: deadlock detected (SQLSTATE 40P01).

  • El servidor no espera infinito: detecta el ciclo tras deadlock_timeout y aborta a un “victim”. El chequeo de deadlock no corre continuamente (es costoso), por eso existe deadlock_timeout. PostgreSQL+1


3) Causas más comunes en Odoo

A) Hot rows (una fila súper disputada)

Ejemplos típicos: parámetros/config, contadores, estados “globales”, colas, inventario con alta contención, etc.

B) Orden de locks inconsistente (la fábrica de deadlocks)

Caso clásico:

  • Tx1 bloquea A y luego intenta B

  • Tx2 bloquea B y luego intenta A

    → deadlock.

C) Transacciones demasiado largas (la fábrica de starvation)

  • loops gigantes con write() por registro

  • llamadas a APIs externas “dentro” de la transacción

  • crons corriendo en paralelo


4) Las 7 reglas de oro para evitar starvation y deadlocks

1) Mantén transacciones cortas (la más rentable)

En transaction pooling, la conexión server se libera al terminar la transacción. PgBouncer

Así que tu objetivo es simple: reduce el tiempo “en transacción”.

Prácticas que ayudan mucho:

  • procesa en lotes (100/500/2000 registros), no “todo el universo”

  • evita I/O externo (HTTP, S3, APIs) dentro de la misma transacción

  • usa “fetch IDs → procesar → siguiente lote”

2) Estandariza el orden de locks (prevención real de deadlocks)

Si necesitas tocar varias filas/tablas:

  • define un orden global (por ejemplo, siempre por id asc, o siempre “padre → hijo”)

  • respétalo en todos los caminos (web, cron, importadores)

Esto reduce brutalmente los deadlocks porque evita ciclos.

3) Usa el lock correcto (no siempre es “FOR UPDATE”)

PostgreSQL tiene distintos niveles de lock de fila: FOR UPDATE, FOR NO KEY UPDATE, FOR SHARE, FOR KEY SHARE. PostgreSQL

En muchos casos, FOR NO KEY UPDATE es suficiente y bloquea menos (por ejemplo, al no tocar claves). (Si quieres profundizar, esto es un tema enorme, pero la idea es: no uses el lock más fuerte si no hace falta). CYBERTEC PostgreSQL | Services & Support

4) Para colas/consumidores: usa NOWAIT o SKIP LOCKED (anti-starvation en colas)

El manual lo dice tal cual:

  • NOWAIT falla inmediatamente si no puede bloquear.

  • SKIP LOCKED salta filas bloqueadas (ideal para tablas tipo “cola”). PostgreSQL

Ejemplo patrón “cola”:

SELECT id
FROM jobs
WHERE state='pending'
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 50;

Ojo: SKIP LOCKED da una vista “inconsistente”, por eso se recomienda para colas, no para lógica de negocio general. PostgreSQL


5) Pon timeouts para cortar esperas infinitas

Dos grandes aliados:

  • lock_timeout: aborta una sentencia si espera demasiado por un lock (se aplica por intento de lock). PostgreSQL Co.

  • statement_timeout e idle_in_transaction_session_timeout: limitan duración de statements y sesiones “idle in transaction” (estas últimas son veneno para VACUUM y contención). PostgreSQL+1

Esto no “arregla” el diseño, pero evita que un problema se convierta en incidente largo.


6) Loguea lock waits para ver el bloqueo real

Activa log_lock_waits=on y ajusta deadlock_timeout para que PostgreSQL loguee cuando una sesión espera más que ese umbral. PostgreSQL+1

Clave: deadlock_timeout también define cuándo se dispara el log de lock-waits si log_lock_waits está activo. PostgreSQL


7) Trata deadlocks/serialización como retryable (idempotencia)

En cargas reales, verás errores como:

  • deadlock detected (40P01)

  • could not serialize access due to concurrent update (40001) en escenarios concurrentes (importaciones, crons vs usuarios, etc.) Odoo+1

La postura más sana:

  • hacer que jobs/acciones sean idempotentes

  • y aplicar reintentos con backoff cuando el error sea de concurrencia y no haya side-effects irreversibles. (En el ecosistema Odoo esto se discute como “expected behavior” y la recomendación práctica suele ser reintentar). GitHub


5) Runbook rápido: cómo detectar “starvation” vs “deadlock” en minutos

A) ¿PgBouncer está haciendo cola?

En la consola admin:

SHOW POOLS;

cl_waiting indica clientes intentando arrancar transacción pero sin conexión server disponible (a menudo por transacciones largas/locks). Runbooks


B) ¿Quién bloquea a quién en PostgreSQL?

Consulta útil:

SELECT pid, usename, state, xact_start, wait_event_type, wait_event, query
FROM pg_stat_activity
WHERE datname = current_database()
ORDER BY xact_start NULLS LAST;

Busca:

  • xact_start muy antiguo (transacción larga)

  • wait_event_type = Lock


Cierre

Si quieres menos incidentes, la receta es:

  1. transacciones cortas (batching),

  2. orden de locks consistente,

  3. NOWAIT/SKIP LOCKED para colas,

  4. timeouts + logging para visibilidad,

  5. retries idempotentes para errores de concurrencia.

Siguiente capítulo ->

Cómo evitar starvation y deadlocks (Odoo + PgBouncer + PostgreSQL)
John Wolf 27 de noviembre de 2025
Compartir
Etiquetas
Archivo
Iniciar sesión dejar un comentario