Backend · PostgreSQL · Confiabilidad

Advisory Locks de PostgreSQL para Jobs Programados

Una expresión cron indica cuándo ejecutar un job. No garantiza que una sola instancia de la aplicación vaya a ejecutarlo. Los advisory locks de PostgreSQL son una herramienta pequeña y útil para coordinar trabajo programado cuando el sistema ya depende de PostgreSQL y una ejecución duplicada sería costosa o insegura.

El problema: cron no es un lock distribuido

Un job programado puede parecer seguro en desarrollo porque existe un solo proceso. En producción, la misma aplicación puede correr en varias instancias, réplicas o workers. Si todas reciben el mismo cron, pueden iniciar el mismo trabajo prácticamente al mismo tiempo.

En un pipeline meteorológico eso puede generar requests duplicados a proveedores externos, escrituras repetidas en la base, evaluaciones de alertas duplicadas y presión innecesaria sobre APIs con límites de uso. El horario es correcto, pero la ejecución no está coordinada.

Usar PostgreSQL como punto de coordinación

Los advisory locks de PostgreSQL permiten asociar una clave entera a una unidad lógica de trabajo. A diferencia de un lock sobre una fila, esa clave no tiene que corresponder a un registro de una tabla. Por eso sirven bien para jobs como refrescar pronósticos, generar resúmenes diarios o reconciliar suscripciones.

Para trabajo programado prefiero la adquisición no bloqueante. Una instancia obtiene el lock y ejecuta; las demás detectan que ya está ocupado y terminan en lugar de esperar. En un cron, ejecutar la misma tarea más tarde por haber quedado esperando normalmente no aporta valor.

SELECT pg_try_advisory_lock($1) AS acquired;

El lock debe mantenerse en la misma conexión

Los advisory locks de sesión pertenecen a la conexión PostgreSQL que los adquirió. Es un detalle fácil de romper cuando la aplicación usa un pool: adquirir el lock con una consulta y liberarlo con otra no garantiza que ambas usen la misma conexión del backend.

La adquisición, el trabajo protegido y la liberación tienen que quedar ligados al mismo cliente o conexión transaccional. La liberación debe ocurrir en un bloque finally para que una falla de la aplicación no deje el lock retenido durante el resto de esa sesión.

const acquired = await db.$queryRaw<boolean[]>`
  SELECT pg_try_advisory_lock(${jobLockId}) AS acquired
`;

if (!acquired[0]?.acquired) return;

try {
  await refreshForecasts();
} finally {
  await db.$queryRaw`
    SELECT pg_advisory_unlock(${jobLockId})
  `;
}

Elegir la clave según el trabajo que realmente queremos serializar

Una única clave global es simple, pero puede serializar trabajos que no tienen relación entre sí. La granularidad depende del job. Un refresco global de pronósticos puede usar una sola clave, mientras que un proceso por empresa o por campo puede derivarla de forma estable a partir del tipo de job y el identificador del tenant o recurso.

Lo importante es que sea determinística: todas las instancias que compiten por el mismo trabajo tienen que calcular la misma clave, y jobs distintos no deberían colisionar accidentalmente.

El advisory lock no resuelve toda la confiabilidad

El lock evita ejecuciones concurrentes mientras está retenido. No vuelve idempotente al job, no decide si los datos todavía están vigentes, no recupera un proveedor externo ni evita escrituras repetidas después de un reintento.

En Vienor combino la coordinación con políticas de vigencia, deduplicación, reintentos y circuit breakers. Los pronósticos recientes pueden reutilizarse, los snapshots sin cambios no necesitan reescribirse y una respuesta de rate limit puede abrir temporalmente un circuito en lugar de hacer que cada ejecución siga llamando a un proveedor que ya está limitado.

Los locks por transacción son otra alternativa

PostgreSQL también ofrece advisory locks ligados a la transacción, como pg_try_advisory_xact_lock. Se liberan automáticamente cuando termina la transacción y eliminan la necesidad de hacer un unlock explícito.

Son útiles cuando la operación protegida entra naturalmente dentro de una transacción corta. Son menos atractivos si el job realiza requests de red largos, porque mantener una transacción abierta mientras se espera a servicios externos puede generar otros problemas operativos.

SELECT pg_try_advisory_xact_lock($1) AS acquired;

Cuándo conviene este patrón

Los advisory locks funcionan bien cuando PostgreSQL ya es una dependencia obligatoria, se necesita poca coordinación distribuida y es aceptable saltear una invocación duplicada. Evitan sumar una cola o un servicio de locks únicamente para resolver un problema acotado de scheduling.

No reemplazan una cola de jobs cuando se necesita entrega durable, reintentos por elemento, prioridades, backpressure u orquestación horizontal de workers. La idea no es convertir PostgreSQL en una cola, sino aprovechar una primitiva existente para un problema que resuelve de manera limpia.

La regla para producción

Un job programado pasa a ser un límite de sistemas distribuidos desde el momento en que más de un proceso puede ejecutarlo. Hay que hacer explícita la propiedad del trabajo, coordinar sin bloquear, respetar la semántica de las conexiones y diseñar el proceso para que reintentos y duplicados sean seguros incluso cuando el lock no sea la única frontera de falla.

Esa combinación importa más que la sintaxis del cron. El schedule inicia el trabajo; los controles de confiabilidad determinan si el sistema se comporta correctamente en producción.

Trabajo relacionado

Ver cómo encaja este patrón en la arquitectura de producción de Vienor

Artículos técnicos relacionados