Skip to content

Commit 1b9d157

Browse files
authored
Merge pull request #192 from Azure/dev
Promote to Master for PyPi release
2 parents d53ec10 + 65188f2 commit 1b9d157

24 files changed

+651
-117
lines changed

azure/durable_functions/models/DurableOrchestrationClient.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ async def wait_for_completion_or_create_check_status_response(
442442
lambda: self._create_http_response(500, status.to_json()),
443443
}
444444

445-
result = switch_statement.get(OrchestrationRuntimeStatus(status.runtime_status))
445+
result = switch_statement.get(status.runtime_status)
446446
if result:
447447
return result()
448448

@@ -546,3 +546,57 @@ def _get_raise_event_url(
546546
request_url += "?" + "&".join(query)
547547

548548
return request_url
549+
550+
async def rewind(self,
551+
instance_id: str,
552+
reason: str,
553+
task_hub_name: Optional[str] = None,
554+
connection_name: Optional[str] = None):
555+
"""Return / "rewind" a failed orchestration instance to a prior "healthy" state.
556+
557+
Parameters
558+
----------
559+
instance_id: str
560+
The ID of the orchestration instance to rewind.
561+
reason: str
562+
The reason for rewinding the orchestration instance.
563+
task_hub_name: Optional[str]
564+
The TaskHub of the orchestration to rewind
565+
connection_name: Optional[str]
566+
Name of the application setting containing the storage
567+
connection string to use.
568+
569+
Raises
570+
------
571+
Exception:
572+
In case of a failure, it reports the reason for the exception
573+
"""
574+
request_url: str = ""
575+
if self._orchestration_bindings.rpc_base_url:
576+
path = f"instances/{instance_id}/rewind?reason={reason}"
577+
query: List[str] = []
578+
if not (task_hub_name is None):
579+
query.append(f"taskHub={task_hub_name}")
580+
if not (connection_name is None):
581+
query.append(f"connection={connection_name}")
582+
if len(query) > 0:
583+
path += "&" + "&".join(query)
584+
585+
request_url = f"{self._orchestration_bindings.rpc_base_url}" + path
586+
else:
587+
raise Exception("The Python SDK only supports RPC endpoints."
588+
+ "Please remove the `localRpcEnabled` setting from host.json")
589+
590+
response = await self._post_async_request(request_url, None)
591+
status: int = response[0]
592+
if status == 200 or status == 202:
593+
return
594+
elif status == 404:
595+
ex_msg = f"No instance with ID {instance_id} found."
596+
raise Exception(ex_msg)
597+
elif status == 410:
598+
ex_msg = "The rewind operation is only supported on failed orchestration instances."
599+
raise Exception(ex_msg)
600+
else:
601+
ex_msg = response[1]
602+
raise Exception(ex_msg)

azure/durable_functions/models/DurableOrchestrationStatus.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from datetime import datetime
22
from dateutil.parser import parse as dt_parse
33
from typing import Any, List, Dict, Optional, Union
4-
4+
from .OrchestrationRuntimeStatus import OrchestrationRuntimeStatus
55
from .utils.json_utils import add_attrib, add_datetime_attrib
66

77

@@ -15,7 +15,8 @@ class DurableOrchestrationStatus:
1515
def __init__(self, name: Optional[str] = None, instanceId: Optional[str] = None,
1616
createdTime: Optional[str] = None, lastUpdatedTime: Optional[str] = None,
1717
input: Optional[Any] = None, output: Optional[Any] = None,
18-
runtimeStatus: Optional[str] = None, customStatus: Optional[Any] = None,
18+
runtimeStatus: Optional[OrchestrationRuntimeStatus] = None,
19+
customStatus: Optional[Any] = None,
1920
history: Optional[List[Any]] = None,
2021
**kwargs):
2122
self._name: Optional[str] = name
@@ -26,7 +27,9 @@ def __init__(self, name: Optional[str] = None, instanceId: Optional[str] = None,
2627
if lastUpdatedTime is not None else None
2728
self._input: Any = input
2829
self._output: Any = output
29-
self._runtime_status: Optional[str] = runtimeStatus # TODO: GH issue 178
30+
self._runtime_status: Optional[OrchestrationRuntimeStatus] = runtimeStatus
31+
if runtimeStatus is not None:
32+
self._runtime_status = OrchestrationRuntimeStatus(runtimeStatus)
3033
self._custom_status: Any = customStatus
3134
self._history: Optional[List[Any]] = history
3235
if kwargs is not None:
@@ -82,7 +85,8 @@ def to_json(self) -> Dict[str, Union[int, str]]:
8285
add_datetime_attrib(json, self, 'last_updated_time', 'lastUpdatedTime')
8386
add_attrib(json, self, 'output')
8487
add_attrib(json, self, 'input_', 'input')
85-
add_attrib(json, self, 'runtime_status', 'runtimeStatus')
88+
if self.runtime_status is not None:
89+
json["runtimeStatus"] = self.runtime_status.name
8690
add_attrib(json, self, 'custom_status', 'customStatus')
8791
add_attrib(json, self, 'history')
8892
return json
@@ -129,7 +133,7 @@ def output(self) -> Any:
129133
return self._output
130134

131135
@property
132-
def runtime_status(self) -> Optional[str]:
136+
def runtime_status(self) -> Optional[OrchestrationRuntimeStatus]:
133137
"""Get the runtime status of the orchestration instance."""
134138
return self._runtime_status
135139

azure/durable_functions/models/Task.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@ class Task:
1717
"""
1818

1919
def __init__(self, is_completed, is_faulted, action,
20-
result=None, timestamp=None, id_=None, exc=None):
20+
result=None, timestamp=None, id_=None, exc=None, is_played=False):
2121
self._is_completed: bool = is_completed
2222
self._is_faulted: bool = is_faulted
2323
self._action: Action = action
2424
self._result = result
2525
self._timestamp: datetime = timestamp
2626
self._id = id_
2727
self._exception = exc
28+
self._is_played = is_played
2829

2930
@property
3031
def is_completed(self) -> bool:

azure/durable_functions/models/TaskSet.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@ class TaskSet:
1717
"""
1818

1919
def __init__(self, is_completed, actions, result, is_faulted=False,
20-
timestamp=None, exception=None):
20+
timestamp=None, exception=None, is_played=False):
2121
self._is_completed: bool = is_completed
2222
self._actions: List[Action] = actions
2323
self._result = result
2424
self._is_faulted: bool = is_faulted
2525
self._timestamp: datetime = timestamp
2626
self._exception = exception
27+
self._is_played = is_played
2728

2829
@property
2930
def is_completed(self) -> bool:

azure/durable_functions/models/utils/http_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,5 @@ async def delete_async_request(url: str) -> List[Union[int, Any]]:
6565
"""
6666
async with aiohttp.ClientSession() as session:
6767
async with session.delete(url) as response:
68-
data = await response.json()
68+
data = await response.json(content_type=None)
6969
return [response.status, data]

azure/durable_functions/orchestrator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def handle(self, context: DurableOrchestrationContext):
9090
continue
9191

9292
self._reset_timestamp()
93+
self.durable_context._is_replaying = generation_state._is_played
9394
generation_state = self._generate_next(generation_state)
9495

9596
except StopIteration as sie:

azure/durable_functions/tasks/call_activity.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def call_activity_task(
4040
is_completed=True,
4141
is_faulted=False,
4242
action=new_action,
43+
is_played=task_completed._is_played,
4344
result=parse_history_event(task_completed),
4445
timestamp=task_completed.timestamp,
4546
id_=task_completed.TaskScheduledId)
@@ -49,6 +50,7 @@ def call_activity_task(
4950
is_completed=True,
5051
is_faulted=True,
5152
action=new_action,
53+
is_played=task_failed._is_played,
5254
result=task_failed.Reason,
5355
timestamp=task_failed.timestamp,
5456
id_=task_failed.TaskScheduledId,
Lines changed: 10 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
from typing import List, Any
22

3-
from .task_utilities import find_task_scheduled, \
4-
find_task_retry_timer_created, set_processed, parse_history_event, \
5-
find_task_completed, find_task_failed, find_task_retry_timer_fired
3+
from .task_utilities import get_retried_task
64
from ..models.RetryOptions import RetryOptions
75
from ..models.Task import (
86
Task)
97
from ..models.actions.CallActivityWithRetryAction import \
108
CallActivityWithRetryAction
11-
from ..models.history import HistoryEvent
9+
from ..models.history import HistoryEvent, HistoryEventType
1210

1311

1412
def call_activity_with_retry_task(
@@ -37,38 +35,12 @@ def call_activity_with_retry_task(
3735
"""
3836
new_action = CallActivityWithRetryAction(
3937
function_name=name, retry_options=retry_options, input_=input_)
40-
for attempt in range(retry_options.max_number_of_attempts):
41-
task_scheduled = find_task_scheduled(state, name)
42-
task_completed = find_task_completed(state, task_scheduled)
43-
task_failed = find_task_failed(state, task_scheduled)
44-
task_retry_timer = find_task_retry_timer_created(state, task_failed)
45-
task_retry_timer_fired = find_task_retry_timer_fired(
46-
state, task_retry_timer)
47-
set_processed([task_scheduled, task_completed,
48-
task_failed, task_retry_timer, task_retry_timer_fired])
4938

50-
if not task_scheduled:
51-
break
52-
53-
if task_completed:
54-
return Task(
55-
is_completed=True,
56-
is_faulted=False,
57-
action=new_action,
58-
result=parse_history_event(task_completed),
59-
timestamp=task_completed.timestamp,
60-
id_=task_completed.TaskScheduledId)
61-
62-
if task_failed and task_retry_timer and attempt + 1 >= \
63-
retry_options.max_number_of_attempts:
64-
return Task(
65-
is_completed=True,
66-
is_faulted=True,
67-
action=new_action,
68-
timestamp=task_failed.timestamp,
69-
id_=task_failed.TaskScheduledId,
70-
exc=Exception(
71-
f"{task_failed.Reason} \n {task_failed.Details}")
72-
)
73-
74-
return Task(is_completed=False, is_faulted=False, action=new_action)
39+
return get_retried_task(
40+
state=state,
41+
max_number_of_attempts=retry_options.max_number_of_attempts,
42+
scheduled_type=HistoryEventType.TASK_SCHEDULED,
43+
completed_type=HistoryEventType.TASK_COMPLETED,
44+
failed_type=HistoryEventType.TASK_FAILED,
45+
action=new_action
46+
)

azure/durable_functions/tasks/call_http.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def call_http(state: List[HistoryEvent], method: str, uri: str, content: Optiona
5757
is_completed=True,
5858
is_faulted=False,
5959
action=new_action,
60+
is_played=task_completed._is_played,
6061
result=parse_history_event(task_completed),
6162
timestamp=task_completed.timestamp,
6263
id_=task_completed.TaskScheduledId)
@@ -66,6 +67,7 @@ def call_http(state: List[HistoryEvent], method: str, uri: str, content: Optiona
6667
is_completed=True,
6768
is_faulted=True,
6869
action=new_action,
70+
is_played=task_failed._is_played,
6971
result=task_failed.Reason,
7072
timestamp=task_failed.timestamp,
7173
id_=task_failed.TaskScheduledId,

azure/durable_functions/tasks/call_suborchestrator.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def call_sub_orchestrator_task(
4848
is_completed=True,
4949
is_faulted=False,
5050
action=new_action,
51+
is_played=task_completed._is_played,
5152
result=parse_history_event(task_completed),
5253
timestamp=task_completed.timestamp,
5354
id_=task_completed.TaskScheduledId)
@@ -57,6 +58,7 @@ def call_sub_orchestrator_task(
5758
is_completed=True,
5859
is_faulted=True,
5960
action=new_action,
61+
is_played=task_failed._is_played,
6062
result=task_failed.Reason,
6163
timestamp=task_failed.timestamp,
6264
id_=task_failed.TaskScheduledId,

0 commit comments

Comments
 (0)