diff --git a/docs/polyglot/python.md b/docs/polyglot/python.md index f64c8abc72c..3ead428dce2 100644 --- a/docs/polyglot/python.md +++ b/docs/polyglot/python.md @@ -1080,6 +1080,42 @@ async def validate(data: dict) -> dict: return data ``` +### Terminal business errors {#terminal-business-errors} + +`non_retryable_error_types` matches the **outermost** exception's short name (`exception_type`) or fully qualified class name (`exception_class`) exactly. For example, `ValidationError` does not match `SpecificValidationError`, even if it is a subclass. A qualified entry such as `app.errors.ValidationError` must match that recorded class name. The policy does not walk subclasses or exception causes. `raise RuntimeError(...) from NonRetryableError(...)` reports the outer `RuntimeError` and can still be retried. + +When converting a known business exception, make `NonRetryableError` the outer exception and keep the original Python cause with `cause=error`. This marks the activity failure non-retryable without relying on a name list. It does not make the entire workflow fail automatically: workflow code receives `ActivityFailed` when the activity has no retries left, either because it is non-retryable or because its retry budget is exhausted. + +```python +from durable_workflow import ActivityFailed, NonRetryableError, activity, workflow + +@activity.defn(name="validate_order") +async def validate_order(order: dict) -> dict: + try: + if order.get("amount", 0) <= 0: + raise ValueError("amount must be positive") + except ValueError as error: + raise NonRetryableError("Order validation failed", cause=error) from error + return order + +@workflow.defn(name="place_order") +class PlaceOrderWorkflow: + def run(self, ctx, order: dict): + try: + yield ctx.schedule_activity("validate_order", [order]) + except ActivityFailed as failure: + if ( + failure.activity_type != "validate_order" + or failure.exception_type != "NonRetryableError" + or not failure.non_retryable + ): + raise + return {"order_id": order["id"], "status": "rejected"} + return {"order_id": order["id"], "status": "accepted"} +``` + +For input `{"id": "order-123", "amount": 0}`, the activity fails once and the workflow completes normally with `status: rejected`. Handle only the business failure you intend to handle; the example rethrows other failures. Continue with further durable steps if your business flow requires them. An uncaught `ActivityFailed` fails the workflow. Preserving a Python cause does not mean retry policy examines that chain or that every SDK exposes an identical cause object. + ### Synchronous and asynchronous activities {#activity-execution} Ordinary `def` remote activities run in a dedicated thread pool bounded by 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 9cf475595fe..66f76127e48 100644 --- a/i18n/de/docusaurus-plugin-content-docs/current/polyglot/python.md +++ b/i18n/de/docusaurus-plugin-content-docs/current/polyglot/python.md @@ -1053,6 +1053,42 @@ async def validate(data: dict) -> dict: return data ``` +### Terminale fachliche Fehler {#terminal-business-errors} + +`non_retryable_error_types` vergleicht den kurzen Namen (`exception_type`) oder vollständig qualifizierten Klassennamen (`exception_class`) der **äußersten** Ausnahme exakt. `ValidationError` passt beispielsweise nicht zu `SpecificValidationError`, auch wenn diese Klasse davon erbt. Ein qualifizierter Eintrag wie `app.errors.ValidationError` muss dem aufgezeichneten Klassennamen entsprechen. Die Richtlinie durchsucht weder Unterklassen noch Ursachen. `raise RuntimeError(...) from NonRetryableError(...)` meldet den äußeren `RuntimeError` und kann weiterhin wiederholt werden. + +Wenn Sie eine bekannte fachliche Ausnahme umwandeln, lösen Sie `NonRetryableError` als äußere Ausnahme aus und erhalten Sie die ursprüngliche Python-Ursache mit `cause=error`. Dadurch wird der Aktivitätsfehler ohne Namensliste als nicht wiederholbar markiert. Das lässt nicht automatisch den ganzen Workflow scheitern: Er erhält `ActivityFailed`, wenn keine Aktivitätsversuche verbleiben, entweder wegen eines nicht wiederholbaren Fehlers oder wegen ausgeschöpften Budgets. + +```python +from durable_workflow import ActivityFailed, NonRetryableError, activity, workflow + +@activity.defn(name="validate_order") +async def validate_order(order: dict) -> dict: + try: + if order.get("amount", 0) <= 0: + raise ValueError("amount must be positive") + except ValueError as error: + raise NonRetryableError("Order validation failed", cause=error) from error + return order + +@workflow.defn(name="place_order") +class PlaceOrderWorkflow: + def run(self, ctx, order: dict): + try: + yield ctx.schedule_activity("validate_order", [order]) + except ActivityFailed as failure: + if ( + failure.activity_type != "validate_order" + or failure.exception_type != "NonRetryableError" + or not failure.non_retryable + ): + raise + return {"order_id": order["id"], "status": "rejected"} + return {"order_id": order["id"], "status": "accepted"} +``` + +Bei der Eingabe `{"id": "order-123", "amount": 0}` schlägt die Aktivität einmal fehl und der Workflow schließt normal mit `status: rejected` ab. Behandeln Sie nur den vorgesehenen fachlichen Fehler; das Beispiel löst andere Fehler erneut aus. Fahren Sie mit weiteren dauerhaften Schritten fort, wenn der Geschäftsablauf es erfordert. Ein unbehandelter `ActivityFailed` lässt den Workflow scheitern. Eine erhaltene Python-Ursache bedeutet weder, dass die Wiederholungsrichtlinie die Kette prüft, noch dass jedes SDK dasselbe Ursachenobjekt bereitstellt. + ### Synchrone und asynchrone Aktivitäten {#activity-execution} Gewöhnliche entfernte `def`-Aktivitäten laufen in einem eigenen Threadpool, diff --git a/i18n/de/source-hashes.json b/i18n/de/source-hashes.json index a60d3f34fb1..6d659f850f1 100644 --- a/i18n/de/source-hashes.json +++ b/i18n/de/source-hashes.json @@ -82,7 +82,7 @@ "polyglot/polling-patterns.md": "30875099a23731c6c471b481a771378acc6ff57eed2fda4405a509057534c989", "polyglot/portable-worker-affinity.md": "40973c6a31a76845b4f95e9a505e68ad76abd5174bbdccf37993cdebc8904959", "polyglot/prerelease-history-migration.md": "a57f05f492228a8a67791f9152648a1e5f80856325b78777cf63b6ef49b7413c", - "polyglot/python.md": "a9fc8330cbc78953244b94a170197038a299a5a25fa78c38c7bfb451fd67d345", + "polyglot/python.md": "174acc170fc749284b5115f995996885c04056340d44bd334c6cee33e288abd1", "polyglot/rust-cloud-quickstart.md": "2303434fc9302a81a8381ad4836423ef9952eb3201a4574c432254e3cb68c5f3", "polyglot/rust.md": "32de05a771b5ee6d8cf54fdd408222372995ce29c8be63102abe58b977ac8ef0", "polyglot/server-api-reference.md": "e94af77a13b341d6fdc9941d4ee94e5053ab932d2e77069b238eb33351414e83", 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 ea02ace512f..7c1ad0f28a7 100644 --- a/i18n/es/docusaurus-plugin-content-docs/current/polyglot/python.md +++ b/i18n/es/docusaurus-plugin-content-docs/current/polyglot/python.md @@ -1030,6 +1030,42 @@ async def validate(data: dict) -> dict: return data ``` +### Errores de negocio terminales {#terminal-business-errors} + +`non_retryable_error_types` compara exactamente el nombre corto (`exception_type`) o el nombre de clase cualificado (`exception_class`) de la excepción **exterior**. Por ejemplo, `ValidationError` no coincide con `SpecificValidationError`, aunque sea una subclase. Una entrada cualificada como `app.errors.ValidationError` debe coincidir con la clase registrada. La política no recorre subclases ni causas. `raise RuntimeError(...) from NonRetryableError(...)` comunica el `RuntimeError` exterior y puede seguir reintentándose. + +Al convertir una excepción de negocio conocida, lance `NonRetryableError` como excepción exterior y conserve la causa original de Python mediante `cause=error`. Esto marca el fallo de actividad como no reintentable sin depender de una lista de nombres. No hace fallar automáticamente el workflow completo: este recibe `ActivityFailed` cuando no quedan reintentos, por una negativa no reintentable o por agotar el presupuesto de intentos. + +```python +from durable_workflow import ActivityFailed, NonRetryableError, activity, workflow + +@activity.defn(name="validate_order") +async def validate_order(order: dict) -> dict: + try: + if order.get("amount", 0) <= 0: + raise ValueError("amount must be positive") + except ValueError as error: + raise NonRetryableError("Order validation failed", cause=error) from error + return order + +@workflow.defn(name="place_order") +class PlaceOrderWorkflow: + def run(self, ctx, order: dict): + try: + yield ctx.schedule_activity("validate_order", [order]) + except ActivityFailed as failure: + if ( + failure.activity_type != "validate_order" + or failure.exception_type != "NonRetryableError" + or not failure.non_retryable + ): + raise + return {"order_id": order["id"], "status": "rejected"} + return {"order_id": order["id"], "status": "accepted"} +``` + +Con la entrada `{"id": "order-123", "amount": 0}`, la actividad falla una vez y el workflow completa normalmente con `status: rejected`. Maneje solo el fallo de negocio previsto; el ejemplo vuelve a lanzar los demás. Continúe con otros pasos durables si lo requiere su flujo. Un `ActivityFailed` no capturado hace fallar el workflow. Conservar la causa Python no significa que la política examine esa cadena ni que todos los SDK expongan un objeto de causa idéntico. + ### Actividades síncronas y asíncronas {#activity-execution} Las actividades remotas normales con `def` se ejecutan en un grupo de hilos diff --git a/i18n/es/source-hashes.json b/i18n/es/source-hashes.json index a60d3f34fb1..6d659f850f1 100644 --- a/i18n/es/source-hashes.json +++ b/i18n/es/source-hashes.json @@ -82,7 +82,7 @@ "polyglot/polling-patterns.md": "30875099a23731c6c471b481a771378acc6ff57eed2fda4405a509057534c989", "polyglot/portable-worker-affinity.md": "40973c6a31a76845b4f95e9a505e68ad76abd5174bbdccf37993cdebc8904959", "polyglot/prerelease-history-migration.md": "a57f05f492228a8a67791f9152648a1e5f80856325b78777cf63b6ef49b7413c", - "polyglot/python.md": "a9fc8330cbc78953244b94a170197038a299a5a25fa78c38c7bfb451fd67d345", + "polyglot/python.md": "174acc170fc749284b5115f995996885c04056340d44bd334c6cee33e288abd1", "polyglot/rust-cloud-quickstart.md": "2303434fc9302a81a8381ad4836423ef9952eb3201a4574c432254e3cb68c5f3", "polyglot/rust.md": "32de05a771b5ee6d8cf54fdd408222372995ce29c8be63102abe58b977ac8ef0", "polyglot/server-api-reference.md": "e94af77a13b341d6fdc9941d4ee94e5053ab932d2e77069b238eb33351414e83", 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 20a4a8df3ae..780395e8eac 100644 --- a/i18n/fr/docusaurus-plugin-content-docs/current/polyglot/python.md +++ b/i18n/fr/docusaurus-plugin-content-docs/current/polyglot/python.md @@ -1053,6 +1053,42 @@ async def validate(data: dict) -> dict: return data ``` +### Erreurs métier terminales {#terminal-business-errors} + +`non_retryable_error_types` correspond exactement au nom court (`exception_type`) ou au nom de classe qualifié (`exception_class`) de l’exception **extérieure**. Par exemple, `ValidationError` ne correspond pas à `SpecificValidationError`, même si c’est une sous-classe. Un nom qualifié comme `app.errors.ValidationError` doit correspondre à la classe enregistrée. La politique ne parcourt ni les sous-classes ni les causes. `raise RuntimeError(...) from NonRetryableError(...)` signale le `RuntimeError` extérieur et peut encore être retenté. + +Pour convertir une exception métier connue, levez `NonRetryableError` comme exception extérieure et conservez la cause Python d’origine avec `cause=error`. L’échec de l’activité devient non réessayable sans liste de noms. Cela ne fait pas automatiquement échouer tout le workflow : son code reçoit `ActivityFailed` quand l’activité n’a plus de tentatives, soit parce que l’erreur est non réessayable, soit parce que le budget est épuisé. + +```python +from durable_workflow import ActivityFailed, NonRetryableError, activity, workflow + +@activity.defn(name="validate_order") +async def validate_order(order: dict) -> dict: + try: + if order.get("amount", 0) <= 0: + raise ValueError("amount must be positive") + except ValueError as error: + raise NonRetryableError("Order validation failed", cause=error) from error + return order + +@workflow.defn(name="place_order") +class PlaceOrderWorkflow: + def run(self, ctx, order: dict): + try: + yield ctx.schedule_activity("validate_order", [order]) + except ActivityFailed as failure: + if ( + failure.activity_type != "validate_order" + or failure.exception_type != "NonRetryableError" + or not failure.non_retryable + ): + raise + return {"order_id": order["id"], "status": "rejected"} + return {"order_id": order["id"], "status": "accepted"} +``` + +Pour l’entrée `{"id": "order-123", "amount": 0}`, l’activité échoue une fois et le workflow termine normalement avec `status: rejected`. Traitez seulement l’échec métier prévu ; l’exemple relance les autres. Poursuivez avec d’autres étapes durables si le parcours métier l’exige. Un `ActivityFailed` non intercepté fait échouer le workflow. Conserver une cause Python ne signifie ni que la politique inspecte cette chaîne ni que tous les SDK exposent le même objet de cause. + ### Activités synchrones et asynchrones {#activity-execution} Les activités distantes ordinaires définies avec `def` s’exécutent dans un pool diff --git a/i18n/fr/source-hashes.json b/i18n/fr/source-hashes.json index a60d3f34fb1..6d659f850f1 100644 --- a/i18n/fr/source-hashes.json +++ b/i18n/fr/source-hashes.json @@ -82,7 +82,7 @@ "polyglot/polling-patterns.md": "30875099a23731c6c471b481a771378acc6ff57eed2fda4405a509057534c989", "polyglot/portable-worker-affinity.md": "40973c6a31a76845b4f95e9a505e68ad76abd5174bbdccf37993cdebc8904959", "polyglot/prerelease-history-migration.md": "a57f05f492228a8a67791f9152648a1e5f80856325b78777cf63b6ef49b7413c", - "polyglot/python.md": "a9fc8330cbc78953244b94a170197038a299a5a25fa78c38c7bfb451fd67d345", + "polyglot/python.md": "174acc170fc749284b5115f995996885c04056340d44bd334c6cee33e288abd1", "polyglot/rust-cloud-quickstart.md": "2303434fc9302a81a8381ad4836423ef9952eb3201a4574c432254e3cb68c5f3", "polyglot/rust.md": "32de05a771b5ee6d8cf54fdd408222372995ce29c8be63102abe58b977ac8ef0", "polyglot/server-api-reference.md": "e94af77a13b341d6fdc9941d4ee94e5053ab932d2e77069b238eb33351414e83", 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 19fbe1c6a73..2979dae23d6 100644 --- a/i18n/ja/docusaurus-plugin-content-docs/current/polyglot/python.md +++ b/i18n/ja/docusaurus-plugin-content-docs/current/polyglot/python.md @@ -874,6 +874,42 @@ async def validate(data: dict) -> dict: return data ``` +### 終端となる業務エラー {#terminal-business-errors} + +`non_retryable_error_types` は、**最も外側の**例外の短い名前(`exception_type`)または完全修飾クラス名(`exception_class`)に完全一致します。例えば `ValidationError` は、サブクラスであっても `SpecificValidationError` には一致しません。`app.errors.ValidationError` のような修飾名は、記録されたクラス名に一致する必要があります。ポリシーはサブクラスや例外の原因をたどりません。`raise RuntimeError(...) from NonRetryableError(...)` は外側の `RuntimeError` を報告するため、再試行される場合があります。 + +既知の業務例外を変換するときは、`NonRetryableError` を外側の例外として送出し、`cause=error` で元の Python の原因を保持してください。名前の一覧に依存せず、アクティビティの失敗を再試行不可にします。これだけでワークフロー全体が自動的に失敗するわけではありません。再試行不可の失敗、または試行予算の消費によって再試行がなくなると、ワークフローコードは `ActivityFailed` を受け取ります。 + +```python +from durable_workflow import ActivityFailed, NonRetryableError, activity, workflow + +@activity.defn(name="validate_order") +async def validate_order(order: dict) -> dict: + try: + if order.get("amount", 0) <= 0: + raise ValueError("amount must be positive") + except ValueError as error: + raise NonRetryableError("Order validation failed", cause=error) from error + return order + +@workflow.defn(name="place_order") +class PlaceOrderWorkflow: + def run(self, ctx, order: dict): + try: + yield ctx.schedule_activity("validate_order", [order]) + except ActivityFailed as failure: + if ( + failure.activity_type != "validate_order" + or failure.exception_type != "NonRetryableError" + or not failure.non_retryable + ): + raise + return {"order_id": order["id"], "status": "rejected"} + return {"order_id": order["id"], "status": "accepted"} +``` + +入力が `{"id": "order-123", "amount": 0}` の場合、アクティビティは一回失敗し、ワークフローは `status: rejected` で正常に完了します。意図した業務失敗だけを処理してください。この例は他の失敗を再送出します。業務フローに必要なら後続の永続ステップへ進めます。未処理の `ActivityFailed` はワークフローを失敗させます。Python の原因を保持しても、再試行ポリシーがその連鎖を検査することや、すべての SDK が同じ原因オブジェクトを提供することを意味しません。 + ### 同期アクティビティと非同期アクティビティ {#activity-execution} 通常のリモート `def` アクティビティは、`max_concurrent_activity_tasks` で上限を設定した専用のスレッドプールで実行されます。 diff --git a/i18n/ja/source-hashes.json b/i18n/ja/source-hashes.json index a60d3f34fb1..6d659f850f1 100644 --- a/i18n/ja/source-hashes.json +++ b/i18n/ja/source-hashes.json @@ -82,7 +82,7 @@ "polyglot/polling-patterns.md": "30875099a23731c6c471b481a771378acc6ff57eed2fda4405a509057534c989", "polyglot/portable-worker-affinity.md": "40973c6a31a76845b4f95e9a505e68ad76abd5174bbdccf37993cdebc8904959", "polyglot/prerelease-history-migration.md": "a57f05f492228a8a67791f9152648a1e5f80856325b78777cf63b6ef49b7413c", - "polyglot/python.md": "a9fc8330cbc78953244b94a170197038a299a5a25fa78c38c7bfb451fd67d345", + "polyglot/python.md": "174acc170fc749284b5115f995996885c04056340d44bd334c6cee33e288abd1", "polyglot/rust-cloud-quickstart.md": "2303434fc9302a81a8381ad4836423ef9952eb3201a4574c432254e3cb68c5f3", "polyglot/rust.md": "32de05a771b5ee6d8cf54fdd408222372995ce29c8be63102abe58b977ac8ef0", "polyglot/server-api-reference.md": "e94af77a13b341d6fdc9941d4ee94e5053ab932d2e77069b238eb33351414e83", 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 d2d6fa30630..831d3c23bbc 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 @@ -1053,6 +1053,42 @@ async def validate(data: dict) -> dict: return data ``` +### Erros de negócio terminais {#terminal-business-errors} + +`non_retryable_error_types` compara exatamente o nome curto (`exception_type`) ou o nome qualificado da classe (`exception_class`) da exceção **externa**. Por exemplo, `ValidationError` não corresponde a `SpecificValidationError`, mesmo sendo uma subclasse. Um nome qualificado como `app.errors.ValidationError` precisa corresponder à classe registrada. A política não percorre subclasses nem causas. `raise RuntimeError(...) from NonRetryableError(...)` informa o `RuntimeError` externo e ainda pode ser repetido. + +Ao converter uma exceção de negócio conhecida, lance `NonRetryableError` como exceção externa e preserve a causa original do Python com `cause=error`. Isso marca a falha da atividade como não repetível sem depender de uma lista de nomes. Não faz o workflow inteiro falhar automaticamente: ele recebe `ActivityFailed` quando não há mais tentativas, por erro não repetível ou pelo esgotamento do orçamento de retries. + +```python +from durable_workflow import ActivityFailed, NonRetryableError, activity, workflow + +@activity.defn(name="validate_order") +async def validate_order(order: dict) -> dict: + try: + if order.get("amount", 0) <= 0: + raise ValueError("amount must be positive") + except ValueError as error: + raise NonRetryableError("Order validation failed", cause=error) from error + return order + +@workflow.defn(name="place_order") +class PlaceOrderWorkflow: + def run(self, ctx, order: dict): + try: + yield ctx.schedule_activity("validate_order", [order]) + except ActivityFailed as failure: + if ( + failure.activity_type != "validate_order" + or failure.exception_type != "NonRetryableError" + or not failure.non_retryable + ): + raise + return {"order_id": order["id"], "status": "rejected"} + return {"order_id": order["id"], "status": "accepted"} +``` + +Com a entrada `{"id": "order-123", "amount": 0}`, a atividade falha uma vez e o workflow conclui normalmente com `status: rejected`. Trate apenas a falha de negócio pretendida; o exemplo relança as demais. Continue com outros passos duráveis se o fluxo exigir. Um `ActivityFailed` não tratado faz o workflow falhar. Preservar uma causa Python não significa que a política examine essa cadeia nem que todos os SDKs exponham um objeto de causa idêntico. + ### Atividades síncronas e assíncronas {#activity-execution} Atividades remotas comuns com `def` executam em um pool de threads dedicado, diff --git a/i18n/pt-BR/source-hashes.json b/i18n/pt-BR/source-hashes.json index a60d3f34fb1..6d659f850f1 100644 --- a/i18n/pt-BR/source-hashes.json +++ b/i18n/pt-BR/source-hashes.json @@ -82,7 +82,7 @@ "polyglot/polling-patterns.md": "30875099a23731c6c471b481a771378acc6ff57eed2fda4405a509057534c989", "polyglot/portable-worker-affinity.md": "40973c6a31a76845b4f95e9a505e68ad76abd5174bbdccf37993cdebc8904959", "polyglot/prerelease-history-migration.md": "a57f05f492228a8a67791f9152648a1e5f80856325b78777cf63b6ef49b7413c", - "polyglot/python.md": "a9fc8330cbc78953244b94a170197038a299a5a25fa78c38c7bfb451fd67d345", + "polyglot/python.md": "174acc170fc749284b5115f995996885c04056340d44bd334c6cee33e288abd1", "polyglot/rust-cloud-quickstart.md": "2303434fc9302a81a8381ad4836423ef9952eb3201a4574c432254e3cb68c5f3", "polyglot/rust.md": "32de05a771b5ee6d8cf54fdd408222372995ce29c8be63102abe58b977ac8ef0", "polyglot/server-api-reference.md": "e94af77a13b341d6fdc9941d4ee94e5053ab932d2e77069b238eb33351414e83", 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 2df4e3ad415..66b9a4d651f 100644 --- a/i18n/uk/docusaurus-plugin-content-docs/current/polyglot/python.md +++ b/i18n/uk/docusaurus-plugin-content-docs/current/polyglot/python.md @@ -1038,6 +1038,42 @@ async def validate(data: dict) -> dict: return data ``` +### Термінальні бізнесові помилки {#terminal-business-errors} + +`non_retryable_error_types` точно зіставляє коротку назву (`exception_type`) або повну назву класу (`exception_class`) **зовнішнього** винятку. Наприклад, `ValidationError` не відповідає `SpecificValidationError`, навіть якщо це підклас. Повна назва на кшталт `app.errors.ValidationError` має точно збігатися із записаним класом. Політика не обходить підкласи чи причини винятків. `raise RuntimeError(...) from NonRetryableError(...)` повідомляє зовнішній `RuntimeError`, тому спроба може повторюватися. + +Перетворюючи відомий бізнесовий виняток, піднімайте `NonRetryableError` як зовнішній виняток і зберігайте початкову причину Python через `cause=error`. Це позначає помилку activity як неповторювану без списку назв. Сам workflow не завершується з помилкою автоматично: він отримує `ActivityFailed`, коли повторів activity більше не буде через неповторювану помилку або вичерпаний бюджет спроб. + +```python +from durable_workflow import ActivityFailed, NonRetryableError, activity, workflow + +@activity.defn(name="validate_order") +async def validate_order(order: dict) -> dict: + try: + if order.get("amount", 0) <= 0: + raise ValueError("amount must be positive") + except ValueError as error: + raise NonRetryableError("Order validation failed", cause=error) from error + return order + +@workflow.defn(name="place_order") +class PlaceOrderWorkflow: + def run(self, ctx, order: dict): + try: + yield ctx.schedule_activity("validate_order", [order]) + except ActivityFailed as failure: + if ( + failure.activity_type != "validate_order" + or failure.exception_type != "NonRetryableError" + or not failure.non_retryable + ): + raise + return {"order_id": order["id"], "status": "rejected"} + return {"order_id": order["id"], "status": "accepted"} +``` + +Для аргументу `{"id": "order-123", "amount": 0}` activity помиляється один раз, а workflow нормально завершується зі `status: rejected`. Обробляйте лише потрібну бізнесову помилку; приклад повторно піднімає інші помилки. За потреби продовжуйте іншими довговічними кроками. Необроблений `ActivityFailed` завершує workflow з помилкою. Збереження причини Python не означає, що політика повторів аналізує цей ланцюжок або що кожний SDK надає однаковий об’єкт причини. + ### Синхронні та асинхронні активності {#activity-execution} Звичайні віддалені активності з `def` виконуються в окремому пулі потоків, diff --git a/i18n/uk/source-hashes.json b/i18n/uk/source-hashes.json index a60d3f34fb1..6d659f850f1 100644 --- a/i18n/uk/source-hashes.json +++ b/i18n/uk/source-hashes.json @@ -82,7 +82,7 @@ "polyglot/polling-patterns.md": "30875099a23731c6c471b481a771378acc6ff57eed2fda4405a509057534c989", "polyglot/portable-worker-affinity.md": "40973c6a31a76845b4f95e9a505e68ad76abd5174bbdccf37993cdebc8904959", "polyglot/prerelease-history-migration.md": "a57f05f492228a8a67791f9152648a1e5f80856325b78777cf63b6ef49b7413c", - "polyglot/python.md": "a9fc8330cbc78953244b94a170197038a299a5a25fa78c38c7bfb451fd67d345", + "polyglot/python.md": "174acc170fc749284b5115f995996885c04056340d44bd334c6cee33e288abd1", "polyglot/rust-cloud-quickstart.md": "2303434fc9302a81a8381ad4836423ef9952eb3201a4574c432254e3cb68c5f3", "polyglot/rust.md": "32de05a771b5ee6d8cf54fdd408222372995ce29c8be63102abe58b977ac8ef0", "polyglot/server-api-reference.md": "e94af77a13b341d6fdc9941d4ee94e5053ab932d2e77069b238eb33351414e83", 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 7ffcaf59fe5..b9947efca19 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 @@ -860,6 +860,42 @@ async def validate(data: dict) -> dict: return data ``` +### 终结的业务错误 {#terminal-business-errors} + +`non_retryable_error_types` 精确匹配**最外层**异常的短名称(`exception_type`)或完全限定类名(`exception_class`)。例如,即使 `SpecificValidationError` 是子类,`ValidationError` 也不会匹配它。`app.errors.ValidationError` 这样的限定名称必须与已记录的类名一致。策略不会遍历子类或异常原因。`raise RuntimeError(...) from NonRetryableError(...)` 报告的是外层 `RuntimeError`,因此仍可能重试。 + +转换已知业务异常时,应将 `NonRetryableError` 作为最外层异常,并使用 `cause=error` 保留原始 Python 原因。这无需名称列表就能将活动失败标记为不可重试,但不会自动使整个工作流失败。活动因不可重试错误或重试预算耗尽而不再重试时,工作流代码会收到 `ActivityFailed`。 + +```python +from durable_workflow import ActivityFailed, NonRetryableError, activity, workflow + +@activity.defn(name="validate_order") +async def validate_order(order: dict) -> dict: + try: + if order.get("amount", 0) <= 0: + raise ValueError("amount must be positive") + except ValueError as error: + raise NonRetryableError("Order validation failed", cause=error) from error + return order + +@workflow.defn(name="place_order") +class PlaceOrderWorkflow: + def run(self, ctx, order: dict): + try: + yield ctx.schedule_activity("validate_order", [order]) + except ActivityFailed as failure: + if ( + failure.activity_type != "validate_order" + or failure.exception_type != "NonRetryableError" + or not failure.non_retryable + ): + raise + return {"order_id": order["id"], "status": "rejected"} + return {"order_id": order["id"], "status": "accepted"} +``` + +输入 `{"id": "order-123", "amount": 0}` 时,活动失败一次,工作流随后以 `status: rejected` 正常完成。只处理预期的业务失败;示例会重新抛出其他失败。业务流程需要时,可以继续执行后续持久步骤。未捕获的 `ActivityFailed` 会使工作流失败。保留 Python 原因不代表重试策略会检查该异常链,也不代表所有 SDK 都暴露完全相同的原因对象。 + ### 同步与异步活动 {#activity-execution} 普通的远程 `def` 活动在专用线程池中执行,线程数受 `max_concurrent_activity_tasks` 限制。 diff --git a/i18n/zh-Hans/source-hashes.json b/i18n/zh-Hans/source-hashes.json index a60d3f34fb1..6d659f850f1 100644 --- a/i18n/zh-Hans/source-hashes.json +++ b/i18n/zh-Hans/source-hashes.json @@ -82,7 +82,7 @@ "polyglot/polling-patterns.md": "30875099a23731c6c471b481a771378acc6ff57eed2fda4405a509057534c989", "polyglot/portable-worker-affinity.md": "40973c6a31a76845b4f95e9a505e68ad76abd5174bbdccf37993cdebc8904959", "polyglot/prerelease-history-migration.md": "a57f05f492228a8a67791f9152648a1e5f80856325b78777cf63b6ef49b7413c", - "polyglot/python.md": "a9fc8330cbc78953244b94a170197038a299a5a25fa78c38c7bfb451fd67d345", + "polyglot/python.md": "174acc170fc749284b5115f995996885c04056340d44bd334c6cee33e288abd1", "polyglot/rust-cloud-quickstart.md": "2303434fc9302a81a8381ad4836423ef9952eb3201a4574c432254e3cb68c5f3", "polyglot/rust.md": "32de05a771b5ee6d8cf54fdd408222372995ce29c8be63102abe58b977ac8ef0", "polyglot/server-api-reference.md": "e94af77a13b341d6fdc9941d4ee94e5053ab932d2e77069b238eb33351414e83",