Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions documentation/user/activities/_index.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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(
Expand Down
8 changes: 6 additions & 2 deletions documentation/user/activities/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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(
Expand Down
89 changes: 83 additions & 6 deletions documentation/user/getting-started/_index.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 :
Expand All @@ -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
```
Expand Down Expand Up @@ -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.

---

Expand All @@ -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')]
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
89 changes: 83 additions & 6 deletions documentation/user/getting-started/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
```
Expand Down Expand Up @@ -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.

---

Expand All @@ -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')]
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 5 additions & 2 deletions documentation/user/packages/_index.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions documentation/user/packages/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <executionId>` prints what the engine holds
Expand Down
Loading