Paquets#
Durable se compose d’une bibliothèque centrale, d’une intégration de framework facultative et d’un backend à choisir. Le backend est l’endroit où vit le journal d’une exécution et ce qui planifie son travail ; le journal est la suite d’événements, en ajout seul, qui enregistre chaque étape d’une exécution (voir le glossaire). Installez ce dont vous avez besoin : la bibliothèque seule suffit pour écrire un workflow et le tester unitairement, et les paquets qui s’ajoutent au-dessus changent seulement l’endroit où l’exécution est enregistrée, jamais le code du workflow.
| Paquet | Apporte | Exige |
|---|---|---|
gplanchat/durable |
workflows, activités, minuteurs, journal d’événements, backend en mémoire | psr/cache |
gplanchat/durable-bundle |
câblage Symfony, transports Messenger, panneau du profileur | la bibliothèque et Symfony Messenger |
gplanchat/durable-bridge-temporal |
le pilote Temporal, en gRPC | la bibliothèque, ext-grpc, un cluster Temporal |
gplanchat/durable-bridge-dbal |
l’exécution durable sur une base SQL | la bibliothèque, Doctrine DBAL 3 ou 4, symfony/lock |
gplanchat/durable-bridge-illuminate |
la même chose, par la couche de base de données de Laravel | la bibliothèque, illuminate/database 11, 12 ou 13 |
gplanchat/durable-laravel |
le câblage Laravel : les ports liés depuis la configuration, le travail sur la file de l’application | la bibliothèque, illuminate/support ; le pont Illuminate ou Temporal, selon le backend choisi |
gplanchat/durable-magento |
un module Magento 2.4 / Mage-OS : déclaration, workers, écran d’administration | la bibliothèque ; Temporal pour tout ce qui doit survivre à un processus |
gplanchat/durable-plugin |
un tableau de bord Sylius pour les exécutions | le bundle, knplabs/knp-menu ; Sylius 2.x pour apparaître dans son menu |
gplanchat/durable-filament |
un tableau de bord des exécutions dans un panneau Filament | l’intégration Laravel, Filament 3 ou 4 |
gplanchat/durable-phpstan |
l’analyse statique des appels de stub face à leur contrat | la bibliothèque, phpstan/phpstan |
gplanchat/durable-rector |
la migration automatisée depuis le SDK PHP de Temporal | la bibliothèque, rector/rector |
Un workflow est la classe PHP qui décrit les étapes d’une exécution, et une activité est une unité d’effet de bord qu’un workflow appelle, comme un appel HTTP ou une écriture en base.
Les trois ponts sont des alternatives et ne se superposent pas : vous installez Temporal, DBAL ou Illuminate, jamais deux d’entre eux.
Les deux derniers paquets sont des outils de développement et se placent en require-dev :
gplanchat/durable-phpstanrésout les appels d’activityStub()et dechildWorkflowStub()face à l’interface de contrat. Une activité mal nommée ou un mauvais argument apparaît alors comme une erreur d’analyse, au lieu d’un échec de sérialisation à l’exécution. Il vérifie aussi qu’un paramètre#[Activities]et son docblock@param ActivityStub<Contrat>nomment le même contrat.gplanchat/durable-rectormigre un projet depuis le SDK PHP officiel de Temporal. Il réécrit les attributs et change le modèle d’exécution, en conservant les noms de type de workflow et d’activité qu’un serveur en cours d’exécution a déjà enregistrés. Il pose un commentaire sur chaque construction qu’il ne peut pas convertir, pour que vous les voyiez avant de commencer. Voir la page de comparaison.
gplanchat/durable, la bibliothèque#
composer config minimum-stability beta
composer config prefer-stable true
composer require gplanchat/durableLa bibliothèque contient le moteur et tout le domaine : WorkflowEnvironment, activités,
minuteurs, effets de bord, signaux, requêtes, mises à jour, workflows enfants, journal
d’événements, et les objets valeur qui décrivent les options de planification.
Elle a une seule dépendance d’exécution, psr/cache, et le pool de cache lui-même est facultatif :
il mémorise la résolution des contrats d’activité, et ActivityContractResolver fonctionne sans.
La bibliothèque n’exige aucun framework. Vous pouvez la piloter depuis un simple script PHP, une
application Laminas, un outil en ligne de commande ou un test.
Elle inclut un backend en mémoire qui fait tout tourner dans un seul processus. Vos tests unitaires l’utilisent, et il ne demande rien d’autre à installer.
Le backend en mémoire ne garde aucun état d’un processus à l’autre. Utilisez-le pour les tests et l’exploration locale ; un workflow qui doit survivre à un déploiement a besoin d’un autre backend. Voir Backends.
gplanchat/durable-bundle, l’intégration Symfony#
composer config minimum-stability beta
composer config prefer-stable true
composer require gplanchat/durable-bundleLe bundle prend en charge ce que vous écririez sinon à la main :
- L’autoconfiguration. Le bundle enregistre chaque classe qui porte
#[AsWorkflow]ou#[AsActivityHandler]. Vous ne listez ces classes dans aucun fichier de conteneur, et vous ne les balisez pas.#[AsActivity]nomme le contrat et n’enregistre rien. - Le câblage Messenger. Les reprises de workflow et les envois d’activité partent vers les
transports que vous nommez dans
durable.yaml: un workflow qui se suspend reprend donc par vos files existantes. - Deux commandes de console pour lire.
debug:durableliste les workflows enregistrés ; avec un type ou une classe de workflow, elle montre les paramètres du point d’entrée et les signaux, requêtes et mises à jour, sous le nom qu’un appelant envoie.durable:execution:diagnose <executionId>affiche ce que le moteur détient d’une exécution : ses métadonnées de workflow, ses liens parent/enfant et son journal d’événements. Le bundle n’ajoute aucune commande de worker ; le worker est lemessenger:consumede Messenger sur les transports ci-dessus. - Le panneau du profileur. Dans la barre d’outils Symfony, le panneau montre chaque exécution, son journal et la chronologie de ses activités, avec la tentative qui a échoué et la raison.
Toute la configuration tient dans un fichier, documenté clé par clé dans la référence de configuration.
gplanchat/durable-bridge-temporal, le pilote Temporal#
composer config minimum-stability beta
composer config prefer-stable true
composer require gplanchat/durable-bridge-temporalLe pont parle à un cluster Temporal directement en gRPC. L’arbre de dépendances ne contient ni le SDK PHP officiel de Temporal ni RoadRunner : les définitions protobuf sont embarquées et les workers sont de simples processus PHP.
Par rapport au backend en mémoire, il apporte :
- des exécutions qui survivent aux redémarrages de processus, aux déploiements et aux plantages ;
- des politiques de réessai côté serveur : une activité en échec est réessayée même si le worker a disparu ;
- les planifications cron, les attributs de recherche, et la visibilité entre processus dans l’interface Temporal ;
- un stockage d’événements en lecture traversante, pour que le profileur montre l’historique d’une vraie exécution.
Il exige ext-grpc et un cluster joignable. En local, une commande démarre un serveur de
développement :
temporal server start-dev --namespace durable-test --port 7233Les planifications cron et les attributs de recherche sont des capacités de Temporal sans équivalent en processus. Les backends à journal ne les exécutent pas. Le
cronScheduled’un workflow enfant y échoue avecUnsupportedByBackendException, de même que sonnamespaceet sontaskQueue. Ses attributs de recherche sont écrits au journal et rien n’agit dessus, et les options de démarrage d’un workflow racine n’existent que sur le client Temporal.
gplanchat/durable-bridge-dbal, le backend SQL#
composer config minimum-stability beta
composer config prefer-stable true
composer require gplanchat/durable-bridge-dbalLe pont fournit l’exécution durable sur une seule base SQL, sans cluster d’orchestration et sans
ext-grpc. DUR030
consigne la décision qui le fonde.
Le pont laisse intacts l’interpréteur de rejeu, les ports de workflow et le tampon de commandes. Le
rejeu est la façon dont une exécution reprend : le code du workflow tourne à nouveau depuis sa
première ligne, et chaque étape enregistrée renvoie son résultat depuis le journal. Le pont rend
seulement persistants quatre stockages locaux au processus : le journal d’événements, les
métadonnées de workflow, les liens parents des workflows enfants, et le catalogue des exécutions,
la liste des exécutions que lit un tableau de bord. Deux classes se partagent la table du
catalogue : DbalWorkflowRunProjection y écrit et DbalWorkflowRunCatalog la lit. Le code de
workflows et d’activités est octet pour octet celui qui tourne sur Temporal ou en mémoire.
| Conservé | Abandonné par rapport à Temporal |
|---|---|
Classes de workflow, activités, WorkflowEnvironment |
Les files de tâches distribuées ; les reprises passent par Symfony Messenger |
| Signaux, requêtes, mises à jour | La planification côté serveur ; les minuteurs passent par le DelayStamp de Messenger |
| La sémantique d’annulation et de compensation | La sérialisation des tâches côté serveur, remplacée par un verrou applicatif |
| Le déterminisme du rejeu et le journal d’événements | La rétention d’historique, l’API de visibilité, l’interface Temporal |
Choisissez-le quand vous avez besoin de durabilité sans opérer de cluster. Il demande une base que
vous sauvegardez déjà et aucune extension à compiler. Le pont ne livre aucune migration :
DurableSchema crée les tables à la première écriture, et bin/console durable:setup les crée
d’avance (voir Backend DBAL).
gplanchat/durable-bridge-illuminate, le backend Laravel#
composer config minimum-stability beta
composer config prefer-stable true
composer require gplanchat/durable gplanchat/durable-bridge-illuminate
php artisan migrateCe pont fournit les mêmes quatre stockages que le pont DBAL, avec les mêmes compromis face à
Temporal : le tableau ci-dessus s’applique mot pour mot. Une seule classe,
IlluminateWorkflowRunCatalog, écrit et lit le catalogue des exécutions. La connexion change. Ces
stockages utilisent Illuminate\Database\Connection et son constructeur de requêtes, sans Eloquent.
Donnez aux stockages leur propre connexion dans config/database.php, distincte de la connexion par
défaut de l’application (DUR054). Sur une connexion partagée, les transactions propres à Durable
s’imbriquent dans celles de l’application : un rollback métier efface des événements du journal, et
une prise de main reste invisible aux autres workers tant que le code métier n’a pas validé. Pour
traiter une activité qui écrit puis meurt, rendez l’activité idempotente. Ne partagez jamais une
transaction avec le code métier à cette fin.
Les cinq tables sont livrées en migrations, chargées directement depuis le paquet : migrate
suffit. Chaque stockage a sa table, et le journal d’événements écrit aussi dans
durable_execution_heads, un compteur par exécution qui empêche une reprise dépassée d’écrire
(DUR053). Pour les modifier, publiez-les avec vendor:publish --tag=durable-migrations ; à partir
de là, vous maintenez les copies publiées. La commande copie tout le répertoire Migrations/ du
paquet, cinq fichiers, dans database/migrations. Gardez les noms des fichiers publiés. Laravel
indexe les migrations par leur nom de base et donne la priorité à database/migrations quand deux
noms coïncident, ce qui fait de votre copie celle qui s’exécute. Si vous renommez un fichier,
Laravel exécute à la fois le fichier du paquet et votre copie. Seule
0001_01_01_000000_create_durable_tables.php échoue à la seconde exécution : ses appels à
Schema::create n’ont aucune garde et s’arrêtent sur une table qui existe déjà. Les quatre autres
migrations vérifient d’abord hasColumn, hasIndex ou hasTable et ne modifient rien la seconde
fois.
ResumeLock couvre ce qu’aucun choix de stockage ne fournit. Il vit dans gplanchat/durable-laravel, qui s’en sert sur tous les backends. Quand deux workers reprennent la
même exécution, tous deux la rejouent, tous deux traitent les commandes qu’elle produit comme
nouvelles, et ces commandes partent en double. Le journal ne l’empêche pas, car il enregistre tout
ce qu’il reçoit, doublons compris. ResumeLock prend une fermeture : un job en file, une commande
artisan ou un worker écrit à la main peuvent tous s’en servir.
Ce pont fournit seulement le stockage. Il ne lie aucun port et ne livre ni commande de worker ni job ;
DurableIlluminateServiceProviderenregistre seulement l’emplacement des migrations.gplanchat/durable-laravel, décrit dans la section suivante, lie les stockages. Si vous installez le pont seul, vous câblez les stockages vous-même, comme le fait une application sans framework.
gplanchat/durable-laravel, l’intégration Laravel#
composer config minimum-stability beta
composer config prefer-stable true
composer require gplanchat/durable-laravel gplanchat/durable-bridge-illuminate
php artisan migrate
php artisan vendor:publish --tag=durable-configLe paquet suggère les deux ponts et n’en exige aucun : vous installez celui du backend choisi,
gplanchat/durable-bridge-illuminate pour une base SQL, comme ci-dessus, ou
gplanchat/durable-bridge-temporal pour un cluster Temporal. En mémoire, aucun des deux. Avec
backend sur illuminate, la valeur par défaut, et le pont absent, l’enregistrement échoue avec la
commande à lancer.
L’auto-discovery des paquets enregistre le provider. À partir d’un seul config/durable.php publié,
le provider lie les quatre ports de stockage, les jobs d’activité et de reprise, et le verrou par
exécution.
Une seule valeur backend lie tous les ports. Un journal sur un backend avec un catalogue
d’exécutions sur un autre est une panne : backend prend donc une seule valeur. Une valeur que ce
paquet ne sert pas fait échouer l’enregistrement, avec une erreur qui la nomme et nomme les trois
backends que le paquet sert : illuminate, memory et temporal.
Vous déclarez les workflows dans la configuration. Laravel n’a pas d’équivalent de
l’autoconfiguration par attribut de Symfony : la clé workflows nomme donc les classes. Les nommer
coûte 0,14 ms, mesuré, et ce coût ne grandit pas avec l’application. Un scan par réflexion coûte
15 ms à mille classes et les charge toutes dans chaque processus pour en trouver cinq. Pour la
même raison, il n’y a pas de durable:cache : config:cache met déjà en cache le fichier qu’il
dupliquerait.
Le travail passe par la file que l’application draine déjà, avec php artisan queue:work pour
seul worker. Activités et reprises sont des jobs ; un minuteur est un job de déclenchement différé
qui utilise le délai natif de la file.
Comparaison avec durable-workflow/workflow#
durable-workflow/workflow, anciennement
laravel-workflow/laravel-workflow, fournit l’exécution durable sur les files de Laravel, avec
son propre stockage. Il s’inspire explicitement de Temporal et d’Azure Durable Functions et compte
plus de mille étoiles. Depuis la 2.0, les workflows s’y écrivent en méthodes linéaires portées par
des Fibers, et il tourne au choix intégré à votre application, sur son propre serveur autonome ou
sur son Cloud géré, avec des SDK PHP, Python et Rust. Il livre une interface de suivi, Waterline. Il
fait bien son travail ; si vous cherchez un moteur pensé d’abord pour Laravel, choisissez-le.
gplanchat/durable-laravel propose un autre choix de backend. Le même code de workflow tourne
contre un cluster Temporal (Temporal Cloud et Nexus compris, avec un historique que l’interface de
Temporal lit) ou contre une base SQL, sans cluster à opérer. Un parc mixte Symfony / Sylius /
Laravel partage aussi un seul moteur : une classe de workflow écrite pour gplanchat/durable-bundle
tourne ici sans modification. durable-workflow/workflow ne propose ni l’un ni l’autre.
Démarrer une exécution#
WorkflowResumeDispatcher::dispatchNewWorkflowRun() démarre une exécution sur chaque backend :
- sur
illuminate, il met en file la première reprise pourqueue:work; - sur
temporal, il démarre le workflow sur le cluster, qui livre tout ce qui suit ; - sur
memory, il met l’exécution en file dans le processus appelant. Menez-la dans le même processus avecapp(InProcessWorkflowResumeDispatcher::class)->drain()après l’envoi. Dans un processus console (tests, commandes, workers de file),Artisan::call('durable:drain')fait de même ; la commande n’est pas enregistrée pendant une requête HTTP. Le drainage rend la main une fois l’exécution terminée, ou quand elle attend un signal ou une échéance au-delà du budget de dix secondes. Le journal de ce backend vit dans le processus : rien hors du processus ne peut faire avancer l’exécution, et unphp artisan durable:drainlancé à part démarre avec une file vide et ne mène rien.
Servir des opérations Nexus#
Une opération Nexus est une opération servie par un autre service, avec son propre contrat, qu’un
workflow appelle comme il appelle une activité (voir le glossaire). En servir une,
c’est répondre à un appel venu d’un autre espace de noms, et seul le cluster route ces appels. La
clé nexus.handlers nomme les gestionnaires et les contrats qu’ils servent :
'nexus' => ['handlers' => [App\Nexus\BillingHandler::class => App\Contracts\BillingService::class]],Un workflow remplit les opérations qu’aucun gestionnaire ne sert. Il porte
#[FulfilsNexusOperation], et il suffit qu’il figure dans la liste workflows ci-dessus. Un
contrat se sépare en deux interfaces parce que PHP ne permet pas d’exprimer une implémentation
partielle ; le registre recolle les deux moitiés.
Une déclaration Nexus sous un backend qui ne route pas fait échouer l’enregistrement, avant le premier appel, avec une erreur qui nomme le backend. Appeler une opération Nexus ne demande aucune déclaration ici : c’est le workflow qui fait l’appel, et c’est le cas le plus courant.
php artisan durable:nexus-worker draine les opérations que le cluster route vers cette
application.
Trois réglages rejetés#
| Réglage | Rejeté | Raison |
|---|---|---|
lock.store: null |
toujours | il accorde tous les verrous, dans tous les déploiements |
lock.store: array |
sous illuminate |
une reprise tourne dans un worker séparé de celui qui l’a dispatchée, donc deux verrous array ne se voient jamais : quinze sections critiques chevauchées sur vingt, mesurées |
la connexion de file sync |
sous illuminate |
elle exécute les jobs sur place : une reprise qui en dispatche une autre récurse jusqu’à épuiser la pile |
array reste accepté sous memory. C’est le cache de test par défaut de Laravel, et un test n’a
besoin d’exclusion mutuelle qu’à l’intérieur d’un seul processus.
Deux comportements qui ressemblent à des bugs#
Le driver sqlite ne peut pas héberger plus d’un worker. Avec quatre workers qui dépilent la
table jobs, la file renvoie SQLSTATE[HY000]: General error: 5 database is locked, et trois sur
quatre meurent à leur premier job, WAL activé et busy_timeout à 60 s. Passez à MySQL, PostgreSQL
ou Redis pour la file dès que vous lancez un second worker.
Le job d’un worker tué reste réservé jusqu’à retry_after, 90 secondes par défaut. Un worker
lancé avec --stop-when-empty dans cette fenêtre trouve une file vide et sort sans rien faire,
ce qui ressemble trait pour trait à une reprise en échec. La reprise n’a pas encore reçu
le job. Un worker supervisé survit à la fenêtre, prend le job, et l’exécution se termine.
Pas dans ce paquet#
Temporal est bien pris en charge. backend: 'temporal' place le journal et le catalogue
d’exécutions dans le cluster, et deux workers drainent ce que la file de l’application ne peut pas
porter : php artisan durable:temporal-worker draine les tâches de workflow, et
php artisan durable:temporal-worker --role=activity les tâches d’activité.
gplanchat/durable-bridge-temporal est seulement suggéré. Il installe huit paquets, dont cinq
composants Symfony qu’une application Laravel ne charge jamais, pour quelque 36 Mo. Une application
qui ne choisit pas ce backend ne les installe jamais, et celle qui le choisit reçoit une erreur qui
nomme le paquet à installer. Scinder le pont, dont la partie couplée à Symfony fait huit fichiers
sur 774, retirerait ce poids ; c’est une modification distincte.
Aucun tableau de bord. gplanchat/durable-filament
exige ce paquet. Ce paquet le suggère dans composer.json et ne l’exige ni ne le détecte jamais.
gplanchat/durable-plugin, le tableau de bord Sylius#
composer config minimum-stability beta
composer config prefer-stable true
composer require gplanchat/durable-pluginLe plugin affiche le tableau de bord dans l’administration Sylius : une entrée
dans le menu d’administration, la liste des exécutions sur des cartes Tabler avec pagination par
curseur, et le détail à côté. Les panneaux, le regroupement et les libellés viennent de
gplanchat/durable lui-même : une exécution se lit donc de la même façon ici et sur l’écran
Magento. Ce paquet fournit l’habillage Sylius autour d’eux.
Les libellés affichent l’ActivityType.name lisible et ne retombent sur les identifiants
techniques que si aucun nom n’est disponible.
Le plugin lit les exécutions et n’exécute rien. Il exige gplanchat/durable-bundle, qui câble
le catalogue d’exécutions qu’il lit : la commande ci-dessus constitue donc toute l’installation.
Les données vivantes viennent du backend installé, quel qu’il soit. Le plugin n’exige aucun pont :
gplanchat/durablesuggère le backend, une fois, pour toutes les intégrations. Sans backend, le plugin s’installe quand même, la route et l’entrée de menu fonctionnent, et le tableau de bord affiche son état dégradé au lieu d’exécutions vivantes.
gplanchat/durable-filament, le tableau de bord Filament#
composer config minimum-stability beta
composer config prefer-stable true
composer require gplanchat/durable-filament// app/Providers/Filament/AdminPanelProvider.php
use Gplanchat\Durable\Filament\DurableFilamentPlugin;
return $panel
// ...
->plugin(DurableFilamentPlugin::make());Le plugin affiche le tableau de bord dans un panneau Filament 3 ou 4 : une entrée Exécutions Durable dans la navigation du panneau, la liste des exécutions avec pagination par curseur et les filtres par nom et par identifiant d’exécution que le backend peut appliquer, et une page par exécution avec son état, ce qu’elle attend, ses opérations Nexus et son historique. Il est disponible en anglais et en français.
Le plugin lit les exécutions et n’exécute rien. Il exige gplanchat/durable-laravel et lit le
catalogue d’exécutions que ce paquet lie pour son backend : en mémoire, Illuminate ou Temporal. Rien
dans le plugin ne nomme un backend, et rien dans gplanchat/durable-laravel ne nomme Filament.
La page d’une exécution liste ses opérations Nexus quand le catalogue les rapporte, et seul celui de Temporal les rapporte : un journal ne peut pas contenir d’opération Nexus, donc en mémoire et sur Illuminate la section n’apparaît jamais.
gplanchat/durable-magento, l’intégration Magento#
composer config minimum-stability beta
composer config prefer-stable true
composer require gplanchat/durable-magentoUn module Magento 2.4 / Mage-OS, listé comme Gplanchat_DurableModule dans
bin/magento module:status. Il déclare les classes de workflow et d’activité au moteur, assemble le
moteur pour un processus Magento, livre les workers en commandes bin/magento, et ajoute un écran
d’administration en lecture seule sous System > Durable processes > Process history.
L’écran utilise l’habillage de Magento : une grille standard (pagination, signets, choix des colonnes, export, et un filtre d’état multi-select dont les options viennent de l’énumération des états elle-même), avec l’état du backend et les compteurs par issue au-dessus. Le contenu de l’écran ne vient ni de Magento ni de ce paquet ; voir le tableau de bord, que chaque hôte affiche dans son propre habillage.
Le conteneur de Magento n’a pas d’équivalent de l’autoconfiguration par tag de Symfony : vous
déclarez donc les classes explicitement, dans deux tableaux de di.xml :
<type name="Gplanchat\DurableModule\Runtime\RuntimeFactory">
<arguments>
<argument name="workflowClasses" xsi:type="array">
<item name="place_order" xsi:type="string">Acme\Shop\Workflow\PlaceOrder</item>
</argument>
<argument name="activityHandlers" xsi:type="array">
<item name="order" xsi:type="object">Acme\Shop\Activity\OrderActivities</item>
</argument>
</arguments>
</type>Vous ne déclarez pas le contrat. La fabrique lit les interfaces de chaque gestionnaire et garde
celles qui portent #[AsActivityMethod], ce qui fait une déclaration de moins à écrire de travers
et laisse les noms d’activité à ceux des attributs.
Trois autres arguments de la même fabrique bornent une exécution, et di.xml est le seul endroit où
les régler :
<argument name="maxActivityRetries" xsi:type="number">3</argument>
<argument name="budgetSeconds" xsi:type="number">30</argument>
<argument name="maxContinuations" xsi:type="number">10</argument>maxActivityRetriesest le plafond de tentatives des activités queMagentoRuntime::run()exécute dans le processus appelant quand aucun DSN n’est configuré, l’équivalent dumax_activity_retriesdu bundle Symfony. La valeur par défaut,0, ne fixe aucun plafond. Les workers Temporal ne le lisent jamais : là, le cluster relance d’après laRetryLimitpropre à l’activité.budgetSecondsborneMagentoRuntime::run(). Sans DSN, l’appel mène un workflow à son terme dans le processus appelant ; avec un DSN, il attend aussi longtemps le résultat du cluster. Au-delà du budget, l’appel lèveWorkflowStuckExceptionau lieu d’attendre encore. La valeur par défaut est10. Dans le processus, le budget existe à cause du plafond de tentatives : sans plafond, une activité qui échoue sans cesse occuperait ce processus pour toujours.maxContinuationsest, sans DSN, le nombre de continue-as-new (une exécution qui referme son journal et passe la main à une exécution neuve ; voir le glossaire) que suit l’exécution dans le processus avant de leverContinuationCapReachedException. La valeur par défaut est10. Avec un DSN, le cluster mène la chaîne.
Les workers et workflowClient() n’en lisent aucun.
Magento prend en charge trois backends, et Composer l’impose. Magento atteint la mémoire,
Temporal et son propre backend de base de données, et le module déclare un conflict sur les deux
ponts SQL, car Magento\Framework\App\ResourceConnection n’est ni une connexion Doctrine DBAL ni
celle d’Illuminate. app/etc/env.php choisit le backend : resource/durable pour le backend de base
de données (voir Backends), un DSN pour Temporal, ni l’un ni l’autre
pour la mémoire ; déclarer les deux échoue avec BackendSelectionException. Un DSN choisit Temporal :
'durable' => [
'temporal' => ['dsn' => 'temporal://temporal:7233?namespace=default&tls=0'],
],Sans ce DSN, le journal vit dans le processus qui l’écrit et disparaît quand ce processus se termine. C’est acceptable pour une commande console, inadapté à tout le reste.
MagentoRuntime::run() suit le même choix. Sans DSN, il exécute le workflow dans le processus
appelant. Avec un DSN, il démarre le workflow sur le cluster et attend son résultat, que produisent
les workers ci-dessous. L’attente dure environ budgetSeconds et se termine par
WorkflowStuckException.
Avec un DSN, un workflow qui échoue arrive chez l’appelant avec la classe que lève le backend en
mémoire. L’exception du workflow est reconstruite par new $class($message, $code) : elle porte le
message et le code enregistrés, sans exception précédente ni autre propriété. Il faut pour cela que
sa classe se charge dans le processus appelant et que son constructeur accepte (message, code) ;
sinon, l’appelant reçoit une WorkflowFailedException, dont le message commence par
Workflow "<execution id>" failed:. Un échec d’activité arrive sous la forme d’une
DurableWorkflowAlgorithmFailureException, avec l’exception de l’activité comme exception
précédente. Seul le DSN ajoute deux issues : WorkflowTimedOutException
quand un délai fixé sur le cluster expire, et WorkflowTerminatedException quand quelqu’un termine
l’exécution. Un workflow qui attend un signal attend tout le budget au lieu d’échouer aussitôt.
Le résultat revient décodé du JSON : un objet que le workflow renvoie arrive sous forme de tableau.
Pour démarrer un workflow depuis un observateur, appelez
RuntimeFactory::resumeDispatcher()->dispatchNewWorkflowRun(). Avec un DSN, il démarre le workflow
sur le cluster et rend la main aussitôt. Sans DSN, il exécute le workflow dans le processus
appelant, dans la limite de budgetSeconds, et la requête l’attend : il bloque comme run(). Un
workflow qui échoue ne lève pas d’exception depuis l’appel, comme sur le cluster ; l’échec part dans
le logger que la fabrique détient. Un workflow non déclaré, lui, lève une exception. Pour démarrer
uniquement sur le cluster, appelez workflowClient()->startAsync().
Les workers sont des commandes bin/magento, pas des consommateurs de file. Supervisez-les
comme n’importe quel processus long :
bin/magento durable:worker --role=journal --time-limit=3600
bin/magento durable:worker --role=activity --time-limit=3600Chaque processus sert un rôle sur une file. Les rôles journal et activité utilisent deux files
Temporal distinctes, et vous réglez leur parallélisme séparément. Rien ne passe par le MessageQueue de Magento : sur
Temporal, une activité est une commande Temporal et une reprise une tâche de workflow, donc un topic
Magento ne ferait qu’ajouter une seconde file à superviser.
Avec resource/durable dans env.php, la même commande vide les files de la base au lieu
d’interroger Temporal. Sans --role, un processus sert les files de reprise, de minuteur et
d’activité ; --role=journal sert les reprises et les minuteurs, --role=activity sert les
activités. Ce backend n’a pas de --role=nexus, et la commande le refuse. Un message est acquitté
une fois traité. Quand un autre worker tient l’exécution, quand une reprise arrive avant le
résultat de son activité, ou quand MySQL répond par un délai d’attente de verrou ou un
interblocage, le message reste dans la file et est livré de nouveau peu après. SIGTERM et
SIGINT arrêtent le worker entre deux messages.
Une boutique qui sert des opérations Nexus liste ses
gestionnaires dans un tableau de plus de la même fabrique, nexusHandlers : un objet par
gestionnaire, dont le #[AsNexusServiceHandler] nomme le contrat servi. Une opération pour laquelle
le gestionnaire n’a pas de méthode vient d’une entrée de workflowClasses qui porte
#[FulfilsNexusOperation]. Un troisième processus, bin/magento durable:worker --role=nexus, les
sert sur la nexus_task_queue du DSN, qui vaut par défaut la file des tâches de workflow. Sans DSN,
ce worker échoue avec A Nexus worker needs a cluster.
Un worker absent se manifeste différemment selon son rôle. Sans --role=journal, rien
n’avance : les exécutions démarrent, leur historique se remplit, et aucun processus ne répond à
leurs tâches de workflow. Sans --role=activity, l’exécution semble fonctionner, ce qui rend le
problème plus difficile à voir : elle avance jusqu’à sa première activité et s’y arrête, la
commande débitée et le stock intact, et c’est le client qui vous l’apprend.
Les bornes --time-limit et --max-tasks servent au superviseur : elles terminent le processus
pour que le superviseur puisse le relancer. Les nouvelles tentatives relèvent du cluster, qui planifie les
tentatives d’une activité qu’un worker écoute ou non. Une exécution dont l’activité « a échoué après
3 tentatives » en quelques secondes signale un worker absent.
Les réglages de file de Magento ne s’appliquent pas à Durable.
retry_inprogress_after, les tâches cronmessagequeue_*etqueue_lockne portent rien de Durable, puisque rien de Durable ne passe parMessageQueue. Réglez-les pour vos propres consommateurs.
Démarrez les exécutions sur le cluster, hors de la requête qui les déclenche. Avec un DSN, un observateur sur
sales_order_place_afterconfie l’exécution à Temporal et rend la main. Un démarrage peut encore lever une exception (un workflow non déclaré) : interceptez-la, car une exception qui sort de l’observateur interrompt le flux propre de la boutique.public function execute(Observer $observer): void { $incrementId = $observer->getEvent()->getData('order')->getIncrementId(); try { $this->runtimeFactory->resumeDispatcher()->dispatchNewWorkflowRun( ExecutionId::fromString('order-' . $incrementId), PlaceOrder::class, ['orderId' => $incrementId], ); } catch (\Throwable $exception) { $this->logger->error('Le workflow n\'a pas démarré : ' . $exception->getMessage()); } }Le même observateur fonctionne sans cluster, avec une différence : le workflow s’exécute dans la requête, qui l’attend, pendant
budgetSecondsau plus (10 par défaut). Un workflow qui attend un signal ou un long minuteur retient la requête pendant tout le budget. Le journal en mémoire disparaît avec la requête : une exécution non terminée à ce moment est perdue. C’est acceptable en développement ; en production, configurez le DSN.
Qu’est-ce que j’installe ?#
Chaque commande ci-dessous est celle que le sélecteur de la page d’accueil vous donne, écrite en toutes lettres.
Le sélecteur lit son état dans l’URL : un lien peut donc ouvrir la page avec une situation déjà choisie, par exemple dans un ticket, un README ou une réponse de support :
https://durable.rocks/fr/?fw=magento&be=temporal#installfw est le framework (none, symfony, laravel, sylius, apiplatform, magento), be
l’endroit où vit l’état (memory, temporal, dbal, illuminate), et dist la base sous une
distribution (none, symfony, laravel). Chaque axe est facultatif. Le sélecteur ignore une
valeur qu’il ne peut pas appliquer (un framework qui n’est pas publié, un backend que l’appariement
interdit) au lieu de la forcer : un vieux lien retombe sur le choix par défaut au lieu d’afficher
une combinaison qui n’existe pas. Choisir dans la page réécrit la barre d’adresse : le lien à
partager est donc celui que vous avez déjà dans la barre d’adresse.
Chaque commande du tableau suppose que le projet accepte d’abord la ligne bêta :
composer config minimum-stability beta
composer config prefer-stable true| Votre situation | Commande |
|---|---|
| Découverte, ou tests unitaires seulement | composer require gplanchat/durable |
| Sans framework, une base SQL | composer require gplanchat/durable gplanchat/durable-bridge-dbal |
| Sans framework, un cluster Temporal | composer require gplanchat/durable gplanchat/durable-bridge-temporal |
| Symfony, tests seulement | composer require gplanchat/durable-bundle |
| Symfony, une base SQL | composer require gplanchat/durable-bundle gplanchat/durable-bridge-dbal |
| Symfony, un cluster Temporal | composer require gplanchat/durable-bundle gplanchat/durable-bridge-temporal |
| Sylius, tests seulement | composer require gplanchat/durable-plugin |
| Sylius, une base SQL | composer require gplanchat/durable-plugin gplanchat/durable-bridge-dbal |
| Sylius, un cluster Temporal | composer require gplanchat/durable-plugin gplanchat/durable-bridge-temporal |
| Laravel, une base SQL | composer require gplanchat/durable-laravel gplanchat/durable-bridge-illuminate |
| Laravel, cluster Temporal | composer require gplanchat/durable-laravel gplanchat/durable-bridge-temporal |
| Magento, un cluster Temporal | composer require gplanchat/durable-magento gplanchat/durable-bridge-temporal |
Chaque ligne ne nomme que l’intégration : le bundle tire la bibliothèque, et le plugin tire le bundle. Sans framework, vous nommez la bibliothèque vous-même, et vous câblez aussi les workers vous-même.
Les lignes Laravel nomment l’intégration et le pont du backend, car gplanchat/durable-laravel
n’exige aucun des deux ponts. C’est un service provider qui lie les quatre ports de stockage, avec
des workflows déclarés dans config/durable.php et le travail sur la file que l’application draine
déjà ; la section ci-dessus décrit ce qu’il fait à votre place. Pour câbler les stockages vous-même,
sans l’intégration, installez la bibliothèque et le pont.
Le même comportement sur tous les backends#
Tous les backends font tourner le même pilote à fibres et le même chemin d’exécution des activités. Un workflow que vous avez testé en mémoire se comporte de la même façon contre DBAL ou contre Temporal, y compris pour le décompte des réessais, la classification des échecs, l’annulation et la compensation.
Certaines capacités n’ont pas d’équivalent sur un backend. Nexus y échoue avec un message explicite ;
le namespace, le taskQueue et le cronSchedule d’un workflow enfant échouent avec
UnsupportedByBackendException, et ses attributs de recherche sont enregistrés sans être
exécutés. Backends liste les différences.
Monorepo et publications#
Durable se développe dans un seul dépôt, gplanchat/durable-dev. Une scission publie chaque paquet
dans son propre dépôt en lecture seule, si bien qu’un composer require tire un petit paquet plutôt
que tout l’arbre.