azure-functions-durable 1.2.10__py3-none-any.whl → 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- azure/durable_functions/decorators/durable_app.py +11 -0
- azure/durable_functions/entity.py +7 -3
- azure/durable_functions/models/DurableOrchestrationClient.py +33 -2
- azure/durable_functions/models/DurableOrchestrationContext.py +46 -7
- azure/durable_functions/models/ReplaySchema.py +1 -0
- azure/durable_functions/models/Task.py +117 -3
- azure/durable_functions/models/TaskOrchestrationExecutor.py +11 -5
- azure/durable_functions/models/__init__.py +3 -1
- azure/durable_functions/models/entities/OperationResult.py +18 -0
- azure/durable_functions/models/utils/http_utils.py +14 -3
- azure/durable_functions/models/utils/json_utils.py +40 -0
- azure/durable_functions/orchestrator.py +2 -0
- azure/durable_functions/testing/OrchestratorGeneratorWrapper.py +42 -0
- azure/durable_functions/testing/__init__.py +6 -0
- {azure_functions_durable-1.2.10.dist-info → azure_functions_durable-1.3.0.dist-info}/METADATA +7 -6
- {azure_functions_durable-1.2.10.dist-info → azure_functions_durable-1.3.0.dist-info}/RECORD +22 -19
- {azure_functions_durable-1.2.10.dist-info → azure_functions_durable-1.3.0.dist-info}/WHEEL +1 -1
- tests/models/test_DurableOrchestrationClient.py +1 -1
- tests/orchestrator/test_entity.py +2 -0
- tests/tasks/test_long_timers.py +70 -0
- {azure_functions_durable-1.2.10.dist-info → azure_functions_durable-1.3.0.dist-info}/LICENSE +0 -0
- {azure_functions_durable-1.2.10.dist-info → azure_functions_durable-1.3.0.dist-info}/top_level.txt +0 -0
|
@@ -198,6 +198,17 @@ class Blueprint(TriggerApi, BindingApi, SettingsApi):
|
|
|
198
198
|
# Invoke user code with rich DF Client binding
|
|
199
199
|
return await user_code(*args, **kwargs)
|
|
200
200
|
|
|
201
|
+
# Todo: This feels awkward - however, there are two reasons that I can't naively implement
|
|
202
|
+
# this in the same way as entities and orchestrators:
|
|
203
|
+
# 1. We intentionally wrap this exported signature with @wraps, to preserve the original
|
|
204
|
+
# signature of the user code. This means that we can't just assign a new object to the
|
|
205
|
+
# fb._function._func, as that would overwrite the original signature.
|
|
206
|
+
# 2. I have not yet fully tested the behavior of overriding __call__ on an object with an
|
|
207
|
+
# async method.
|
|
208
|
+
# Here we lose type hinting and auto-documentation - not great. Need to find a better way
|
|
209
|
+
# to do this.
|
|
210
|
+
df_client_middleware.client_function = fb._function._func
|
|
211
|
+
|
|
201
212
|
user_code_with_rich_client = df_client_middleware
|
|
202
213
|
fb._function._func = user_code_with_rich_client
|
|
203
214
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from .models import DurableEntityContext
|
|
2
2
|
from .models.entities import OperationResult, EntityState
|
|
3
|
-
from datetime import datetime
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
4
|
from typing import Callable, Any, List, Dict
|
|
5
5
|
|
|
6
6
|
|
|
@@ -49,7 +49,7 @@ class Entity:
|
|
|
49
49
|
for operation_data in batch:
|
|
50
50
|
result: Any = None
|
|
51
51
|
is_error: bool = False
|
|
52
|
-
start_time: datetime = datetime.now()
|
|
52
|
+
start_time: datetime = datetime.now(timezone.utc)
|
|
53
53
|
|
|
54
54
|
try:
|
|
55
55
|
# populate context
|
|
@@ -74,6 +74,7 @@ class Entity:
|
|
|
74
74
|
operation_result = OperationResult(
|
|
75
75
|
is_error=is_error,
|
|
76
76
|
duration=duration,
|
|
77
|
+
execution_start_time_ms=int(start_time.timestamp() * 1000),
|
|
77
78
|
result=result
|
|
78
79
|
)
|
|
79
80
|
response.results.append(operation_result)
|
|
@@ -104,6 +105,9 @@ class Entity:
|
|
|
104
105
|
context_body = context
|
|
105
106
|
ctx, batch = DurableEntityContext.from_json(context_body)
|
|
106
107
|
return Entity(fn).handle(ctx, batch)
|
|
108
|
+
|
|
109
|
+
handle.entity_function = fn
|
|
110
|
+
|
|
107
111
|
return handle
|
|
108
112
|
|
|
109
113
|
def _elapsed_milliseconds_since(self, start_time: datetime) -> int:
|
|
@@ -119,7 +123,7 @@ class Entity:
|
|
|
119
123
|
int
|
|
120
124
|
The time, in millseconds, from start_time to now
|
|
121
125
|
"""
|
|
122
|
-
end_time = datetime.now()
|
|
126
|
+
end_time = datetime.now(timezone.utc)
|
|
123
127
|
time_diff = end_time - start_time
|
|
124
128
|
elapsed_time = int(time_diff.total_seconds() * 1000)
|
|
125
129
|
return elapsed_time
|
|
@@ -4,6 +4,7 @@ from typing import List, Any, Optional, Dict, Union
|
|
|
4
4
|
from time import time
|
|
5
5
|
from asyncio import sleep
|
|
6
6
|
from urllib.parse import urlparse, quote
|
|
7
|
+
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
|
|
7
8
|
|
|
8
9
|
import azure.functions as func
|
|
9
10
|
|
|
@@ -71,8 +72,13 @@ class DurableOrchestrationClient:
|
|
|
71
72
|
request_url = self._get_start_new_url(
|
|
72
73
|
instance_id=instance_id, orchestration_function_name=orchestration_function_name)
|
|
73
74
|
|
|
75
|
+
trace_parent, trace_state = DurableOrchestrationClient._get_current_activity_context()
|
|
76
|
+
|
|
74
77
|
response: List[Any] = await self._post_async_request(
|
|
75
|
-
request_url,
|
|
78
|
+
request_url,
|
|
79
|
+
self._get_json_input(client_input),
|
|
80
|
+
trace_parent,
|
|
81
|
+
trace_state)
|
|
76
82
|
|
|
77
83
|
status_code: int = response[0]
|
|
78
84
|
if status_code <= 202 and response[1]:
|
|
@@ -545,9 +551,14 @@ class DurableOrchestrationClient:
|
|
|
545
551
|
entity_Id=entityId)
|
|
546
552
|
|
|
547
553
|
request_url = options.to_url(self._orchestration_bindings.rpc_base_url)
|
|
554
|
+
|
|
555
|
+
trace_parent, trace_state = DurableOrchestrationClient._get_current_activity_context()
|
|
556
|
+
|
|
548
557
|
response = await self._post_async_request(
|
|
549
558
|
request_url,
|
|
550
|
-
json.dumps(operation_input) if operation_input else None
|
|
559
|
+
json.dumps(operation_input) if operation_input else None,
|
|
560
|
+
trace_parent,
|
|
561
|
+
trace_state)
|
|
551
562
|
|
|
552
563
|
switch_statement = {
|
|
553
564
|
202: lambda: None # signal accepted
|
|
@@ -779,3 +790,23 @@ class DurableOrchestrationClient:
|
|
|
779
790
|
error_message = has_error_message()
|
|
780
791
|
if error_message:
|
|
781
792
|
raise Exception(error_message)
|
|
793
|
+
|
|
794
|
+
"""Gets the current trace activity traceparent and tracestate
|
|
795
|
+
|
|
796
|
+
Returns
|
|
797
|
+
-------
|
|
798
|
+
tuple[str, str]
|
|
799
|
+
A tuple containing the (traceparent, tracestate)
|
|
800
|
+
"""
|
|
801
|
+
@staticmethod
|
|
802
|
+
def _get_current_activity_context() -> tuple[str, str]:
|
|
803
|
+
carrier = {}
|
|
804
|
+
|
|
805
|
+
# Inject the current trace context into the carrier
|
|
806
|
+
TraceContextTextMapPropagator().inject(carrier)
|
|
807
|
+
|
|
808
|
+
# Extract the traceparent and optionally the tracestate
|
|
809
|
+
trace_parent = carrier.get("traceparent")
|
|
810
|
+
trace_state = carrier.get("tracestate")
|
|
811
|
+
|
|
812
|
+
return trace_parent, trace_state
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from collections import defaultdict
|
|
2
2
|
from azure.durable_functions.models.actions.SignalEntityAction import SignalEntityAction
|
|
3
3
|
from azure.durable_functions.models.actions.CallEntityAction import CallEntityAction
|
|
4
|
-
from azure.durable_functions.models.Task import TaskBase, TimerTask
|
|
4
|
+
from azure.durable_functions.models.Task import LongTimerTask, TaskBase, TimerTask
|
|
5
5
|
from azure.durable_functions.models.actions.CallHttpAction import CallHttpAction
|
|
6
6
|
from azure.durable_functions.models.DurableHttpRequest import DurableHttpRequest
|
|
7
7
|
from azure.durable_functions.models.actions.CallSubOrchestratorWithRetryAction import \
|
|
@@ -26,6 +26,8 @@ from typing import DefaultDict, List, Any, Dict, Optional, Tuple, Union, Callabl
|
|
|
26
26
|
from uuid import UUID, uuid5, NAMESPACE_URL, NAMESPACE_OID
|
|
27
27
|
from datetime import timezone
|
|
28
28
|
|
|
29
|
+
from azure.durable_functions.models.utils.json_utils import parse_timespan_attrib
|
|
30
|
+
|
|
29
31
|
from .RetryOptions import RetryOptions
|
|
30
32
|
from .FunctionContext import FunctionContext
|
|
31
33
|
from .history import HistoryEvent, HistoryEventType
|
|
@@ -48,11 +50,22 @@ class DurableOrchestrationContext:
|
|
|
48
50
|
# parameter names are as defined by JSON schema and do not conform to PEP8 naming conventions
|
|
49
51
|
def __init__(self,
|
|
50
52
|
history: List[Dict[Any, Any]], instanceId: str, isReplaying: bool,
|
|
51
|
-
parentInstanceId: str, input: Any = None, upperSchemaVersion: int = 0,
|
|
53
|
+
parentInstanceId: str, input: Any = None, upperSchemaVersion: int = 0,
|
|
54
|
+
maximumShortTimerDuration: str = None,
|
|
55
|
+
longRunningTimerIntervalDuration: str = None, upperSchemaVersionNew: int = None,
|
|
56
|
+
**kwargs):
|
|
52
57
|
self._histories: List[HistoryEvent] = [HistoryEvent(**he) for he in history]
|
|
53
58
|
self._instance_id: str = instanceId
|
|
54
59
|
self._is_replaying: bool = isReplaying
|
|
55
60
|
self._parent_instance_id: str = parentInstanceId
|
|
61
|
+
self._maximum_short_timer_duration: datetime.timedelta = None
|
|
62
|
+
if maximumShortTimerDuration is not None:
|
|
63
|
+
max_short_duration = parse_timespan_attrib(maximumShortTimerDuration)
|
|
64
|
+
self._maximum_short_timer_duration = max_short_duration
|
|
65
|
+
self._long_timer_interval_duration: datetime.timedelta = None
|
|
66
|
+
if longRunningTimerIntervalDuration is not None:
|
|
67
|
+
long_interval_duration = parse_timespan_attrib(longRunningTimerIntervalDuration)
|
|
68
|
+
self._long_timer_interval_duration = long_interval_duration
|
|
56
69
|
self._custom_status: Any = None
|
|
57
70
|
self._new_uuid_counter: int = 0
|
|
58
71
|
self._sub_orchestrator_counter: int = 0
|
|
@@ -66,6 +79,13 @@ class DurableOrchestrationContext:
|
|
|
66
79
|
self._function_context: FunctionContext = FunctionContext(**kwargs)
|
|
67
80
|
self._sequence_number = 0
|
|
68
81
|
self._replay_schema = ReplaySchema(upperSchemaVersion)
|
|
82
|
+
if (upperSchemaVersionNew is not None
|
|
83
|
+
and upperSchemaVersionNew > self._replay_schema.value):
|
|
84
|
+
valid_schema_values = [enum_member.value for enum_member in ReplaySchema]
|
|
85
|
+
if upperSchemaVersionNew in valid_schema_values:
|
|
86
|
+
self._replay_schema = ReplaySchema(upperSchemaVersionNew)
|
|
87
|
+
else:
|
|
88
|
+
self._replay_schema = ReplaySchema(max(valid_schema_values))
|
|
69
89
|
|
|
70
90
|
self._action_payload_v1: List[List[Action]] = []
|
|
71
91
|
self._action_payload_v2: List[Action] = []
|
|
@@ -532,10 +552,10 @@ class DurableOrchestrationContext:
|
|
|
532
552
|
The action to append
|
|
533
553
|
"""
|
|
534
554
|
new_action: Union[List[Action], Action]
|
|
535
|
-
if self._replay_schema is ReplaySchema.
|
|
536
|
-
new_action = action
|
|
537
|
-
else:
|
|
555
|
+
if self._replay_schema is ReplaySchema.V1:
|
|
538
556
|
new_action = [action]
|
|
557
|
+
else:
|
|
558
|
+
new_action = action
|
|
539
559
|
self._add_to_actions(new_action)
|
|
540
560
|
self._sequence_number += 1
|
|
541
561
|
|
|
@@ -580,6 +600,23 @@ class DurableOrchestrationContext:
|
|
|
580
600
|
TaskBase
|
|
581
601
|
A Durable Timer Task that schedules the timer to wake up the activity
|
|
582
602
|
"""
|
|
603
|
+
if self._replay_schema.value >= ReplaySchema.V3.value:
|
|
604
|
+
if not self._maximum_short_timer_duration or not self._long_timer_interval_duration:
|
|
605
|
+
raise Exception(
|
|
606
|
+
"A framework-internal error was detected: "
|
|
607
|
+
"replay schema version >= V3 is being used, "
|
|
608
|
+
"but one or more of the properties `maximumShortTimerDuration`"
|
|
609
|
+
"and `longRunningTimerIntervalDuration` are not defined. "
|
|
610
|
+
"This is likely an issue with the Durable Functions Extension. "
|
|
611
|
+
"Please report this bug here: "
|
|
612
|
+
"https://github.com/Azure/azure-functions-durable-python/issues\n"
|
|
613
|
+
f"maximumShortTimerDuration: {self._maximum_short_timer_duration}\n"
|
|
614
|
+
f"longRunningTimerIntervalDuration: {self._long_timer_interval_duration}"
|
|
615
|
+
)
|
|
616
|
+
if fire_at > self.current_utc_datetime + self._maximum_short_timer_duration:
|
|
617
|
+
action = CreateTimerAction(fire_at)
|
|
618
|
+
return LongTimerTask(None, action, self)
|
|
619
|
+
|
|
583
620
|
action = CreateTimerAction(fire_at)
|
|
584
621
|
task = self._generate_task(action, task_constructor=TimerTask)
|
|
585
622
|
return task
|
|
@@ -656,7 +693,8 @@ class DurableOrchestrationContext:
|
|
|
656
693
|
|
|
657
694
|
if self._replay_schema is ReplaySchema.V1 and isinstance(action_repr, list):
|
|
658
695
|
self._action_payload_v1.append(action_repr)
|
|
659
|
-
elif self._replay_schema
|
|
696
|
+
elif (self._replay_schema.value >= ReplaySchema.V2.value
|
|
697
|
+
and isinstance(action_repr, Action)):
|
|
660
698
|
self._action_payload_v2.append(action_repr)
|
|
661
699
|
else:
|
|
662
700
|
raise Exception(f"DF-internal exception: ActionRepr of signature {type(action_repr)}"
|
|
@@ -684,7 +722,8 @@ class DurableOrchestrationContext:
|
|
|
684
722
|
task.id = self._sequence_number
|
|
685
723
|
self._sequence_number += 1
|
|
686
724
|
self.open_tasks[task.id] = task
|
|
687
|
-
elif task.id != -1:
|
|
725
|
+
elif task.id != -1 and self.open_tasks[task.id] != task:
|
|
726
|
+
# Case when returning task_any with multiple external events having the same ID
|
|
688
727
|
self.open_tasks[task.id].append(task)
|
|
689
728
|
|
|
690
729
|
if task.id in self.deferred_tasks:
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from datetime import datetime
|
|
1
2
|
from azure.durable_functions.models.actions.NoOpAction import NoOpAction
|
|
2
3
|
from azure.durable_functions.models.actions.CompoundAction import CompoundAction
|
|
3
4
|
from azure.durable_functions.models.RetryOptions import RetryOptions
|
|
@@ -170,7 +171,7 @@ class CompoundTask(TaskBase):
|
|
|
170
171
|
child_actions.append(action_repr)
|
|
171
172
|
if compound_action_constructor is None:
|
|
172
173
|
self.action_repr = child_actions
|
|
173
|
-
else: # replay_schema
|
|
174
|
+
else: # replay_schema >= ReplaySchema.V2
|
|
174
175
|
self.action_repr = compound_action_constructor(child_actions)
|
|
175
176
|
self._first_error: Optional[Exception] = None
|
|
176
177
|
self.pending_tasks: Set[TaskBase] = set(tasks)
|
|
@@ -292,7 +293,7 @@ class WhenAllTask(CompoundTask):
|
|
|
292
293
|
The ReplaySchema, which determines the inner action payload representation
|
|
293
294
|
"""
|
|
294
295
|
compound_action_constructor = None
|
|
295
|
-
if replay_schema
|
|
296
|
+
if replay_schema.value >= ReplaySchema.V2.value:
|
|
296
297
|
compound_action_constructor = WhenAllAction
|
|
297
298
|
super().__init__(task, compound_action_constructor)
|
|
298
299
|
|
|
@@ -317,6 +318,119 @@ class WhenAllTask(CompoundTask):
|
|
|
317
318
|
self.set_value(is_error=True, value=self._first_error)
|
|
318
319
|
|
|
319
320
|
|
|
321
|
+
class LongTimerTask(WhenAllTask):
|
|
322
|
+
"""A Timer Task for intervals longer than supported by the storage backend."""
|
|
323
|
+
|
|
324
|
+
def __init__(self, id_, action: CreateTimerAction, orchestration_context):
|
|
325
|
+
"""Initialize a LongTimerTask.
|
|
326
|
+
|
|
327
|
+
Parameters
|
|
328
|
+
----------
|
|
329
|
+
id_ : int
|
|
330
|
+
An ID for the task
|
|
331
|
+
action : CreateTimerAction
|
|
332
|
+
The action this task represents
|
|
333
|
+
orchestration_context: DurableOrchestrationContext
|
|
334
|
+
The orchestration context this task was created in
|
|
335
|
+
"""
|
|
336
|
+
current_time = orchestration_context.current_utc_datetime
|
|
337
|
+
final_fire_time = action.fire_at
|
|
338
|
+
duration_until_fire = final_fire_time - current_time
|
|
339
|
+
|
|
340
|
+
if duration_until_fire > orchestration_context._maximum_short_timer_duration:
|
|
341
|
+
next_fire_time = current_time + orchestration_context._long_timer_interval_duration
|
|
342
|
+
else:
|
|
343
|
+
next_fire_time = final_fire_time
|
|
344
|
+
|
|
345
|
+
next_timer_action = CreateTimerAction(next_fire_time)
|
|
346
|
+
next_timer_task = TimerTask(None, next_timer_action)
|
|
347
|
+
super().__init__([next_timer_task], orchestration_context._replay_schema)
|
|
348
|
+
|
|
349
|
+
self.id = id_
|
|
350
|
+
self.action = action
|
|
351
|
+
self._orchestration_context = orchestration_context
|
|
352
|
+
self._max_short_timer_duration = self._orchestration_context._maximum_short_timer_duration
|
|
353
|
+
self._long_timer_interval = self._orchestration_context._long_timer_interval_duration
|
|
354
|
+
|
|
355
|
+
def is_canceled(self) -> bool:
|
|
356
|
+
"""Check if the LongTimer is cancelled.
|
|
357
|
+
|
|
358
|
+
Returns
|
|
359
|
+
-------
|
|
360
|
+
bool
|
|
361
|
+
Returns whether the timer has been cancelled or not
|
|
362
|
+
"""
|
|
363
|
+
return self.action.is_cancelled
|
|
364
|
+
|
|
365
|
+
def cancel(self):
|
|
366
|
+
"""Cancel a timer.
|
|
367
|
+
|
|
368
|
+
Raises
|
|
369
|
+
------
|
|
370
|
+
ValueError
|
|
371
|
+
Raises an error if the task is already completed and an attempt is made to cancel it
|
|
372
|
+
"""
|
|
373
|
+
if (self.result):
|
|
374
|
+
raise Exception("Cannot cancel a completed task.")
|
|
375
|
+
self.action.is_cancelled = True
|
|
376
|
+
|
|
377
|
+
def try_set_value(self, child: TimerTask):
|
|
378
|
+
"""Transition this LongTimer Task to a terminal state and set its value.
|
|
379
|
+
|
|
380
|
+
If the LongTimer has not yet reached the designated completion time, starts a new
|
|
381
|
+
TimerTask for the next interval and does not close.
|
|
382
|
+
|
|
383
|
+
Parameters
|
|
384
|
+
----------
|
|
385
|
+
child : TimerTask
|
|
386
|
+
A timer sub-task that just completed
|
|
387
|
+
"""
|
|
388
|
+
current_time = self._orchestration_context.current_utc_datetime
|
|
389
|
+
final_fire_time = self.action.fire_at
|
|
390
|
+
if final_fire_time > current_time:
|
|
391
|
+
next_timer = self.get_next_timer_task(final_fire_time, current_time)
|
|
392
|
+
self.add_new_child(next_timer)
|
|
393
|
+
return super().try_set_value(child)
|
|
394
|
+
|
|
395
|
+
def get_next_timer_task(self, final_fire_time: datetime, current_time: datetime) -> TimerTask:
|
|
396
|
+
"""Create a TimerTask to represent the next interval of the LongTimer.
|
|
397
|
+
|
|
398
|
+
Parameters
|
|
399
|
+
----------
|
|
400
|
+
final_fire_time : datetime.datetime
|
|
401
|
+
The final firing time of the LongTimer
|
|
402
|
+
current_time : datetime.datetime
|
|
403
|
+
The current time
|
|
404
|
+
|
|
405
|
+
Returns
|
|
406
|
+
-------
|
|
407
|
+
TimerTask
|
|
408
|
+
A TimerTask representing the next interval of the LongTimer
|
|
409
|
+
"""
|
|
410
|
+
duration_until_fire = final_fire_time - current_time
|
|
411
|
+
if duration_until_fire > self._max_short_timer_duration:
|
|
412
|
+
next_fire_time = current_time + self._long_timer_interval
|
|
413
|
+
else:
|
|
414
|
+
next_fire_time = final_fire_time
|
|
415
|
+
return TimerTask(None, CreateTimerAction(next_fire_time))
|
|
416
|
+
|
|
417
|
+
def add_new_child(self, child_timer: TimerTask):
|
|
418
|
+
"""Add the TimerTask to this task's children.
|
|
419
|
+
|
|
420
|
+
Also register the TimerTask with the orchestration context.
|
|
421
|
+
|
|
422
|
+
Parameters
|
|
423
|
+
----------
|
|
424
|
+
child_timer : TimerTask
|
|
425
|
+
The newly created TimerTask to add
|
|
426
|
+
"""
|
|
427
|
+
child_timer.parent = self
|
|
428
|
+
self.pending_tasks.add(child_timer)
|
|
429
|
+
self._orchestration_context._add_to_open_tasks(child_timer)
|
|
430
|
+
self._orchestration_context._add_to_actions(child_timer.action_repr)
|
|
431
|
+
child_timer._set_is_scheduled(True)
|
|
432
|
+
|
|
433
|
+
|
|
320
434
|
class WhenAnyTask(CompoundTask):
|
|
321
435
|
"""A Task representing `when_any` scenarios."""
|
|
322
436
|
|
|
@@ -331,7 +445,7 @@ class WhenAnyTask(CompoundTask):
|
|
|
331
445
|
The ReplaySchema, which determines the inner action payload representation
|
|
332
446
|
"""
|
|
333
447
|
compound_action_constructor = None
|
|
334
|
-
if replay_schema
|
|
448
|
+
if replay_schema.value >= ReplaySchema.V2.value:
|
|
335
449
|
compound_action_constructor = WhenAnyAction
|
|
336
450
|
super().__init__(task, compound_action_constructor)
|
|
337
451
|
|
|
@@ -245,14 +245,14 @@ class TaskOrchestrationExecutor:
|
|
|
245
245
|
|
|
246
246
|
self.current_task = new_task
|
|
247
247
|
if not (new_task is None):
|
|
248
|
+
if not (self.current_task._is_scheduled):
|
|
249
|
+
# new task is received. it needs to be resolved to a value
|
|
250
|
+
self.context._add_to_actions(self.current_task.action_repr)
|
|
251
|
+
self._mark_as_scheduled(self.current_task)
|
|
248
252
|
if not (new_task.state is TaskState.RUNNING):
|
|
249
253
|
# user yielded the same task multiple times, continue executing code
|
|
250
254
|
# until a new/not-previously-yielded task is encountered
|
|
251
255
|
self.resume_user_code()
|
|
252
|
-
elif not (self.current_task._is_scheduled):
|
|
253
|
-
# new task is received. it needs to be resolved to a value
|
|
254
|
-
self.context._add_to_actions(self.current_task.action_repr)
|
|
255
|
-
self._mark_as_scheduled(self.current_task)
|
|
256
256
|
|
|
257
257
|
def _mark_as_scheduled(self, task: TaskBase):
|
|
258
258
|
if isinstance(task, CompoundTask):
|
|
@@ -286,12 +286,18 @@ class TaskOrchestrationExecutor:
|
|
|
286
286
|
self.output = None
|
|
287
287
|
self.exception = e
|
|
288
288
|
|
|
289
|
+
exception_str = None
|
|
290
|
+
if self.exception is not None:
|
|
291
|
+
exception_str = str(self.exception)
|
|
292
|
+
if not exception_str:
|
|
293
|
+
exception_str = str(type(self.exception))
|
|
294
|
+
|
|
289
295
|
state = OrchestratorState(
|
|
290
296
|
is_done=self.orchestration_invocation_succeeded,
|
|
291
297
|
actions=self.context._actions,
|
|
292
298
|
output=self.output,
|
|
293
299
|
replay_schema=self.context._replay_schema,
|
|
294
|
-
error=
|
|
300
|
+
error=exception_str,
|
|
295
301
|
custom_status=self.context.custom_status
|
|
296
302
|
)
|
|
297
303
|
|
|
@@ -9,6 +9,7 @@ from .RetryOptions import RetryOptions
|
|
|
9
9
|
from .DurableHttpRequest import DurableHttpRequest
|
|
10
10
|
from .TokenSource import ManagedIdentityTokenSource
|
|
11
11
|
from .DurableEntityContext import DurableEntityContext
|
|
12
|
+
from .Task import TaskBase
|
|
12
13
|
|
|
13
14
|
__all__ = [
|
|
14
15
|
'DurableOrchestrationBindings',
|
|
@@ -20,5 +21,6 @@ __all__ = [
|
|
|
20
21
|
'OrchestratorState',
|
|
21
22
|
'OrchestrationRuntimeStatus',
|
|
22
23
|
'PurgeHistoryResult',
|
|
23
|
-
'RetryOptions'
|
|
24
|
+
'RetryOptions',
|
|
25
|
+
'TaskBase'
|
|
24
26
|
]
|
|
@@ -12,6 +12,7 @@ class OperationResult:
|
|
|
12
12
|
def __init__(self,
|
|
13
13
|
is_error: bool,
|
|
14
14
|
duration: int,
|
|
15
|
+
execution_start_time_ms: int,
|
|
15
16
|
result: Optional[str] = None):
|
|
16
17
|
"""Instantiate an OperationResult.
|
|
17
18
|
|
|
@@ -21,11 +22,15 @@ class OperationResult:
|
|
|
21
22
|
Whether or not the operation resulted in an exception.
|
|
22
23
|
duration: int
|
|
23
24
|
How long the operation took, in milliseconds.
|
|
25
|
+
start_time: int
|
|
26
|
+
The start time of this operation's execution, in milliseconds,
|
|
27
|
+
since January 1st 1970 midnight in UTC.
|
|
24
28
|
result: Optional[str]
|
|
25
29
|
The operation result. Defaults to None.
|
|
26
30
|
"""
|
|
27
31
|
self._is_error: bool = is_error
|
|
28
32
|
self._duration: int = duration
|
|
33
|
+
self._execution_start_time_ms: int = execution_start_time_ms
|
|
29
34
|
self._result: Optional[str] = result
|
|
30
35
|
|
|
31
36
|
@property
|
|
@@ -50,6 +55,18 @@ class OperationResult:
|
|
|
50
55
|
"""
|
|
51
56
|
return self._duration
|
|
52
57
|
|
|
58
|
+
@property
|
|
59
|
+
def execution_start_time_ms(self) -> int:
|
|
60
|
+
"""Get the start time of this operation.
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
int:
|
|
65
|
+
The start time of this operation's execution, in milliseconds,
|
|
66
|
+
since January 1st 1970 midnight in UTC.
|
|
67
|
+
"""
|
|
68
|
+
return self._execution_start_time_ms
|
|
69
|
+
|
|
53
70
|
@property
|
|
54
71
|
def result(self) -> Any:
|
|
55
72
|
"""Get the operation's result.
|
|
@@ -72,5 +89,6 @@ class OperationResult:
|
|
|
72
89
|
to_json: Dict[str, Any] = {}
|
|
73
90
|
to_json["isError"] = self.is_error
|
|
74
91
|
to_json["duration"] = self.duration
|
|
92
|
+
to_json["startTime"] = self.execution_start_time_ms
|
|
75
93
|
to_json["result"] = json.dumps(self.result, default=_serialize_custom_object)
|
|
76
94
|
return to_json
|
|
@@ -3,7 +3,10 @@ from typing import Any, List, Union
|
|
|
3
3
|
import aiohttp
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
async def post_async_request(url: str,
|
|
6
|
+
async def post_async_request(url: str,
|
|
7
|
+
data: Any = None,
|
|
8
|
+
trace_parent: str = None,
|
|
9
|
+
trace_state: str = None) -> List[Union[int, Any]]:
|
|
7
10
|
"""Post request with the data provided to the url provided.
|
|
8
11
|
|
|
9
12
|
Parameters
|
|
@@ -12,6 +15,10 @@ async def post_async_request(url: str, data: Any = None) -> List[Union[int, Any]
|
|
|
12
15
|
url to make the post to
|
|
13
16
|
data: Any
|
|
14
17
|
object to post
|
|
18
|
+
trace_parent: str
|
|
19
|
+
traceparent header to send with the request
|
|
20
|
+
trace_state: str
|
|
21
|
+
tracestate header to send with the request
|
|
15
22
|
|
|
16
23
|
Returns
|
|
17
24
|
-------
|
|
@@ -19,8 +26,12 @@ async def post_async_request(url: str, data: Any = None) -> List[Union[int, Any]
|
|
|
19
26
|
Tuple with the Response status code and the data returned from the request
|
|
20
27
|
"""
|
|
21
28
|
async with aiohttp.ClientSession() as session:
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
headers = {}
|
|
30
|
+
if trace_parent:
|
|
31
|
+
headers["traceparent"] = trace_parent
|
|
32
|
+
if trace_state:
|
|
33
|
+
headers["tracestate"] = trace_state
|
|
34
|
+
async with session.post(url, json=data, headers=headers) as response:
|
|
24
35
|
# We disable aiohttp's input type validation
|
|
25
36
|
# as the server may respond with alternative
|
|
26
37
|
# data encodings. This is potentially unsafe.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import re
|
|
1
3
|
from typing import Dict, Any
|
|
2
4
|
|
|
3
5
|
from ...constants import DATETIME_STRING_FORMAT
|
|
@@ -37,6 +39,44 @@ def add_datetime_attrib(json_dict: Dict[str, Any], object_,
|
|
|
37
39
|
getattr(object_, attribute_name).strftime(DATETIME_STRING_FORMAT)
|
|
38
40
|
|
|
39
41
|
|
|
42
|
+
# When we recieve properties from WebJobs extension originally parsed as
|
|
43
|
+
# TimeSpan objects through Newtonsoft, the format complies with the constant
|
|
44
|
+
# format specifier for TimeSpan in .NET.
|
|
45
|
+
# Python offers no convenient way to parse these back into timedeltas,
|
|
46
|
+
# so we use this regex method instead
|
|
47
|
+
def parse_timespan_attrib(from_str: str) -> datetime.timedelta:
|
|
48
|
+
"""Convert a string representing TimeSpan.ToString("c") in .NET to a python timedelta.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
from_str: The string format of the TimeSpan to convert
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
timespan.timedelta
|
|
57
|
+
The TimeSpan expressed as a Python datetime.timedelta
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
match = re.match(r"^(?P<negative>-)?(?:(?P<days>[0-9]*)\.)?"
|
|
61
|
+
r"(?P<hours>[0-9]{2}):(?P<minutes>[0-9]{2})"
|
|
62
|
+
r":(?P<seconds>[0-9]{2})(?:\.(?P<ticks>[0-9]{7}))?$",
|
|
63
|
+
from_str)
|
|
64
|
+
if match:
|
|
65
|
+
groups = match.groupdict()
|
|
66
|
+
span = datetime.timedelta(
|
|
67
|
+
days=int(groups['days'] or "0"),
|
|
68
|
+
hours=int(groups['hours']),
|
|
69
|
+
minutes=int(groups['minutes']),
|
|
70
|
+
seconds=int(groups['seconds']),
|
|
71
|
+
microseconds=int(groups['ticks'] or "0") // 10)
|
|
72
|
+
|
|
73
|
+
if groups['negative'] == '-':
|
|
74
|
+
span = -span
|
|
75
|
+
return span
|
|
76
|
+
else:
|
|
77
|
+
raise Exception(f"Format of TimeSpan failed attempted conversion to timedelta: {from_str}")
|
|
78
|
+
|
|
79
|
+
|
|
40
80
|
def add_json_attrib(json_dict: Dict[str, Any], object_,
|
|
41
81
|
attribute_name: str, alt_name: str = None):
|
|
42
82
|
"""Add the results of the to_json() function call of the attribute from the object to the dict.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Generator, Any, Union
|
|
2
|
+
|
|
3
|
+
from azure.durable_functions.models import TaskBase
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def orchestrator_generator_wrapper(
|
|
7
|
+
generator: Generator[TaskBase, Any, Any]) -> Generator[Union[TaskBase, Any], None, None]:
|
|
8
|
+
"""Wrap a user-defined orchestrator function in a way that simulates the Durable replay logic.
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
generator: Generator[TaskBase, Any, Any]
|
|
13
|
+
Generator orchestrator as defined in the user function app. This generator is expected
|
|
14
|
+
to yield a series of TaskBase objects and receive the results of these tasks until
|
|
15
|
+
returning the result of the orchestrator.
|
|
16
|
+
|
|
17
|
+
Returns
|
|
18
|
+
-------
|
|
19
|
+
Generator[Union[TaskBase, Any], None, None]
|
|
20
|
+
A simplified version of the orchestrator which takes no inputs. This generator will
|
|
21
|
+
yield back the TaskBase objects that are yielded from the user orchestrator as well
|
|
22
|
+
as the final result of the orchestrator. Exception handling is also simulated here
|
|
23
|
+
in the same way as replay, where tasks returning exceptions are thrown back into the
|
|
24
|
+
orchestrator.
|
|
25
|
+
"""
|
|
26
|
+
previous = next(generator)
|
|
27
|
+
yield previous
|
|
28
|
+
while True:
|
|
29
|
+
try:
|
|
30
|
+
previous_result = None
|
|
31
|
+
try:
|
|
32
|
+
previous_result = previous.result
|
|
33
|
+
except Exception as e:
|
|
34
|
+
# Simulated activity exceptions, timer interrupted exceptions,
|
|
35
|
+
# or anytime a task would throw.
|
|
36
|
+
previous = generator.throw(e)
|
|
37
|
+
else:
|
|
38
|
+
previous = generator.send(previous_result)
|
|
39
|
+
yield previous
|
|
40
|
+
except StopIteration as e:
|
|
41
|
+
yield e.value
|
|
42
|
+
return
|
{azure_functions_durable-1.2.10.dist-info → azure_functions_durable-1.3.0.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: azure-functions-durable
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Durable Functions For Python
|
|
5
5
|
Home-page: https://github.com/Azure/azure-functions-durable-python
|
|
6
6
|
Author: Azure Functions team at Microsoft Corp.
|
|
@@ -18,11 +18,12 @@ Classifier: Environment :: Web Environment
|
|
|
18
18
|
Classifier: Development Status :: 5 - Production/Stable
|
|
19
19
|
Requires-Python: >=3.6,<4
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
|
-
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
Requires-Dist:
|
|
24
|
-
Requires-Dist:
|
|
25
|
-
Requires-Dist:
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: azure-functions>=1.12.0
|
|
23
|
+
Requires-Dist: aiohttp>=3.6.2
|
|
24
|
+
Requires-Dist: requests==2.*
|
|
25
|
+
Requires-Dist: python-dateutil>=2.8.0
|
|
26
|
+
Requires-Dist: furl>=2.1.0
|
|
26
27
|
|
|
27
28
|
|Branch|Status|
|
|
28
29
|
|---|---|
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
azure/durable_functions/__init__.py,sha256=Syz9yrT8sHREVyxqvwjg8_c_h8yMTyuBHcDMsuQs1YM,2910
|
|
2
2
|
azure/durable_functions/constants.py,sha256=JtknDhaVihMeo-ygY9QNofiO2KEqnQvopdfZ6Qatnik,414
|
|
3
|
-
azure/durable_functions/entity.py,sha256=
|
|
4
|
-
azure/durable_functions/orchestrator.py,sha256=
|
|
3
|
+
azure/durable_functions/entity.py,sha256=mUUzb1BZiDrUJjvxOTlnVURnKPyDGPJ3mXXMN0DKT7M,4649
|
|
4
|
+
azure/durable_functions/orchestrator.py,sha256=SZni90Aweq0OZykHyMblfJpUndJ2woJmySarcsDiIK4,2554
|
|
5
5
|
azure/durable_functions/decorators/__init__.py,sha256=wEubgP2rUUISwidZWgKx6mmzEeGKsSnpGmetjIUi1nw,150
|
|
6
|
-
azure/durable_functions/decorators/durable_app.py,sha256=
|
|
6
|
+
azure/durable_functions/decorators/durable_app.py,sha256=8K4gevZVYcDvifxXVuvTt8YzF8jx63c1PwdBBk1Vt0g,10114
|
|
7
7
|
azure/durable_functions/decorators/metadata.py,sha256=p91rdCe6OSRYJaKAXnrfR0QCV3PoHK7aGy1m6WAnPIE,2828
|
|
8
8
|
azure/durable_functions/models/DurableEntityContext.py,sha256=cyZmjjZu18oV9S4A2NpnXfjd1JQxPxp9EMmAR424UK0,5830
|
|
9
9
|
azure/durable_functions/models/DurableHttpRequest.py,sha256=a5kgRdg4eA0sgyDcpmQWc0dbwP-o3BwWW2Ive0BYO_Q,2021
|
|
10
10
|
azure/durable_functions/models/DurableOrchestrationBindings.py,sha256=_hp61WjN3bQYCqYFQuvUaDdRu7C14fPg7lFbaA9TRe4,2408
|
|
11
|
-
azure/durable_functions/models/DurableOrchestrationClient.py,sha256=
|
|
12
|
-
azure/durable_functions/models/DurableOrchestrationContext.py,sha256=
|
|
11
|
+
azure/durable_functions/models/DurableOrchestrationClient.py,sha256=kQBqeKugvi2mi-7dDbCxlu67r20doEhDknlchYxcLBE,33018
|
|
12
|
+
azure/durable_functions/models/DurableOrchestrationContext.py,sha256=mbA1Do_1NP_uuqhCUoEsegqBtlNugUB0_tdVWQ1PRoI,31849
|
|
13
13
|
azure/durable_functions/models/DurableOrchestrationStatus.py,sha256=BXWz9L7np4Q9k6z4NsfLX97i2U2IFh94TVeRSV2BjM4,6049
|
|
14
14
|
azure/durable_functions/models/EntityStateResponse.py,sha256=f48W8gmlb-D5iJw3eDyUMYVwHpmIxP6k6a7o2TRHwII,674
|
|
15
15
|
azure/durable_functions/models/FunctionContext.py,sha256=4gHTmIo8DZN-bZLM-hyjoQFlv-AbsfLMT1_X4WxWxqY,274
|
|
16
16
|
azure/durable_functions/models/OrchestrationRuntimeStatus.py,sha256=lCT31d85B9dHA12n7Twdd9gQ-ISCtt-bkEwi3TXVHoE,969
|
|
17
17
|
azure/durable_functions/models/OrchestratorState.py,sha256=xgoEz8Ya8V5kFO997GiS70IVS2EXhDcMZwoclVaseu0,4073
|
|
18
18
|
azure/durable_functions/models/PurgeHistoryResult.py,sha256=C_Ppdk7TEn0fuYZ971u3I_GHQBUqPuLqCrVyvp6YRsY,1092
|
|
19
|
-
azure/durable_functions/models/ReplaySchema.py,sha256=
|
|
19
|
+
azure/durable_functions/models/ReplaySchema.py,sha256=85YZD-QWgMQSfZsRQlfQfLPzK9FMITcWK241Vofw6pU,170
|
|
20
20
|
azure/durable_functions/models/RetryOptions.py,sha256=-rmv3mQmzQ_2utFy1d-ontqpcgP139B8MQroanfN54w,1988
|
|
21
21
|
azure/durable_functions/models/RpcManagementOptions.py,sha256=aEOWx_xUWl4Rwb2-7kpyml8rzTX9Vl4s71LkqYPLnHw,3482
|
|
22
|
-
azure/durable_functions/models/Task.py,sha256=
|
|
23
|
-
azure/durable_functions/models/TaskOrchestrationExecutor.py,sha256=
|
|
22
|
+
azure/durable_functions/models/Task.py,sha256=e548_wfJkYY_UCpJkwqVVM65tyP6jchox_YNsf8SslM,19559
|
|
23
|
+
azure/durable_functions/models/TaskOrchestrationExecutor.py,sha256=PBbS3aEKn1JCFhvjR9oz4-WmIW_9vh6-ILzImxVN7zw,16085
|
|
24
24
|
azure/durable_functions/models/TokenSource.py,sha256=9uLxiOV8lcDj--3tD0XxcQnigk9AozjdOoJyskctErU,1822
|
|
25
|
-
azure/durable_functions/models/__init__.py,sha256=
|
|
25
|
+
azure/durable_functions/models/__init__.py,sha256=L7ynxb_mBGCvV1iEAfpJU9_b-8ubKIEJKaZa2aoqjek,999
|
|
26
26
|
azure/durable_functions/models/actions/Action.py,sha256=0jp-SP_12YmZWWctOXmwl48Ozw3dMMq5-crUAkK8Qk0,598
|
|
27
27
|
azure/durable_functions/models/actions/ActionType.py,sha256=FAQh_EPcFru7rOPYWKpYFBSbZpFEQc2-jsrapRJvFG0,507
|
|
28
28
|
azure/durable_functions/models/actions/CallActivityAction.py,sha256=G9O9JML-Z0_A_WWB4iTbDkvE3iNRtNkES2iNsUQ2SVc,1496
|
|
@@ -41,7 +41,7 @@ azure/durable_functions/models/actions/WhenAllAction.py,sha256=xfcvSsJ3zsRL6IbpM
|
|
|
41
41
|
azure/durable_functions/models/actions/WhenAnyAction.py,sha256=VyKu2iv5ML2862nu4d_ZG0Cbm50546MnhJ6RZyxghso,478
|
|
42
42
|
azure/durable_functions/models/actions/__init__.py,sha256=q3dRJc1r7q6-IYjOGM5LJqoBPp2PkE4argVJ2SUjne4,861
|
|
43
43
|
azure/durable_functions/models/entities/EntityState.py,sha256=lVmkM18z7xExdt1wO1IFJc5MYliVBO3k0fiPyIcu97o,2257
|
|
44
|
-
azure/durable_functions/models/entities/OperationResult.py,sha256=
|
|
44
|
+
azure/durable_functions/models/entities/OperationResult.py,sha256=RHGoCcLeAiMtDiZH5aYW-s4-IDuVtXArlxhktf49kWs,2766
|
|
45
45
|
azure/durable_functions/models/entities/RequestMessage.py,sha256=y2BG1-HQuPbN91qOh_XrFainFp27YbSk9dotA2ePOPA,1742
|
|
46
46
|
azure/durable_functions/models/entities/ResponseMessage.py,sha256=GC6HnD3mkIfkGptpazq4yOGJfciL2kkVxbcv1R80B7I,1588
|
|
47
47
|
azure/durable_functions/models/entities/Signal.py,sha256=TjyvkG9kzxyjyRGz1bItxt1MI-QN2QS2YK3e7rFP41g,1313
|
|
@@ -51,13 +51,15 @@ azure/durable_functions/models/history/HistoryEventType.py,sha256=NdCQQrqvWFw5Gi
|
|
|
51
51
|
azure/durable_functions/models/history/__init__.py,sha256=otJhZJN9OeGtWrW3lKbk2C1Nyf6I2wJfwuXpCZ2oxYM,237
|
|
52
52
|
azure/durable_functions/models/utils/__init__.py,sha256=dQ6-HRUPsCtDIqGjRJ3TA6NXSYXzhw5yLA2OP-zkm-s,221
|
|
53
53
|
azure/durable_functions/models/utils/entity_utils.py,sha256=TqNTtRC8VuKFtqWLq9oEAloioV-FyinjgRYVKkCldHo,2881
|
|
54
|
-
azure/durable_functions/models/utils/http_utils.py,sha256=
|
|
55
|
-
azure/durable_functions/models/utils/json_utils.py,sha256=
|
|
54
|
+
azure/durable_functions/models/utils/http_utils.py,sha256=AoCWjCapd_984J_4296iJ8cNJWEG8GIdhRttBPt0HnA,2551
|
|
55
|
+
azure/durable_functions/models/utils/json_utils.py,sha256=zUn62pm3dQw054ZlK7F4uRP-UELjQC8EmZBU1WncHMg,3811
|
|
56
|
+
azure/durable_functions/testing/OrchestratorGeneratorWrapper.py,sha256=cjh-HAq5rVNCoR0pIbfGrqy6cKSf4S1KMQxrBMWU1-s,1728
|
|
57
|
+
azure/durable_functions/testing/__init__.py,sha256=NLbltPtoPXK-0iMTwcKTKPjQlAWrEq55oDYmrhYz6vg,189
|
|
56
58
|
tests/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
57
59
|
tests/models/test_DecoratorMetadata.py,sha256=0PeUDszF_gAJZMZR-K-Ro7c3I1D960amOLtbT88L_dk,3918
|
|
58
60
|
tests/models/test_Decorators.py,sha256=y2dhoSlP74J5uAVBDY2JfFkSA-AhyagVBZO5tGi6KaQ,2925
|
|
59
61
|
tests/models/test_DurableOrchestrationBindings.py,sha256=pjuoKlpEc6KAIL-Nq2taoqW0HYWXoupgUxcsPwc1Psg,2961
|
|
60
|
-
tests/models/test_DurableOrchestrationClient.py,sha256=
|
|
62
|
+
tests/models/test_DurableOrchestrationClient.py,sha256=7htzuMMfkRU9Hf-9Gr-rYHpJXJdpnAp0WheAFpMKHNo,31791
|
|
61
63
|
tests/models/test_DurableOrchestrationContext.py,sha256=7fWdnvpSEces0VM4Xm11uLZmshXqGl7wtYo8ELixypc,3930
|
|
62
64
|
tests/models/test_DurableOrchestrationStatus.py,sha256=fnUZxrHGy771OoaD5TInELhaG836aB8XqtMdNjnEFp8,2485
|
|
63
65
|
tests/models/test_OrchestrationState.py,sha256=L-k8ScrqoDIZEqIUORbxXA7yCuMbVAUPr-7VmyuQkUc,1272
|
|
@@ -68,7 +70,7 @@ tests/orchestrator/orchestrator_test_utils.py,sha256=ldgQGGuOVALcI98enRU08nF_VJd
|
|
|
68
70
|
tests/orchestrator/test_call_http.py,sha256=CvemeCayrQLjmjl3lpB1fk_CpV-DRPgfxLgD4iNgStQ,9103
|
|
69
71
|
tests/orchestrator/test_continue_as_new.py,sha256=k9mS8tHE1GX9uogCgrrgXJynydxExrlAlGIpC4ojNP0,2575
|
|
70
72
|
tests/orchestrator/test_create_timer.py,sha256=HPaJrYYmyvWqBFqmu2jatLK911DA8knfF88GMRWMhJY,5576
|
|
71
|
-
tests/orchestrator/test_entity.py,sha256=
|
|
73
|
+
tests/orchestrator/test_entity.py,sha256=fcHhh1kcLDv9GXnChMUsFwYWiRiBRXKLlFp823pooWs,13922
|
|
72
74
|
tests/orchestrator/test_external_event.py,sha256=DfeOFCdt2haxTwVQ1_lQtwkrdhb3EVlXsavVGo8HrmE,2534
|
|
73
75
|
tests/orchestrator/test_fan_out_fan_in.py,sha256=8pNPA2remGz8GLThqINAuYi5FVIMNdE1Ya5zqhXHDKc,7218
|
|
74
76
|
tests/orchestrator/test_is_replaying_flag.py,sha256=se8GOmI_5EP7jae8gdWFFSBEIfWbpgaQstyJwje5sdY,4386
|
|
@@ -84,6 +86,7 @@ tests/orchestrator/models/OrchestrationInstance.py,sha256=CQ3qyNumjksuFNMujbESsv
|
|
|
84
86
|
tests/orchestrator/schemas/OrchetrationStateSchema.py,sha256=EyTsDUZ3K-9mVljLIlg8f_h2zsK228E0PMvvtyEWw24,2718
|
|
85
87
|
tests/tasks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
86
88
|
tests/tasks/tasks_test_utils.py,sha256=Ymc5GESJpzybBq3n2mT4IABDRNTVCerAZrlMilv0Pdk,737
|
|
89
|
+
tests/tasks/test_long_timers.py,sha256=KDN0AM4R1Jqaf3L1i7xUXEUIyPUzP6PFRplmD2IamlI,3494
|
|
87
90
|
tests/tasks/test_new_uuid.py,sha256=iaQcF5iPZdU3e1SiKSiP6vQrQeMFehUp354ShMRHE7s,1419
|
|
88
91
|
tests/test_utils/ContextBuilder.py,sha256=l1dDeI1FXdTaiUYwcYNPOtYiivh2c2xUfoOo_AcXgL0,7459
|
|
89
92
|
tests/test_utils/EntityContextBuilder.py,sha256=Gjh0ERX6ND5RZ3blCmix8z0RrmfOVfoY0xm8LuUoZGo,1939
|
|
@@ -93,8 +96,8 @@ tests/test_utils/json_utils.py,sha256=B0q3COMya7TGxbH-7sD_0ypWDSuaF4fpD4QV_oJPgG
|
|
|
93
96
|
tests/test_utils/testClasses.py,sha256=U_u5qKxC9U81SzjLo7ejjPjEn_cE5qjaqoq8edGD6l8,1521
|
|
94
97
|
tests/utils/__init__.py,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
|
95
98
|
tests/utils/test_entity_utils.py,sha256=kdk5_DV_-bFu_5q2mw9o1yjyzh8Lcxv1jo1Q7is_ukA,748
|
|
96
|
-
azure_functions_durable-1.
|
|
97
|
-
azure_functions_durable-1.
|
|
98
|
-
azure_functions_durable-1.
|
|
99
|
-
azure_functions_durable-1.
|
|
100
|
-
azure_functions_durable-1.
|
|
99
|
+
azure_functions_durable-1.3.0.dist-info/LICENSE,sha256=-VS-Izmxdykuae1Xc4vHtVUx02rNQi6SSQlONvvuYeQ,1090
|
|
100
|
+
azure_functions_durable-1.3.0.dist-info/METADATA,sha256=J5O8Dj6UelbQLakvfSnQdiZoIK9qJ2tg-yaAxrS1fOI,3438
|
|
101
|
+
azure_functions_durable-1.3.0.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
102
|
+
azure_functions_durable-1.3.0.dist-info/top_level.txt,sha256=h-L8XDVPJ9YzBbHlPvM7FVo1cqNGToNK9ix99ySGOUY,12
|
|
103
|
+
azure_functions_durable-1.3.0.dist-info/RECORD,,
|
|
@@ -67,7 +67,7 @@ class MockRequest:
|
|
|
67
67
|
assert url == self._expected_url
|
|
68
68
|
return self._response
|
|
69
69
|
|
|
70
|
-
async def post(self, url: str, data: Any = None):
|
|
70
|
+
async def post(self, url: str, data: Any = None, trace_parent: str = None, trace_state: str = None):
|
|
71
71
|
assert url == self._expected_url
|
|
72
72
|
return self._response
|
|
73
73
|
|
|
@@ -209,9 +209,11 @@ def apply_operation(entity_state: EntityState, result: Any, state: Any, is_error
|
|
|
209
209
|
# We cannot control duration, so default it to zero and avoid checking for it
|
|
210
210
|
# in later asserts
|
|
211
211
|
duration = 0
|
|
212
|
+
start_time = 0
|
|
212
213
|
operation_result = OperationResult(
|
|
213
214
|
is_error=is_error,
|
|
214
215
|
duration=duration,
|
|
216
|
+
execution_start_time_ms=start_time,
|
|
215
217
|
result=result
|
|
216
218
|
)
|
|
217
219
|
entity_state._results.append(operation_result)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from azure.durable_functions.models.DurableOrchestrationContext import DurableOrchestrationContext
|
|
5
|
+
from azure.durable_functions.models.Task import LongTimerTask, TaskState, TimerTask
|
|
6
|
+
from azure.durable_functions.models.actions.CreateTimerAction import CreateTimerAction
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def starting_context_v3():
|
|
11
|
+
context = DurableOrchestrationContext.from_json(
|
|
12
|
+
'{"history":[{"EventType":12,"EventId":-1,"IsPlayed":false,'
|
|
13
|
+
'"Timestamp":"'
|
|
14
|
+
f'{datetime.datetime.now(datetime.timezone.utc).isoformat()}'
|
|
15
|
+
'"}, {"OrchestrationInstance":{'
|
|
16
|
+
'"InstanceId":"48d0f95957504c2fa579e810a390b938", '
|
|
17
|
+
'"ExecutionId":"fd183ee02e4b4fd18c95b773cfb5452b"},"EventType":0,'
|
|
18
|
+
'"ParentInstance":null, '
|
|
19
|
+
'"Name":"DurableOrchestratorTrigger","Version":"","Input":"null",'
|
|
20
|
+
'"Tags":null,"EventId":-1,"IsPlayed":false, '
|
|
21
|
+
'"Timestamp":"'
|
|
22
|
+
f'{datetime.datetime.now(datetime.timezone.utc).isoformat()}'
|
|
23
|
+
'"}],"input":null,'
|
|
24
|
+
'"instanceId":"48d0f95957504c2fa579e810a390b938", '
|
|
25
|
+
'"upperSchemaVersion": 2, '
|
|
26
|
+
'"upperSchemaVersionNew": 3, '
|
|
27
|
+
'"isReplaying":false,"parentInstanceId":null, '
|
|
28
|
+
'"maximumShortTimerDuration":"0.16:00:00", '
|
|
29
|
+
'"longRunningTimerIntervalDuration":"0.08:00:00" } ')
|
|
30
|
+
return context
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_durable_context_creates_correct_timer(starting_context_v3):
|
|
34
|
+
timer = starting_context_v3.create_timer(datetime.datetime.now(datetime.timezone.utc) +
|
|
35
|
+
datetime.timedelta(minutes=30))
|
|
36
|
+
assert isinstance(timer, TimerTask)
|
|
37
|
+
|
|
38
|
+
timer2 = starting_context_v3.create_timer(datetime.datetime.now(datetime.timezone.utc) +
|
|
39
|
+
datetime.timedelta(days=1))
|
|
40
|
+
assert isinstance(timer2, LongTimerTask)
|
|
41
|
+
|
|
42
|
+
def test_long_timer_fires_appropriately(starting_context_v3):
|
|
43
|
+
starting_time = starting_context_v3.current_utc_datetime
|
|
44
|
+
final_fire_time = starting_time + datetime.timedelta(hours=20)
|
|
45
|
+
long_timer_action = CreateTimerAction(final_fire_time)
|
|
46
|
+
long_timer = LongTimerTask(None, long_timer_action, starting_context_v3)
|
|
47
|
+
assert long_timer.action.fire_at == final_fire_time
|
|
48
|
+
assert long_timer.action == long_timer_action
|
|
49
|
+
|
|
50
|
+
# Check the first "inner" timer and simulate firing it
|
|
51
|
+
short_timer = long_timer.pending_tasks.pop()
|
|
52
|
+
assert short_timer.action_repr.fire_at == starting_time + datetime.timedelta(hours=8)
|
|
53
|
+
# This happens when the task is reconstructed during replay, doing it manually for the test
|
|
54
|
+
long_timer._orchestration_context.current_utc_datetime = short_timer.action_repr.fire_at
|
|
55
|
+
short_timer.state = TaskState.SUCCEEDED
|
|
56
|
+
long_timer.try_set_value(short_timer)
|
|
57
|
+
|
|
58
|
+
assert long_timer.state == TaskState.RUNNING
|
|
59
|
+
|
|
60
|
+
# Check the scond "inner" timer and simulate firing it. This one should be set to the final
|
|
61
|
+
# fire time, the remaining time (12 hours) is less than the max long timer duration (16 hours)
|
|
62
|
+
short_timer = long_timer.pending_tasks.pop()
|
|
63
|
+
assert short_timer.action_repr.fire_at == final_fire_time
|
|
64
|
+
long_timer._orchestration_context.current_utc_datetime = short_timer.action_repr.fire_at
|
|
65
|
+
short_timer.state = TaskState.SUCCEEDED
|
|
66
|
+
long_timer.try_set_value(short_timer)
|
|
67
|
+
|
|
68
|
+
# Ensure the LongTimerTask finished
|
|
69
|
+
assert len(long_timer.pending_tasks) == 0
|
|
70
|
+
assert long_timer.state == TaskState.SUCCEEDED
|
{azure_functions_durable-1.2.10.dist-info → azure_functions_durable-1.3.0.dist-info}/LICENSE
RENAMED
|
File without changes
|
{azure_functions_durable-1.2.10.dist-info → azure_functions_durable-1.3.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|