Skip to content
12 changes: 12 additions & 0 deletions src/Client/Core/PurgeInstancesFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,16 @@
DateTimeOffset? CreatedTo = null,
IEnumerable<OrchestrationRuntimeStatus>? Statuses = null)
{
/// <summary>
/// Gets or sets the maximum amount of time to spend purging instances in a single call.
/// If <c>null</c> (default), all matching instances are purged with no time limit.
/// When set, the purge operation stops deleting additional instances after this duration elapses
/// and returns a partial result. Callers can check <see cref="PurgeResult.IsComplete"/> and
/// re-invoke the purge to continue where it left off.
/// The value of <see cref="PurgeResult.IsComplete"/> depends on the backend implementation:
/// it may be <c>false</c> if the purge timed out, <c>true</c> if all instances were purged,
/// or <c>null</c> if the backend does not support reporting completion status.
/// Not all backends support this property; those that do not will ignore it.
/// </summary>
public TimeSpan? Timeout { get; init; }

Check warning on line 28 in src/Client/Core/PurgeInstancesFilter.cs

View workflow job for this annotation

GitHub Actions / smoke-tests

The property's documentation summary text should begin with: 'Gets' (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1623.md)
}
13 changes: 13 additions & 0 deletions src/Client/Grpc/GrpcDurableTaskClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,19 @@ public override Task<PurgeResult> PurgeAllInstancesAsync(
request.PurgeInstanceFilter.RuntimeStatus.AddRange(filter.Statuses.Select(x => x.ToGrpcStatus()));
}

if (filter?.Timeout is not null)
{
if (filter.Timeout.Value <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(
nameof(filter),
filter.Timeout.Value,
"PurgeInstancesFilter.Timeout must be a positive TimeSpan.");
}

request.PurgeInstanceFilter.Timeout = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(filter.Timeout.Value);
}

return this.PurgeInstancesCoreAsync(request, cancellation);
}

Expand Down
1 change: 1 addition & 0 deletions src/Grpc/orchestrator_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,7 @@ message PurgeInstanceFilter {
google.protobuf.Timestamp createdTimeFrom = 1;
google.protobuf.Timestamp createdTimeTo = 2;
repeated OrchestrationStatus runtimeStatus = 3;
google.protobuf.Duration timeout = 4;
}

message PurgeInstancesResponse {
Expand Down
39 changes: 39 additions & 0 deletions test/Client/Grpc.Tests/GrpcDurableTaskClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,5 +127,44 @@ public async Task ScheduleNewOrchestrationInstanceAsync_ValidDedupeStatus_DoesNo
var exception = await act.Should().ThrowAsync<Exception>();
exception.Which.Should().NotBeOfType<ArgumentException>();
}

[Fact]
public async Task PurgeAllInstancesAsync_NegativeTimeout_ThrowsArgumentOutOfRangeException()
{
// Arrange
var client = this.CreateClient();
var filter = new PurgeInstancesFilter { Timeout = TimeSpan.FromSeconds(-1) };

// Act & Assert
Func<Task> act = async () => await client.PurgeAllInstancesAsync(filter);
var exception = await act.Should().ThrowAsync<ArgumentOutOfRangeException>();
exception.Which.Message.Should().Contain("Timeout must be a positive TimeSpan.");
}

[Fact]
public async Task PurgeAllInstancesAsync_ZeroTimeout_ThrowsArgumentOutOfRangeException()
{
// Arrange
var client = this.CreateClient();
var filter = new PurgeInstancesFilter { Timeout = TimeSpan.Zero };

// Act & Assert
Func<Task> act = async () => await client.PurgeAllInstancesAsync(filter);
var exception = await act.Should().ThrowAsync<ArgumentOutOfRangeException>();
exception.Which.Message.Should().Contain("Timeout must be a positive TimeSpan.");
}

[Fact]
public async Task PurgeAllInstancesAsync_PositiveTimeout_DoesNotThrowValidationError()
{
// Arrange
var client = this.CreateClient();
var filter = new PurgeInstancesFilter { Timeout = TimeSpan.FromSeconds(30) };

// Act & Assert - validation should pass; the call will fail at gRPC level, not validation
Func<Task> act = async () => await client.PurgeAllInstancesAsync(filter);
var exception = await act.Should().ThrowAsync<Exception>();
exception.Which.Should().NotBeOfType<ArgumentOutOfRangeException>();
}
}

Loading