Skip to content
Merged
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
62 changes: 62 additions & 0 deletions docs/features/versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,68 @@ Restore code that accepts the original recorded input, then use the [supported s

Safe replacement routing also needs the [entry-validation worker capability](../polyglot/worker-compatibility-routing.md#definition-fingerprint-evolution). A changed fingerprint is neither proof of replay compatibility nor a reason to rewrite a run's original fingerprint. Version durable-command changes inside a compatible entry method and test cold replay before promotion.

## Evolving Activity Arguments and Results {#evolving-activity-arguments-and-results}

An activity has two stored contracts: the arguments recorded when it was scheduled and the result recorded when it completed. A deployment does not migrate either. This applies to embedded PHP and service workers using the PHP, Python and Rust SDKs.

A pending attempt or retry receives the original scheduled arguments. It runs the activity implementation selected by the worker's registration and build-routing policy, which may still be an older build. Deploying a new activity implementation does not guarantee every attempt uses it, and does not update queued arguments.

When a completed activity is replayed, the current workflow code receives its recorded result without running the activity again. Matching an activity step and type is not a schema check of its arguments or result. A changed payload shape can therefore fail application code without a history mismatch.

### Add Optional Input Without Reinterpreting Old Values {#additive-activity-input}

Keep existing positional arguments and their meaning. In embedded PHP, an optional trailing parameter lets an old scheduled input such as `[7]` continue to bind:

```php
use Workflow\V2\Activity;

final class PriceActivity extends Activity
{
public function handle(int $amount, string $currency = 'USD'): array
{
return ['amount' => $amount, 'currency' => $currency];
}
}
```

The default fills a missing argument. It does not convert an incompatible recorded value or repair a reordered argument. Keep the activity's external effects compatible too: a retry can happen after the deployment and must remain idempotent.

PHP service handlers receive their activity context followed by recorded positional arguments. Python handlers receive the recorded positional arguments directly. Both need defaults for omitted arguments. Python annotations alone do not validate or convert values. Rust typed activities deserialize one request value; add an optional field or a field with an explicit Serde default while preserving the existing representation. A newly required field can fail decoding before the Rust handler runs. See the [PHP](https://php.durable-workflow.com/), [Python](https://python.durable-workflow.com/) and [Rust](https://rust.durable-workflow.com/) SDK guides for their activity APIs.

### Read Older Results With a Fallback {#additive-activity-result}

When a new activity result adds `currency`, workflows must still accept older completed results that contain only `amount`:

```php
$result = activity(PriceActivity::class, 7);
$currency = $result['currency'] ?? 'USD';
```

Choose a fallback that preserves the old business meaning. Retain it for every history that may be replayed, including queries and recovery. Deploy compatible readers before writers that emit a changed representation, and verify rollback readers against newly recorded results too. A default on the activity's input does not add a field to an already recorded result.

### Use a New Type for a Different Contract {#incompatible-activity-contract}

For a renamed field, a different representation or a changed business meaning, keep the old activity type available and introduce a new one. Use a workflow `getVersion()` branch at the scheduling call when old histories must keep their old activity step and new runs should schedule the new type. A branch inside the activity cannot fix arguments that fail before its body runs, and a version marker does not rewrite results already in history.

Test two cold deployment boundaries against the actual installed packages: schedule with old code and execute with candidate activity code; then complete with old activity code and replay with candidate workflow code. Include incompatible inputs and results as failure controls. Do not assume the workflow-entry safeguards above also protect activity signatures or application result access. The confirmed embedded activity DTO defect is tracked in [Workflow #910](https://github.com/durable-workflow/workflow/issues/910).

### Verified Failure Boundaries {#activity-compatibility-failures}

The [published-package checks in #363](https://github.com/durable-workflow/durable-workflow.github.io/issues/363) used fresh processes across each deployment boundary. These are observed baselines, not minimum supported versions:

| Published package | Incompatible activity input | Incompatible result reader |
| --- | --- | --- |
| Embedded Workflow 2.11.2 | A missing required argument produces `ArgumentCountError` and an activity failure. | Accessing a missing array field under Laravel error handling produces `ErrorException` and workflow failure. |
| PHP SDK 3.2.0 with Server 2.11.0 | A missing required positional argument is reported as `ArgumentCountError` in the activity failure. | Passing a missing field to a required string reader produces `TypeError` and workflow failure. |
| Python SDK 2.9.0 with Server 2.11.0 | A missing required positional argument produces `TypeError` and an activity failure. | Indexing a missing result key produces `KeyError` and workflow failure. |
| Rust SDK 4.2.2 with Server 2.11.0 | A newly required request field fails typed input decoding before the activity handler runs. | A newly required result field fails typed result decoding in the workflow. |

The service checks recorded `max_attempts = 1`; the unhandled activity failure then failed the workflow. In general, activity failures follow their recorded retry and timeout policy. A workflow can handle an exhausted activity failure. A result-reader error is an application workflow failure, not a request to rerun the completed activity. All additive controls completed with the original amount `7` and default currency `USD`, with one activity attempt and no repairs. In result controls, the completed activity stayed completed.

Embedded Workflow 2.11.2 also accepts a changed activity signature that replaces recorded scalar `7` with an injected DTO whose default amount is `0`, and can incorrectly complete with `0`. [Workflow #910](https://github.com/durable-workflow/workflow/issues/910) tracks that product defect. Do not use dependency injection as an activity-input migration or assume the workflow-entry safeguard covers it. Restoring compatible code does not reopen an already terminal run; inspect the actual run and follow the [recovery guide](../failures-and-recovery.md) without changing recorded input, history or deadlines.

Review [recorded workflow start arguments](#evolving-recorded-entry-arguments) and the [signal payload evolution recipe](signals.md#evolve-a-signal-payload) alongside these activity contracts.

## Using `getVersion`

`getVersion()` is a replay-safe straight-line helper. Each change point records a durable version marker for the run on first execution, and every later replay reuses that committed value instead of recalculating the branch from today's code. Old runs that predate a newly introduced branch point conservatively receive `DEFAULT_VERSION`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,68 @@ Stellen Sie Code wieder her, der die ursprüngliche aufgezeichnete Eingabe akzep

Sicheres Routing zu einem Ersatz benötigt außerdem die [Worker-Fähigkeit zur Einstiegsprüfung](../polyglot/worker-compatibility-routing.md#definition-fingerprint-evolution). Ein geänderter Fingerabdruck beweist weder Replay-Kompatibilität noch rechtfertigt er, den ursprünglichen Fingerabdruck eines Laufs umzuschreiben. Versionieren Sie Änderungen an dauerhaften Befehlen innerhalb einer kompatiblen Einstiegsmethode und testen Sie kaltes Replay vor der Promotion.

## Argumente und Ergebnisse von Aktivitäten weiterentwickeln {#evolving-activity-arguments-and-results}

Eine Aktivität hat zwei gespeicherte Verträge: die beim Einplanen aufgezeichneten Argumente und das beim Abschluss aufgezeichnete Ergebnis. Ein Deployment migriert keines von beiden. Das gilt für eingebettetes PHP und für Service-Worker mit den PHP-, Python- und Rust-SDKs.

Ein ausstehender Versuch oder eine Wiederholung erhält die ursprünglich eingeplanten Argumente. Welche Implementierung ausgeführt wird, bestimmen die Worker-Registrierung und die Routing-Richtlinie für Builds. Dabei kann weiterhin ein älterer Build zum Einsatz kommen. Ein neuer Build garantiert weder, dass jeder Versuch ihn verwendet, noch aktualisiert er Argumente in der Warteschlange.

Beim Replay einer abgeschlossenen Aktivität erhält der aktuelle Workflow-Code das aufgezeichnete Ergebnis, ohne die Aktivität erneut auszuführen. Ein passender Schritt und Aktivitätstyp sind keine Schemaprüfung der Argumente oder des Ergebnisses. Eine geänderte Datenstruktur kann deshalb im Anwendungscode fehlschlagen, ohne einen History-Konflikt auszulösen.

### Optionale Eingaben ergänzen und die Bedeutung alter Werte erhalten {#additive-activity-input}

Behalten Sie vorhandene Positionsargumente und ihre Bedeutung bei. In eingebettetem PHP ermöglicht ein optionaler letzter Parameter, bereits eingeplante Argumente wie `[7]` weiterhin anzunehmen:

```php
use Workflow\V2\Activity;

final class PriceActivity extends Activity
{
public function handle(int $amount, string $currency = 'USD'): array
{
return ['amount' => $amount, 'currency' => $currency];
}
}
```

Der Standardwert füllt ein fehlendes Argument. Er konvertiert weder einen inkompatiblen aufgezeichneten Wert noch korrigiert er eine andere Reihenfolge. Auch externe Auswirkungen müssen kompatibel bleiben: Eine Wiederholung kann nach dem Deployment erfolgen und muss weiterhin idempotent sein.

PHP-Handler im Service-Modus erhalten zuerst den Aktivitätskontext und danach die aufgezeichneten Positionsargumente. Python-Handler erhalten die Positionsargumente direkt. Beide benötigen Standardwerte für ausgelassene Argumente. Python-Typannotationen allein prüfen oder konvertieren keine Werte. Typisierte Rust-Aktivitäten deserialisieren einen einzelnen Anfragewert. Ergänzen Sie ein optionales Feld oder ein Feld mit einem expliziten Serde-Standardwert und behalten Sie die vorhandene Darstellung bei. Ein neues Pflichtfeld kann die Dekodierung bereits vor dem Rust-Handler scheitern lassen. Die Aktivitäts-APIs stehen in den SDK-Anleitungen für [PHP](https://php.durable-workflow.com/), [Python](https://python.durable-workflow.com/) und [Rust](https://rust.durable-workflow.com/).

### Alte Ergebnisse mit einem Ersatzwert lesen {#additive-activity-result}

Wenn ein neues Aktivitätsergebnis `currency` ergänzt, müssen Workflows weiterhin ältere abgeschlossene Ergebnisse annehmen, die nur `amount` enthalten:

```php
$result = activity(PriceActivity::class, 7);
$currency = $result['currency'] ?? 'USD';
```

Wählen Sie einen Ersatzwert, der die bisherige fachliche Bedeutung erhält. Behalten Sie ihn für jede History bei, die erneut abgespielt werden kann, einschließlich Abfragen und Wiederherstellung. Stellen Sie kompatible Leser vor dem Code bereit, der die neue Darstellung schreibt. Prüfen Sie auch die Leser eines Rollbacks mit neu aufgezeichneten Ergebnissen. Ein Standardwert für ein Aktivitätsargument ergänzt kein Feld in einem bereits aufgezeichneten Ergebnis.

### Für einen anderen Vertrag einen neuen Typ verwenden {#incompatible-activity-contract}

Wenn Sie ein Feld umbenennen, seine Darstellung oder seine fachliche Bedeutung ändern, behalten Sie den alten Aktivitätstyp bei und führen Sie einen neuen ein. Verwenden Sie einen `getVersion()`-Zweig im Workflow am Aufruf zur Einplanung, wenn alte Histories ihren bisherigen Aktivitätsschritt behalten und neue Runs den neuen Typ einplanen sollen. Ein Zweig innerhalb der Aktivität kann Argumente nicht reparieren, die schon vor Eintritt in den Funktionsrumpf fehlschlagen. Ein Versionsmarker schreibt auch keine Ergebnisse in der History um.

Prüfen Sie mit den tatsächlich installierten Paketen zwei Deployment-Grenzen in neuen Prozessen: mit altem Code einplanen und mit dem neuen Aktivitätscode ausführen; danach mit alter Aktivität abschließen und mit dem neuen Workflow-Code wiedergeben. Nehmen Sie inkompatible Eingaben und Ergebnisse als Fehlerkontrollen auf. Nehmen Sie nicht an, dass die oben beschriebenen Prüfungen am Workflow-Einstieg auch Aktivitätssignaturen oder den Ergebniszugriff der Anwendung schützen. Der bestätigte DTO-Fehler in eingebetteten Aktivitäten wird in [Workflow #910](https://github.com/durable-workflow/workflow/issues/910) verfolgt.

Prüfen Sie diese Aktivitätsverträge zusammen mit den Regeln für [aufgezeichnete Workflow-Startargumente](#evolving-recorded-entry-arguments) und der [Anleitung zum Weiterentwickeln von Signal-Nutzdaten](signals.md#evolve-a-signal-payload).

### Verifizierte Fehlergrenzen {#activity-compatibility-failures}

Die [Prüfungen veröffentlichter Pakete in #363](https://github.com/durable-workflow/durable-workflow.github.io/issues/363) nutzten neue Prozesse an jeder Deployment-Grenze. Dies sind geprüfte Ausgangsversionen, keine unterstützten Mindestversionen:

| Veröffentlichtes Paket | Inkompatible Aktivitätseingabe | Inkompatibler Ergebnisleser |
| --- | --- | --- |
| Eingebetteter Workflow 2.11.2 | Ein fehlendes Pflichtargument erzeugt `ArgumentCountError` und einen Aktivitätsfehler. | Der Zugriff auf ein fehlendes Array-Feld unter Laravels Fehlerbehandlung erzeugt `ErrorException` und einen Workflow-Fehler. |
| PHP SDK 3.2.0 mit Server 2.11.0 | Ein fehlendes Positionspflichtargument wird im Aktivitätsfehler als `ArgumentCountError` gemeldet. | Ein fehlendes Feld an einen Leser mit erforderlichem String-Argument zu übergeben erzeugt `TypeError` und einen Workflow-Fehler. |
| Python SDK 2.9.0 mit Server 2.11.0 | Ein fehlendes Positionspflichtargument erzeugt `TypeError` und einen Aktivitätsfehler. | Der Zugriff auf einen fehlenden Ergebnisschlüssel erzeugt `KeyError` und einen Workflow-Fehler. |
| Rust SDK 4.2.2 mit Server 2.11.0 | Ein neues Pflichtfeld der Anfrage lässt die typisierte Eingabedekodierung vor dem Aktivitätshandler scheitern. | Ein neues Pflichtfeld des Ergebnisses lässt die typisierte Ergebnisdekodierung im Workflow scheitern. |

Die Service-Prüfungen zeichneten `max_attempts = 1` auf. Der unbehandelte Aktivitätsfehler ließ anschließend den Workflow scheitern. Allgemein folgen Aktivitätsfehler ihrer aufgezeichneten Wiederholungs- und Timeout-Richtlinie. Ein Workflow kann einen Aktivitätsfehler nach ausgeschöpften Versuchen behandeln. Ein Fehler beim Ergebnislesen ist ein Anwendungsfehler im Workflow und keine Aufforderung, die abgeschlossene Aktivität zu wiederholen. Alle kompatiblen Erweiterungskontrollen schlossen mit dem ursprünglichen Betrag `7` und der Standardwährung `USD` ab, mit einem Aktivitätsversuch und ohne Reparaturen. Bei den Ergebniskontrollen blieb die abgeschlossene Aktivität abgeschlossen.

Eingebetteter Workflow 2.11.2 akzeptiert außerdem eine Signaturänderung, die den aufgezeichneten Skalar `7` durch ein injiziertes DTO mit Standardbetrag `0` ersetzt, und kann fälschlich mit `0` abschließen. [Workflow #910](https://github.com/durable-workflow/workflow/issues/910) verfolgt diesen Produktfehler. Verwenden Sie Dependency Injection nicht zur Migration von Aktivitätseingaben und nehmen Sie nicht an, dass die Workflow-Einstiegsprüfung diesen Fall abdeckt. Kompatiblen Code wiederherzustellen öffnet keinen bereits terminalen Run. Prüfen Sie den tatsächlichen Zustand und folgen Sie der [Wiederherstellungsanleitung](../failures-and-recovery.md), ohne aufgezeichnete Eingaben, History oder Fristen zu ändern.

## `getVersion` verwenden {#using-getversion}

`getVersion()` ist ein Replay-sicherer geradliniger Helfer. Jeder Änderungspunkt
Expand Down
2 changes: 1 addition & 1 deletion i18n/de/source-hashes.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"features/timeouts.md": "785dea4e9b8f8fa9c0c9e880b8c20f1847745c244391800e289fa9ccd9438699",
"features/timers.md": "94a017d85c9dc7e8e598ce895bb722523422e3729b1935ede777ed7dd2734db7",
"features/updates.md": "ecc24a4ae764edf0281430583216d47d4b75fd9f58d183f7dd5bc9794cbd1c06",
"features/versioning.md": "b9d9f70ecd6e194d1cb0134d612a9b0ef5528970762a9cf7f948dfa5cd31e866",
"features/versioning.md": "3686d25c27aa8a4f576b1fb01c554c6d58f8fa91363d64314830a325872a1ed3",
"features/webhooks.md": "edcdfc05ccb0d34a77c51594f85bc3cab343e787f8e0e08d2cd4ddebd048e8c8",
"features/worker-sessions.md": "2b08a0cdaee486970e099ea8bcb91d7f6daf135b0bc98629ed47f3885e64c77d",
"how-it-works.md": "90454c37f4244297d5c11bcc59549859acf59f88d15ecd26f0a6951c115f86eb",
Expand Down
Loading
Loading