Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add suspend/resume tests #3028

Merged
merged 10 commits into from
Feb 6, 2025
1 change: 1 addition & 0 deletions test/e2e/Apps/BasicDotNetIsolated/HelloCities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.Extensions.Logging;

namespace Microsoft.Azure.Durable.Tests.E2E;

public static class HelloCities
{
[Function(nameof(HelloCities))]
Expand Down
1 change: 1 addition & 0 deletions test/e2e/Apps/BasicDotNetIsolated/OrchestrationQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.DurableTask.Client;

namespace Microsoft.Azure.Durable.Tests.E2E;

public static class OrchestrationQueryFunctions
{
[Function(nameof(GetAllInstances))]
Expand Down
65 changes: 32 additions & 33 deletions test/e2e/Apps/BasicDotNetIsolated/PurgeOrchestrationHistory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,43 @@
using Microsoft.DurableTask.Client;
using Microsoft.Extensions.Logging;

namespace Microsoft.Azure.Durable.Tests.E2E
namespace Microsoft.Azure.Durable.Tests.E2E;

public static class PurgeOrchestrationHistory
{
public static class PurgeOrchestrationHistory
[Function(nameof(PurgeOrchestrationHistory))]
public static async Task<HttpResponseData> PurgeHistory(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
[DurableClient] DurableTaskClient client,
FunctionContext executionContext,
DateTime? purgeStartTime=null,
DateTime? purgeEndTime=null)
{
[Function(nameof(PurgeOrchestrationHistory))]
public static async Task<HttpResponseData> 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");
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>{
OrchestrationRuntimeStatus.Completed,
OrchestrationRuntimeStatus.Failed,
OrchestrationRuntimeStatus.Terminated
}));
logger.LogInformation("Starting purge all instance history");
try
{
var requestPurgeResult = await client.PurgeAllInstancesAsync(new PurgeInstancesFilter(purgeStartTime, purgeEndTime, new List<OrchestrationRuntimeStatus>{
OrchestrationRuntimeStatus.Completed,
OrchestrationRuntimeStatus.Failed,
OrchestrationRuntimeStatus.Terminated
}));

logger.LogInformation("Finished purge all instance history");
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;
}
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.DurableTask.Client;

namespace Microsoft.Azure.Durable.Tests.E2E;

public static class SuspendResumeOrchestration
{
[Function("SuspendInstance")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.Extensions.Logging;

namespace Microsoft.Azure.Durable.Tests.E2E;

public static class LongRunningOrchestration
{
[Function(nameof(LongRunningOrchestrator))]
Expand Down
15 changes: 15 additions & 0 deletions test/e2e/Tests/Helpers/DurableHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ internal static async Task<OrchestrationStatusDetails> GetRunningOrchestrationDe
return new OrchestrationStatusDetails(statusQueryResponseString);
}

internal static async Task WaitForOrchestrationStateAsync(string statusQueryGetUri, string desiredState, int maxTimeoutSeconds)
{
DateTime timeoutTime = DateTime.Now + TimeSpan.FromSeconds(maxTimeoutSeconds);
while (DateTime.Now < timeoutTime)
{
var currentStatus = await GetRunningOrchestrationDetailsAsync(statusQueryGetUri);
if (currentStatus.RuntimeStatus == desiredState)
{
return;
}
cgillum marked this conversation as resolved.
Show resolved Hide resolved
await Task.Delay(100);
}
throw new TimeoutException($"Orchestration did not reach {desiredState} status within {maxTimeoutSeconds} seconds.");
}

private static string TokenizeAndGetValueFromKeyAsString(string? json, string key)
{
if (string.IsNullOrEmpty(json))
Expand Down
37 changes: 15 additions & 22 deletions test/e2e/Tests/Tests/HelloCitiesTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,15 @@ public async Task HttpTriggerTests(string functionName, HttpStatusCode expectedS

Assert.Equal(expectedStatusCode, response.StatusCode);
string statusQueryGetUri = await DurableHelpers.ParseStatusQueryGetUriAsync(response);
Thread.Sleep(1000);

await DurableHelpers.WaitForOrchestrationStateAsync(statusQueryGetUri, "Completed", 5);

var orchestrationDetails = await DurableHelpers.GetRunningOrchestrationDetailsAsync(statusQueryGetUri);
Assert.Equal("Completed", orchestrationDetails.RuntimeStatus);
Assert.Contains(partialExpectedOutput, orchestrationDetails.Output);
}

[Theory]
[InlineData("HelloCities_HttpStart_Scheduled", 10, HttpStatusCode.Accepted)]
[InlineData("HelloCities_HttpStart_Scheduled", 5, HttpStatusCode.Accepted)]
[InlineData("HelloCities_HttpStart_Scheduled", -5, HttpStatusCode.Accepted)]
public async Task ScheduledStartTests(string functionName, int startDelaySeconds, HttpStatusCode expectedStatusCode)
{
Expand All @@ -65,28 +66,20 @@ public async Task ScheduledStartTests(string functionName, int startDelaySeconds

Assert.Equal(expectedStatusCode, response.StatusCode);

var orchestrationDetails = await DurableHelpers.GetRunningOrchestrationDetailsAsync(statusQueryGetUri);
while (DateTime.UtcNow < scheduledStartTime + TimeSpan.FromSeconds(-1))
if (scheduledStartTime > DateTime.UtcNow + TimeSpan.FromSeconds(1))
{
WriteOutput($"Test scheduled for {scheduledStartTime}, current time {DateTime.Now}");
orchestrationDetails = await DurableHelpers.GetRunningOrchestrationDetailsAsync(statusQueryGetUri);
Assert.Equal("Pending", orchestrationDetails.RuntimeStatus);
Thread.Sleep(1000);
await DurableHelpers.WaitForOrchestrationStateAsync(statusQueryGetUri, "Pending", 5);
}

// Give a small amount of time for the orchestration to complete, even if scheduled to run immediately
Thread.Sleep(3000);
WriteOutput($"Test scheduled for {scheduledStartTime}, current time {DateTime.Now}, looking for completed");
var finalOrchestrationDetails = await DurableHelpers.GetRunningOrchestrationDetailsAsync(statusQueryGetUri);
int retryAttempts = 0;
while (finalOrchestrationDetails.RuntimeStatus != "Completed" && retryAttempts < 10)
{
Thread.Sleep(1000);
finalOrchestrationDetails = await DurableHelpers.GetRunningOrchestrationDetailsAsync(statusQueryGetUri);
retryAttempts++;
}
Assert.Equal("Completed", finalOrchestrationDetails.RuntimeStatus);
await DurableHelpers.WaitForOrchestrationStateAsync(statusQueryGetUri, "Completed", Math.Max(startDelaySeconds, 0) + 5);

Assert.True(finalOrchestrationDetails.LastUpdatedTime > scheduledStartTime);
// This +1s should not be necessary - however, experimentally the orchestration may run up to one second before the scheduled time.
// It is unclear currently whether this is a bug where orchestrations run early, or a clock difference/error,
// but leaving this logic in for now until further investigation.
Assert.True(DateTime.UtcNow + TimeSpan.FromSeconds(1) >= scheduledStartTime);

var finalOrchestrationDetails = await DurableHelpers.GetRunningOrchestrationDetailsAsync(statusQueryGetUri);
WriteOutput($"Last updated at {finalOrchestrationDetails.LastUpdatedTime}, scheduled to complete at {scheduledStartTime}");
Assert.True(finalOrchestrationDetails.LastUpdatedTime + TimeSpan.FromSeconds(1) >= scheduledStartTime);
}
}
6 changes: 1 addition & 5 deletions test/e2e/Tests/Tests/OrchestrationQueryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,9 @@ public async Task ListRunningOrchestrations_ShouldContainRunningOrchestration()
string instanceId = await DurableHelpers.ParseInstanceIdAsync(response);
string statusQueryGetUri = await DurableHelpers.ParseStatusQueryGetUriAsync(response);

Thread.Sleep(1000);

await DurableHelpers.WaitForOrchestrationStateAsync(statusQueryGetUri, "Running", 5);
try
{
var orchestrationDetails = await DurableHelpers.GetRunningOrchestrationDetailsAsync(statusQueryGetUri);
Assert.Equal("Running", orchestrationDetails.RuntimeStatus);

using HttpResponseMessage statusResponse = await HttpHelpers.InvokeHttpTrigger("GetRunningInstances", "");

Assert.Equal(HttpStatusCode.OK, statusResponse.StatusCode);
Expand Down
4 changes: 3 additions & 1 deletion test/e2e/Tests/Tests/PurgeInstancesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ public async Task PurgeOrchestrationHistoryAfterInvocation_Succeeds()
{
using HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger("HelloCities_HttpStart", "");
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
Thread.Sleep(1000);
string statusQueryGetUri = await DurableHelpers.ParseStatusQueryGetUriAsync(response);

await DurableHelpers.WaitForOrchestrationStateAsync(statusQueryGetUri, "Completed", 5);

DateTime purgeEndTime = DateTime.UtcNow + TimeSpan.FromMinutes(1);
using HttpResponseMessage purgeResponse = await HttpHelpers.InvokeHttpTrigger("PurgeOrchestrationHistory", $"?purgeEndTime={purgeEndTime:o}");
Expand Down
51 changes: 15 additions & 36 deletions test/e2e/Tests/Tests/SuspendResumeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,30 +30,18 @@ public async Task SuspendAndResumeRunningOrchestration_ShouldSucceed()
string instanceId = await DurableHelpers.ParseInstanceIdAsync(response);
string statusQueryGetUri = await DurableHelpers.ParseStatusQueryGetUriAsync(response);

Thread.Sleep(1000);

var orchestrationDetails = await DurableHelpers.GetRunningOrchestrationDetailsAsync(statusQueryGetUri);
Assert.Equal("Running", orchestrationDetails.RuntimeStatus);

await DurableHelpers.WaitForOrchestrationStateAsync(statusQueryGetUri, "Running", 5);
try
{
using HttpResponseMessage suspendResponse = await HttpHelpers.InvokeHttpTrigger("SuspendInstance", $"?instanceId={instanceId}");
await AssertRequestSucceedsAsync(suspendResponse);

Thread.Sleep(1000);

orchestrationDetails = await DurableHelpers.GetRunningOrchestrationDetailsAsync(statusQueryGetUri);
Assert.Equal("Suspended", orchestrationDetails.RuntimeStatus);

Thread.Sleep(1000);
await DurableHelpers.WaitForOrchestrationStateAsync(statusQueryGetUri, "Suspended", 5);

using HttpResponseMessage resumeResponse = await HttpHelpers.InvokeHttpTrigger("ResumeInstance", $"?instanceId={instanceId}");
await AssertRequestSucceedsAsync(resumeResponse);

Thread.Sleep(1000);

orchestrationDetails = await DurableHelpers.GetRunningOrchestrationDetailsAsync(statusQueryGetUri);
Assert.Equal("Running", orchestrationDetails.RuntimeStatus);
await DurableHelpers.WaitForOrchestrationStateAsync(statusQueryGetUri, "Running", 5);
}
finally
{
Expand All @@ -70,17 +58,13 @@ public async Task SuspendSuspendedOrchestration_ShouldFail()
string instanceId = await DurableHelpers.ParseInstanceIdAsync(response);
string statusQueryGetUri = await DurableHelpers.ParseStatusQueryGetUriAsync(response);

Thread.Sleep(1000);

await DurableHelpers.WaitForOrchestrationStateAsync(statusQueryGetUri, "Running", 5);
try
{
var orchestrationDetails = await DurableHelpers.GetRunningOrchestrationDetailsAsync(statusQueryGetUri);
Assert.Equal("Running", orchestrationDetails.RuntimeStatus);

using HttpResponseMessage suspendResponse = await HttpHelpers.InvokeHttpTrigger("SuspendInstance", $"?instanceId={instanceId}");
await AssertRequestSucceedsAsync(suspendResponse);

Thread.Sleep(1000);
await DurableHelpers.WaitForOrchestrationStateAsync(statusQueryGetUri, "Suspended", 5);

using HttpResponseMessage resumeResponse = await HttpHelpers.InvokeHttpTrigger("SuspendInstance", $"?instanceId={instanceId}");
await AssertRequestFailsAsync(resumeResponse);
Expand All @@ -107,14 +91,9 @@ public async Task ResumeRunningOrchestration_ShouldFail()
string instanceId = await DurableHelpers.ParseInstanceIdAsync(response);
string statusQueryGetUri = await DurableHelpers.ParseStatusQueryGetUriAsync(response);

Thread.Sleep(1000);
await DurableHelpers.WaitForOrchestrationStateAsync(statusQueryGetUri, "Running", 5);
try
{
var orchestrationDetails = await DurableHelpers.GetRunningOrchestrationDetailsAsync(statusQueryGetUri);
Assert.Equal("Running", orchestrationDetails.RuntimeStatus);

Thread.Sleep(1000);

using HttpResponseMessage resumeResponse = await HttpHelpers.InvokeHttpTrigger("ResumeInstance", $"?instanceId={instanceId}");
await AssertRequestFailsAsync(resumeResponse);

Expand All @@ -140,7 +119,7 @@ public async Task SuspendResumeCompletedOrchestration_ShouldFail()
string instanceId = await DurableHelpers.ParseInstanceIdAsync(response);
string statusQueryGetUri = await DurableHelpers.ParseStatusQueryGetUriAsync(response);

Thread.Sleep(1000);
await DurableHelpers.WaitForOrchestrationStateAsync(statusQueryGetUri, "Completed", 5);
try
{
using HttpResponseMessage suspendResponse = await HttpHelpers.InvokeHttpTrigger("SuspendInstance", $"?instanceId={instanceId}");
Expand All @@ -164,23 +143,23 @@ public async Task SuspendResumeCompletedOrchestration_ShouldFail()
}
}

private static async Task AssertRequestSucceedsAsync(HttpResponseMessage suspendResponse)
private static async Task AssertRequestSucceedsAsync(HttpResponseMessage response)
{
Assert.Equal(HttpStatusCode.OK, suspendResponse.StatusCode);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);

string? responseMessage = await suspendResponse.Content.ReadAsStringAsync();
string? responseMessage = await response.Content.ReadAsStringAsync();
Assert.NotNull(responseMessage);
Assert.Empty(responseMessage);
}

private static async Task AssertRequestFailsAsync(HttpResponseMessage terminateResponse)
private static async Task AssertRequestFailsAsync(HttpResponseMessage response)
{
Assert.Equal(HttpStatusCode.BadRequest, terminateResponse.StatusCode);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);

string? terminateResponseMessage = await terminateResponse.Content.ReadAsStringAsync();
Assert.NotNull(terminateResponseMessage);
string? responseMessage = await response.Content.ReadAsStringAsync();
Assert.NotNull(responseMessage);
// Unclear error message - see https://github.com/Azure/azure-functions-durable-extension/issues/3027, will update this code when that bug is fixed
Assert.Equal("Status(StatusCode=\"Unknown\", Detail=\"Exception was thrown by handler.\")", terminateResponseMessage);
Assert.Equal("Status(StatusCode=\"Unknown\", Detail=\"Exception was thrown by handler.\")", responseMessage);
}

private static async Task<bool> TryTerminateInstanceAsync(string instanceId)
Expand Down
Loading
Loading