diff --git a/src/WebJobs.Extensions.DurableTask/ProtobufUtils.cs b/src/WebJobs.Extensions.DurableTask/ProtobufUtils.cs index a2da7cda4..9b67e93ec 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.CreatedTimeTo.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..5fc7c2d3a --- /dev/null +++ b/test/e2e/Apps/BasicDotNetIsolated/PurgeOrchestrationHistory.cs @@ -0,0 +1,38 @@ +// 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 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) + { + ILogger logger = executionContext.GetLogger("HelloCities_HttpStart"); + + logger.LogInformation("Starting purge all instance history"); + + var requestPurgeResult = await client.PurgeAllInstancesAsync(new PurgeInstancesFilter(null, DateTime.UtcNow, 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; + } + } +} diff --git a/test/e2e/Tests/Tests/PurgeInstancesTests.cs b/test/e2e/Tests/Tests/PurgeInstancesTests.cs new file mode 100644 index 000000000..ef86a5d94 --- /dev/null +++ b/test/e2e/Tests/Tests/PurgeInstancesTests.cs @@ -0,0 +1,49 @@ +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; + } + + // Due to some kind of asynchronous race condition in XUnit, when running these tests in pipelines, + // the output may be disposed before the message is written. Just ignore these types of errors for now. + private void WriteOutput(string message) + { + try + { + _output.WriteLine(message); + } + catch + { + // Ignore + } + } + + [Theory] + [InlineData("PurgeOrchestrationHistory", HttpStatusCode.OK, @"^Purged [0-9]* records$")] + public async Task HttpTriggerTests(string functionName, HttpStatusCode expectedStatusCode, string responseRegex) + { + using HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger(functionName, ""); + string actualMessage = await response.Content.ReadAsStringAsync(); + Assert.Matches(responseRegex, actualMessage); + Assert.Equal(expectedStatusCode, response.StatusCode); + } +}