diff --git a/src/DurableTask.AzureStorage/AzureStorageOrchestrationService.cs b/src/DurableTask.AzureStorage/AzureStorageOrchestrationService.cs
index 606e07b41..62183b622 100644
--- a/src/DurableTask.AzureStorage/AzureStorageOrchestrationService.cs
+++ b/src/DurableTask.AzureStorage/AzureStorageOrchestrationService.cs
@@ -1106,6 +1106,17 @@ public async Task CompleteTaskOrchestrationWorkItemAsync(
TaskMessage continuedAsNewMessage,
OrchestrationState orchestrationState)
{
+ // for backwards compatibility, we transform timer timestamps to UTC prior to persisting in Azure Storage.
+ // see: https://github.com/Azure/durabletask/pull/1138
+ foreach (var orchestratorMessage in orchestratorMessages)
+ {
+ Utils.ConvertDateTimeInHistoryEventsToUTC(orchestratorMessage.Event);
+ }
+ foreach (var timerMessage in timerMessages)
+ {
+ Utils.ConvertDateTimeInHistoryEventsToUTC(timerMessage.Event);
+ }
+
OrchestrationSession session;
if (!this.orchestrationSessionManager.TryGetExistingSession(workItem.InstanceId, out session))
{
@@ -1687,6 +1698,8 @@ public async Task CreateTaskOrchestrationAsync(TaskMessage creationMessage, Orch
throw new ArgumentException($"Only {nameof(EventType.ExecutionStarted)} messages are supported.", nameof(creationMessage));
}
+ Utils.ConvertDateTimeInHistoryEventsToUTC(creationMessage.Event);
+
// Client operations will auto-create the task hub if it doesn't already exist.
await this.EnsureTaskHubAsync();
diff --git a/src/DurableTask.AzureStorage/Tracking/AzureTableTrackingStore.cs b/src/DurableTask.AzureStorage/Tracking/AzureTableTrackingStore.cs
index bcb3f1f2c..ee50191e7 100644
--- a/src/DurableTask.AzureStorage/Tracking/AzureTableTrackingStore.cs
+++ b/src/DurableTask.AzureStorage/Tracking/AzureTableTrackingStore.cs
@@ -846,6 +846,9 @@ public override Task StartAsync(CancellationToken cancellationToken = default)
bool isFinalEvent = i == newEvents.Count - 1;
HistoryEvent historyEvent = newEvents[i];
+ // For backwards compatibility, we convert timer timestamps to UTC prior to persisting to Azure Storage
+ // see: https://github.com/Azure/durabletask/pull/1138
+ Utils.ConvertDateTimeInHistoryEventsToUTC(historyEvent);
var historyEntity = TableEntityConverter.Serialize(historyEvent);
historyEntity.PartitionKey = sanitizedInstanceId;
diff --git a/src/DurableTask.AzureStorage/Utils.cs b/src/DurableTask.AzureStorage/Utils.cs
index 7db878ffc..3e9ca4524 100644
--- a/src/DurableTask.AzureStorage/Utils.cs
+++ b/src/DurableTask.AzureStorage/Utils.cs
@@ -272,5 +272,36 @@ public static object DeserializeFromJson(JsonSerializer serializer, string jsonS
}
return obj;
}
+
+ public static void ConvertDateTimeInHistoryEventsToUTC(HistoryEvent historyEvent)
+ {
+ switch (historyEvent.EventType)
+ {
+ case EventType.ExecutionStarted:
+ var executionStartedEvent = (ExecutionStartedEvent)historyEvent;
+ if (executionStartedEvent.ScheduledStartTime.HasValue &&
+ executionStartedEvent.ScheduledStartTime.Value.Kind != DateTimeKind.Utc)
+ {
+ executionStartedEvent.ScheduledStartTime = executionStartedEvent.ScheduledStartTime.Value.ToUniversalTime();
+ }
+ break;
+
+ case EventType.TimerCreated:
+ var timerCreatedEvent = (TimerCreatedEvent)historyEvent;
+ if (timerCreatedEvent.FireAt.Kind != DateTimeKind.Utc)
+ {
+ timerCreatedEvent.FireAt = timerCreatedEvent.FireAt.ToUniversalTime();
+ }
+ break;
+
+ case EventType.TimerFired:
+ var timerFiredEvent = (TimerFiredEvent)historyEvent;
+ if (timerFiredEvent.FireAt.Kind != DateTimeKind.Utc)
+ {
+ timerFiredEvent.FireAt = timerFiredEvent.FireAt.ToUniversalTime();
+ }
+ break;
+ }
+ }
}
}
diff --git a/test/DurableTask.AzureStorage.Tests/AzureStorageScenarioTests.cs b/test/DurableTask.AzureStorage.Tests/AzureStorageScenarioTests.cs
index 7dadb01ee..96495fde4 100644
--- a/test/DurableTask.AzureStorage.Tests/AzureStorageScenarioTests.cs
+++ b/test/DurableTask.AzureStorage.Tests/AzureStorageScenarioTests.cs
@@ -1285,6 +1285,71 @@ public async Task TimerExpiration(bool enableExtendedSessions)
}
}
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public async Task TimerDelay(bool useUtc)
+ {
+ using (TestOrchestrationHost host = TestHelpers.GetTestOrchestrationHost(false))
+ {
+ await host.StartAsync();
+ // by convention, DateTime objects are expected to be in UTC, but previous version of DTFx.AzureStorage
+ // performed a implicit conversions to UTC when different timezones where used. This test ensures
+ // that behavior is backwards compatible, despite not being recommended.
+ var startTime = useUtc ? DateTime.UtcNow : DateTime.Now;
+ var delay = TimeSpan.FromSeconds(5);
+ var fireAt = startTime.Add(delay);
+ var client = await host.StartOrchestrationAsync(typeof(Orchestrations.DelayedCurrentTimeInline), fireAt);
+
+ var status = await client.WaitForCompletionAsync(TimeSpan.FromSeconds(30));
+ Assert.AreEqual(OrchestrationStatus.Completed, status?.OrchestrationStatus);
+
+ var actualDelay = DateTime.UtcNow - startTime.ToUniversalTime();
+ Assert.IsTrue(
+ actualDelay >= delay && actualDelay < delay + TimeSpan.FromSeconds(10),
+ $"Expected delay: {delay}, ActualDelay: {actualDelay}");
+
+ await host.StopAsync();
+ }
+ }
+
+ [DataTestMethod]
+ [DataRow(false)]
+ [DataRow(true)]
+ public async Task OrchestratorStartAtAcceptsAllDateTimeKinds(bool useUtc)
+ {
+ using (TestOrchestrationHost host = TestHelpers.GetTestOrchestrationHost(false))
+ {
+ await host.StartAsync();
+ // by convention, DateTime objects are expected to be in UTC, but previous version of DTFx.AzureStorage
+ // performed a implicit conversions to UTC when different timezones where used. This test ensures
+ // that behavior is backwards compatible, despite not being recommended.
+
+ // set up orchestrator start time
+ var currentTime = DateTime.Now;
+ var delay = TimeSpan.FromSeconds(5);
+ var startAt = currentTime.Add(delay);
+
+ if (useUtc)
+ {
+ startAt = startAt.ToUniversalTime();
+ }
+
+
+ var client = await host.StartOrchestrationAsync(typeof(Orchestrations.CurrentTimeInline), input: string.Empty, startAt: startAt);
+
+ var status = await client.WaitForCompletionAsync(TimeSpan.FromSeconds(30));
+ Assert.AreEqual(OrchestrationStatus.Completed, status?.OrchestrationStatus);
+
+ var orchestratorState = await client.GetStateAsync(client.InstanceId);
+ var actualScheduledStartTime = status.ScheduledStartTime;
+
+ // internal representation of DateTime is always UTC
+ var expectedScheduledStartTime = startAt.ToUniversalTime();
+ Assert.AreEqual(expectedScheduledStartTime, actualScheduledStartTime);
+ await host.StopAsync();
+ }
+ }
///
/// End-to-end test which validates that orchestrations run concurrently of each other (up to 100 by default).
///
@@ -3304,11 +3369,11 @@ public override Task RunTask(OrchestrationContext context, string inpu
}
}
- internal class DelayedCurrentTimeInline : TaskOrchestration
+ internal class DelayedCurrentTimeInline : TaskOrchestration
{
- public override async Task RunTask(OrchestrationContext context, string input)
+ public override async Task RunTask(OrchestrationContext context, DateTime fireAt)
{
- await context.CreateTimer(context.CurrentUtcDateTime.Add(TimeSpan.FromSeconds(3)), true);
+ await context.CreateTimer(fireAt, true);
return context.CurrentUtcDateTime;
}
}