diff --git a/documentation/user/activities/_index.fr.md b/documentation/user/activities/_index.fr.md index 981d8dbf..82eff594 100644 --- a/documentation/user/activities/_index.fr.md +++ b/documentation/user/activities/_index.fr.md @@ -10,7 +10,7 @@ Cette page résume comment on **écrit** des activités en Durable. Le détail n ## Deux pièces 1. **L'interface de contrat d'activité** — les méthodes que le workflow a le droit d'appeler, chacune marquée d'un **`#[AsActivityMethod]`**. Depuis le workflow, on passe par un **`ActivityStub`** (**ActivityInvoker** dans les ADR). -2. **La classe d'implémentation** — une classe concrète (souvent annotée d'un **`#[AsActivity]`** pour son nom) qui **implémente** le contrat et fait le vrai travail. +2. **La classe d'implémentation** — une classe concrète portant **`#[AsActivityHandler]`**, qui nomme le contrat qu'elle implémente. C'est cet attribut qui l'enregistre : le bundle l'autoconfigure, et sans lui le workflow ne trouve aucun gestionnaire à l'exécution. ## Exemple : contrat et implémentation @@ -22,15 +22,19 @@ L'**interface** énumère les méthodes que le workflow peut planifier. Chaque m declare(strict_types=1); use Gplanchat\Durable\Attribute\AsActivity; +use Gplanchat\Durable\Attribute\AsActivityHandler; use Gplanchat\Durable\Attribute\AsActivityMethod; +// `#[AsActivity]` est optionnel et se pose sur le **contrat** : il préfixe le nom des activités +// déclarées en dessous. Sur une classe d'implémentation, personne ne le lit. +#[AsActivity(name: 'order-activities')] interface OrderActivities { #[AsActivityMethod(name: 'charge-order')] public function charge(string $orderId): string; // type de retour synchrone, côté worker } -#[AsActivity(name: 'order-activities')] +#[AsActivityHandler(contract: OrderActivities::class)] final class OrderActivitiesHandler implements OrderActivities { public function __construct( diff --git a/documentation/user/activities/_index.md b/documentation/user/activities/_index.md index 7181b52c..82139075 100644 --- a/documentation/user/activities/_index.md +++ b/documentation/user/activities/_index.md @@ -10,7 +10,7 @@ This page summarizes how you **author** activities in Durable. Normative detail ## Two pieces 1. **Activity contract interface** — methods the workflow may call, each marked with **`#[AsActivityMethod]`**. From the workflow you interact through **`ActivityStub`** (**ActivityInvoker** in ADRs). -2. **Activity implementation class** — concrete class (often annotated with **`#[AsActivity]`** for naming) that **implements** the contract and performs real work. +2. **Activity implementation class** — concrete class carrying **`#[AsActivityHandler]`**, naming the contract it implements. That attribute is what registers the class: the bundle autoconfigures it, and without it the workflow finds no handler at run time. ## Example: activity contract and implementation @@ -22,15 +22,19 @@ The **interface** lists methods the workflow may schedule. Each exposed method c declare(strict_types=1); use Gplanchat\Durable\Attribute\AsActivity; +use Gplanchat\Durable\Attribute\AsActivityHandler; use Gplanchat\Durable\Attribute\AsActivityMethod; +// `#[AsActivity]` is optional and belongs on the **contract**: it prefixes the names of the +// activities declared below. On an implementation class nothing reads it. +#[AsActivity(name: 'order-activities')] interface OrderActivities { #[AsActivityMethod(name: 'charge-order')] public function charge(string $orderId): string; // synchronous return type on the worker } -#[AsActivity(name: 'order-activities')] +#[AsActivityHandler(contract: OrderActivities::class)] final class OrderActivitiesHandler implements OrderActivities { public function __construct( diff --git a/documentation/user/getting-started/_index.fr.md b/documentation/user/getting-started/_index.fr.md index 0566b3f3..e28b5ff5 100644 --- a/documentation/user/getting-started/_index.fr.md +++ b/documentation/user/getting-started/_index.fr.md @@ -84,7 +84,7 @@ durable: activity_contracts: cache: cache.app contracts: - - App\Workflow\Activity\OrderActivities # listez ici vos interfaces d'activité + - App\Workflow\Activity\GreetingActivities # listez ici vos interfaces d'activité ``` Basculez sur Temporal à l'exécution en définissant `DURABLE_DSN` dans votre environnement : @@ -110,7 +110,7 @@ framework: routing: Gplanchat\Durable\Transport\ResumeWorkflowMessage: durable_workflows Gplanchat\Durable\Transport\ActivityMessage: durable_activities - Gplanchat\Durable\Transport\FireWorkflowTimersMessage: sync + Gplanchat\Durable\Transport\FireWorkflowTimersMessage: durable_workflows Gplanchat\Durable\Transport\DeliverWorkflowSignalMessage: sync Gplanchat\Durable\Transport\DeliverWorkflowUpdateMessage: sync ``` @@ -158,12 +158,18 @@ Toute classe portant `#[AsWorkflow]` dans votre espace de noms de workflows est # config/services.yaml App\Workflow\: resource: '../src/Workflow/' + exclude: '../src/Workflow/Activity/' tags: [durable.workflow] ``` +L'`exclude` compte. La balise ne filtre rien : chaque service qu'elle attrape est passé au registre +des workflows, qui exige exactement un `#[AsWorkflowMethod]` et lève sinon. Baliser un dossier qui +porte aussi vos gestionnaires d'activité, et le conteneur cesse de se construire sur une erreur +nommant une classe que vous n'enregistriez pas exprès. Au dossier balisé, ses seuls workflows. + ### Déclarer les implémentations d'activité -Les classes d'implémentation d'activité sont des services Symfony ordinaires (l'autowiring s'applique). Si vous posez `#[AsActivityHandler]` sur la classe, le bundle les ramasse tout seul dès que le service est marqué. +Rien à écrire. Une classe portant `#[AsActivityHandler]` est ramassée par l'autoconfiguration du bundle dès qu'elle est un service — ce qu'avec l'`autoconfigure: true` par défaut d'une application Symfony elle est déjà. C'est là que les workflows ci-dessus diffèrent : eux ont encore besoin de la balise. --- @@ -178,8 +184,11 @@ declare(strict_types=1); namespace App\Workflow\Activity; +use Gplanchat\Durable\Attribute\AsActivity; use Gplanchat\Durable\Attribute\AsActivityMethod; +// Optionnel : préfixe le nom des activités déclarées en dessous. +#[AsActivity(name: 'greeting-activities')] interface GreetingActivities { #[AsActivityMethod(name: 'greet')] @@ -196,9 +205,10 @@ declare(strict_types=1); namespace App\Workflow\Activity; -use Gplanchat\Durable\Attribute\AsActivity; +use Gplanchat\Durable\Attribute\AsActivityHandler; -#[AsActivity(name: 'greeting-activities')] +// C'est cet attribut qui enregistre la classe ; le bundle l'autoconfigure. +#[AsActivityHandler(contract: GreetingActivities::class)] final class GreetingActivitiesHandler implements GreetingActivities { public function greet(string $name): string @@ -260,11 +270,78 @@ final class GreetController $executionId = 'greet-'.uniqid(); $this->dispatcher->dispatchNewWorkflowRun($executionId, 'greet', ['name' => $name]); - return new JsonResponse(['executionId' => $executionId]); + // 202 : le run est en file, pas terminé. Répondre 200 ici est la première chose qui + // fait attendre un résultat qu'aucun consommateur n'a encore produit. + return new JsonResponse(['executionId' => $executionId], JsonResponse::HTTP_ACCEPTED); } } ``` +### 5 — Faire tourner un consommateur, sinon rien n'arrive + +`dispatchNewWorkflowRun()` rend `void` et fait exactement ce que son nom dit : il *envoie*. Le +workflow s'exécute quand quelque chose consomme les transports configurés plus haut. D'ici là +l'exécution attend en file — un tableau de bord la dira `RUNNING`, ce qui est vrai et inutile : ça +veut dire *pas terminée*, pas *quelqu'un s'en occupe*. + +```bash +php bin/console messenger:consume durable_workflows durable_activities +``` + +Ces deux noms sont les transports que **vous** avez déclarés dans `messenger.yaml`. Aucun document +ne peut vous donner cette commande sans que vous ayez écrit ce fichier d'abord — c'est ce qui fait +qu'on cherche la pièce manquante partout sauf dans sa propre configuration. + +Pour voir ce que le moteur retient d'une exécution : + +```bash +php bin/console durable:execution:diagnose greet-abc123 +``` + +#### Dans quel profil êtes-vous ? + +Deux configurations fonctionnent. Les mélanger est le faux pas habituel, et il échoue en silence. + +**Un seul processus — les tests.** Transports `in-memory://` et magasins en mémoire. Envoi, reprise +et activité se passent dans un même processus PHP, donc un test envoie et draine d'un seul geste. Un +transport en mémoire **ne survit pas à son processus** : y envoyer depuis une requête web pour +consommer dans un worker séparé ne peut pas marcher, et le rejeu non plus — le journal dont le +worker aurait besoin vit dans la mémoire du processus web. + +**Plusieurs processus — développement local et production.** De vrais transports **et** un magasin +durable, sinon le worker prend une entrée nommant un workflow dont il ne voit pas le journal. + +Ce profil demande deux paquets que la prise en main ci-dessus n'installe pas — le journal DBAL, et +DoctrineBundle pour le service `doctrine.dbal.default_connection` qu'il nomme : + +```bash +composer require gplanchat/durable-bridge-dbal doctrine/doctrine-bundle +``` + + +```yaml +durable: + event_store: + type: dbal + workflow_metadata: + type: dbal + child_workflow: + parent_link_store: + type: dbal + dbal: + connection: doctrine.dbal.default_connection +``` + +```dotenv +MESSENGER_DURABLE_WORKFLOW_DSN=doctrine://default +MESSENGER_DURABLE_ACTIVITY_DSN=doctrine://default +``` + +La règle derrière les deux profils : **une exécution survit exactement à ce à quoi survivent son +journal et sa file.** Routez `ResumeWorkflowMessage` ou `ActivityMessage` vers un transport qu'un +worker séparé ne peut pas lire, et le workflow rejoue dans la requête web qui l'a démarré puis meurt +avec le processus — précisément la panne que l'exécution durable existe pour supprimer. + --- ## Démarrer les workers Temporal (production / mode dev) diff --git a/documentation/user/getting-started/_index.md b/documentation/user/getting-started/_index.md index 1703ec75..47bd3d54 100644 --- a/documentation/user/getting-started/_index.md +++ b/documentation/user/getting-started/_index.md @@ -84,7 +84,7 @@ durable: activity_contracts: cache: cache.app contracts: - - App\Workflow\Activity\OrderActivities # list your activity interfaces here + - App\Workflow\Activity\GreetingActivities # list your activity interfaces here ``` Switch to Temporal at runtime by setting `DURABLE_DSN` in your environment: @@ -110,7 +110,7 @@ framework: routing: Gplanchat\Durable\Transport\ResumeWorkflowMessage: durable_workflows Gplanchat\Durable\Transport\ActivityMessage: durable_activities - Gplanchat\Durable\Transport\FireWorkflowTimersMessage: sync + Gplanchat\Durable\Transport\FireWorkflowTimersMessage: durable_workflows Gplanchat\Durable\Transport\DeliverWorkflowSignalMessage: sync Gplanchat\Durable\Transport\DeliverWorkflowUpdateMessage: sync ``` @@ -158,12 +158,18 @@ Any class annotated with `#[AsWorkflow]` in your workflow namespace is auto-regi # config/services.yaml App\Workflow\: resource: '../src/Workflow/' + exclude: '../src/Workflow/Activity/' tags: [durable.workflow] ``` +The `exclude` matters. The tag is not a filter: every service it matches is handed to the workflow +registry, which requires exactly one `#[AsWorkflowMethod]` and throws otherwise. Tag a folder that +also holds your activity handlers and the container stops building, with an error naming a class you +never meant to register. Keep the tagged folder to workflows, or exclude what is not one. + ### Register activity implementations -Activity implementation classes are registered as normal Symfony services (autowiring applies). If you use `#[AsActivityHandler]` on the class, the bundle picks them up automatically when the service is tagged. +Nothing to write. A class carrying `#[AsActivityHandler]` is picked up by the bundle's autoconfiguration as soon as it is a service — which, with the default `autoconfigure: true` of a Symfony application, it already is. This is where workflows above differ: those still need the tag. --- @@ -178,8 +184,11 @@ declare(strict_types=1); namespace App\Workflow\Activity; +use Gplanchat\Durable\Attribute\AsActivity; use Gplanchat\Durable\Attribute\AsActivityMethod; +// Optional: prefixes the names of the activities declared below. +#[AsActivity(name: 'greeting-activities')] interface GreetingActivities { #[AsActivityMethod(name: 'greet')] @@ -196,9 +205,10 @@ declare(strict_types=1); namespace App\Workflow\Activity; -use Gplanchat\Durable\Attribute\AsActivity; +use Gplanchat\Durable\Attribute\AsActivityHandler; -#[AsActivity(name: 'greeting-activities')] +// This attribute is what registers the class; the bundle autoconfigures it. +#[AsActivityHandler(contract: GreetingActivities::class)] final class GreetingActivitiesHandler implements GreetingActivities { public function greet(string $name): string @@ -260,11 +270,78 @@ final class GreetController $executionId = 'greet-'.uniqid(); $this->dispatcher->dispatchNewWorkflowRun($executionId, 'greet', ['name' => $name]); - return new JsonResponse(['executionId' => $executionId]); + // 202: the run is queued, not done. Answering 200 here is the first thing that makes a + // caller poll for a result that no consumer has produced yet. + return new JsonResponse(['executionId' => $executionId], JsonResponse::HTTP_ACCEPTED); } } ``` +### 5 — Run a consumer, or nothing happens + +`dispatchNewWorkflowRun()` returns `void`, and does exactly what its name says: it *dispatches*. The +workflow runs when something consumes the transports you configured above. Until then the execution +sits in a queue — a dashboard will call it `RUNNING`, which is true and unhelpful: it means *not +finished*, not *someone is working on it*. + +```bash +php bin/console messenger:consume durable_workflows durable_activities +``` + +Those two names are the transports **you** declared in `messenger.yaml`. No document can hand you +this command without you having written that file first, which is why it is easy to look for the +missing piece everywhere except in your own configuration. + +To see what the engine holds for one run: + +```bash +php bin/console durable:execution:diagnose greet-abc123 +``` + +#### Which profile are you in? + +Two configurations work. Mixing them is the usual first stumble, and it fails silently. + +**One process — tests.** `in-memory://` transports with the in-memory stores. Dispatch, resume and +activity all happen inside a single PHP process, so a test can dispatch and drain in one go. An +in-memory transport **does not outlive its process**: dispatching from a web request and consuming +in a separate worker cannot work here, and neither can replay — the journal the worker would need +lives in the web process's memory. + +**Several processes — local dev and production.** Real transports **and** a durable store. Both, or +the worker picks up a queue entry naming a workflow whose journal it cannot see. + +This profile needs two packages the quick start above does not install — the DBAL journal, and +DoctrineBundle for the `doctrine.dbal.default_connection` service it names: + +```bash +composer require gplanchat/durable-bridge-dbal doctrine/doctrine-bundle +``` + + +```yaml +durable: + event_store: + type: dbal + workflow_metadata: + type: dbal + child_workflow: + parent_link_store: + type: dbal + dbal: + connection: doctrine.dbal.default_connection +``` + +```dotenv +MESSENGER_DURABLE_WORKFLOW_DSN=doctrine://default +MESSENGER_DURABLE_ACTIVITY_DSN=doctrine://default +``` + +The rule behind both profiles: **an execution survives exactly what its journal and its queue +survive.** Route `ResumeWorkflowMessage` or `ActivityMessage` to a transport a separate worker +cannot read, and the workflow replays inside the web request that started it and dies with the +process — the very failure durable execution exists to remove. + --- ## Start Temporal workers (production / dev mode) diff --git a/documentation/user/packages/_index.fr.md b/documentation/user/packages/_index.fr.md index 919e2bdf..b1047e80 100644 --- a/documentation/user/packages/_index.fr.md +++ b/documentation/user/packages/_index.fr.md @@ -72,8 +72,11 @@ composer require gplanchat/durable-bundle Ce qu'il fait, et que vous écririez autrement à la main : -- **L'autoconfiguration.** Les classes portant `#[AsWorkflow]` et `#[AsActivity]` s'enregistrent seules ; - vous ne les listez pas dans un fichier de conteneur. +- **L'autoconfiguration.** Les classes portant `#[AsActivityHandler]` s'enregistrent seules sur + l'exécuteur d'activités ; vous ne les listez pas dans un fichier de conteneur. `#[AsActivity]` est + un attribut de **nommage** posé sur le contrat, pas d'enregistrement, et `#[AsWorkflow]` n'est + **pas** autoconfiguré à ce jour — une classe de workflow a encore besoin de la balise + `durable.workflow`, que le guide de démarrage montre. - **Le câblage Messenger.** Reprises de workflow et envois d'activité sont routés vers les transports que vous nommez dans `durable.yaml`, si bien qu'un workflow qui se suspend reprend par vos files existantes. diff --git a/documentation/user/packages/_index.md b/documentation/user/packages/_index.md index b6917bfd..c7024db3 100644 --- a/documentation/user/packages/_index.md +++ b/documentation/user/packages/_index.md @@ -68,8 +68,10 @@ composer require gplanchat/durable-bundle What it does that you would otherwise write by hand: -- **Autoconfiguration.** Classes carrying `#[AsWorkflow]` and `#[AsActivity]` are registered on their - own; you do not list them in a container file. +- **Autoconfiguration.** Classes carrying `#[AsActivityHandler]` register themselves on the activity + executor; you do not list them in a container file. `#[AsActivity]` is a naming attribute on the + contract, not a registration one, and `#[AsWorkflow]` is **not** autoconfigured today — a workflow + class still needs the `durable.workflow` tag, which the getting-started guide shows. - **Messenger wiring.** Workflow resumes and activity dispatches are routed to the transports you name in `durable.yaml`, so a workflow that suspends resumes through your existing queues. - **One console command.** `durable:execution:diagnose ` prints what the engine holds