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 abstract UpdateStatus methods to OperationResult #45475

Merged
merged 8 commits into from
Aug 13, 2024
1 change: 1 addition & 0 deletions sdk/core/System.ClientModel/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
### Other Changes

- Removed `ReturnWhen` enum in favor of using bool `waitUntilCompleted` parameter in third-party client LRO method signatures.
- Added abstract `UpdateStatus` method to `OperationResult`.

## 1.1.0-beta.6 (2024-08-01)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,12 @@ public ModelReaderWriterOptions(string format) { }
public abstract partial class OperationResult : System.ClientModel.ClientResult
{
protected OperationResult(System.ClientModel.Primitives.PipelineResponse response) { }
public abstract bool IsCompleted { get; protected set; }
public bool HasCompleted { get { throw null; } protected set { } }
public abstract System.ClientModel.ContinuationToken? RehydrationToken { get; protected set; }
public abstract void WaitForCompletion(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
public abstract System.Threading.Tasks.Task WaitForCompletionAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
public abstract System.ClientModel.ClientResult UpdateStatus(System.ClientModel.Primitives.RequestOptions? options = null);
public abstract System.Threading.Tasks.ValueTask<System.ClientModel.ClientResult> UpdateStatusAsync(System.ClientModel.Primitives.RequestOptions? options = null);
public virtual void WaitForCompletion(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { }
public virtual System.Threading.Tasks.ValueTask WaitForCompletionAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
}
[System.AttributeUsageAttribute(System.AttributeTargets.Class)]
public sealed partial class PersistableModelProxyAttribute : System.Attribute
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,12 @@ public ModelReaderWriterOptions(string format) { }
public abstract partial class OperationResult : System.ClientModel.ClientResult
{
protected OperationResult(System.ClientModel.Primitives.PipelineResponse response) { }
public abstract bool IsCompleted { get; protected set; }
public bool HasCompleted { get { throw null; } protected set { } }
public abstract System.ClientModel.ContinuationToken? RehydrationToken { get; protected set; }
public abstract void WaitForCompletion(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
public abstract System.Threading.Tasks.Task WaitForCompletionAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
public abstract System.ClientModel.ClientResult UpdateStatus(System.ClientModel.Primitives.RequestOptions? options = null);
public abstract System.Threading.Tasks.ValueTask<System.ClientModel.ClientResult> UpdateStatusAsync(System.ClientModel.Primitives.RequestOptions? options = null);
public virtual void WaitForCompletion(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { }
public virtual System.Threading.Tasks.ValueTask WaitForCompletionAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
}
[System.AttributeUsageAttribute(System.AttributeTargets.Class)]
public sealed partial class PersistableModelProxyAttribute : System.Attribute
Expand Down
115 changes: 100 additions & 15 deletions sdk/core/System.ClientModel/src/Convenience/OperationResult.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.ClientModel.Internal;
using System.Threading;
using System.Threading.Tasks;

Expand Down Expand Up @@ -34,44 +35,128 @@ protected OperationResult(PipelineResponse response)
/// Gets a value that indicates whether the operation has completed.
/// </summary>
/// <value><c>true</c> if the operation has reached a terminal state
/// (that is, is has finished successfully, ended due to an error condition,
/// (that is, it has finished successfully, ended due to an error condition,
/// or has been cancelled by a user); otherwise, <c>false</c>.
/// </value>
public abstract bool IsCompleted { get; protected set; }
/// <remarks><see cref="HasCompleted"/> is updated by the
/// <see cref="UpdateStatus"/> method, based on the response received from
/// the service regarding the operation's status. Users must call
/// <see cref="WaitForCompletion"/>, <see cref="UpdateStatus"/>, or other
/// method provided by the derived type to ensure that the value of the
/// <see cref="HasCompleted"/> property reflects the current status of the
/// operation running on the service.
/// </remarks>
public bool HasCompleted { get; protected set; }

/// <summary>
/// Gets a token that can be used to rehydrate the operation.
/// </summary>
/// <value>A token that can be used to rehydrate the operation, for example
/// to monitor its progress or to obtain its final result, from a process
/// different thatn the one that started the operation.</value>
/// different than the one that started the operation.</value>
public abstract ContinuationToken? RehydrationToken { get; protected set; }

/// <summary>
/// Sends a request to the service to get the current status of the
/// operation and updates <see cref="HasCompleted"/> and other relevant
/// properties.
/// </summary>
/// <param name="options">The <see cref="RequestOptions"/> to be used when
/// sending the request to the service.</param>
/// <returns>The <see cref="ClientResult"/> returned from the service call.
/// </returns>
/// <remarks>This method updates the value returned from
/// <see cref="ClientResult.GetRawResponse"/> and will update
/// <see cref="HasCompleted"/> to <c>true</c> once the operation has finished
/// running on the service. It will also update <c>Value</c> or
/// <c>Status</c> properties if present on the <see cref="OperationResult"/>
/// derived type.</remarks>
public abstract ValueTask<ClientResult> UpdateStatusAsync(RequestOptions? options = default);

/// <summary>
/// Sends a request to the service to get the current status of the
/// operation and updates <see cref="HasCompleted"/> and other relevant
/// properties.
/// </summary>
/// <param name="options">The <see cref="RequestOptions"/> to be used when
/// sending the request to the service.</param>
/// <returns>The <see cref="ClientResult"/> returned from the service call.
/// </returns>
/// <remarks>This method updates the value returned from
/// <see cref="ClientResult.GetRawResponse"/> and will update
/// <see cref="HasCompleted"/> to <c>true</c> once the operation has finished
/// running on the service. It will also update <c>Value</c> or
/// <c>Status</c> properties if present on the <see cref="OperationResult"/>
/// derived type.</remarks>
public abstract ClientResult UpdateStatus(RequestOptions? options = default);

/// <summary>
/// Waits for the operation to complete processing on the service.
/// </summary>
/// <remarks>Derived types may implement <see cref="WaitForCompletionAsync"/>
/// using different mechanisms to obtain updates from the service regarding
/// the progress of the operation. If the derived type polls for status
/// updates, it provides overloads of <see cref="WaitForCompletionAsync"/>
/// <remarks>Derived types may override <see cref="WaitForCompletionAsync"/>
/// to implement different mechanisms for obtaining updates from the service
/// regarding the progress of the operation. For example, if the derived
/// type polls for status updates, it may provides overloads of
/// <see cref="WaitForCompletionAsync"/>
/// that allow the caller to specify the polling interval or delay strategy
/// used to wait between sending request for updates.
/// used to wait between sending request for updates. By default,
/// <see cref="WaitForCompletionAsync"/> waits a default interval between
/// calling <see cref="UpdateStatusAsync"/> to obtain a status updates, so
/// if updates are delivered via streaming or another mechanism where a wait
/// time is not required, derived types can override this method to update
/// the status more frequently.
/// </remarks>
/// <exception cref="OperationCanceledException">The <paramref name="cancellationToken"/>
/// was cancelled.</exception>
public abstract Task WaitForCompletionAsync(CancellationToken cancellationToken = default);
public virtual async ValueTask WaitForCompletionAsync(CancellationToken cancellationToken = default)
{
PollingInterval pollingInterval = new();

while (!HasCompleted)
{
PipelineResponse response = GetRawResponse();

await pollingInterval.WaitAsync(response, cancellationToken).ConfigureAwait(false);

RequestOptions? options = RequestOptions.FromCancellationToken(cancellationToken);
ClientResult result = await UpdateStatusAsync(options).ConfigureAwait(false);

SetRawResponse(result.GetRawResponse());
}
}

/// <summary>
/// Waits for the operation to complete processing on the service.
/// </summary>
/// <remarks>Derived types may implement <see cref="WaitForCompletion"/>
/// using different mechanisms to obtain updates from the service regarding
/// the progress of the operation. If the derived type polls for status
/// updates, it provides overloads of <see cref="WaitForCompletion"/>
/// <remarks>Derived types may override <see cref="WaitForCompletion"/>
/// to implement different mechanisms for obtaining updates from the service
/// regarding the progress of the operation. For example, if the derived
/// type polls for status updates, it may provides overloads of
/// <see cref="WaitForCompletion"/>
/// that allow the caller to specify the polling interval or delay strategy
/// used to wait between sending request for updates.
/// used to wait between sending request for updates. By default,
/// <see cref="WaitForCompletion"/> waits a default interval between
/// calling <see cref="UpdateStatus"/> to obtain a status updates, so
/// if updates are delivered via streaming or another mechanism where a wait
/// time is not required, derived types can override this method to update
/// the status more frequently.
/// </remarks>
/// <exception cref="OperationCanceledException">The <paramref name="cancellationToken"/>
/// was cancelled.</exception>
public abstract void WaitForCompletion(CancellationToken cancellationToken = default);
public virtual void WaitForCompletion(CancellationToken cancellationToken = default)
{
PollingInterval pollingInterval = new();

while (!HasCompleted)
{
PipelineResponse response = GetRawResponse();

pollingInterval.Wait(response, cancellationToken);

RequestOptions? options = RequestOptions.FromCancellationToken(cancellationToken);
ClientResult result = UpdateStatus(options);

SetRawResponse(result.GetRawResponse());
}
}
}
47 changes: 47 additions & 0 deletions sdk/core/System.ClientModel/src/Internal/PollingInterval.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.ClientModel.Primitives;
using System.Threading;
using System.Threading.Tasks;

namespace System.ClientModel.Internal;

internal sealed class PollingInterval
{
private static readonly TimeSpan DefaultDelay = TimeSpan.FromSeconds(1.0);

private readonly TimeSpan _interval;

public PollingInterval(TimeSpan? interval = default)
{
_interval = interval ?? DefaultDelay;
}

public async Task WaitAsync(PipelineResponse response, CancellationToken cancellationToken)
{
TimeSpan delay = GetDelay(response);

await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
}

public void Wait(PipelineResponse response, CancellationToken cancellationToken)
{
TimeSpan delay = GetDelay(response);

if (!cancellationToken.CanBeCanceled)
{
Thread.Sleep(delay);
return;
}

if (cancellationToken.WaitHandle.WaitOne(delay))
{
cancellationToken.ThrowIfCancellationRequested();
}
}

private TimeSpan GetDelay(PipelineResponse response)
=> PipelineResponseHeaders.TryGetRetryAfter(response, out TimeSpan retryAfter)
&& retryAfter > _interval ? retryAfter : _interval;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ namespace System.ClientModel.Primitives;
/// </summary>
public abstract class PipelineResponseHeaders : IEnumerable<KeyValuePair<string, string>>
{
private const string RetryAfterHeaderName = "Retry-After";

/// <summary>
/// Attempts to retrieve the value associated with the specified header
/// name.
Expand All @@ -37,4 +39,26 @@ public abstract class PipelineResponseHeaders : IEnumerable<KeyValuePair<string,
public abstract IEnumerator<KeyValuePair<string, string>> GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

internal static bool TryGetRetryAfter(PipelineResponse response, out TimeSpan value)
{
// See: https://www.rfc-editor.org/rfc/rfc7231#section-7.1.3
if (response.Headers.TryGetValue(RetryAfterHeaderName, out string? retryAfter))
{
if (int.TryParse(retryAfter, out var delaySeconds))
{
value = TimeSpan.FromSeconds(delaySeconds);
return true;
}

if (DateTimeOffset.TryParse(retryAfter, out DateTimeOffset retryAfterDate))
{
value = retryAfterDate - DateTimeOffset.Now;
return true;
}
}

value = default;
return false;
}
}
13 changes: 13 additions & 0 deletions sdk/core/System.ClientModel/src/Options/RequestOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,19 @@ public RequestOptions()
{
}

internal static RequestOptions? FromCancellationToken(CancellationToken cancellationToken)
{
if (cancellationToken == default)
{
return null;
}

return new RequestOptions()
{
CancellationToken = cancellationToken
};
}

/// <summary>
/// Gets or sets the <see cref="CancellationToken"/> used for the duration
/// of the call to <see cref="ClientPipeline.Send(PipelineMessage)"/>.
Expand Down
25 changes: 1 addition & 24 deletions sdk/core/System.ClientModel/src/Pipeline/ClientRetryPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ public class ClientRetryPolicy : PipelinePolicy

private const int DefaultMaxRetries = 3;
private static readonly TimeSpan DefaultInitialDelay = TimeSpan.FromSeconds(0.8);
private const string RetryAfterHeaderName = "Retry-After";

private readonly int _maxRetries;
private readonly TimeSpan _initialDelay;
Expand Down Expand Up @@ -276,7 +275,7 @@ protected virtual TimeSpan GetNextDelay(PipelineMessage message, int tryCount)
double nextDelayMilliseconds = (1 << (tryCount - 1)) * _initialDelay.TotalMilliseconds;

if (message.Response is not null &&
TryGetRetryAfter(message.Response, out TimeSpan retryAfter) &&
PipelineResponseHeaders.TryGetRetryAfter(message.Response, out TimeSpan retryAfter) &&
retryAfter.TotalMilliseconds > nextDelayMilliseconds)
{
return retryAfter;
Expand Down Expand Up @@ -319,26 +318,4 @@ protected virtual void Wait(TimeSpan time, CancellationToken cancellationToken)
CancellationHelper.ThrowIfCancellationRequested(cancellationToken);
}
}

private static bool TryGetRetryAfter(PipelineResponse response, out TimeSpan value)
{
// See: https://www.rfc-editor.org/rfc/rfc7231#section-7.1.3
if (response.Headers.TryGetValue(RetryAfterHeaderName, out string? retryAfter))
{
if (int.TryParse(retryAfter, out var delaySeconds))
{
value = TimeSpan.FromSeconds(delaySeconds);
return true;
}

if (DateTimeOffset.TryParse(retryAfter, out DateTimeOffset retryAfterDate))
{
value = retryAfterDate - DateTimeOffset.Now;
return true;
}
}

value = default;
return false;
}
}
Loading