From b01ef2760da6e1f035950ea78cb5b0b1fcbffab5 Mon Sep 17 00:00:00 2001 From: Anatoli Beliaev Date: Tue, 23 Jul 2024 16:16:19 -0700 Subject: [PATCH] Convert timer target to UTC (#1138) Convert timer target time to UTC --------- Co-authored-by: David Justo --- .../AzureStorageOrchestrationService.cs | 13 ++++ .../Tracking/AzureTableTrackingStore.cs | 3 + src/DurableTask.AzureStorage/Utils.cs | 31 ++++++++ .../AzureStorageScenarioTests.cs | 71 ++++++++++++++++++- 4 files changed, 115 insertions(+), 3 deletions(-) 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; } }