Skip to content

Commit 8d703a3

Browse files
authored
Improved datetime comparison logic (#170)
* improve datetime comparison logic * fixed linting error * added test to prevent regressions, cant validate schema
1 parent e70c357 commit 8d703a3

File tree

3 files changed

+74
-2
lines changed

3 files changed

+74
-2
lines changed

azure/durable_functions/tasks/task_utilities.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from ..models.history import HistoryEventType, HistoryEvent
33
from ..constants import DATETIME_STRING_FORMAT
44
from azure.functions._durable_functions import _deserialize_custom_object
5+
from datetime import datetime
56
from typing import List, Optional
67

78

@@ -131,10 +132,13 @@ def find_task_timer_created(state, fire_at):
131132
if fire_at is None:
132133
return None
133134

135+
# We remove the timezone metadata,
136+
# to enable comparisons with timezone-naive datetime objects. This may be dangerous
137+
fire_at = fire_at.replace(tzinfo=None)
134138
tasks = []
135139
for e in state:
136140
if e.event_type == HistoryEventType.TIMER_CREATED and hasattr(e, "FireAt"):
137-
if e.FireAt == fire_at.strftime(DATETIME_STRING_FORMAT):
141+
if datetime.strptime(e.FireAt, DATETIME_STRING_FORMAT) == fire_at:
138142
tasks.append(e)
139143

140144
if len(tasks) == 0:
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from tests.test_utils.ContextBuilder import ContextBuilder
2+
from .orchestrator_test_utils \
3+
import get_orchestration_state_result, assert_orchestration_state_equals, assert_valid_schema
4+
from azure.durable_functions.models.actions.CreateTimerAction import CreateTimerAction
5+
from azure.durable_functions.models.OrchestratorState import OrchestratorState
6+
from azure.durable_functions.constants import DATETIME_STRING_FORMAT
7+
from datetime import datetime, timedelta, timezone
8+
9+
10+
def base_expected_state(output=None) -> OrchestratorState:
11+
return OrchestratorState(is_done=False, actions=[], output=output)
12+
13+
def add_timer_fired_events(context_builder: ContextBuilder, id_: int, timestamp: str):
14+
fire_at: str = context_builder.add_timer_created_event(id_, timestamp)
15+
context_builder.add_orchestrator_completed_event()
16+
context_builder.add_orchestrator_started_event()
17+
context_builder.add_timer_fired_event(id_=id_, fire_at=fire_at)
18+
19+
def generator_function(context):
20+
21+
# Create a timezone aware datetime object, just like a normal
22+
# call to `context.current_utc_datetime` would create
23+
timestamp = "2020-07-23T21:56:54.936700Z"
24+
fire_at = datetime.strptime(timestamp, DATETIME_STRING_FORMAT)
25+
fire_at = fire_at.replace(tzinfo=timezone.utc)
26+
27+
yield context.create_timer(fire_at)
28+
return "Done!"
29+
30+
def add_timer_action(state: OrchestratorState, fire_at: datetime):
31+
action = CreateTimerAction(fire_at= fire_at)
32+
state._actions.append([action]) # Todo: brackets?
33+
34+
def test_timers_comparison_with_relaxed_precision():
35+
"""Test if that two `datetime` different but equivalent
36+
serializations of timer deadlines are found to be equivalent.
37+
38+
The Durable Extension may sometimes drop redundant zeroes on
39+
a datetime object. For instance, the date
40+
2020-07-23T21:56:54.936700Z
41+
may get transformed into
42+
2020-07-23T21:56:54.9367Z
43+
This test ensures that dropping redundant zeroes does not affect
44+
our ability to recognize that a timer has been fired.
45+
"""
46+
47+
# equivalent to 2020-07-23T21:56:54.936700Z
48+
relaxed_timestamp = "2020-07-23T21:56:54.9367Z"
49+
fire_at = datetime.strptime(relaxed_timestamp, DATETIME_STRING_FORMAT)
50+
51+
context_builder = ContextBuilder("relaxed precision")
52+
add_timer_fired_events(context_builder, 0, relaxed_timestamp)
53+
54+
result = get_orchestration_state_result(
55+
context_builder, generator_function)
56+
57+
expected_state = base_expected_state(output='Done!')
58+
add_timer_action(expected_state, fire_at)
59+
60+
expected_state._is_done = True
61+
expected = expected_state.to_json()
62+
63+
#assert_valid_schema(result)
64+
# TODO: getting the following error when validating the schema
65+
# "Additional properties are not allowed ('fireAt', 'isCanceled' were unexpected)">
66+
assert_orchestration_state_equals(expected, result)

tests/test_utils/ContextBuilder.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,10 @@ def add_task_failed_event(self, id_: int, reason: str, details: str):
8484
event.TaskScheduledId = id_
8585
self.history_events.append(event)
8686

87-
def add_timer_created_event(self, id_: int):
87+
def add_timer_created_event(self, id_: int, timestamp: str = None):
8888
fire_at = self.current_datetime.strftime(DATETIME_STRING_FORMAT)
89+
if timestamp is not None:
90+
fire_at = timestamp
8991
event = self.get_base_event(HistoryEventType.TIMER_CREATED, id_=id_)
9092
event.FireAt = fire_at
9193
self.history_events.append(event)

0 commit comments

Comments
 (0)