You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+89-2Lines changed: 89 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -126,10 +126,97 @@ Orchestrations can be continued as new using the `continue_as_new` API. This API
126
126
127
127
Orchestrations can be suspended using the `suspend_orchestration` client API and will remain suspended until resumed using the `resume_orchestration` client API. A suspended orchestration will stop processing new events, but will continue to buffer any that happen to arrive until resumed, ensuring that no data is lost. An orchestration can also be terminated using the `terminate_orchestration` client API. Terminated orchestrations will stop processing new events and will discard any buffered events.
128
128
129
-
### Retry policies (TODO)
129
+
### Retry policies
130
130
131
131
Orchestrations can specify retry policies for activities and sub-orchestrations. These policies control how many times and how frequently an activity or sub-orchestration will be retried in the event of a transient error.
132
132
133
+
#### Creating a retry policy
134
+
135
+
```python
136
+
from datetime import timedelta
137
+
from durabletask import task
138
+
139
+
retry_policy = task.RetryPolicy(
140
+
first_retry_interval=timedelta(seconds=1), # Initial delay before first retry
141
+
max_number_of_attempts=5, # Maximum total attempts (includes first attempt)
142
+
backoff_coefficient=2.0, # Exponential backoff multiplier (must be >= 1)
143
+
max_retry_interval=timedelta(seconds=30), # Cap on retry delay
144
+
retry_timeout=timedelta(minutes=5), # Total time limit for all retries (optional)
145
+
)
146
+
```
147
+
148
+
**Notes:**
149
+
-`max_number_of_attempts`**includes the initial attempt**. For example, `max_number_of_attempts=5` means 1 initial attempt + up to 4 retries.
150
+
-`retry_timeout` is optional. If omitted or set to `None`, retries continue until `max_number_of_attempts` is reached.
-`non_retryable_error_types` (optional) can specify additional exception types to treat as non-retryable (e.g., `[ValueError, TypeError]`). `NonRetryableError` is always non-retryable regardless of this setting.
153
+
154
+
#### Using retry policies
155
+
156
+
Apply retry policies to activities or sub-orchestrations:
result =yield ctx.call_activity(my_activity, input=data, retry_policy=retry_policy)
162
+
163
+
# Retry a sub-orchestration
164
+
result =yield ctx.call_sub_orchestrator(child_orchestrator, input=data, retry_policy=retry_policy)
165
+
```
166
+
167
+
#### Non-retryable errors
168
+
169
+
For errors that should not be retried (e.g., validation failures, permanent errors), raise a `NonRetryableError`:
170
+
171
+
```python
172
+
from durabletask.task import NonRetryableError
173
+
174
+
defmy_activity(ctx: task.ActivityContext, input):
175
+
ifinputisNone:
176
+
# This error will bypass retry logic and fail immediately
177
+
raise NonRetryableError("Input cannot be None")
178
+
179
+
# Transient errors (network, timeouts, etc.) will be retried
180
+
return call_external_service(input)
181
+
```
182
+
183
+
Even with a retry policy configured, `NonRetryableError` will fail immediately without retrying.
184
+
185
+
#### Error type matching behavior
186
+
187
+
**Important:** Error type matching uses **exact class name comparison**, not `isinstance()` checks. This is because exception objects are serialized to gRPC protobuf messages, where only the class name (as a string) survives serialization.
188
+
189
+
**Key implications:**
190
+
191
+
-**Not inheritance-aware**: If you specify `ValueError` in `non_retryable_error_types`, it will only match exceptions with the exact class name `"ValueError"`. A custom subclass like `CustomValueError(ValueError)` will NOT match.
192
+
-**Workaround**: List all exception types explicitly, including subclasses you want to handle.
193
+
-**Built-in exception**: `NonRetryableError` is always treated as non-retryable, matched by the name `"NonRetryableError"`.
194
+
195
+
**Example:**
196
+
197
+
```python
198
+
from datetime import timedelta
199
+
from durabletask import task
200
+
201
+
# Custom exception hierarchy
202
+
classValidationError(ValueError):
203
+
pass
204
+
205
+
# This policy ONLY matches exact "ValueError" by name
206
+
retry_policy = task.RetryPolicy(
207
+
first_retry_interval=timedelta(seconds=1),
208
+
max_number_of_attempts=3,
209
+
non_retryable_error_types=[ValueError] # Won't match ValidationError subclass!
210
+
)
211
+
212
+
# To handle both, list them explicitly:
213
+
retry_policy = task.RetryPolicy(
214
+
first_retry_interval=timedelta(seconds=1),
215
+
max_number_of_attempts=3,
216
+
non_retryable_error_types=[ValueError, ValidationError] # Both converted to name strings
217
+
)
218
+
```
219
+
133
220
## Getting Started
134
221
135
222
### Prerequisites
@@ -194,7 +281,7 @@ Certain aspects like multi-app activities require the full dapr runtime to be ru
194
281
```shell
195
282
dapr init ||true
196
283
197
-
dapr run --app-id test-app --dapr-grpc-port 4001 --components-path ./examples/components/
284
+
dapr run --app-id test-app --dapr-grpc-port 4001 --resources-path ./examples/components/
198
285
```
199
286
200
287
To run the E2E tests on a specific python version (eg: 3.11), run the following command from the project root:
0 commit comments