Backends#
Durable prend en charge quatre backends d’exécution : en mémoire, plus les trois ponts entre lesquels vous choisissez.
| Backend | Usage |
|---|---|
| En mémoire | Tests unitaires, tests fonctionnels, exploration locale — aucun serveur nécessaire. |
| DBAL | Production sans cluster d’orchestration — une base SQL, pas d’ext-grpc. |
| Illuminate | Le même, sur la connexion de Laravel plutôt que sur celle de Doctrine. |
| Temporal | Production et recette à l’échelle, tests d’intégration réalistes — ext-grpc et un cluster Temporal requis. |
Sur Magento, aucune des deux lignes SQL n’existe.
gplanchat/durable-magentodéclare unconflictComposer sur les deux ponts SQL :Magento\Framework\App\ResourceConnectionn’est ni une connexion Doctrine DBAL ni celle d’Illuminate, donc aucun des deux n’a de quoi se lier. L’état vit dans une grappe Temporal, ou il vit dans un processus — et le choix se fait par la présence dedurable/temporal/dsndansapp/etc/env.php, pas par un réglage.
Les quatre font tourner le même pilote à fibres et le même code de workflows et d’activités.
Trois d’entre eux se choisissent par durable.event_store.type (et DURABLE_DSN pour Temporal) ;
Illuminate n’en est pas une valeur et ne le sera jamais — voir
le backend Illuminate pour ce qui le lie à la place.
Le backend en mémoire#
Le backend en mémoire tourne entièrement dans un seul processus PHP. Aucun serveur externe, aucun gRPC, et aucune persistance d’une requête à l’autre.
Comment il fonctionne#
- Les messages de workflow et d’activité passent par des transports Symfony Messenger en mémoire.
- L’historique d’événements vit dans un
InMemoryEventStore. - La vidange Messenger traite les messages de façon synchrone quand vous appelez
drainMessengerUntilSettled()ou son équivalent.
Configuration#
# config/packages/durable.yaml (ou when@test:)
durable:
event_store:
type: in_memory
temporal:
dsn: null
workflow_metadata:
type: in_memory
activity_transport:
type: messenger
transport_name: durable_activities
# config/packages/messenger.yaml (ou when@test:)
framework:
messenger:
transports:
durable_workflows: 'in-memory://'
durable_activities: 'in-memory://'
routing:
Gplanchat\Durable\Transport\ResumeWorkflowMessage: durable_workflows
Gplanchat\Durable\Transport\ActivityMessage: durable_activitiesQuand l’employer#
- Pour tous les tests unitaires et fonctionnels (voir Tester des workflows).
- En développement local, quand vous n’avez besoin ni de l’historique durable ni de l’interface de Temporal.
- Pour les jobs d’intégration continue qui tournent sans Docker.
Le backend Temporal#
Le backend Temporal délègue l’orchestration à un vrai cluster Temporal. Le processus PHP
communique en gRPC, via ext-grpc.
Comment il fonctionne#
- Quand
DURABLE_DSNest défini,DurableExtensionenregistre les services propres à Temporal (WorkflowClient,TemporalHistoryCursor, les workers). - Démarrer un workflow appelle le gRPC
StartWorkflowExecutionsur Temporal. - Les tâches de workflow sont récupérées par le consommateur Messenger
durable_temporal_journal. - Les tâches d’activité sont récupérées par le consommateur
durable_temporal_activity. - Chaque tâche de workflow rejoue l’historique via le
WorkflowTaskRunnerà fibres et renvoie ses commandes à Temporal.
Prérequis#
- L’extension PHP
ext-grpc, compilée contre la version du paquetgrpc/grpcqu’exige le pont. - Un cluster Temporal en marche.
Installer ext-grpc#
pecl install grpc
# À ajouter dans php.ini : extension=grpcVérification :
php -m | grep grpcDans une image de conteneur, ne la recompilez pas. pecl install grpc prend environ sept
minutes, et votre construction d’image les paie sur chaque branche. Des extensions préconstruites
sont publiées pour PHP 8.2 à 8.5, en versions thread-safe et non thread-safe — voir
gRPC dans votre image de conteneur pour les recettes
COPY --from, php-fpm, mod_php et FrankenPHP compris.
Mise en place Docker Compose (local / intégration continue)#
Le dépôt fournit un compose.yaml prêt à l’emploi sous symfony/, qui démarre :
- PostgreSQL 16 (partagé entre l’application et Temporal) ;
temporalio/auto-setup:1.25.2(configure le schéma au démarrage) ;- l’interface Temporal (sur le port 8088).
cd symfony
docker compose up -dAttendez que la pile soit saine, puis démarrez les workers Symfony :
php bin/console messenger:consume durable_temporal_journal --time-limit=3600
php bin/console messenger:consume durable_temporal_activity --time-limit=3600Le binaire symfony serve lit .symfony.local.yaml et démarre les workers tout seul s’ils y sont
configurés.
Configuration#
# .env.local (dev/prod)
DURABLE_DSN=temporal://127.0.0.1:7233?namespace=default&journal_task_queue=durable-journal&activity_task_queue=durable-activities&tls=0
MESSENGER_DURABLE_WORKFLOW_DSN=in-memory://
MESSENGER_DURABLE_ACTIVITY_DSN=in-memory://# config/packages/durable.yaml
durable:
event_store:
type: in_memory # Temporal est la vraie source de l'historique ; l'en-mémoire sert de cache local en écriture traversante
temporal:
dsn: '%env(DURABLE_DSN)%'
# config/packages/messenger.yaml
when@dev:
framework:
messenger:
transports:
durable_temporal_journal:
dsn: '%env(DURABLE_DSN)%'
durable_temporal_activity:
dsn: '%env(DURABLE_DSN)%'
options:
purpose: activity_worker
routing:
Gplanchat\Durable\Transport\FireWorkflowTimersMessage: durable_workflowsL’interface Temporal#
Avec la configuration Docker par défaut, l’interface web de Temporal est disponible sur http://localhost:8088. Elle montre les workflows en cours et terminés, leur historique, et les activités en échec.
Les paramètres du DSN#
| Paramètre | Requis | Exemple | Description |
|---|---|---|---|
namespace |
oui | default |
Espace de noms Temporal. Prenez des espaces distincts par application et par environnement. |
journal_task_queue |
oui | durable-journal |
File de tâches du worker de tâches de workflow. |
activity_task_queue |
oui | durable-activities |
File de tâches du worker d’activités. |
tls |
non (défaut 0) |
tls=1 |
Active TLS pour gRPC. Requis pour Temporal Cloud. |
Temporal Cloud#
Pour Temporal Cloud, activez TLS et pointez le point d’entrée Cloud :
DURABLE_DSN=temporal://ACCOUNT.REGION.tmprl.cloud:7233?namespace=NAMESPACE.ACCOUNT&journal_task_queue=durable-journal&activity_task_queue=durable-activities&tls=1Les certificats TLS se montent et se configurent par les identifiants de canal gRPC (voir les points d’extension dans les sources du pont).
Le backend DBAL#
Le backend DBAL persiste le journal, les métadonnées de reprise et les liens parent/enfant dans une
seule base SQL, à travers Doctrine DBAL. Pas de serveur d’orchestration, pas de sidecar, pas
d’ext-grpc. Voir DUR030.
Comment il fonctionne#
- Les trois stockages locaux au processus deviennent des tables SQL ; tout le reste — rejeu, tampon de commandes, cycle de vie — est le code que le backend en mémoire fait déjà tourner.
- Reprises et activités voyagent par Symfony Messenger : prenez donc un transport durable
(Doctrine, Redis, AMQP). Un transport
in-memory://jette ce que le journal SQL vient de persister. - Les minuteurs voyagent par le
DelayStampde Messenger, viaFireWorkflowTimersHandler. - Les tables sont créées à la première écriture — aucune migration à jouer, aucune dépendance à
doctrine/migrations.
Configuration#
# config/packages/durable.yaml
durable:
dbal:
connection: doctrine.dbal.default_connection
lock_factory: lock.factory
event_store:
type: dbal
workflow_metadata:
type: dbal
child_workflow:
parent_link_store:
type: dbal
activity_transport:
type: messenger
transport_name: durable_activities
framework:
lock:
default: '%env(LOCK_DSN)%' # doctrine://default, redis://… — doit être partagé entre les workersPoser event_store.type: dbal en même temps qu’un temporal.dsn non vide lève à la compilation :
le journal ne peut pas avoir deux sources de vérité.
Une reprise à la fois — la chose à ne pas rater#
Temporal sérialise les tâches de workflow d’une exécution côté serveur. Ici il n’y a pas de serveur : deux consommateurs peuvent donc défiler deux reprises de la même exécution et rejouer la même fibre en parallèle, chacun ajoutant ses propres commandes — activités dupliquées, journal bifurqué.
Durable l’empêche par un verrou par exécution (SingleResumeLockMiddleware), enregistré
automatiquement quand le stockage d’événements DBAL est actif. Il ne vaut que ce que vaut votre
magasin de verrous : un lock.factory en mémoire ou local au processus, avec plusieurs workers,
vous redonne exactement la panne que le verrou existe pour empêcher. Configurez-en un partagé.
Quand l’employer#
- En production, sans opérer de cluster — une application Symfony qui a déjà une base de données et un transport Messenger.
- Pour des workflows longs qui doivent survivre aux déploiements et aux redémarrages, à une échelle qu’une seule base peut tenir.
Pas pour : les requêtes par attributs de recherche, les planifications cron, ni le débit et la visibilité qu’apporte un cluster Temporal. Voir la matrice de capacités plus bas.
Le backend Illuminate#
Les mêmes quatre stockages existent sur Illuminate\Database\Connection, sous le nom
gplanchat/durable-bridge-illuminate
— même journal, même échange face à Temporal.
L’échange face à Temporal est celui du pont DBAL, mot pour mot. Ce qui change est la connexion,
et pourquoi : un stockage sur DB::connection() est dans DB::transaction() par construction, ce
qu’exige DUR030. Voir DUR047.
Ce qui le lie n’est pas le YAML de cette page#
Ce n’est pas une quatrième valeur d’event_store.type, et ça ne le sera jamais : une
application Laravel ne lit pas le YAML de cette page. Le pont est la moitié stockage, et ce qui le
lie, c’est gplanchat/durable-laravel, par son propre config/durable.php publié.
Ce paquet porte aussi le côté file — activités et reprises en jobs, un minuteur comme reprise différée sur le délai natif de la file, et l’exclusion par exécution que décrit la section DBAL. Son entrée dans la page Paquets donne la configuration, les trois réglages qu’il refuse plutôt que de les tolérer, et les deux comportements qui ressemblent à des bugs sans en être.
Choisir un backend par environnement#
| Environnement | Backend |
|---|---|
| Tests unitaires | En mémoire (DurableTestCase) |
| Tests d’intégration | En mémoire (DurableBundleTestTrait + KernelTestCase) |
| Intégration continue avec Temporal | Temporal (groupe temporal-integration) |
| Développement local | Au choix — en mémoire pour la vitesse, un backend à journal pour le réalisme |
| Production, sans cluster | DBAL sous Symfony, Illuminate sous Laravel |
| Production, à l’échelle | Temporal |
Matrice de capacités#
Les quatre backends font tourner le même pilote à fibres et le même chemin d’exécution des activités. Ce qui diffère, c’est ce que la plateforme autour sait offrir. Les deux colonnes SQL ne diffèrent que par la connexion sur laquelle elles reposent : elles répondent pareil partout, sauf sur le transport.
| Capacité | En mémoire | DBAL | Illuminate | Temporal |
|---|---|---|---|---|
| Activités, réessais, délais | ✅ | ✅ | ✅ | ✅ |
| Minuteurs, effets de bord | ✅ | ✅ (délais Messenger) | ✅ (délais de la file) | ✅ |
| Signaux, mises à jour, requêtes | ✅ | ✅ | ✅ | ✅ |
| Workflows enfants | ✅ | ✅ | ✅ | ✅ |
Cascade ParentClosePolicy |
✅ | ✅ | ✅ | ✅ (pilotée par le serveur) |
| Continue-as-new | ✅ | ✅ | ✅ | ✅ |
| Annulation avec compensation | ✅ | ✅ | ✅ | ✅ |
| Survit au redémarrage du processus | ❌ | ✅ | ✅ | ✅ |
| Sérialisation des tâches par exécution | sans objet (processus unique) | verrou applicatif | verrou applicatif | ✅ côté serveur |
| Attributs de recherche | journalisés seulement | journalisés seulement | journalisés seulement | ✅ indexés et interrogeables |
| Planifications cron | ❌ pas d’ordonnanceur | ❌ pas d’ordonnanceur | ❌ pas d’ordonnanceur | ✅ |
| Rétention d’historique / API de visibilité | ❌ | votre table SQL | votre table SQL | ✅ |
| Opérations Nexus (appeler et servir) | ❌ | ❌ | ❌ | ✅ |
Aucun backend hors Temporal n’a d’ordonnanceur ou de frontière entre espaces de noms : cron et Nexus n’ont donc pas d’équivalent sur les trois autres. Là où une capacité manque, elle échoue explicitement plutôt que d’être ignorée en silence — pour un appel Nexus, à l’appel ; pour un gestionnaire Nexus, au montage du conteneur, puisqu’un gestionnaire sans route n’est pas un appel qui échoue mais un service qui ne reçoit jamais rien.
Les réessais ont la même sémantique partout#
Une activité sans borne de tentatives réessaie indéfiniment sur tous les backends — c’est le
défaut de Temporal. Le max_activity_retries du bundle agit toujours comme un plafond quand une
activité n’en pose pas ; à 0, il ne plafonne rien.
Voir Échecs et réessais et Options.
Écrire son propre backend#
Deux ports définissent un backend : WorkflowCommandBufferInterface pour ce qu’un workflow demande,
et WorkflowHistorySourceInterface pour ce qui s’est déjà passé.
Les deux portent des objets valeur, pas des primitives. Une implémentation reçoit les options telles que l’appelant les a construites — limites de réessai, délais, files de tâches, planifications cron — et lui appartient la traduction vers sa propre représentation, sérialisation et lecture d’horloge comprises.
startTimer() reçoit un délai, pas une échéance : en faire un instant est votre décision, avec
votre horloge. C’est ce qui permet à un harnais de test d’avancer une horloge virtuelle, et au
pilote Temporal de passer la durée que le serveur attend.
La décision de contribution est DUR031.
Voir aussi#
- Référence de configuration — la liste complète des clés de
durable.yaml. - Premiers pas — routage Messenger et commandes du worker.
- Tester des workflows — se servir du backend en mémoire dans les tests.