Skip to content

Commit 216f384

Browse files
authored
Add integration test coverage for Suspend/Resume operations (#546)
1 parent 031c825 commit 216f384

File tree

1 file changed

+219
-20
lines changed

1 file changed

+219
-20
lines changed

test/Grpc.IntegrationTests/GrpcDurableTaskClientIntegrationTests.cs

Lines changed: 219 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ namespace Microsoft.DurableTask.Grpc.Tests;
1616
public class DurableTaskGrpcClientIntegrationTests : IntegrationTestBase
1717
{
1818
const string OrchestrationName = "TestOrchestration";
19+
const int PollingTimeoutSeconds = 5;
20+
const int PollingIntervalMilliseconds = 100;
1921

2022
public DurableTaskGrpcClientIntegrationTests(ITestOutputHelper output, GrpcSidecarFixture sidecarFixture)
2123
: base(output, sidecarFixture)
@@ -291,26 +293,6 @@ await restartAction.Should().ThrowAsync<ArgumentException>()
291293
.WithMessage("*An orchestration with the instanceId non-existent-instance-id was not found*");
292294
}
293295

294-
Task<HostTestLifetime> StartAsync()
295-
{
296-
static async Task<string> Orchestration(TaskOrchestrationContext context, bool shouldThrow)
297-
{
298-
context.SetCustomStatus("waiting");
299-
await context.WaitForExternalEvent<string>("event");
300-
if (shouldThrow)
301-
{
302-
throw new InvalidOperationException("Orchestration failed");
303-
}
304-
305-
return $"{shouldThrow} -> output";
306-
}
307-
308-
return this.StartWorkerAsync(b =>
309-
{
310-
b.AddTasks(tasks => tasks.AddOrchestratorFunc<bool, string>(OrchestrationName, Orchestration));
311-
});
312-
}
313-
314296
[Fact]
315297
public async Task ScheduleNewOrchestrationInstance_WithDedupeStatuses_ThrowsWhenInstanceExists()
316298
{
@@ -482,6 +464,223 @@ await createAction.Should().ThrowAsync<RpcException>()
482464
.Where(e => e.StatusCode == StatusCode.AlreadyExists);
483465
}
484466

467+
[Fact]
468+
public async Task SuspendAndResumeInstance_EndToEnd()
469+
{
470+
// Arrange
471+
await using HostTestLifetime server = await this.StartLongRunningAsync();
472+
473+
string instanceId = await server.Client.ScheduleNewOrchestrationInstanceAsync(
474+
OrchestrationName, input: false);
475+
476+
// Wait for the orchestration to start
477+
await server.Client.WaitForInstanceStartAsync(instanceId, default);
478+
479+
// Act - Suspend the orchestration
480+
await server.Client.SuspendInstanceAsync(instanceId, "Test suspension", default);
481+
482+
// Poll for suspended status
483+
OrchestrationMetadata? suspendedMetadata = await this.PollForStatusAsync(
484+
server.Client, instanceId, OrchestrationRuntimeStatus.Suspended, default);
485+
486+
// Assert - Verify orchestration is suspended
487+
suspendedMetadata.Should().NotBeNull();
488+
suspendedMetadata!.RuntimeStatus.Should().Be(OrchestrationRuntimeStatus.Suspended);
489+
suspendedMetadata.InstanceId.Should().Be(instanceId);
490+
491+
// Act - Resume the orchestration
492+
await server.Client.ResumeInstanceAsync(instanceId, "Test resumption", default);
493+
494+
// Poll for running status
495+
OrchestrationMetadata? resumedMetadata = await this.PollForStatusAsync(
496+
server.Client, instanceId, OrchestrationRuntimeStatus.Running, default);
497+
498+
// Assert - Verify orchestration is running again
499+
resumedMetadata.Should().NotBeNull();
500+
resumedMetadata!.RuntimeStatus.Should().Be(OrchestrationRuntimeStatus.Running);
501+
502+
// Complete the orchestration
503+
await server.Client.RaiseEventAsync(instanceId, "event", default);
504+
await server.Client.WaitForInstanceCompletionAsync(instanceId, default);
505+
506+
// Verify the orchestration completed successfully
507+
OrchestrationMetadata? completedMetadata = await server.Client.GetInstanceAsync(instanceId, false);
508+
completedMetadata.Should().NotBeNull();
509+
completedMetadata!.RuntimeStatus.Should().Be(OrchestrationRuntimeStatus.Completed);
510+
}
511+
512+
[Fact]
513+
public async Task SuspendInstance_WithoutReason_Succeeds()
514+
{
515+
// Arrange
516+
await using HostTestLifetime server = await this.StartLongRunningAsync();
517+
518+
string instanceId = await server.Client.ScheduleNewOrchestrationInstanceAsync(
519+
OrchestrationName, input: false);
520+
521+
await server.Client.WaitForInstanceStartAsync(instanceId, default);
522+
523+
// Act - Suspend without a reason
524+
await server.Client.SuspendInstanceAsync(instanceId, cancellation: default);
525+
526+
// Poll for suspended status
527+
OrchestrationMetadata? suspendedMetadata = await this.PollForStatusAsync(
528+
server.Client, instanceId, OrchestrationRuntimeStatus.Suspended, default);
529+
530+
// Assert
531+
suspendedMetadata.Should().NotBeNull();
532+
suspendedMetadata!.RuntimeStatus.Should().Be(OrchestrationRuntimeStatus.Suspended);
533+
}
534+
535+
[Fact]
536+
public async Task ResumeInstance_WithoutReason_Succeeds()
537+
{
538+
// Arrange
539+
await using HostTestLifetime server = await this.StartLongRunningAsync();
540+
541+
string instanceId = await server.Client.ScheduleNewOrchestrationInstanceAsync(
542+
OrchestrationName, input: false);
543+
544+
await server.Client.WaitForInstanceStartAsync(instanceId, default);
545+
await server.Client.SuspendInstanceAsync(instanceId, "Test suspension", default);
546+
547+
// Wait for suspension
548+
await this.PollForStatusAsync(server.Client, instanceId, OrchestrationRuntimeStatus.Suspended, default);
549+
550+
// Act - Resume without a reason
551+
await server.Client.ResumeInstanceAsync(instanceId, cancellation: default);
552+
553+
// Poll for running status
554+
OrchestrationMetadata? resumedMetadata = await this.PollForStatusAsync(
555+
server.Client, instanceId, OrchestrationRuntimeStatus.Running, default);
556+
557+
// Assert
558+
resumedMetadata.Should().NotBeNull();
559+
resumedMetadata!.RuntimeStatus.Should().Be(OrchestrationRuntimeStatus.Running);
560+
}
561+
562+
[Fact]
563+
public async Task SuspendInstance_AlreadyCompleted_NoError()
564+
{
565+
// Arrange
566+
await using HostTestLifetime server = await this.StartAsync();
567+
568+
string instanceId = await server.Client.ScheduleNewOrchestrationInstanceAsync(
569+
OrchestrationName, input: false);
570+
571+
await server.Client.WaitForInstanceStartAsync(instanceId, default);
572+
await server.Client.RaiseEventAsync(instanceId, "event", default);
573+
await server.Client.WaitForInstanceCompletionAsync(instanceId, default);
574+
575+
// Verify it's completed
576+
OrchestrationMetadata? completedMetadata = await server.Client.GetInstanceAsync(instanceId, false);
577+
completedMetadata.Should().NotBeNull();
578+
completedMetadata!.RuntimeStatus.Should().Be(OrchestrationRuntimeStatus.Completed);
579+
580+
// Act - Try to suspend a completed orchestration (should not throw)
581+
await server.Client.SuspendInstanceAsync(instanceId, "Test suspension", default);
582+
583+
// Assert - Status should remain completed
584+
OrchestrationMetadata? metadata = await server.Client.GetInstanceAsync(instanceId, false);
585+
metadata.Should().NotBeNull();
586+
metadata!.RuntimeStatus.Should().Be(OrchestrationRuntimeStatus.Completed);
587+
}
588+
589+
[Fact]
590+
public async Task ResumeInstance_NotSuspended_NoError()
591+
{
592+
// Arrange
593+
await using HostTestLifetime server = await this.StartLongRunningAsync();
594+
595+
string instanceId = await server.Client.ScheduleNewOrchestrationInstanceAsync(
596+
OrchestrationName, input: false);
597+
598+
await server.Client.WaitForInstanceStartAsync(instanceId, default);
599+
600+
// Verify it's running
601+
OrchestrationMetadata? runningMetadata = await server.Client.GetInstanceAsync(instanceId, false);
602+
runningMetadata.Should().NotBeNull();
603+
runningMetadata!.RuntimeStatus.Should().Be(OrchestrationRuntimeStatus.Running);
604+
605+
// Act - Try to resume an already running orchestration (should not throw)
606+
await server.Client.ResumeInstanceAsync(instanceId, "Test resumption", default);
607+
608+
// Assert - Status should remain running
609+
OrchestrationMetadata? metadata = await server.Client.GetInstanceAsync(instanceId, false);
610+
metadata.Should().NotBeNull();
611+
metadata!.RuntimeStatus.Should().Be(OrchestrationRuntimeStatus.Running);
612+
}
613+
614+
Task<HostTestLifetime> StartAsync()
615+
{
616+
static async Task<string> Orchestration(TaskOrchestrationContext context, bool shouldThrow)
617+
{
618+
context.SetCustomStatus("waiting");
619+
await context.WaitForExternalEvent<string>("event");
620+
if (shouldThrow)
621+
{
622+
throw new InvalidOperationException("Orchestration failed");
623+
}
624+
625+
return $"{shouldThrow} -> output";
626+
}
627+
628+
return this.StartWorkerAsync(b =>
629+
{
630+
b.AddTasks(tasks => tasks.AddOrchestratorFunc<bool, string>(OrchestrationName, Orchestration));
631+
});
632+
}
633+
634+
Task<HostTestLifetime> StartLongRunningAsync()
635+
{
636+
static async Task<string> LongRunningOrchestration(TaskOrchestrationContext context, bool shouldThrow)
637+
{
638+
context.SetCustomStatus("waiting");
639+
// Wait for external event or a timer (30 seconds) to allow suspend/resume operations
640+
Task<string> eventTask = context.WaitForExternalEvent<string>("event");
641+
Task timerTask = context.CreateTimer(TimeSpan.FromSeconds(30), CancellationToken.None);
642+
Task completedTask = await Task.WhenAny(eventTask, timerTask);
643+
644+
if (completedTask == timerTask)
645+
{
646+
throw new TimeoutException("Timed out waiting for external event 'event'.");
647+
}
648+
649+
if (shouldThrow)
650+
{
651+
throw new InvalidOperationException("Orchestration failed");
652+
}
653+
654+
return $"{shouldThrow} -> output";
655+
}
656+
657+
return this.StartWorkerAsync(b =>
658+
{
659+
b.AddTasks(tasks => tasks.AddOrchestratorFunc<bool, string>(OrchestrationName, LongRunningOrchestration));
660+
});
661+
}
662+
663+
async Task<OrchestrationMetadata?> PollForStatusAsync(
664+
DurableTaskClient client,
665+
string instanceId,
666+
OrchestrationRuntimeStatus expectedStatus,
667+
CancellationToken cancellation = default)
668+
{
669+
DateTime deadline = DateTime.UtcNow.AddSeconds(PollingTimeoutSeconds);
670+
while (DateTime.UtcNow < deadline)
671+
{
672+
OrchestrationMetadata? metadata = await client.GetInstanceAsync(instanceId, false, cancellation);
673+
if (metadata?.RuntimeStatus == expectedStatus)
674+
{
675+
return metadata;
676+
}
677+
678+
await Task.Delay(TimeSpan.FromMilliseconds(PollingIntervalMilliseconds), cancellation);
679+
}
680+
681+
return await client.GetInstanceAsync(instanceId, false, cancellation);
682+
}
683+
485684
class DateTimeToleranceComparer : IEqualityComparer<DateTimeOffset>
486685
{
487686
public bool Equals(DateTimeOffset x, DateTimeOffset y) => (x - y).Duration() < TimeSpan.FromMilliseconds(100);

0 commit comments

Comments
 (0)