La gestión de flujos de trabajo de larga duración y tolerantes a fallos es un problema fundamental en la computación distribuida. Tradicionalmente, esto se ha resuelto mediante la composición de múltiples servicios externos a la base de datos: cron jobs, colas de mensajes, workers y tablas de estado personalizadas. Este enfoque introduce complejidad operacional, latencia de comunicación entre componentes y una visibilidad fragmentada del estado del flujo.
pg_durable aborda este problema consolidando la lógica de orquestación y el estado de los flujos de trabajo dentro de PostgreSQL. Al ejecutar los flujos de trabajo como funciones SQL duraderas, aprovecha las propiedades ACID de la base de datos para garantizar la atomicidad, consistencia, aislamiento y durabilidad de cada paso, simplificando la resiliencia y la auditoría. Esto es especialmente relevante en arquitecturas donde la lógica de negocio está fuertemente acoplada a los datos, como en pipelines de datos o de IA.
Arquitectura del Sistema
pg_durable se implementa como una extensión de PostgreSQL, construida con pgrx, lo que significa que todo su runtime opera dentro del proceso del servidor PostgreSQL. La extensión expone un Lenguaje de Definición de Servicios (DSL) basado en SQL que permite a los usuarios definir grafos de funciones. Estos grafos representan los pasos del flujo de trabajo y sus dependencias, utilizando operadores composables como |=> (secuencial) y ~> (paralelo).
Internamente, pg_durable registra un background worker de PostgreSQL que aloja el runtime de orquestación. Este runtime se basa en dos bibliotecas Rust de bajo nivel: duroxide, que proporciona el framework de tareas duraderas con capacidades de replay determinista, puntos de control y sub-orquestaciones; y duroxide-pg, que actúa como un proveedor de estado para duroxide, persistiendo el estado del runtime (instancias, historial, colas de trabajo) en un esquema dedicado duroxide.* dentro de PostgreSQL. Los flujos de trabajo definidos por el usuario se almacenan en el esquema df.*, que incluye tablas para nodos, instancias y variables. La seguridad se gestiona mediante el modelo de privilegios de PostgreSQL, con Row-Level Security (RLS) para aislar las instancias de los usuarios, aunque el worker de fondo opera con privilegios de superusuario para gestionar todas las instancias.
Ciclo de Vida de un Flujo de Trabajo pg_durable
- 1 Definición SQL El usuario define el flujo de trabajo como un grafo de pasos SQL usando el DS...
- 2 df.start() El usuario invoca df.start() para iniciar una nueva instancia del flujo de tr...
- 3 Background Worker El worker de pg_durable (duroxide runtime) detecta la nueva instancia.
- 4 Ejecución de Paso El worker ejecuta un paso SQL del flujo de trabajo.
- 5 Checkpoint El estado del paso y el progreso se persisten en las tablas duroxide.* como u...
- 6 Fallo/Reinicio Si ocurre un fallo o reinicio, el worker reanuda desde el último checkpoint d...
- 7 Consulta de Estado El usuario puede consultar df.instances para el estado y resultados del flujo.
- 8 Finalización El flujo de trabajo se completa, registrando su estado final y resultados.
| Capa | Tecnología | Justificación |
|---|---|---|
| storage | PostgreSQL | Base de datos principal para la persistencia de datos de negocio y el estado de los flujos de trabajo de pg_durable. shared_preload_libraries para cargar la extensión. |
| compute | Rust | Lenguaje de programación para el desarrollo de la extensión pg_durable, incluyendo las bibliotecas duroxide y duroxide-pg, aprovechando su seguridad de memoria y rendimiento. vs C/C++, PL/pgSQL Uso de pgrx para la integración con PostgreSQL. |
| orchestration | duroxide | Framework de tareas duraderas que proporciona el runtime de orquestación, incluyendo replay determinista, checkpoints y sub-orquestaciones. vs Temporal.io, AWS Step Functions, Apache Airflow |
| storage | duroxide-pg | Proveedor de estado para duroxide, persistiendo el estado del runtime (instancias, historial, colas de trabajo) en tablas dedicadas dentro de PostgreSQL. vs Redis, Apache Kafka Esquema `duroxide.*` para el estado interno. |
Trade-offs
Ganancias
- ▲ Simplificación de la arquitectura
- ▲ Tolerancia a fallos inherente
- ▲ Visibilidad operacional unificada
- ▲ Reducción de infraestructura externa
Costes
- ▲ Acoplamiento fuerte a PostgreSQL
- △ Limitaciones del DSL SQL para lógica compleja
- △ Impacto potencial en el rendimiento de la base de datos
-- A durable function that processes data in steps
SELECT df.start(
'SELECT id FROM documents WHERE processed = false LIMIT 100' |=> 'batch'
~> 'UPDATE documents SET processed = true WHERE id = ANY($batch)'
);-- Grant to specific roles after CREATE EXTENSION
SELECT df.grant_usage('app_role');
-- Alternatively, create an indirection role and grant membership to application roles:
CREATE ROLE pg_durable_user NOLOGIN;
SELECT df.grant_usage('pg_durable_user');
GRANT pg_durable_user TO app_backend, etl_service;Fundamentos Teóricos
El concepto de ejecución duradera y tolerante a fallos tiene sus raíces en los sistemas distribuidos y la teoría de la computación tolerante a fallos. Principios como los "transactional workflows" o "compensating transactions" de la década de 1990 (ej. Gray y Reuter, 'Transaction Processing: Concepts and Techniques', 1993) buscaban extender las propiedades ACID a operaciones de larga duración. Más recientemente, el patrón de "durable execution" o "workflow orchestration" ha sido popularizado por sistemas como Microsoft Durable Functions, Temporal.io y AWS Step Functions, que se basan en el concepto de "event sourcing" y "deterministic replay" para reconstruir el estado de un flujo de trabajo tras un fallo. La idea de mover la lógica de orquestación cerca de los datos, o incluso dentro de la base de datos, se alinea con el principio de "data gravity", minimizando la latencia y la complejidad de la consistencia al reducir los movimientos de datos y las interacciones entre servicios.