From 5ac1cb85f348bc895e7ce7e4d955dca83402dd97 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Tue, 20 Oct 2026 08:04:12 +0000 Subject: [PATCH 1/2] docs: explain fixed Python workflow time and durable waits --- docs/polyglot/python.md | 84 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/docs/polyglot/python.md b/docs/polyglot/python.md index 9c6deaf1fe1..cc946349a3b 100644 --- a/docs/polyglot/python.md +++ b/docs/polyglot/python.md @@ -674,11 +674,91 @@ The `WorkflowContext` passed to `run` provides deterministic operations: | `ctx.get_version(change_id, min, max)` | Safe workflow code versioning | | `ctx.upsert_search_attributes(attrs)` | Update search attributes | | `ctx.continue_as_new(*args)` | Restart the workflow with new input | -| `ctx.now()` | Deterministic clock (from history) | +| `ctx.now()` | Persisted workflow-start time, fixed for this run | | `ctx.random()` | Seeded random generator | | `ctx.uuid4()` | Deterministic UUID | | `ctx.logger` | Logger that is silent during replay | +### Workflow time and durable waits + +During worker replay, `ctx.now()` returns the run's **persisted start time**. +It does not advance after an activity, timer, signal, or other durable step. +Repeated cold replays of the same history return that same start value. A +continue-as-new successor has its own start time; it does not keep the +predecessor's clock anchor. + +The [published Python implementation](https://github.com/durable-workflow/sdk-python/blob/150c6f6bc14e3516f75cc615b2f75f093ed84ea1/src/durable_workflow/workflow.py#L2594-L2601) +uses `WorkflowStarted.payload.timestamp` when present, otherwise the canonical +`WorkflowStarted.recorded_at`. Missing or invalid persisted replay time is an +explicit clock error, not permission to read the worker's wall clock. This is +the fixed-start contract settled in [SDK #146](https://github.com/durable-workflow/sdk-python/issues/146). + +Wait for a durable operation instead of looping until `ctx.now()` changes: + +```python +from durable_workflow import workflow + + +@workflow.defn(name="wait_without_clock_loop") +class WaitWithoutClockLoop: + def run(self, ctx): + started_at = ctx.now() + yield ctx.start_timer(30) + return { + "run_started_at": started_at.isoformat(), + "after_wait": ctx.now().isoformat(), + } +``` + +The timer's durable completion resumes the workflow. Both returned timestamps +are the same start time. A loop such as `while ctx.now() < deadline` will not +become false merely because timers fire. + +For a relative delay, yield `ctx.start_timer(seconds)` or `ctx.sleep(seconds)`. +Use [first-completion selection](#first-completion-selection) when work should +race a durable timer. Set `execution_timeout_seconds` and `run_timeout_seconds` +on `client.start_workflow(...)` when Server must enforce a workflow deadline, +including while workers are stopped. An HTTP or `get_result()` waiting timeout +only limits that local request; it does not replace the workflow's persisted +deadline. Keep an application deadline across continue-as-new by explicitly +passing it as input, without replacing the Server's original execution deadline. + +For measured elapsed time within one activity invocation, read a monotonic +clock there and return the duration as the activity result. For an external +UTC deadline, let an activity check the authoritative external clock and +return its decision; workflow code replays that recorded result. Never compare +UTC timestamps with monotonic values, or subtract monotonic readings from +different processes or hosts. `ctx.now()` subtraction measures neither active +processing time nor elapsed wall time in Python. + +#### Clock differences across SDKs + +These APIs are deterministic where provided, but they do not share one clock: + +| Authoring surface | Repeated calls and durable steps | Continue-as-new successor | +| --- | --- | --- | +| Python `ctx.now()` | Fixed to this run's persisted start time, including after a timer or activity. | Uses the successor run's start anchor. | +| Embedded PHP `Workflow::now()` | Starts at the run's start time and advances to the recorded event time consumed by replay. See [workflow time](../features/timers.md#reading-deterministic-time). | Starts with the new run, then advances through that run's consumed events. | +| PHP service-mode SDK | No general public workflow `now()` helper. Use durable operations or recorded activity/side-effect values. | Pass application timing state explicitly when it must survive the handoff. | +| Rust service-mode SDK | No general public `WorkflowContext::now()` helper. A `side_effect` can record a small wall-clock snapshot; it is not an advancing clock. | A side effect in the new run is a new operation; pass earlier timing state explicitly if needed. | + +The embedded PHP behavior is present in +[published Core](https://github.com/durable-workflow/workflow/blob/64efa176e4c37e5d6ae80e767ba3b41464024e53/src/V2/Support/WorkflowFiberContext.php); +the separate [PHP SDK context](https://github.com/durable-workflow/sdk-php/blob/2.5.2/src/Worker/WorkflowContext.php) +and [Rust SDK context](https://github.com/durable-workflow/sdk-rust/blob/3.5.5/src/lib.rs) +have their own APIs. Port the durable operation and deadline policy rather than +assuming that a clock helper has identical semantics in every SDK. + +#### Order emitted events with sequence values + +A workflow timestamp is not an event ordering key. Python events stamped with +`ctx.now()` can all have the same timestamp. Use +[Workflow Stream offsets](./workflow-streams.md), the applicable +[embedded outbox sequence](../features/message-streams.md), or a deterministic +workflow-local counter. Pair a run-local counter with `run_id`; carry it through +continue-as-new explicitly if the application needs one logical sequence. +Independent streams or runs do not gain a global order from their timestamps. + ### Activity Retries and Timeouts Use `ActivityRetryPolicy` and timeout keyword arguments on `ctx.schedule_activity(...)` @@ -716,7 +796,7 @@ later deploys do not change already-running attempts. Workflow code is replayed from history. It must not perform any non-deterministic operations directly: - No I/O (HTTP calls, file reads, database queries) -- No `datetime.now()` or `time.time()` — use `ctx.now()` +- No `datetime.now()` or `time.time()` — use `ctx.now()` only for the fixed run-start anchor; use durable timers for waits - No `random.random()` — use `ctx.random()` - No `uuid.uuid4()` — use `ctx.uuid4()` From 3ff0610722549d2559e791d8481f24759e59bbe8 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Tue, 20 Oct 2026 08:08:50 +0000 Subject: [PATCH 2/2] docs: translate workflow clock contract into all offered languages --- .../current/polyglot/python.md | 91 ++++++++++++++++++- i18n/de/source-hashes.json | 2 +- .../current/polyglot/python.md | 88 +++++++++++++++++- i18n/es/source-hashes.json | 2 +- .../current/polyglot/python.md | 91 ++++++++++++++++++- i18n/fr/source-hashes.json | 2 +- .../current/polyglot/python.md | 86 +++++++++++++++++- i18n/ja/source-hashes.json | 2 +- .../current/polyglot/python.md | 89 +++++++++++++++++- i18n/pt-BR/source-hashes.json | 2 +- .../current/polyglot/python.md | 90 +++++++++++++++++- i18n/uk/source-hashes.json | 2 +- .../current/polyglot/python.md | 75 ++++++++++++++- i18n/zh-Hans/source-hashes.json | 2 +- 14 files changed, 603 insertions(+), 21 deletions(-) diff --git a/i18n/de/docusaurus-plugin-content-docs/current/polyglot/python.md b/i18n/de/docusaurus-plugin-content-docs/current/polyglot/python.md index f3116d54bc3..e99333d93da 100644 --- a/i18n/de/docusaurus-plugin-content-docs/current/polyglot/python.md +++ b/i18n/de/docusaurus-plugin-content-docs/current/polyglot/python.md @@ -677,11 +677,98 @@ Der an `run` übergebene `WorkflowContext` bietet deterministische Operationen: | `ctx.get_version(change_id, min, max)` | Workflow-Code sicher versionieren | | `ctx.upsert_search_attributes(attrs)` | Suchattribute aktualisieren | | `ctx.continue_as_new(*args)` | Workflow mit neuer Eingabe neu starten | -| `ctx.now()` | Deterministische Uhr aus der Historie | +| `ctx.now()` | Gespeicherte Workflow-Startzeit, während dieses Runs fest | | `ctx.random()` | Zufallsgenerator mit festgelegtem Startwert | | `ctx.uuid4()` | Deterministische UUID | | `ctx.logger` | Beim Replay stummer Logger | +### Workflow-Zeit und dauerhafte Warteoperationen {#workflow-time-and-durable-waits} + +Beim Replay im Worker gibt `ctx.now()` die **gespeicherte Startzeit des Runs** +zurück. Sie schreitet nach einer Aktivität, einem Timer, Signal oder anderen +dauerhaften Schritt nicht fort. Wiederholte Cold-Replays derselben Historie +liefern denselben Startwert. Ein Continue-as-new-Nachfolger hat seine eigene +Startzeit und übernimmt nicht den Uhranker des Vorgängers. + +Die [veröffentlichte Python-Implementierung](https://github.com/durable-workflow/sdk-python/blob/150c6f6bc14e3516f75cc615b2f75f093ed84ea1/src/durable_workflow/workflow.py#L2594-L2601) +verwendet `WorkflowStarted.payload.timestamp`, falls vorhanden, andernfalls das +kanonische `WorkflowStarted.recorded_at`. Fehlende oder ungültige gespeicherte +Replay-Zeit führt zu einem ausdrücklichen Uhrfehler, nicht zum Lesen der +Wanduhr des Workers. Das ist der in [SDK #146](https://github.com/durable-workflow/sdk-python/issues/146) +vereinbarte Vertrag mit fester Startzeit. + +Warten Sie auf eine dauerhafte Operation, statt auf eine Änderung von +`ctx.now()` zu warten: + +```python +from durable_workflow import workflow + + +@workflow.defn(name="wait_without_clock_loop") +class WaitWithoutClockLoop: + def run(self, ctx): + started_at = ctx.now() + yield ctx.start_timer(30) + return { + "run_started_at": started_at.isoformat(), + "after_wait": ctx.now().isoformat(), + } +``` + +Der dauerhafte Abschluss des Timers setzt den Workflow fort. Beide +zurückgegebenen Zeitstempel enthalten dieselbe Startzeit. Eine Schleife wie +`while ctx.now() < deadline` wird nicht allein durch ausgelöste Timer falsch. + +Für eine relative Verzögerung verwenden Sie `yield ctx.start_timer(seconds)` +oder `yield ctx.sleep(seconds)`. Verwenden Sie die +[Auswahl des ersten Abschlusses](#first-completion-selection), wenn Arbeit mit +einem dauerhaften Timer konkurrieren soll. Setzen Sie `execution_timeout_seconds` +und `run_timeout_seconds` bei `client.start_workflow(...)`, wenn der Server eine +Workflow-Frist auch bei gestoppten Workern durchsetzen muss. Ein HTTP- oder +`get_result()`-Wartezeitlimit begrenzt nur die lokale Anfrage und ersetzt keine +gespeicherte Workflow-Frist. Geben Sie eine Anwendungsfrist ausdrücklich als +Eingabe an Continue-as-new weiter, ohne die ursprüngliche Ausführungsfrist des +Servers zu ersetzen. + +Um die tatsächliche Dauer innerhalb eines Aktivitätsaufrufs zu messen, lesen +Sie dort eine monotone Uhr und geben die Dauer als Aktivitätsergebnis zurück. +Für eine externe UTC-Frist lassen Sie eine Aktivität die maßgebliche externe +Uhr prüfen und ihre Entscheidung zurückgeben; der Workflow spielt dieses +gespeicherte Ergebnis ab. Vergleichen Sie UTC-Zeitstempel nie mit monotonen +Werten und subtrahieren Sie keine monotonen Messwerte verschiedener Prozesse +oder Hosts. Die Differenz von `ctx.now()`-Werten misst in Python weder aktive +Verarbeitungszeit noch verstrichene Wanduhrzeit. + +#### Unterschiede zwischen den SDK-Uhren {#clock-differences-across-sdks} + +Diese APIs sind, soweit vorhanden, deterministisch, teilen aber keine gemeinsame Uhr: + +| Autorenschnittstelle | Wiederholte Aufrufe und dauerhafte Schritte | Continue-as-new-Nachfolger | +| --- | --- | --- | +| Python `ctx.now()` | Bleibt auf der gespeicherten Startzeit dieses Runs, auch nach einem Timer oder einer Aktivität. | Verwendet den Startanker des Nachfolger-Runs. | +| Embedded PHP `Workflow::now()` | Beginnt mit der Run-Startzeit und schreitet zur aufgezeichneten Ereigniszeit fort, die das Replay verarbeitet hat. Siehe [Workflow-Zeit](../features/timers.md#reading-deterministic-time). | Beginnt mit dem neuen Run und schreitet durch dessen verarbeitete Ereignisse fort. | +| PHP-SDK im Service-Modus | Kein allgemeiner öffentlicher Workflow-Helfer `now()`. Verwenden Sie dauerhafte Operationen oder aufgezeichnete Aktivitäts-/Seiteneffektwerte. | Übergeben Sie Anwendungszeitdaten ausdrücklich, wenn sie die Übergabe überleben müssen. | +| Rust-SDK im Service-Modus | Kein allgemeiner öffentlicher Helfer `WorkflowContext::now()`. Ein `side_effect` kann eine kleine Wanduhrmomentaufnahme speichern; sie ist keine fortschreitende Uhr. | Ein Seiteneffekt im neuen Run ist eine neue Operation; übergeben Sie bei Bedarf frühere Zeitdaten ausdrücklich. | + +Das Embedded-PHP-Verhalten ist im +[veröffentlichten Core](https://github.com/durable-workflow/workflow/blob/64efa176e4c37e5d6ae80e767ba3b41464024e53/src/V2/Support/WorkflowFiberContext.php) +vorhanden. Der separate [PHP-SDK-Kontext](https://github.com/durable-workflow/sdk-php/blob/2.5.2/src/Worker/WorkflowContext.php) +und [Rust-SDK-Kontext](https://github.com/durable-workflow/sdk-rust/blob/3.5.5/src/lib.rs) +haben eigene APIs. Übertragen Sie die dauerhafte Operation und Fristenregel, +statt für jeden SDK-Uhrhelfer dieselbe Semantik anzunehmen. + +#### Ausgegebene Ereignisse mit Sequenzwerten ordnen {#order-emitted-events-with-sequence-values} + +Ein Workflow-Zeitstempel ist kein Sortierschlüssel für Ereignisse. Python-Ereignisse +mit `ctx.now()` können alle denselben Zeitstempel haben. Verwenden Sie +[Workflow-Stream-Offsets](./workflow-streams.md), die passende +[Embedded-Outbox-Sequenz](../features/message-streams.md) oder einen +deterministischen Workflow-lokalen Zähler. Kombinieren Sie einen Run-lokalen +Zähler mit `run_id`; übergeben Sie ihn bei Continue-as-new ausdrücklich, wenn +die Anwendung eine logische Sequenz benötigt. Zeitstempel geben unabhängigen +Streams oder Runs keine globale Reihenfolge. + + ### Aktivitätswiederholungen und -Timeouts {#activity-retries-and-timeouts} Verwenden Sie `ActivityRetryPolicy` und Timeout-Schlüsselwortargumente an @@ -720,7 +807,7 @@ Versuche nicht. Workflow-Code wird aus der Historie wiedergegeben. Er darf keine nicht deterministischen Operationen direkt ausführen: - Keine E/A (HTTP-Aufrufe, Dateilesen, Datenbankabfragen). -- Kein `datetime.now()` oder `time.time()`: `ctx.now()` verwenden. +- Kein `datetime.now()` oder `time.time()`: `ctx.now()` nur als festen Run-Startanker verwenden; für Warteoperationen dauerhafte Timer verwenden. - Kein `random.random()`: `ctx.random()` verwenden. - Kein `uuid.uuid4()`: `ctx.uuid4()` verwenden. diff --git a/i18n/de/source-hashes.json b/i18n/de/source-hashes.json index 3168bc6fb32..1af71072e35 100644 --- a/i18n/de/source-hashes.json +++ b/i18n/de/source-hashes.json @@ -80,7 +80,7 @@ "polyglot/php.md": "9ab2f2b6cb617475e3873609667f7c40eaeed8512f1f041834d74bb63d1a6040", "polyglot/portable-worker-affinity.md": "40973c6a31a76845b4f95e9a505e68ad76abd5174bbdccf37993cdebc8904959", "polyglot/prerelease-history-migration.md": "a57f05f492228a8a67791f9152648a1e5f80856325b78777cf63b6ef49b7413c", - "polyglot/python.md": "61cc21945d97c755cb81a904234252f9dfce042bd171d1ae4d9a9f3f1b1fb428", + "polyglot/python.md": "2eacd002337e00f6bb27da329f131b3607280b52664715d6fbd06b6a747dc2ba", "polyglot/rust-cloud-quickstart.md": "2303434fc9302a81a8381ad4836423ef9952eb3201a4574c432254e3cb68c5f3", "polyglot/rust.md": "b5d3b44c4e68e8b707804001bc46bd5e758b0180eec2a9a2ebdb69a6183dced5", "polyglot/server-api-reference.md": "6029fa5573106ebb40d4cf6fc384c4c5aa3ac77d435b3d2c30c4c505cce7ac23", diff --git a/i18n/es/docusaurus-plugin-content-docs/current/polyglot/python.md b/i18n/es/docusaurus-plugin-content-docs/current/polyglot/python.md index 06c0e73f42d..20e6ad11349 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/polyglot/python.md +++ b/i18n/es/docusaurus-plugin-content-docs/current/polyglot/python.md @@ -654,11 +654,95 @@ paridad de validación. | `ctx.get_version(change_id, min, max)` | Versionado seguro del código del workflow | | `ctx.upsert_search_attributes(attrs)` | Actualiza atributos de búsqueda | | `ctx.continue_as_new(*args)` | Reinicia el workflow con otra entrada | -| `ctx.now()` | Reloj determinista desde historial | +| `ctx.now()` | Hora de inicio persistida del workflow, fija durante este run | | `ctx.random()` | Generador aleatorio con semilla | | `ctx.uuid4()` | UUID determinista | | `ctx.logger` | Logger silencioso durante la reproducción | +### Tiempo del workflow y esperas duraderas {#workflow-time-and-durable-waits} + +Durante la reproducción del worker, `ctx.now()` devuelve la **hora de inicio +persistida del run**. No avanza tras una actividad, temporizador, señal ni otro +paso duradero. Las reproducciones en frío del mismo historial devuelven ese +mismo valor inicial. Un sucesor de continue-as-new tiene su propia hora de +inicio; no conserva el ancla del reloj de su predecesor. + +La [implementación publicada de Python](https://github.com/durable-workflow/sdk-python/blob/150c6f6bc14e3516f75cc615b2f75f093ed84ea1/src/durable_workflow/workflow.py#L2594-L2601) +usa `WorkflowStarted.payload.timestamp` cuando existe y, si no, el valor +canónico `WorkflowStarted.recorded_at`. Si falta la hora persistida de +reproducción o no es válida, se produce un error explícito de reloj, no se lee +el reloj de pared del worker. Este es el contrato de inicio fijo acordado en +[SDK #146](https://github.com/durable-workflow/sdk-python/issues/146). + +Espera una operación duradera en vez de repetir un bucle hasta que cambie `ctx.now()`: + +```python +from durable_workflow import workflow + + +@workflow.defn(name="wait_without_clock_loop") +class WaitWithoutClockLoop: + def run(self, ctx): + started_at = ctx.now() + yield ctx.start_timer(30) + return { + "run_started_at": started_at.isoformat(), + "after_wait": ctx.now().isoformat(), + } +``` + +La finalización duradera del temporizador reanuda el workflow. Las dos marcas +devueltas son la misma hora de inicio. Un bucle como `while ctx.now() < deadline` +no pasa a falso solo porque se activen temporizadores. + +Para una demora relativa, usa `yield ctx.start_timer(seconds)` o +`yield ctx.sleep(seconds)`. Usa la [selección de la primera finalización](#first-completion-selection) +cuando el trabajo deba competir con un temporizador duradero. Configura +`execution_timeout_seconds` y `run_timeout_seconds` en +`client.start_workflow(...)` cuando Server deba aplicar un plazo al workflow, +incluso con los workers detenidos. Un tiempo de espera HTTP o de `get_result()` +solo limita esa petición local; no sustituye el plazo persistido del workflow. +Para conservar un plazo de aplicación en continue-as-new, pásalo explícitamente +como entrada sin sustituir el plazo de ejecución original de Server. + +Para medir el tiempo transcurrido dentro de una invocación de actividad, lee +allí un reloj monotónico y devuelve la duración como resultado de la actividad. +Para un plazo UTC externo, haz que una actividad consulte el reloj externo +autoritativo y devuelva su decisión; el workflow reproduce ese resultado +registrado. Nunca compares marcas UTC con valores monotónicos ni restes +lecturas monotónicas de procesos o equipos diferentes. Restar `ctx.now()` no +mide ni el tiempo de procesamiento activo ni el tiempo real transcurrido en Python. + +#### Diferencias entre los relojes de los SDK {#clock-differences-across-sdks} + +Estas API son deterministas cuando existen, pero no comparten un único reloj: + +| Interfaz de autoría | Llamadas repetidas y pasos duraderos | Sucesor de continue-as-new | +| --- | --- | --- | +| Python `ctx.now()` | Fija la hora de inicio persistida de este run, incluso después de un temporizador o actividad. | Usa el ancla inicial del run sucesor. | +| PHP integrado `Workflow::now()` | Parte de la hora inicial del run y avanza a la hora registrada del evento consumido durante la reproducción. Consulta [tiempo del workflow](../features/timers.md#reading-deterministic-time). | Parte del nuevo run y avanza por sus propios eventos consumidos. | +| SDK PHP en modo servicio | No hay un ayudante público general `now()` para workflows. Usa operaciones duraderas o valores registrados de actividades o efectos secundarios. | Pasa explícitamente el estado temporal de la aplicación si debe sobrevivir al traspaso. | +| SDK Rust en modo servicio | No hay un ayudante público general `WorkflowContext::now()`. Un `side_effect` puede registrar una pequeña instantánea del reloj de pared; no es un reloj que avance. | Un efecto secundario en el nuevo run es otra operación; pasa explícitamente el estado temporal anterior si hace falta. | + +El comportamiento de PHP integrado existe en el +[Core publicado](https://github.com/durable-workflow/workflow/blob/64efa176e4c37e5d6ae80e767ba3b41464024e53/src/V2/Support/WorkflowFiberContext.php). +Los contextos independientes del [SDK PHP](https://github.com/durable-workflow/sdk-php/blob/2.5.2/src/Worker/WorkflowContext.php) +y del [SDK Rust](https://github.com/durable-workflow/sdk-rust/blob/3.5.5/src/lib.rs) +tienen sus propias API. Porta la operación duradera y la política de plazos +sin suponer que un ayudante de reloj significa lo mismo en todos los SDK. + +#### Ordena eventos emitidos mediante secuencias {#order-emitted-events-with-sequence-values} + +Una marca temporal del workflow no es una clave de orden de eventos. Los +eventos de Python marcados con `ctx.now()` pueden tener todos la misma hora. +Usa [offsets de Workflow Streams](./workflow-streams.md), la +[secuencia del outbox integrado](../features/message-streams.md) correspondiente +o un contador determinista local al workflow. Combina un contador local al run +con `run_id`; pásalo explícitamente por continue-as-new si la aplicación +necesita una secuencia lógica única. Los streams o runs independientes no +obtienen un orden global a partir de sus marcas temporales. + + ### Reintentos y plazos de actividades {#activity-retries-and-timeouts} Usa `ActivityRetryPolicy` y argumentos de plazo en `ctx.schedule_activity(...)` @@ -697,7 +781,7 @@ El código se reproduce desde historial. No debe realizar directamente operacion no deterministas: - Sin E/S (HTTP, lecturas de archivos, consultas de base) -- Sin `datetime.now()` ni `time.time()`; usa `ctx.now()` +- Sin `datetime.now()` ni `time.time()`; usa `ctx.now()` solo como ancla fija del inicio del run y temporizadores duraderos para esperar. - Sin `random.random()`; usa `ctx.random()` - Sin `uuid.uuid4()`; usa `ctx.uuid4()` diff --git a/i18n/es/source-hashes.json b/i18n/es/source-hashes.json index 3168bc6fb32..1af71072e35 100644 --- a/i18n/es/source-hashes.json +++ b/i18n/es/source-hashes.json @@ -80,7 +80,7 @@ "polyglot/php.md": "9ab2f2b6cb617475e3873609667f7c40eaeed8512f1f041834d74bb63d1a6040", "polyglot/portable-worker-affinity.md": "40973c6a31a76845b4f95e9a505e68ad76abd5174bbdccf37993cdebc8904959", "polyglot/prerelease-history-migration.md": "a57f05f492228a8a67791f9152648a1e5f80856325b78777cf63b6ef49b7413c", - "polyglot/python.md": "61cc21945d97c755cb81a904234252f9dfce042bd171d1ae4d9a9f3f1b1fb428", + "polyglot/python.md": "2eacd002337e00f6bb27da329f131b3607280b52664715d6fbd06b6a747dc2ba", "polyglot/rust-cloud-quickstart.md": "2303434fc9302a81a8381ad4836423ef9952eb3201a4574c432254e3cb68c5f3", "polyglot/rust.md": "b5d3b44c4e68e8b707804001bc46bd5e758b0180eec2a9a2ebdb69a6183dced5", "polyglot/server-api-reference.md": "6029fa5573106ebb40d4cf6fc384c4c5aa3ac77d435b3d2c30c4c505cce7ac23", diff --git a/i18n/fr/docusaurus-plugin-content-docs/current/polyglot/python.md b/i18n/fr/docusaurus-plugin-content-docs/current/polyglot/python.md index 505cfe0fe34..5e766acf68c 100644 --- a/i18n/fr/docusaurus-plugin-content-docs/current/polyglot/python.md +++ b/i18n/fr/docusaurus-plugin-content-docs/current/polyglot/python.md @@ -672,11 +672,98 @@ Le `WorkflowContext` transmis à `run` fournit des opérations déterministes : | `ctx.get_version(change_id, min, max)` | Versionner le code du workflow sans risque | | `ctx.upsert_search_attributes(attrs)` | Mettre à jour les attributs de recherche | | `ctx.continue_as_new(*args)` | Redémarrer le workflow avec de nouvelles entrées | -| `ctx.now()` | Horloge déterministe issue de l'historique | +| `ctx.now()` | Heure de début persistée du workflow, fixe pour ce run | | `ctx.random()` | Générateur pseudoaléatoire à graine fixée | | `ctx.uuid4()` | UUID déterministe | | `ctx.logger` | Logger silencieux pendant la relecture | +### Temps du workflow et attentes durables {#workflow-time-and-durable-waits} + +Pendant la relecture par le worker, `ctx.now()` renvoie **l'heure de début +persistée du run**. Elle n'avance pas après une activité, un temporisateur, un +signal ou une autre étape durable. Les relectures à froid du même historique +renvoient cette même valeur initiale. Un successeur continue-as-new possède sa +propre heure de début, sans conserver l'ancrage de l'horloge du prédécesseur. + +L'[implémentation Python publiée](https://github.com/durable-workflow/sdk-python/blob/150c6f6bc14e3516f75cc615b2f75f093ed84ea1/src/durable_workflow/workflow.py#L2594-L2601) +utilise `WorkflowStarted.payload.timestamp` s'il est présent, sinon la valeur +canonique `WorkflowStarted.recorded_at`. Une heure persistée absente ou invalide +produit une erreur d'horloge explicite, sans autoriser la lecture de l'horloge +murale du worker. C'est le contrat à début fixe établi dans +[SDK #146](https://github.com/durable-workflow/sdk-python/issues/146). + +Attendez une opération durable plutôt que de boucler jusqu'à ce que `ctx.now()` change : + +```python +from durable_workflow import workflow + + +@workflow.defn(name="wait_without_clock_loop") +class WaitWithoutClockLoop: + def run(self, ctx): + started_at = ctx.now() + yield ctx.start_timer(30) + return { + "run_started_at": started_at.isoformat(), + "after_wait": ctx.now().isoformat(), + } +``` + +La fin durable du temporisateur reprend le workflow. Les deux horodatages +renvoyés sont la même heure de début. Une boucle comme +`while ctx.now() < deadline` ne devient pas fausse simplement parce que des +temporisateurs se déclenchent. + +Pour un délai relatif, utilisez `yield ctx.start_timer(seconds)` ou +`yield ctx.sleep(seconds)`. Utilisez la [sélection de la première fin](#first-completion-selection) +pour mettre un travail en concurrence avec un temporisateur durable. Définissez +`execution_timeout_seconds` et `run_timeout_seconds` dans +`client.start_workflow(...)` lorsque Server doit imposer une échéance au +workflow, y compris lorsque les workers sont arrêtés. Le délai d'attente HTTP +ou de `get_result()` limite seulement cette requête locale ; il ne remplace +pas l'échéance persistée du workflow. Transmettez explicitement une échéance +applicative en entrée de continue-as-new pour la conserver, sans remplacer +l'échéance d'exécution initiale de Server. + +Pour mesurer le temps écoulé au sein d'un appel d'activité, lisez une horloge +monotone dans cette activité et renvoyez la durée dans son résultat. Pour une +échéance UTC externe, faites vérifier l'horloge externe faisant autorité par une +activité et renvoyez sa décision ; le workflow relit ce résultat enregistré. +Ne comparez jamais des horodatages UTC à des valeurs monotones et ne soustrayez +pas des mesures monotones de processus ou d'hôtes différents. La soustraction +de `ctx.now()` ne mesure en Python ni le temps de traitement actif ni le temps +mural écoulé. + +#### Différences d'horloge entre SDK {#clock-differences-across-sdks} + +Ces API sont déterministes lorsqu'elles existent, mais ne partagent pas une même horloge : + +| Surface d'écriture | Appels répétés et étapes durables | Successeur continue-as-new | +| --- | --- | --- | +| Python `ctx.now()` | Fixe l'heure de début persistée de ce run, même après un temporisateur ou une activité. | Utilise l'ancrage initial du run successeur. | +| PHP intégré `Workflow::now()` | Commence à l'heure de début du run et avance vers l'heure enregistrée de l'événement consommé par la relecture. Voir [temps du workflow](../features/timers.md#reading-deterministic-time). | Commence avec le nouveau run, puis avance au fil de ses propres événements consommés. | +| SDK PHP en mode service | Pas d'aide publique générale `now()` pour le workflow. Utilisez des opérations durables ou les valeurs enregistrées d'activités ou d'effets de bord. | Transmettez explicitement l'état temporel de l'application s'il doit survivre au passage. | +| SDK Rust en mode service | Pas d'aide publique générale `WorkflowContext::now()`. Un `side_effect` peut enregistrer un petit instantané de l'horloge murale, sans constituer une horloge qui avance. | Un effet de bord dans le nouveau run est une nouvelle opération ; transmettez explicitement l'état temporel antérieur si nécessaire. | + +Le comportement PHP intégré existe dans le +[Core publié](https://github.com/durable-workflow/workflow/blob/64efa176e4c37e5d6ae80e767ba3b41464024e53/src/V2/Support/WorkflowFiberContext.php). +Les contextes distincts du [SDK PHP](https://github.com/durable-workflow/sdk-php/blob/2.5.2/src/Worker/WorkflowContext.php) +et du [SDK Rust](https://github.com/durable-workflow/sdk-rust/blob/3.5.5/src/lib.rs) +ont leurs propres API. Portez l'opération durable et la politique d'échéance +sans supposer qu'une aide d'horloge possède la même sémantique dans chaque SDK. + +#### Ordonner les événements émis avec des séquences {#order-emitted-events-with-sequence-values} + +Un horodatage de workflow n'est pas une clé d'ordre des événements. Tous les +événements Python horodatés avec `ctx.now()` peuvent avoir la même heure. +Utilisez les [offsets des Workflow Streams](./workflow-streams.md), la +[séquence de l'outbox intégrée](../features/message-streams.md) appropriée ou un +compteur déterministe local au workflow. Associez un compteur local au run à +`run_id` ; transmettez-le explicitement via continue-as-new si l'application +a besoin d'une séquence logique unique. Les horodatages ne donnent pas d'ordre +global aux streams ou runs indépendants. + + ### Tentatives et délais d'activité {#activity-retries-and-timeouts} Utilisez `ActivityRetryPolicy` et les arguments nommés de délai de @@ -715,7 +802,7 @@ Le code du workflow est relu depuis l'historique. Il ne doit effectuer aucune opération non déterministe directement : - Aucune entrée/sortie : appels HTTP, fichiers, requêtes de base -- Aucun `datetime.now()` ou `time.time()` : utilisez `ctx.now()` +- Aucun `datetime.now()` ou `time.time()` : utilisez `ctx.now()` uniquement comme ancrage fixe du début du run et des temporisateurs durables pour les attentes. - Aucun `random.random()` : utilisez `ctx.random()` - Aucun `uuid.uuid4()` : utilisez `ctx.uuid4()` diff --git a/i18n/fr/source-hashes.json b/i18n/fr/source-hashes.json index 3168bc6fb32..1af71072e35 100644 --- a/i18n/fr/source-hashes.json +++ b/i18n/fr/source-hashes.json @@ -80,7 +80,7 @@ "polyglot/php.md": "9ab2f2b6cb617475e3873609667f7c40eaeed8512f1f041834d74bb63d1a6040", "polyglot/portable-worker-affinity.md": "40973c6a31a76845b4f95e9a505e68ad76abd5174bbdccf37993cdebc8904959", "polyglot/prerelease-history-migration.md": "a57f05f492228a8a67791f9152648a1e5f80856325b78777cf63b6ef49b7413c", - "polyglot/python.md": "61cc21945d97c755cb81a904234252f9dfce042bd171d1ae4d9a9f3f1b1fb428", + "polyglot/python.md": "2eacd002337e00f6bb27da329f131b3607280b52664715d6fbd06b6a747dc2ba", "polyglot/rust-cloud-quickstart.md": "2303434fc9302a81a8381ad4836423ef9952eb3201a4574c432254e3cb68c5f3", "polyglot/rust.md": "b5d3b44c4e68e8b707804001bc46bd5e758b0180eec2a9a2ebdb69a6183dced5", "polyglot/server-api-reference.md": "6029fa5573106ebb40d4cf6fc384c4c5aa3ac77d435b3d2c30c4c505cce7ac23", diff --git a/i18n/ja/docusaurus-plugin-content-docs/current/polyglot/python.md b/i18n/ja/docusaurus-plugin-content-docs/current/polyglot/python.md index 21a8821b798..abd109b5991 100644 --- a/i18n/ja/docusaurus-plugin-content-docs/current/polyglot/python.md +++ b/i18n/ja/docusaurus-plugin-content-docs/current/polyglot/python.md @@ -513,11 +513,93 @@ Python Worker は Server へクエリタスク結果を返して、ルーティ | `ctx.get_version(change_id, min, max)` | 安全なワークフローコードのバージョニング | | `ctx.upsert_search_attributes(attrs)` | 検索属性を更新する | | `ctx.continue_as_new(*args)` | 新しい入力でワークフローを再開する | -| `ctx.now()` | 決定的な時計(履歴由来) | +| `ctx.now()` | 永続化されたワークフロー開始時刻。この実行中は固定 | | `ctx.random()` | シード付き乱数生成器 | | `ctx.uuid4()` | 決定的な UUID | | `ctx.logger` | リプレイ中は出力しないロガー | +### ワークフロー時刻と永続的な待機 {#workflow-time-and-durable-waits} + +Worker でのリプレイ中、`ctx.now()` は実行の**永続化された開始時刻**を返します。 +アクティビティ、タイマー、シグナルなどの永続的なステップが終わっても進みません。 +同じ履歴をコールドリプレイすると、常に同じ開始値が返ります。 +continue-as-new の後続実行には独自の開始時刻があり、前の実行の時計の基準を引き継ぎません。 + +[公開済みの Python 実装](https://github.com/durable-workflow/sdk-python/blob/150c6f6bc14e3516f75cc615b2f75f093ed84ea1/src/durable_workflow/workflow.py#L2594-L2601) +は、`WorkflowStarted.payload.timestamp` があればそれを使い、なければ正規の +`WorkflowStarted.recorded_at` を使います。永続化されたリプレイ時刻が存在しない場合や +無効な場合は明示的な時計エラーになります。Worker の実時間時計を読む許可にはなりません。 +これは [SDK #146](https://github.com/durable-workflow/sdk-python/issues/146) で合意した、 +開始時刻を固定する契約です。 + +`ctx.now()` が変わるまでループするのではなく、永続的な操作を待ちます。 + +```python +from durable_workflow import workflow + + +@workflow.defn(name="wait_without_clock_loop") +class WaitWithoutClockLoop: + def run(self, ctx): + started_at = ctx.now() + yield ctx.start_timer(30) + return { + "run_started_at": started_at.isoformat(), + "after_wait": ctx.now().isoformat(), + } +``` + +タイマーの永続的な完了によってワークフローが再開します。返される2つの時刻は +同じ開始時刻です。`while ctx.now() < deadline` のようなループは、 +タイマーが発火しただけでは条件が偽になりません。 + +相対的な遅延には `yield ctx.start_timer(seconds)` または `yield ctx.sleep(seconds)` +を使います。作業と永続タイマーを競合させる場合は、 +[最初の完了の選択](#first-completion-selection)を使います。Worker の停止中も +Server にワークフローの期限を強制させる場合は、`client.start_workflow(...)` の +`execution_timeout_seconds` と `run_timeout_seconds` を設定します。 +HTTP や `get_result()` の待機タイムアウトが制限するのは、そのローカルリクエストだけです。 +ワークフローの永続化された期限の代わりにはなりません。アプリケーションの期限を +continue-as-new に引き継ぐ必要がある場合は、Server の元の実行期限を置き換えずに、 +入力として明示的に渡します。 + +1回のアクティビティ呼び出し内で実際の経過時間を測定するには、その中で単調時計を読み、 +所要時間をアクティビティ結果として返します。外部の UTC 期限については、 +アクティビティで権威ある外部時計を確認し、その判断を返します。 +ワークフローは記録済みの結果をリプレイします。UTC タイムスタンプと単調時計の値を比較したり、 +異なるプロセスやホストの単調時計の値を引き算したりしないでください。 +Python で `ctx.now()` 同士を引いても、実処理時間や実時間での経過時間にはなりません。 + +#### SDK 間の時計の違い {#clock-differences-across-sdks} + +これらの API は、提供されている場合には決定的ですが、共通の時計ではありません。 + +| 記述インターフェース | 繰り返し呼び出しと永続的なステップ | continue-as-new の後続実行 | +| --- | --- | --- | +| Python `ctx.now()` | タイマーやアクティビティの後も、この実行の永続化された開始時刻に固定されます。 | 後続実行の開始時刻を基準にします。 | +| 組み込み PHP `Workflow::now()` | 実行の開始時刻から始まり、リプレイが消費したイベントの記録時刻へ進みます。[ワークフロー時刻](../features/timers.md#reading-deterministic-time)を参照してください。 | 新しい実行から始まり、その実行で消費したイベントに沿って進みます。 | +| サービスモードの PHP SDK | 汎用の公開ワークフローヘルパー `now()` はありません。永続的な操作か、記録済みのアクティビティ/副作用の値を使います。 | 引き継ぎ後も必要なアプリケーションの時刻状態は、明示的に渡します。 | +| サービスモードの Rust SDK | 汎用の公開ヘルパー `WorkflowContext::now()` はありません。`side_effect` で小さな実時間時計のスナップショットを記録できますが、進む時計ではありません。 | 新しい実行の副作用は新しい操作です。以前の時刻状態が必要なら明示的に渡します。 | + +組み込み PHP の動作は +[公開済み Core](https://github.com/durable-workflow/workflow/blob/64efa176e4c37e5d6ae80e767ba3b41464024e53/src/V2/Support/WorkflowFiberContext.php) +にあります。別の [PHP SDK コンテキスト](https://github.com/durable-workflow/sdk-php/blob/2.5.2/src/Worker/WorkflowContext.php) +と [Rust SDK コンテキスト](https://github.com/durable-workflow/sdk-rust/blob/3.5.5/src/lib.rs) +には独自の API があります。すべての SDK で時計ヘルパーの意味が同じだと仮定せず、 +永続的な操作と期限ポリシーを移植してください。 + +#### 発行イベントをシーケンス値で順序付けする {#order-emitted-events-with-sequence-values} + +ワークフローのタイムスタンプはイベントの順序キーではありません。 +`ctx.now()` を付けた Python イベントは、すべて同じ時刻になり得ます。 +[Workflow Streams のオフセット](./workflow-streams.md)、対応する +[組み込み送信ボックスのシーケンス](../features/message-streams.md)、または決定的な +ワークフローローカルのカウンターを使います。実行ローカルのカウンターには `run_id` +を組み合わせます。アプリケーションが1つの論理シーケンスを必要とする場合は、 +continue-as-new で明示的に引き継ぎます。独立したストリームや実行のタイムスタンプから、 +全体的な順序が得られるわけではありません。 + + ### アクティビティの再試行とタイムアウト {#activity-retries-and-timeouts} アクティビティ呼び出しに、既定の単一試行とは異なる再試行上限や期限が必要な場合、`ctx.schedule_activity(...)` の `ActivityRetryPolicy` とタイムアウトのキーワード引数を使います。 @@ -548,7 +630,7 @@ receipt = yield ctx.schedule_activity( ワークフローコードは履歴からリプレイされます。非決定的な操作を直接行ってはいけません。 - I/O(HTTP 呼び出し、ファイル読み取り、データベースクエリ)を行わない -- `datetime.now()` や `time.time()` の代わりに `ctx.now()` を使う +- `datetime.now()` や `time.time()` は使わず、`ctx.now()` は実行開始の固定基準にだけ使います。待機には永続タイマーを使います。 - `random.random()` の代わりに `ctx.random()` を使う - `uuid.uuid4()` の代わりに `ctx.uuid4()` を使う diff --git a/i18n/ja/source-hashes.json b/i18n/ja/source-hashes.json index 3168bc6fb32..1af71072e35 100644 --- a/i18n/ja/source-hashes.json +++ b/i18n/ja/source-hashes.json @@ -80,7 +80,7 @@ "polyglot/php.md": "9ab2f2b6cb617475e3873609667f7c40eaeed8512f1f041834d74bb63d1a6040", "polyglot/portable-worker-affinity.md": "40973c6a31a76845b4f95e9a505e68ad76abd5174bbdccf37993cdebc8904959", "polyglot/prerelease-history-migration.md": "a57f05f492228a8a67791f9152648a1e5f80856325b78777cf63b6ef49b7413c", - "polyglot/python.md": "61cc21945d97c755cb81a904234252f9dfce042bd171d1ae4d9a9f3f1b1fb428", + "polyglot/python.md": "2eacd002337e00f6bb27da329f131b3607280b52664715d6fbd06b6a747dc2ba", "polyglot/rust-cloud-quickstart.md": "2303434fc9302a81a8381ad4836423ef9952eb3201a4574c432254e3cb68c5f3", "polyglot/rust.md": "b5d3b44c4e68e8b707804001bc46bd5e758b0180eec2a9a2ebdb69a6183dced5", "polyglot/server-api-reference.md": "6029fa5573106ebb40d4cf6fc384c4c5aa3ac77d435b3d2c30c4c505cce7ac23", diff --git a/i18n/pt-BR/docusaurus-plugin-content-docs/current/polyglot/python.md b/i18n/pt-BR/docusaurus-plugin-content-docs/current/polyglot/python.md index a60f18c314e..605d9308336 100644 --- a/i18n/pt-BR/docusaurus-plugin-content-docs/current/polyglot/python.md +++ b/i18n/pt-BR/docusaurus-plugin-content-docs/current/polyglot/python.md @@ -678,11 +678,96 @@ O `WorkflowContext` passado a `run` fornece operações determinísticas: | `ctx.get_version(change_id, min, max)` | Versionamento seguro do código de workflow | | `ctx.upsert_search_attributes(attrs)` | Atualiza atributos de busca | | `ctx.continue_as_new(*args)` | Reinicia o workflow com nova entrada | -| `ctx.now()` | Relógio determinístico (do histórico) | +| `ctx.now()` | Horário de início persistido do workflow, fixo durante este run | | `ctx.random()` | Gerador aleatório com semente | | `ctx.uuid4()` | UUID determinístico | | `ctx.logger` | Logger silencioso durante replay | +### Tempo do workflow e esperas duráveis {#workflow-time-and-durable-waits} + +Durante o replay no worker, `ctx.now()` retorna o **horário de início +persistido do run**. Ele não avança após uma atividade, temporizador, sinal ou +outra etapa durável. Replays a frio do mesmo histórico retornam o mesmo valor +inicial. Um sucessor continue-as-new tem seu próprio horário de início e não +mantém a referência do relógio do predecessor. + +A [implementação Python publicada](https://github.com/durable-workflow/sdk-python/blob/150c6f6bc14e3516f75cc615b2f75f093ed84ea1/src/durable_workflow/workflow.py#L2594-L2601) +usa `WorkflowStarted.payload.timestamp` quando presente; caso contrário, usa o +valor canônico `WorkflowStarted.recorded_at`. Um horário persistido ausente ou +inválido gera um erro explícito de relógio, sem autorizar a leitura do relógio +de parede do worker. Esse é o contrato de início fixo definido no +[SDK #146](https://github.com/durable-workflow/sdk-python/issues/146). + +Aguarde uma operação durável em vez de repetir um laço até `ctx.now()` mudar: + +```python +from durable_workflow import workflow + + +@workflow.defn(name="wait_without_clock_loop") +class WaitWithoutClockLoop: + def run(self, ctx): + started_at = ctx.now() + yield ctx.start_timer(30) + return { + "run_started_at": started_at.isoformat(), + "after_wait": ctx.now().isoformat(), + } +``` + +A conclusão durável do temporizador retoma o workflow. Os dois horários +retornados são o mesmo início. Um laço como `while ctx.now() < deadline` não +passa a falso apenas porque os temporizadores disparam. + +Para um atraso relativo, use `yield ctx.start_timer(seconds)` ou +`yield ctx.sleep(seconds)`. Use a [seleção da primeira conclusão](#first-completion-selection) +quando um trabalho precisar disputar com um temporizador durável. Configure +`execution_timeout_seconds` e `run_timeout_seconds` em +`client.start_workflow(...)` quando o Server precisar impor um prazo ao +workflow, inclusive com os workers parados. Um timeout HTTP ou da espera de +`get_result()` limita apenas essa requisição local; ele não substitui o prazo +persistido do workflow. Para manter um prazo da aplicação em continue-as-new, +passe-o explicitamente como entrada sem substituir o prazo original de +execução do Server. + +Para medir o tempo decorrido dentro de uma invocação de atividade, leia ali +um relógio monotônico e retorne a duração como resultado da atividade. Para um +prazo UTC externo, faça uma atividade consultar o relógio externo que é a +fonte de autoridade e retornar sua decisão; o workflow reproduz esse resultado +registrado. Nunca compare horários UTC com valores monotônicos nem subtraia +leituras monotônicas de processos ou hosts diferentes. Subtrair `ctx.now()` +não mede tempo de processamento ativo nem tempo de parede decorrido em Python. + +#### Diferenças entre os relógios dos SDKs {#clock-differences-across-sdks} + +Essas APIs são determinísticas quando disponíveis, mas não compartilham um único relógio: + +| Superfície de autoria | Chamadas repetidas e etapas duráveis | Sucessor continue-as-new | +| --- | --- | --- | +| Python `ctx.now()` | Fixo no horário de início persistido deste run, inclusive depois de um temporizador ou atividade. | Usa a referência inicial do run sucessor. | +| PHP embutido `Workflow::now()` | Começa no início do run e avança para o horário registrado do evento consumido pelo replay. Veja [tempo do workflow](../features/timers.md#reading-deterministic-time). | Começa com o novo run e avança pelos eventos consumidos desse run. | +| SDK PHP no modo serviço | Sem um helper público geral `now()` para workflows. Use operações duráveis ou valores registrados de atividades ou efeitos colaterais. | Passe explicitamente o estado temporal da aplicação se ele precisar sobreviver à transferência. | +| SDK Rust no modo serviço | Sem um helper público geral `WorkflowContext::now()`. Um `side_effect` pode registrar uma pequena leitura do relógio de parede; ele não é um relógio que avança. | Um efeito colateral no novo run é uma nova operação; passe explicitamente o estado temporal anterior se necessário. | + +O comportamento do PHP embutido está no +[Core publicado](https://github.com/durable-workflow/workflow/blob/64efa176e4c37e5d6ae80e767ba3b41464024e53/src/V2/Support/WorkflowFiberContext.php). +Os contextos separados do [SDK PHP](https://github.com/durable-workflow/sdk-php/blob/2.5.2/src/Worker/WorkflowContext.php) +e do [SDK Rust](https://github.com/durable-workflow/sdk-rust/blob/3.5.5/src/lib.rs) +têm APIs próprias. Porte a operação durável e a política de prazos sem supor +que um helper de relógio tenha a mesma semântica em todos os SDKs. + +#### Ordene eventos emitidos com valores de sequência {#order-emitted-events-with-sequence-values} + +Um horário do workflow não é uma chave de ordenação de eventos. Eventos Python +marcados com `ctx.now()` podem ter todos o mesmo horário. Use os +[offsets de Workflow Streams](./workflow-streams.md), a +[sequência da outbox embutida](../features/message-streams.md) correspondente ou +um contador determinístico local ao workflow. Combine um contador local ao run +com `run_id`; passe-o explicitamente por continue-as-new se a aplicação +precisar de uma sequência lógica única. Streams ou runs independentes não +ganham uma ordem global a partir de seus horários. + + ### Novas tentativas e timeouts de atividades {#activity-retries-and-timeouts} Use `ActivityRetryPolicy` e argumentos nomeados de timeout em @@ -721,7 +806,7 @@ O código do workflow é reproduzido do histórico. Não deve realizar diretamen operações não determinísticas: - Sem E/S (chamadas HTTP, leitura de arquivos, consultas ao banco) -- Sem `datetime.now()` ou `time.time()`; use `ctx.now()` +- Sem `datetime.now()` ou `time.time()`; use `ctx.now()` apenas como referência fixa do início do run e temporizadores duráveis para esperar. - Sem `random.random()`; use `ctx.random()` - Sem `uuid.uuid4()`; use `ctx.uuid4()` diff --git a/i18n/pt-BR/source-hashes.json b/i18n/pt-BR/source-hashes.json index 3168bc6fb32..1af71072e35 100644 --- a/i18n/pt-BR/source-hashes.json +++ b/i18n/pt-BR/source-hashes.json @@ -80,7 +80,7 @@ "polyglot/php.md": "9ab2f2b6cb617475e3873609667f7c40eaeed8512f1f041834d74bb63d1a6040", "polyglot/portable-worker-affinity.md": "40973c6a31a76845b4f95e9a505e68ad76abd5174bbdccf37993cdebc8904959", "polyglot/prerelease-history-migration.md": "a57f05f492228a8a67791f9152648a1e5f80856325b78777cf63b6ef49b7413c", - "polyglot/python.md": "61cc21945d97c755cb81a904234252f9dfce042bd171d1ae4d9a9f3f1b1fb428", + "polyglot/python.md": "2eacd002337e00f6bb27da329f131b3607280b52664715d6fbd06b6a747dc2ba", "polyglot/rust-cloud-quickstart.md": "2303434fc9302a81a8381ad4836423ef9952eb3201a4574c432254e3cb68c5f3", "polyglot/rust.md": "b5d3b44c4e68e8b707804001bc46bd5e758b0180eec2a9a2ebdb69a6183dced5", "polyglot/server-api-reference.md": "6029fa5573106ebb40d4cf6fc384c4c5aa3ac77d435b3d2c30c4c505cce7ac23", diff --git a/i18n/uk/docusaurus-plugin-content-docs/current/polyglot/python.md b/i18n/uk/docusaurus-plugin-content-docs/current/polyglot/python.md index f3949987c2d..7ef9abb232b 100644 --- a/i18n/uk/docusaurus-plugin-content-docs/current/polyglot/python.md +++ b/i18n/uk/docusaurus-plugin-content-docs/current/polyglot/python.md @@ -662,11 +662,97 @@ workflow, замість заявляти паритет валідаторів. | `ctx.get_version(change_id, min, max)` | Безпечне версіонування коду workflow | | `ctx.upsert_search_attributes(attrs)` | Оновлення атрибутів пошуку | | `ctx.continue_as_new(*args)` | Перезапуск workflow з новим вводом | -| `ctx.now()` | Детермінований годинник з історії | +| `ctx.now()` | Збережений час початку workflow, фіксований для цього run | | `ctx.random()` | Генератор випадкових чисел зі сталим seed | | `ctx.uuid4()` | Детермінований UUID | | `ctx.logger` | Logger, що мовчить під час replay | +### Час workflow та стійкі очікування {#workflow-time-and-durable-waits} + +Під час replay у worker `ctx.now()` повертає **збережений час початку run**. +Він не просувається після activity, timer, signal чи іншого стійкого кроку. +Повторні холодні replay тієї самої історії повертають те саме початкове +значення. Наступник continue-as-new має власний час початку й не зберігає +часову опору попередника. + +[Опублікована реалізація Python](https://github.com/durable-workflow/sdk-python/blob/150c6f6bc14e3516f75cc615b2f75f093ed84ea1/src/durable_workflow/workflow.py#L2594-L2601) +використовує `WorkflowStarted.payload.timestamp`, якщо він наявний, інакше +канонічний `WorkflowStarted.recorded_at`. Відсутній чи некоректний збережений +час replay спричиняє явну помилку годинника, а не дозволяє читати реальний +годинник worker. Це контракт фіксованого початку, узгоджений у +[SDK #146](https://github.com/durable-workflow/sdk-python/issues/146). + +Очікуйте стійкої операції замість циклу до зміни `ctx.now()`: + +```python +from durable_workflow import workflow + + +@workflow.defn(name="wait_without_clock_loop") +class WaitWithoutClockLoop: + def run(self, ctx): + started_at = ctx.now() + yield ctx.start_timer(30) + return { + "run_started_at": started_at.isoformat(), + "after_wait": ctx.now().isoformat(), + } +``` + +Стійке завершення timer відновлює workflow. Обидві повернені часові позначки +є тим самим часом початку. Умова циклу на кшталт `while ctx.now() < deadline` +не стане хибною лише через спрацювання timer. + +Для відносної затримки використовуйте `yield ctx.start_timer(seconds)` або +`yield ctx.sleep(seconds)`. Використовуйте +[вибір першого завершення](#first-completion-selection), коли робота має +змагатися зі стійким timer. Задайте `execution_timeout_seconds` і +`run_timeout_seconds` у `client.start_workflow(...)`, якщо Server має +забезпечувати строк виконання workflow, зокрема за зупинених worker. +Тайм-аут HTTP чи очікування `get_result()` обмежує лише локальний запит; +він не замінює збереженого строку workflow. Щоб зберегти строк застосунку +через continue-as-new, явно передайте його як вхідні дані, не замінюючи +початкового строку виконання Server. + +Для вимірювання тривалості всередині одного виклику activity читайте там +монотонний годинник і повертайте тривалість як результат activity. Для +зовнішнього строку UTC нехай activity перевіряє авторитетний зовнішній +годинник і повертає рішення; workflow відтворює цей записаний результат. +Ніколи не порівнюйте часові позначки UTC з монотонними значеннями та не +віднімайте покази монотонних годинників різних процесів чи машин. Різниця +`ctx.now()` у Python не вимірює ні активного часу обробки, ні реальної +тривалості. + +#### Відмінності годинників SDK {#clock-differences-across-sdks} + +Ці API детерміновані, коли вони надаються, але не мають одного спільного годинника: + +| Інтерфейс написання | Повторні виклики та стійкі кроки | Наступник continue-as-new | +| --- | --- | --- | +| Python `ctx.now()` | Фіксований збережений час початку цього run, зокрема після timer чи activity. | Використовує початкову часову опору наступного run. | +| Вбудований PHP `Workflow::now()` | Починає з часу початку run та просувається до записаного часу події, яку спожив replay. Дивіться [час workflow](../features/timers.md#reading-deterministic-time). | Починає з нового run, потім просувається через спожиті події цього run. | +| PHP SDK сервісного режиму | Загального публічного помічника `now()` для workflow немає. Використовуйте стійкі операції або записані результати activity чи побічних ефектів. | Явно передавайте часовий стан застосунку, якщо він має пережити передавання. | +| Rust SDK сервісного режиму | Загального публічного помічника `WorkflowContext::now()` немає. `side_effect` може записати невеликий знімок реального годинника, але не є годинником, що просувається. | Побічний ефект у новому run є новою операцією; за потреби явно передавайте попередній часовий стан. | + +Поведінка вбудованого PHP наявна в +[опублікованому Core](https://github.com/durable-workflow/workflow/blob/64efa176e4c37e5d6ae80e767ba3b41464024e53/src/V2/Support/WorkflowFiberContext.php). +Окремі контексти [PHP SDK](https://github.com/durable-workflow/sdk-php/blob/2.5.2/src/Worker/WorkflowContext.php) +та [Rust SDK](https://github.com/durable-workflow/sdk-rust/blob/3.5.5/src/lib.rs) +мають власні API. Переносьте стійку операцію та політику строків замість +припущення однакової семантики помічника годинника в кожному SDK. + +#### Упорядковуйте створені події за послідовністю {#order-emitted-events-with-sequence-values} + +Часова позначка workflow не є ключем порядку подій. Усі події Python, +позначені `ctx.now()`, можуть мати той самий час. Використовуйте +[зміщення Workflow Streams](./workflow-streams.md), відповідну +[послідовність вбудованої вихідної скриньки](../features/message-streams.md) +або детермінований локальний лічильник workflow. Поєднуйте лічильник run з +`run_id`; явно передавайте його через continue-as-new, якщо застосунку +потрібна одна логічна послідовність. Незалежні потоки чи run не отримують +глобального порядку зі своїх часових позначок. + + ### Повторні спроби й тайм-аути activity {#activity-retries-and-timeouts} Використовуйте `ActivityRetryPolicy` і параметри тайм-ауту @@ -705,7 +791,7 @@ receipt = yield ctx.schedule_activity( недетерміновані операції: - Без I/O: HTTP викликів, читання файлів, запитів бази -- Без `datetime.now()` чи `time.time()`: використовуйте `ctx.now()` +- Без `datetime.now()` чи `time.time()`: використовуйте `ctx.now()` лише як фіксовану часову опору початку run, а для очікувань — стійкі timer. - Без `random.random()`: використовуйте `ctx.random()` - Без `uuid.uuid4()`: використовуйте `ctx.uuid4()` diff --git a/i18n/uk/source-hashes.json b/i18n/uk/source-hashes.json index 3168bc6fb32..1af71072e35 100644 --- a/i18n/uk/source-hashes.json +++ b/i18n/uk/source-hashes.json @@ -80,7 +80,7 @@ "polyglot/php.md": "9ab2f2b6cb617475e3873609667f7c40eaeed8512f1f041834d74bb63d1a6040", "polyglot/portable-worker-affinity.md": "40973c6a31a76845b4f95e9a505e68ad76abd5174bbdccf37993cdebc8904959", "polyglot/prerelease-history-migration.md": "a57f05f492228a8a67791f9152648a1e5f80856325b78777cf63b6ef49b7413c", - "polyglot/python.md": "61cc21945d97c755cb81a904234252f9dfce042bd171d1ae4d9a9f3f1b1fb428", + "polyglot/python.md": "2eacd002337e00f6bb27da329f131b3607280b52664715d6fbd06b6a747dc2ba", "polyglot/rust-cloud-quickstart.md": "2303434fc9302a81a8381ad4836423ef9952eb3201a4574c432254e3cb68c5f3", "polyglot/rust.md": "b5d3b44c4e68e8b707804001bc46bd5e758b0180eec2a9a2ebdb69a6183dced5", "polyglot/server-api-reference.md": "6029fa5573106ebb40d4cf6fc384c4c5aa3ac77d435b3d2c30c4c505cce7ac23", diff --git a/i18n/zh-Hans/docusaurus-plugin-content-docs/current/polyglot/python.md b/i18n/zh-Hans/docusaurus-plugin-content-docs/current/polyglot/python.md index 6246101d577..4db2525781c 100644 --- a/i18n/zh-Hans/docusaurus-plugin-content-docs/current/polyglot/python.md +++ b/i18n/zh-Hans/docusaurus-plugin-content-docs/current/polyglot/python.md @@ -513,11 +513,82 @@ Python Worker 通过向 Server 返回查询任务结果来完成 Server 路由 | `ctx.get_version(change_id, min, max)` | 安全的工作流代码版本管理 | | `ctx.upsert_search_attributes(attrs)` | 更新搜索属性 | | `ctx.continue_as_new(*args)` | 使用新输入重启工作流 | -| `ctx.now()` | 确定性时钟(来自历史) | +| `ctx.now()` | 持久化的工作流开始时间,在本次运行内固定 | | `ctx.random()` | 带种子的随机生成器 | | `ctx.uuid4()` | 确定性 UUID | | `ctx.logger` | 重放期间保持静默的日志器 | +### 工作流时间与持久等待 {#workflow-time-and-durable-waits} + +在 Worker 重放期间,`ctx.now()` 返回运行的**持久化开始时间**。 +活动、定时器、信号或其他持久步骤结束后,这个值也不会推进。 +对同一历史进行多次冷重放会得到同一个起始值。 +continue-as-new 后继运行有自己的开始时间,不会沿用前一运行的时钟基准。 + +[已发布的 Python 实现](https://github.com/durable-workflow/sdk-python/blob/150c6f6bc14e3516f75cc615b2f75f093ed84ea1/src/durable_workflow/workflow.py#L2594-L2601) +优先使用存在的 `WorkflowStarted.payload.timestamp`,否则使用规范的 +`WorkflowStarted.recorded_at`。持久化重放时间缺失或无效时,会明确报告时钟错误, +而不是允许读取 Worker 的墙上时钟。这是 +[SDK #146](https://github.com/durable-workflow/sdk-python/issues/146) 已确定的固定开始时间契约。 + +应等待持久操作,而不是循环等待 `ctx.now()` 改变: + +```python +from durable_workflow import workflow + + +@workflow.defn(name="wait_without_clock_loop") +class WaitWithoutClockLoop: + def run(self, ctx): + started_at = ctx.now() + yield ctx.start_timer(30) + return { + "run_started_at": started_at.isoformat(), + "after_wait": ctx.now().isoformat(), + } +``` + +定时器的持久完成会恢复工作流。返回的两个时间戳都是同一个开始时间。 +即使定时器已触发,`while ctx.now() < deadline` 这样的循环条件也不会因此变为假。 + +需要相对延迟时,使用 `yield ctx.start_timer(seconds)` 或 `yield ctx.sleep(seconds)`。 +工作需要与持久定时器竞争时,使用[首次完成选择](#first-completion-selection)。 +需要 Server 强制执行工作流期限(包括 Worker 已停止的情况)时,在 +`client.start_workflow(...)` 中设置 `execution_timeout_seconds` 和 `run_timeout_seconds`。 +HTTP 或 `get_result()` 的等待超时只限制该本地请求,并不能替代工作流的持久化期限。 +需要跨 continue-as-new 保留应用期限时,应将其明确传入输入,且不要替换 Server 原有的执行期限。 + +需要测量一次活动调用内的实际耗时时,应在活动中读取单调时钟,并将时长作为活动结果返回。 +对于外部 UTC 期限,让活动检查有权威性的外部时钟并返回判断;工作流重放该已记录结果。 +绝不要将 UTC 时间戳与单调时钟值比较,也不要对不同进程或主机的单调时钟读数做差。 +在 Python 中,对 `ctx.now()` 做差既不能测量有效处理时间,也不能测量实际经过的墙上时间。 + +#### 各 SDK 的时钟差异 {#clock-differences-across-sdks} + +这些 API 在提供时是确定性的,但并不共享同一个时钟: + +| 编写接口 | 重复调用与持久步骤 | continue-as-new 后继运行 | +| --- | --- | --- | +| Python `ctx.now()` | 固定为本次运行的持久化开始时间,定时器或活动之后也不变。 | 使用后继运行的开始时间基准。 | +| 嵌入式 PHP `Workflow::now()` | 从运行开始时间起步,并推进到重放已消费事件的记录时间。参见[工作流时间](../features/timers.md#reading-deterministic-time)。 | 从新运行开始,随后沿该运行已消费的事件推进。 | +| 服务模式 PHP SDK | 没有通用的公开工作流 `now()` 辅助方法。使用持久操作或已记录的活动/副作用值。 | 需要跨交接保留应用时间状态时,必须明确传递。 | +| 服务模式 Rust SDK | 没有通用的公开 `WorkflowContext::now()` 辅助方法。`side_effect` 可以记录一个小型墙上时钟快照,但它不是会推进的时钟。 | 新运行中的副作用是新操作;需要时应明确传递先前的时间状态。 | + +嵌入式 PHP 的行为已存在于 +[已发布的 Core](https://github.com/durable-workflow/workflow/blob/64efa176e4c37e5d6ae80e767ba3b41464024e53/src/V2/Support/WorkflowFiberContext.php)。 +独立的 [PHP SDK 上下文](https://github.com/durable-workflow/sdk-php/blob/2.5.2/src/Worker/WorkflowContext.php) +和 [Rust SDK 上下文](https://github.com/durable-workflow/sdk-rust/blob/3.5.5/src/lib.rs) +各有自己的 API。应迁移持久操作与期限策略,而不是假定每个 SDK 的时钟辅助方法都有相同语义。 + +#### 使用序列值排列发出的事件 {#order-emitted-events-with-sequence-values} + +工作流时间戳不是事件排序键。使用 `ctx.now()` 标记的 Python 事件可能都有同一个时间戳。 +应使用 [Workflow Streams 偏移量](./workflow-streams.md)、相应的 +[嵌入式发件箱序列](../features/message-streams.md),或确定性的工作流本地计数器。 +运行本地计数器应配合 `run_id` 使用;如果应用需要一个逻辑序列,必须通过 +continue-as-new 明确传递。独立流或运行不会因拥有时间戳而获得全局顺序。 + + ### 活动重试与超时 {#activity-retries-and-timeouts} 某个活动调用需要不同于默认单次尝试行为的重试预算或截止时间时,在 `ctx.schedule_activity(...)` 上使用 `ActivityRetryPolicy` 和超时关键字参数: @@ -548,7 +619,7 @@ receipt = yield ctx.schedule_activity( 工作流代码根据历史重放,不得直接执行任何非确定性操作: - 不执行 I/O(HTTP 调用、文件读取、数据库查询) -- 不使用 `datetime.now()` 或 `time.time()`,改用 `ctx.now()` +- 不使用 `datetime.now()` 或 `time.time()`;`ctx.now()` 仅用于固定的运行开始时间基准,等待应使用持久定时器。 - 不使用 `random.random()`,改用 `ctx.random()` - 不使用 `uuid.uuid4()`,改用 `ctx.uuid4()` diff --git a/i18n/zh-Hans/source-hashes.json b/i18n/zh-Hans/source-hashes.json index 3168bc6fb32..1af71072e35 100644 --- a/i18n/zh-Hans/source-hashes.json +++ b/i18n/zh-Hans/source-hashes.json @@ -80,7 +80,7 @@ "polyglot/php.md": "9ab2f2b6cb617475e3873609667f7c40eaeed8512f1f041834d74bb63d1a6040", "polyglot/portable-worker-affinity.md": "40973c6a31a76845b4f95e9a505e68ad76abd5174bbdccf37993cdebc8904959", "polyglot/prerelease-history-migration.md": "a57f05f492228a8a67791f9152648a1e5f80856325b78777cf63b6ef49b7413c", - "polyglot/python.md": "61cc21945d97c755cb81a904234252f9dfce042bd171d1ae4d9a9f3f1b1fb428", + "polyglot/python.md": "2eacd002337e00f6bb27da329f131b3607280b52664715d6fbd06b6a747dc2ba", "polyglot/rust-cloud-quickstart.md": "2303434fc9302a81a8381ad4836423ef9952eb3201a4574c432254e3cb68c5f3", "polyglot/rust.md": "b5d3b44c4e68e8b707804001bc46bd5e758b0180eec2a9a2ebdb69a6183dced5", "polyglot/server-api-reference.md": "6029fa5573106ebb40d4cf6fc384c4c5aa3ac77d435b3d2c30c4c505cce7ac23",