From c838aa0167b83dfc24f36e0b526b0b33476771f1 Mon Sep 17 00:00:00 2001 From: andystaples <77818326+andystaples@users.noreply.github.com> Date: Wed, 5 Feb 2025 11:18:28 -0800 Subject: [PATCH] Add a test for PurgeAllInstancesAsync (#3021) - Add a test for PurgeOrchestrationHistory - Fix bug with PurgeOrchestrationHistory relating to null start date --- release_notes.md | 1 + .../ProtobufUtils.cs | 4 +- .../PurgeOrchestrationHistory.cs | 51 +++++++++ test/e2e/Tests/Tests/PurgeInstancesTests.cs | 101 ++++++++++++++++++ 4 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 test/e2e/Apps/BasicDotNetIsolated/PurgeOrchestrationHistory.cs create mode 100644 test/e2e/Tests/Tests/PurgeInstancesTests.cs diff --git a/release_notes.md b/release_notes.md index e52a1aa47..43e6487f4 100644 --- a/release_notes.md +++ b/release_notes.md @@ -12,6 +12,7 @@ - Fix custom connection name not working when using IDurableClientFactory.CreateClient() - contributed by [@hctan](https://github.com/hctan) - Made durable extension for isolated worker configuration idempotent, allowing multiple calls safely. (#2950) - Fixes a bug with Out of Memory exception handling in Isolated, improving reliability of retries for this case. (part of #3020) +- Fixed issue with passing null CreatedFrom date in PurgeInstancesFilter to client.PurgeAllInstancesAsync (#3021) ### Breaking Changes diff --git a/src/WebJobs.Extensions.DurableTask/ProtobufUtils.cs b/src/WebJobs.Extensions.DurableTask/ProtobufUtils.cs index a2da7cda4..f43b49350 100644 --- a/src/WebJobs.Extensions.DurableTask/ProtobufUtils.cs +++ b/src/WebJobs.Extensions.DurableTask/ProtobufUtils.cs @@ -438,8 +438,10 @@ internal static PurgeInstanceFilter ToPurgeInstanceFilter(P.PurgeInstancesReques statusFilter = request.PurgeInstanceFilter.RuntimeStatus?.Select(status => (OrchestrationStatus)status).ToList(); } + // This ternary condition is necessary because the protobuf spec __insists__ that CreatedTimeFrom may never be null, + // but nonetheless if you pass null in function code, the value will be null here return new PurgeInstanceFilter( - request.PurgeInstanceFilter.CreatedTimeFrom.ToDateTime(), + request.PurgeInstanceFilter.CreatedTimeFrom == null ? DateTime.MinValue : request.PurgeInstanceFilter.CreatedTimeFrom.ToDateTime(), request.PurgeInstanceFilter.CreatedTimeTo?.ToDateTime(), statusFilter); } diff --git a/test/e2e/Apps/BasicDotNetIsolated/PurgeOrchestrationHistory.cs b/test/e2e/Apps/BasicDotNetIsolated/PurgeOrchestrationHistory.cs new file mode 100644 index 000000000..77b111113 --- /dev/null +++ b/test/e2e/Apps/BasicDotNetIsolated/PurgeOrchestrationHistory.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Net; +using Grpc.Core; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.DurableTask.Client; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Azure.Durable.Tests.E2E +{ + public static class PurgeOrchestrationHistory + { + [Function(nameof(PurgeOrchestrationHistory))] + public static async Task PurgeHistory( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, + [DurableClient] DurableTaskClient client, + FunctionContext executionContext, + DateTime? purgeStartTime=null, + DateTime? purgeEndTime=null) + { + ILogger logger = executionContext.GetLogger("HelloCities_HttpStart"); + + logger.LogInformation("Starting purge all instance history"); + try + { + var requestPurgeResult = await client.PurgeAllInstancesAsync(new PurgeInstancesFilter(purgeStartTime, purgeEndTime, new List{ + OrchestrationRuntimeStatus.Completed, + OrchestrationRuntimeStatus.Failed, + OrchestrationRuntimeStatus.Terminated + })); + + logger.LogInformation("Finished purge all instance history"); + + var response = req.CreateResponse(HttpStatusCode.OK); + response.Headers.Add("Content-Type", "text/plain"); + await response.WriteStringAsync($"Purged {requestPurgeResult.PurgedInstanceCount} records"); + return response; + } + catch (RpcException ex) + { + logger.LogError(ex, "Failed to purge all instance history"); + var response = req.CreateResponse(HttpStatusCode.InternalServerError); + response.Headers.Add("Content-Type", "text/plain"); + await response.WriteStringAsync($"Failed to purge all instance history: {ex.Message}"); + return response; + } + } + } +} diff --git a/test/e2e/Tests/Tests/PurgeInstancesTests.cs b/test/e2e/Tests/Tests/PurgeInstancesTests.cs new file mode 100644 index 000000000..5ffe72751 --- /dev/null +++ b/test/e2e/Tests/Tests/PurgeInstancesTests.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Azure.Durable.Tests.DotnetIsolatedE2E; + +[Collection(Constants.FunctionAppCollectionName)] +public class PurgeInstancesTests +{ + private readonly FunctionAppFixture _fixture; + private readonly ITestOutputHelper _output; + + public PurgeInstancesTests(FunctionAppFixture fixture, ITestOutputHelper testOutputHelper) + { + _fixture = fixture; + _fixture.TestLogs.UseTestLogger(testOutputHelper); + _output = testOutputHelper; + } + + [Fact] + public async Task PurgeOrchestrationHistory_StartAndEnd_Succeeds() + { + DateTime purgeStartTime = DateTime.MinValue; + DateTime purgeEndTime = DateTime.UtcNow; + string queryParams = $"?purgeStartTime={purgeStartTime:o}&purgeEndTime={purgeEndTime:o}"; + using HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger("PurgeOrchestrationHistory", queryParams); + string actualMessage = await response.Content.ReadAsStringAsync(); + Assert.Matches(@"^Purged [0-9]* records$", actualMessage); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task PurgeOrchestrationHistory_Start_Succeeds() + { + DateTime purgeStartTime = DateTime.MinValue; + DateTime purgeEndTime = DateTime.UtcNow; + string queryParams = $"?purgeStartTime={purgeStartTime:o}"; + using HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger("PurgeOrchestrationHistory", queryParams); + string actualMessage = await response.Content.ReadAsStringAsync(); + Assert.Matches(@"^Purged [0-9]* records$", actualMessage); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task PurgeOrchestrationHistory_End_Succeeds() + { + DateTime purgeStartTime = DateTime.MinValue; + DateTime purgeEndTime = DateTime.UtcNow; + string queryParams = $"?purgeEndTime={purgeEndTime:o}"; + using HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger("PurgeOrchestrationHistory", queryParams); + string actualMessage = await response.Content.ReadAsStringAsync(); + Assert.Matches(@"^Purged [0-9]* records$", actualMessage); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task PurgeOrchestrationHistory_NoBoundaries_Succeeds() + { + DateTime purgeStartTime = DateTime.MinValue; + DateTime purgeEndTime = DateTime.UtcNow; + string queryParams = $""; + using HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger("PurgeOrchestrationHistory", queryParams); + string actualMessage = await response.Content.ReadAsStringAsync(); + Assert.Matches(@"^Purged [0-9]* records$", actualMessage); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task PurgeOrchestrationHistoryAfterInvocation_Succeeds() + { + using HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger("HelloCities_HttpStart", ""); + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + Thread.Sleep(1000); + + DateTime purgeEndTime = DateTime.UtcNow + TimeSpan.FromMinutes(1); + using HttpResponseMessage purgeResponse = await HttpHelpers.InvokeHttpTrigger("PurgeOrchestrationHistory", $"?purgeEndTime={purgeEndTime:o}"); + string purgeMessage = await purgeResponse.Content.ReadAsStringAsync(); + Assert.Matches(@"^Purged [0-9]* records$", purgeMessage); + Assert.DoesNotMatch(@"^Purged 0 records$", purgeMessage); + Assert.Equal(HttpStatusCode.OK, purgeResponse.StatusCode); + } + + [Fact] + public async Task PurgeAfterPurge_ZeroRows() + { + DateTime purgeEndTime = DateTime.UtcNow + TimeSpan.FromMinutes(1); + using HttpResponseMessage purgeResponse = await HttpHelpers.InvokeHttpTrigger("PurgeOrchestrationHistory", $"?purgeEndTime={purgeEndTime:o}"); + string purgeMessage = await purgeResponse.Content.ReadAsStringAsync(); + Assert.Matches(@"^Purged [0-9]* records$", purgeMessage); + using HttpResponseMessage purgeAgainResponse = await HttpHelpers.InvokeHttpTrigger("PurgeOrchestrationHistory", $"?purgeEndTime={purgeEndTime:o}"); + string purgeAgainMessage = await purgeAgainResponse.Content.ReadAsStringAsync(); + Assert.Matches(@"^Purged 0 records$", purgeAgainMessage); + Assert.Equal(HttpStatusCode.OK, purgeAgainResponse.StatusCode); + } +}