pg_durable - Fonctions SQL durables pour PostgreSQL
(github.com/microsoft)- Extension de durable functions pour orchestrer reprises, planification, fan-out parallèle et branchement conditionnel directement dans PostgreSQL avec une petite DSL SQL
- Fonctionne uniquement avec Postgres et des background workers, sans conteneurs ni services externes
- Chaque étape enregistre son état comme point de contrôle dans PostgreSQL, ce qui permet de reprendre au point d’arrêt après un crash, un redémarrage ou une coupure de connexion
- Plus besoin d’implémenter soi-même la gestion de file, le suivi d’état, la reprise après crash, la coordination des étapes ou les retries : il suffit d’écrire du SQL et le moteur d’orchestration s’en charge
- Remplace des tâches qui exigeraient plus de 300 lignes de boilerplate par un seul appel DSL, disponible immédiatement en open source sur PostgreSQL 17
Vue d’ensemble et proposition de valeur
- Durable function intégrée à Postgres et résistante aux crashs (crash-proof), qui orchestre retries, planification, fan-out parallèle et branchement conditionnel avec une petite DSL SQL
- Fonctionne uniquement avec Postgres + background workers, sans infrastructure supplémentaire, conteneur séparé ni service externe
- Joue le rôle de moteur d’orchestration en prenant en charge la gestion des files, le suivi d’état, la reprise après crash, la coordination des étapes et les retries ; l’utilisateur n’écrit que du SQL
Implémentation sans pg_durable
- Pour exécuter 3 agrégations en parallèle, puis rafraîchir un dashboard avec retries et reprise après crash, il faut plus de 300 lignes de boilerplate
- Éléments à construire soi-même : configuration et mise en place de la file, gestion des workers et polling, traitement des messages et suivi d’état, gestion des erreurs et retries, coordination manuelle des étapes
- Le code d’exemple inclut de nombreuses tables d’état comme
job_queue,job_results,job_state,workflow_steps,step_variables,scheduled_jobs, ainsi qu’un worker de polling, la progression du workflow, la reprise après crash, un coordinateur d’exécution parallèle, la transmission de variables, la planification et des fonctions de nettoyage - Le calcul de
next_runpour la planification nécessite en plus une bibliothèque externe de parsing cron
Implémentation avec pg_durable
- La même agrégation parallèle suivie d’un rafraîchissement de dashboard s’exprime en un seul appel
df.start(), avec fan-out via l’opérateur&puis join avec~>- Exemple : 3 requêtes partent en parallèle puis convergent vers l’étape
refresh dashboardpour produire le résultat - Dans l’exemple d’exécution en direct, les 3 étapes s’exécutent en parallèle puis sont jointes ; le dashboard est prêt de manière durable en 1,9 seconde
- Exemple : 3 requêtes partent en parallèle puis convergent vers l’étape
- Gestion des files, suivi d’état, reprise après crash, coordination des étapes et retries sont entièrement pris en charge par pg_durable
Principales caractéristiques
-
Durable par défaut
- Chaque étape enregistre son état comme point de contrôle dans PostgreSQL, ce qui permet au workflow de survivre à un crash, un redémarrage ou une coupure de connexion
- Reprise exactement au point d’interruption
-
Retries automatiques
- Logique de retry intégrée pour les tâches instables ; en cas d’échec d’une étape, seule cette étape est relancée et le reste du workflow continue
- Plus besoin de code manuel de gestion des erreurs
-
Observabilité complète en SQL
- Tous les états du workflow sont stockés dans des tables Postgres, ce qui permet d’interroger l’historique d’exécution, de vérifier la sortie des étapes et de déboguer les échecs en SQL standard
- Aucun dashboard externe n’est nécessaire
-
Exécution parallèle
- L’opérateur
&oudf.join()permet de faire du fan-out sur des tâches indépendantes, avec exécution simultanée et coordination automatique pour des agrégations, appels API ou étapes ETL
- L’opérateur
Patterns possibles
-
Pipelines ETL
- Enchaîner cleanup → transform → load avec garantie d’ordre séquentiel, chaque étape attendant la précédente et stoppant proprement le pipeline en cas d’échec (
~> sequence,|=> variables)
- Enchaîner cleanup → transform → load avec garantie d’ordre séquentiel, chaque étape attendant la précédente et stoppant proprement le pipeline en cas d’échec (
-
Agrégation parallèle
- Calculer simultanément le nombre d’utilisateurs, le total du chiffre d’affaires et l’état du stock, avec fan-out sur plusieurs requêtes puis attente de la fin globale (
&,df.join())
- Calculer simultanément le nombre d’utilisateurs, le total du chiffre d’affaires et l’état du stock, avec fan-out sur plusieurs requêtes puis attente de la fin globale (
-
Traitement de commandes
- Capturer un ID de commande puis le transmettre aux étapes de validation, traitement et finalisation, avec circulation automatique des variables entre étapes (
|=> capture,$var substitution,df.sleep())
- Capturer un ID de commande puis le transmettre aux étapes de validation, traitement et finalisation, avec circulation automatique des variables entre étapes (
-
Jobs planifiés
- Polling d’API, archivage d’enregistrements et synchronisation de données sur planification cron, avec boucle persistante qui survit aux redémarrages (
@> loop,df.wait_for_schedule())
- Polling d’API, archivage d’enregistrements et synchronisation de données sur planification cron, avec boucle persistante qui survit aux redémarrages (
-
Branchement conditionnel
- Vérifier des tâches en attente, un nombre de lignes ou des flags pour décider de traiter ou d’ignorer, avec une logique de branchement située en SQL et non dans l’application (
df.if(),?> conditional)
- Vérifier des tâches en attente, un nombre de lignes ou des flags pour décider de traiter ou d’ignorer, avec une logique de branchement située en SQL et non dans l’application (
-
Validation en plusieurs étapes
- Récupération des données → validation du schéma → vérification des règles métier → approbation/rejet, chaque étape étant checkpointée pour ne pas perdre l’avancement en cas d’échec
-
Maintenance de base de données
- Détecter les facteurs bloquant autovacuum, le bloat de tables et les risques de wraparound, puis les exposer pour revue avant correction durable même après redémarrage (
?> conditional,df.wait_for_signal(),@> loop)
- Détecter les facteurs bloquant autovacuum, le bloat de tables et les risques de wraparound, puis les exposer pour revue avant correction durable même après redémarrage (
-
Azure Functions & HTTP
- Avec
df.http(), appeler directement depuis SQL des Azure Functions ou des endpoints HTTPS autorisés, pour faire en ligne du chunking de documents, de l’enrichissement de lignes ou de la classification d’enregistrements
- Avec
-
Approbation humaine dans la boucle
- Approuver automatiquement les tâches courantes et mettre en pause les tâches à haut risque (grosses factures, opérations destructrices) jusqu’à réception d’un signal d’approbation humaine (
df.wait_for_signal(),df.if())
- Approuver automatiquement les tâches courantes et mettre en pause les tâches à haut risque (grosses factures, opérations destructrices) jusqu’à réception d’un signal d’approbation humaine (
Assistance à l’écriture par l’IA
- Il suffit de décrire le workflow en anglais courant pour que Copilot génère le SQL correct de durable function, sans avoir à apprendre la syntaxe
- Le dépôt inclut la compétence d’agent réutilisable
pg-durable-sql, qui apprend à GitHub Copilot et à d’autres agents à générer correctement les opérateurs, substitutions de variables, boucles, joins parallèles, etc.
Disponible en open source
- Fourni entièrement en open source, sans liste d’attente ni lock-in : il suffit de cloner le dépôt, de le compiler et de l’exécuter immédiatement sur son propre PostgreSQL
- Permet d’appliquer une orchestration durable sur un notebook, un serveur ou dans le cloud
Option managée Azure HorizonDB
- Azure HorizonDB est le nouveau service cloud PostgreSQL de Microsoft, avec pg_durable intégré, ce qui permet de conserver telles quelles les durable functions écrites tout en ajoutant montée en charge d’entreprise, sécurité et IA
- Jusqu’à 3× plus rapide, extension automatique du stockage jusqu’à 128 To, scale-out compute jusqu’à 3 072 vCore
- Détection des menaces en temps réel avec Microsoft Defender, gestion des identités avec Microsoft Entra ID
- Recherche vectorielle Filtered DiskANN, classement sémantique, curation de modèles d’IA dans la base
- Mirroring quasi temps réel avec Microsoft Fabric, intégration VS Code, liaison avec GitHub Copilot
-
Pipeline IA intégré
- HorizonDB superpose un pipeline IA managé de bout en bout sur l’exécution durable de pg_durable, avec point de contrôle, retry et sécurité face aux crashs à chaque étape
- Flux d’étapes : Ingest (chargement de documents et données) → Chunk (découpage du contenu) → Embed (vectorisation) → Index (stockage DiskANN) → Serve (recherche et ranking)
1 commentaires
Commentaires sur Hacker News
2026 semble être l’année des files d’attente Postgres : il y a une dynamique avec DBOS[0], pgQue[1], et c’est chouette que la communauté crée ce genre d’options
Cela dit, en tant qu’ancien ingénieur applicatif, je préfère que la logique de queue reste dans le code et dans Git. Avec les bons outils, je pourrais peut-être changer d’avis
[0]: https://www.dbos.dev/
[1]: https://github.com/NikolayS/pgque
Je me demande comment on gère le versioning, le débogage, les tests et les releases. Tout mettre au même endroit pour la localité des données et la simplification de la stack paraît séduisant, mais j’ai l’impression qu’on perd beaucoup de savoir utile sur la manière de le faire « correctement »
C’est aussi pour ça que j’ai vraiment détesté le fait que, sur Supabase, dès qu’on voulait faire quelque chose d’un peu complexe, il fallait créer des fonctions Postgres. Cela dit, dans une précédente startup, nous avions construit nous-mêmes une file de tâches simple sur Postgres, et s’il y avait eu quelque chose comme pgQue, cela aurait sans doute été bien plus abouti
Même les extensions multi-master ne sont pas vraiment du genre immédiatement utilisables et totalement sûres, donc je reste prudent à l’idée d’y mettre des traitements complexes très orientés écriture, qui accélèrent le besoin d’étendre la base de données
Pour la configuration locale, il nous est même arrivé de les injecter dans des migrations Django afin que les triggers soient présents dans la base locale
Ça sent les procédures stockées. C’est difficile à tester unitairement, difficile à versionner, et la logique métier se retrouve cachée dans la base de données comme un « cerveau caché »
C’est aussi difficile d’isoler les workloads bruyants, il n’y a pas d’observabilité, et toute la pression de montée en charge retombe sur Postgres. Surtout, les entrées/sorties comme les appels d’API manquent. Pour des tâches qui tournent uniquement dans la base locale, pourquoi pas, mais l’usage semble limité
Bien sûr, il faut avoir une vraie procédure de mise à niveau de la base. Si les membres de l’équipe exécutent des migrations SQL arbitraires en root, vous allez souffrir
Les tests unitaires sont possibles comme pour n’importe quel autre test SQL, il faut simplement lancer une base de données. Si vous ne pouvez pas tester des procédures stockées, cela signifie que vous ne pouvez pas tester le SQL lui-même, et c’est ça le vrai problème
L’alternative aux procédures stockées, ce n’est pas de ne mettre aucune logique métier dans la base, mais souvent d’avoir du SQL éparpillé partout dans le code, difficile à tester, mal versionné, mal encapsulé, et inutilement lent
Pour l’observabilité, c’est partiellement vrai : inspecter des problèmes SQL demande généralement plus d’effort qu’avec la plupart des langages de programmation. Mais si des procédures stockées créent des problèmes d’entrées/sorties et de montée en charge, c’est qu’elles sont mal utilisées ; bien employées, elles réduisent souvent fortement les entrées/sorties et améliorent la scalabilité
Si j’ai bien compris, Absurd, créé par les développeurs du harnais Pi LLM, semble aller dans le sens d’une réduction maximale des accès directs à la base de données. Je viens seulement de commencer à regarder le sujet
https://github.com/earendil-works/absurd
Bien sûr, je ne connais pas tous les détails, donc je suis sincèrement curieux
Dans la section « quand ne pas l’utiliser », il est écrit « lorsque le workflow est majoritairement hors de Postgres et s’étend sur plusieurs systèmes hétérogènes » ; dans ce cas, je ne vois pas comment ce projet peut être comparé à quelque chose comme Temporal
Je me demande si je comprends mal les limites implicites de cette recommandation
Techniquement, c’est peut-être une réalisation intéressante, mais lire du SQL comme ça est assez étrange
SELECT df.start(@> (($$SELECT ... FROM demo.invoices WHERE status = 'pending'$$ |=> 'inv')~> df.if_rows('inv',$$UPDATE ... SET status = 'processing'$$~> (df.http(...) |=> 'resp')~> df.if($$SELECT $r.ok$$,-- classify, branch, wait for signal ...),df.sleep(5))),'invoice-approval-pipeline');On est coincés sur Azure au boulot, et on attend toujours qu’Azure PostgreSQL rattrape les fonctionnalités modernes.
Par exemple, on ne peut pas utiliser ceci : https://www.paradedb.com/blog/hybrid-search-in-postgresql-th...
Il n’y a pas non plus de prise en charge des vecteurs de très grande dimension. C’est bien de publier pg_durable en open source, mais j’aimerais déjà qu’ils adoptent les fonctionnalités de base qu’on a naturellement sur AWS
Pour être transparent, je suis mainteneur de pg_textsearch et je suis maintenant chez Azure. Je n’ai pas bien compris la remarque sur la prise en charge des vecteurs : est-ce que vous cherchez quelque chose qui aille au-delà de pgvector + diskann proposés sur Azure ?
Pour la recherche hybride (BM25 + vecteur), le pg_search de ParadeDB n’est pas non plus une fonctionnalité native AWS ; il faut l’héberger soi-même sur EC2. Sur Azure PostgreSQL, nous avons développé nativement pg_textsearch, qui fournit le même modèle de classement BM25, et son principal contributeur fait actuellement partie de l’équipe Azure Postgres.
Documentation : https://learn.microsoft.com/en-us/azure/horizondb/ai/full-te...
Pour les vecteurs de grande dimension, c’est même un domaine où nous sommes en avance. pgvector avec HNSW a une limite de 2 000 dimensions, mais Azure prend en charge pgvector pour le stockage et la recherche vectoriels, et pour les charges de travail à grande échelle et en haute dimension, propose pg_diskann, l’index vectoriel orienté graphe de Microsoft. Il prend en charge jusqu’à 16 000 dimensions et fournit aussi un filtrage avancé directement dans l’index, avec évaluation des conditions WHERE pendant la traversée du graphe, ce qui évite de perdre en rappel sur les conditions sélectives.
pgvector : https://learn.microsoft.com/en-us/azure/horizondb/ai/vector-...
Prise en charge haute dimension de DiskANN : https://learn.microsoft.com/en-us/azure/horizondb/ai/vector-...
Ces fonctionnalités sont actuellement disponibles sur Azure PostgreSQL, en particulier dans Azure HorizonDB Preview. Si vous avez une charge de travail précise en tête, on peut l’examiner plus en détail
J’ai l’impression que c’est une mauvaise réponse à un vieux problème que des planificateurs de DAG comme Apache Airflow résolvent depuis longtemps.
Je trouve étrange de vouloir stocker le flux de contrôle dans la base de données plutôt que dans le code. Ce n’est pas pour rabaisser le projet, c’est juste que je ne comprends pas encore bien
Ce projet semble davantage viser un cas d’usage spécialisé base de données. L’avantage est sans doute qu’on peut suivre l’état exact du travail directement dans la base elle-même, sans avoir à faire du traçage ligne par ligne en recoupant les logs du workflow avec la base de code. La charge et la latence seraient aussi plus faibles, et cela réduit probablement d’un composant ce qu’il faut faire tourner en production.
[1] https://learn.microsoft.com/en-us/azure/durable-task/common/...
À l’inverse, même si cela ne semble pas être le cas actuellement, cette approche pourrait potentiellement s’autoréguler grâce à un retour de performance quasi temps réel, sans coût de latence lié aux allers-retours
Même après avoir lu la documentation et les exemples, certains points restent flous. Je me demande comment fonctionne
df.wait_for_schedule()Si on l’appelle depuis l’application, est-ce idempotent ? Si on l’exécute deux fois avec les mêmes paramètres, est-ce que le tick se produit deux fois ? Est-ce quelque chose qu’on appelle manuellement une seule fois depuis une console SQL, ou qu’on exécute dans le cadre d’un script de migration ? Je ne le comprends pas bien
Je me demande aussi si
timed_outdans l’exemple[0] est une constante fixe renvoyée en cas de dépassement de temps. La gestion des erreurs ou des exceptions ne saute pas non plus immédiatement aux yeux[0] https://github.com/microsoft/pg_durable/blob/main/examples/i...
df.start(), cela crée une fonction durable et démarre son exécution en même temps. Cet appel renvoie un ID d’instance représentant cette exécution, qui peut ensuite servir à la référencerÀ l’intérieur de cette fonction durable, on appelle
df.wait_for_signal(), et cet appel n’est exécuté qu’une seule fois exactement dans cette instance de fonction, donc il ne peut pas y avoir de doublon. Si l’appel àdf.start()lui-même expire et est relancé, cela peut produire un doublon, mais dans ce cas une autre instance de fonction est crééeSi une erreur non gérée survient pendant l’exécution SQL, l’instance de fonction échoue et l’état remonte exactement l’erreur qui s’est produite
Pourrais-tu expliquer pourquoi utiliser ça plutôt qu’un outil d’orchestration en dehors de la base de données ? Même après avoir lu le README et les exemples, je ne comprends toujours pas bien
Il n’est pas nécessaire de synchroniser les sauvegardes avec d’autres composants appartenant au même stockage d’état, ce qui est bien pour les pipelines ETL ou les tâches de type machine à états. Si l’ETL est majoritairement en SQL, le fait que le travail réel s’exécute sur le même serveur aide aussi
Si tout l’état se trouve dans une seule base, on a aussi plus de chances d’obtenir des sauvegardes cohérentes
Sur https://transport.data.gouv.fr, Postgres est utilisé à ce genre de fin, et c’est utile dans une application Elixir qui fait pas mal de traitement. Je ne connais pas encore bien pg_durable, mais j’ai déjà utilisé ou implémenté des solutions similaires, donc ça me parle
La base de données n’est-elle pas déjà l’un des composants d’infrastructure les plus difficiles à faire évoluer ? Je ne vois pas pourquoi on voudrait en plus y mettre des tâches de longue durée
Au final, ce type de charge correspond à des tâches qui s’exécuteront contre la base de données, qu’elles soient déclenchées ou non par un composant externe. Dans les pipelines de données ou d’IA, il est aussi devenu plus courant d’envoyer des requêtes HTTP depuis la base de données pour éviter des allers-retours et des points de défaillance supplémentaires liés à des composants externes. Cela dit, faire venir le calcul vers les données ou les données vers le calcul reste un choix d’architecture important et très débattu
Cela donne l’impression d’un énième https://en.wikipedia.org/wiki/Inner-platform_effect qui n’aurait pas été nécessaire si les langages de programmation ou machines virtuelles populaires prenaient déjà en charge le déterminisme, une exécution pas à pas mesurable et contrôlable, la suspension de l’état d’exécution, ainsi que la sérialisation/désérialisation et la reprise