Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)

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

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

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),
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ArgumentOutOfRangeException uses paramName: nameof(filter), which points at the whole filter rather than the Timeout property that’s invalid. Consider using nameof(PurgeInstancesFilter.Timeout) / nameof(filter.Timeout) so callers get a more precise exception parameter name.

Suggested change
nameof(filter),
nameof(PurgeInstancesFilter.Timeout),

Copilot uses AI. Check for mistakes.
filter.Timeout.Value,
"Timeout must be a positive TimeSpan.");
}

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

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duration.FromTimeSpan(...) will throw ArgumentOutOfRangeException for very large values (e.g., TimeSpan.MaxValue). Since this is a public opt-in API, consider validating/catching that case and throwing a clearer ArgumentOutOfRangeException that consistently references the Timeout parameter.

Suggested change
request.PurgeInstanceFilter.Timeout = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(filter.Timeout.Value);
try
{
request.PurgeInstanceFilter.Timeout = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(filter.Timeout.Value);
}
catch (ArgumentOutOfRangeException ex)
{
throw new ArgumentOutOfRangeException(
nameof(filter.Timeout),
filter.Timeout.Value,
"Timeout is outside the supported range for gRPC duration values.",
ex);
}

Copilot uses AI. Check for mistakes.
}

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) };
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PurgeInstancesFilter is a positional record without an explicit parameterless constructor; object creation without () (i.e., new PurgeInstancesFilter { ... }) won’t compile. Use new PurgeInstancesFilter() (or provide the positional args) before the object initializer.

Copilot uses AI. Check for mistakes.

// 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 };
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PurgeInstancesFilter is a positional record without an explicit parameterless constructor; object creation without () (i.e., new PurgeInstancesFilter { ... }) won’t compile. Use new PurgeInstancesFilter() (or provide the positional args) before the object initializer.

Copilot uses AI. Check for mistakes.

// 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) };
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PurgeInstancesFilter is a positional record without an explicit parameterless constructor; object creation without () (i.e., new PurgeInstancesFilter { ... }) won’t compile. Use new PurgeInstancesFilter() (or provide the positional args) before the object initializer.

Copilot uses AI. Check for mistakes.

// 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