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

ClientModel: Add retry classification to MessageClassifier #41586

Merged
merged 6 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ public partial class PipelineMessage : System.IDisposable
protected internal PipelineMessage(System.ClientModel.Primitives.PipelineRequest request) { }
public bool BufferResponse { get { throw null; } set { } }
public System.Threading.CancellationToken CancellationToken { get { throw null; } protected internal set { } }
public System.ClientModel.Primitives.PipelineMessageClassifier? MessageClassifier { get { throw null; } set { } }
public System.ClientModel.Primitives.PipelineMessageClassifier MessageClassifier { get { throw null; } set { } }
public System.TimeSpan? NetworkTimeout { get { throw null; } set { } }
public System.ClientModel.Primitives.PipelineRequest Request { get { throw null; } }
public System.ClientModel.Primitives.PipelineResponse? Response { get { throw null; } protected internal set { } }
Expand All @@ -159,9 +159,11 @@ public void SetProperty(System.Type type, object value) { }
}
public partial class PipelineMessageClassifier
{
protected internal PipelineMessageClassifier() { }
protected PipelineMessageClassifier() { }
public static System.ClientModel.Primitives.PipelineMessageClassifier Default { get { throw null; } }
public static System.ClientModel.Primitives.PipelineMessageClassifier Create(System.ReadOnlySpan<ushort> successStatusCodes) { throw null; }
public virtual bool IsErrorResponse(System.ClientModel.Primitives.PipelineMessage message) { throw null; }
public virtual bool TryClassify(System.ClientModel.Primitives.PipelineMessage message, out bool isError) { throw null; }
public virtual bool TryClassify(System.ClientModel.Primitives.PipelineMessage message, System.Exception? exception, out bool isRetriable) { throw null; }
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
}
public abstract partial class PipelinePolicy
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public partial class PipelineMessage : System.IDisposable
protected internal PipelineMessage(System.ClientModel.Primitives.PipelineRequest request) { }
public bool BufferResponse { get { throw null; } set { } }
public System.Threading.CancellationToken CancellationToken { get { throw null; } protected internal set { } }
public System.ClientModel.Primitives.PipelineMessageClassifier? MessageClassifier { get { throw null; } set { } }
public System.ClientModel.Primitives.PipelineMessageClassifier MessageClassifier { get { throw null; } set { } }
public System.TimeSpan? NetworkTimeout { get { throw null; } set { } }
public System.ClientModel.Primitives.PipelineRequest Request { get { throw null; } }
public System.ClientModel.Primitives.PipelineResponse? Response { get { throw null; } protected internal set { } }
Expand All @@ -158,9 +158,11 @@ public void SetProperty(System.Type type, object value) { }
}
public partial class PipelineMessageClassifier
{
protected internal PipelineMessageClassifier() { }
protected PipelineMessageClassifier() { }
public static System.ClientModel.Primitives.PipelineMessageClassifier Default { get { throw null; } }
public static System.ClientModel.Primitives.PipelineMessageClassifier Create(System.ReadOnlySpan<ushort> successStatusCodes) { throw null; }
public virtual bool IsErrorResponse(System.ClientModel.Primitives.PipelineMessage message) { throw null; }
public virtual bool TryClassify(System.ClientModel.Primitives.PipelineMessage message, out bool isError) { throw null; }
public virtual bool TryClassify(System.ClientModel.Primitives.PipelineMessage message, System.Exception? exception, out bool isRetriable) { throw null; }
}
public abstract partial class PipelinePolicy
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ protected internal PipelineMessage(PipelineRequest request)
_propertyBag = new ArrayBackedPropertyBag<ulong, object>();

BufferResponse = true;
MessageClassifier = PipelineMessageClassifier.Default;
}

public PipelineRequest Request { get; }
Expand Down Expand Up @@ -64,7 +65,7 @@ public CancellationToken CancellationToken
// the client-provided classifier or compose a chain of classification
// handlers that preserve the functionality of the client-provided classifier
// at the end of the chain.
public PipelineMessageClassifier? MessageClassifier { get; set; }
public PipelineMessageClassifier MessageClassifier { get; set; }

public void Apply(RequestOptions options)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,91 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.IO;

namespace System.ClientModel.Primitives;

public class PipelineMessageClassifier
{
internal static PipelineMessageClassifier Default { get; } = new PipelineMessageClassifier();
public static PipelineMessageClassifier Default { get; } = new EndOfChainClassifier();

public static PipelineMessageClassifier Create(ReadOnlySpan<ushort> successStatusCodes)
=> new ResponseStatusClassifier(successStatusCodes);

protected internal PipelineMessageClassifier() { }
protected PipelineMessageClassifier() { }

public virtual bool TryClassify(PipelineMessage message, out bool isError)
{
isError = false;
return false;
}

public virtual bool TryClassify(PipelineMessage message, Exception? exception, out bool isRetriable)
{
isRetriable = false;
return false;
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
/// Specifies if the response contained in the <paramref name="message"/> is not successful.
/// </summary>
public virtual bool IsErrorResponse(PipelineMessage message)
internal class EndOfChainClassifier : PipelineMessageClassifier
{
message.AssertResponse();
public override bool TryClassify(PipelineMessage message, out bool isError)
{
message.AssertResponse();

int statusKind = message.Response!.Status / 100;
isError = statusKind == 4 || statusKind == 5;

// Always classify the message
return true;
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
}

public override bool TryClassify(PipelineMessage message, Exception? exception, out bool isRetriable)
{
isRetriable = exception is null ?
IsRetriable(message) :
IsRetriable(message, exception);

// Always classify the message
return true;
}

private static bool IsRetriable(PipelineMessage message)
{
message.AssertResponse();

return message.Response!.Status switch
{
// Request Timeout
408 => true,

// Too Many Requests
429 => true,

// Internal Server Error
500 => true,

// Bad Gateway
502 => true,

// Service Unavailable
503 => true,

// Gateway Timeout
504 => true,

// Default case
_ => false
};
}

private static bool IsRetriable(PipelineMessage message, Exception exception)
=> IsRetriable(exception) ||
// Retry non-user initiated cancellations
(exception is OperationCanceledException &&
!message.CancellationToken.IsCancellationRequested);

int statusKind = message.Response!.Status / 100;
return statusKind == 4 || statusKind == 5;
private static bool IsRetriable(Exception exception)
=> (exception is IOException) ||
(exception is ClientResultException ex && ex.Status == 0);
}
}
6 changes: 0 additions & 6 deletions sdk/core/System.ClientModel/src/Options/RequestOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,6 @@ internal void Apply(PipelineMessage message)
// cancellation token will be set again in HttpPipeline.Send.
message.CancellationToken = CancellationToken;

// We don't overwrite the classifier on the message if it's already set.
// This preserves any values set by the client author, and is also
// needed for Azure.Core-based clients so we don't overwrite a default
// Azure.Core ResponseClassifier.
message.MessageClassifier ??= PipelineMessageClassifier.Default;

// Copy custom pipeline policies to the message.
message.PerCallPolicies = _perCallPolicies;
message.PerTryPolicies = _perTryPolicies;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,41 @@

using System.ClientModel.Internal;

namespace System.ClientModel.Primitives
namespace System.ClientModel.Primitives;

internal class ResponseStatusClassifier : PipelineMessageClassifier
{
internal class ResponseStatusClassifier : PipelineMessageClassifier
private BitVector640 _successCodes;

/// <summary>
/// Creates a new instance of <see cref="ResponseStatusClassifier"/>.
/// </summary>
/// <param name="successStatusCodes">The status codes that this classifier
/// will consider not to be errors.</param>
public ResponseStatusClassifier(ReadOnlySpan<ushort> successStatusCodes)
{
private BitVector640 _successCodes;

/// <summary>
/// Creates a new instance of <see cref="ResponseStatusClassifier"/>.
/// </summary>
/// <param name="successStatusCodes">The status codes that this classifier
/// will consider not to be errors.</param>
public ResponseStatusClassifier(ReadOnlySpan<ushort> successStatusCodes)
{
_successCodes = new();
_successCodes = new();

foreach (int statusCode in successStatusCodes)
{
AddClassifier(statusCode, isError: false);
}
foreach (int statusCode in successStatusCodes)
{
AddClassifier(statusCode, isError: false);
}
}

public sealed override bool IsErrorResponse(PipelineMessage message)
{
message.AssertResponse();
public override bool TryClassify(PipelineMessage message, out bool isError)
{
message.AssertResponse();

return !_successCodes[message.Response!.Status];
}
isError = !_successCodes[message.Response!.Status];

private void AddClassifier(int statusCode, bool isError)
{
Argument.AssertInRange(statusCode, 0, 639, nameof(statusCode));
// BitVector-based classifiers should always end any composition chain.
return true;
}

_successCodes[statusCode] = !isError;
}
private void AddClassifier(int statusCode, bool isError)
{
Argument.AssertInRange(statusCode, 0, 639, nameof(statusCode));

_successCodes[statusCode] = !isError;
}
}
}
57 changes: 9 additions & 48 deletions sdk/core/System.ClientModel/src/Pipeline/ClientRetryPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

using System.ClientModel.Internal;
using System.Collections.Generic;
using System.IO;
using System.Diagnostics;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -165,9 +165,14 @@ protected virtual bool ShouldRetryCore(PipelineMessage message, Exception? excep
return false;
}

return exception is null ?
IsRetriable(message) :
IsRetriable(message, exception);
if (message.MessageClassifier.TryClassify(message, exception, out bool isRetriable))
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
{
bool classified = PipelineMessageClassifier.Default.TryClassify(message, exception, out isRetriable);

Debug.Assert(classified);
}

return isRetriable;
}

protected virtual ValueTask<bool> ShouldRetryCoreAsync(PipelineMessage message, Exception? exception)
Expand Down Expand Up @@ -200,48 +205,4 @@ protected virtual void WaitCore(TimeSpan time, CancellationToken cancellationTok
CancellationHelper.ThrowIfCancellationRequested(cancellationToken);
}
}

#region Retry Classifier

// Overriding response-retriable classification will be added in a later ClientModel release.
private static bool IsRetriable(PipelineMessage message)
{
message.AssertResponse();

return message.Response!.Status switch
{
// Request Timeout
408 => true,

// Too Many Requests
429 => true,

// Internal Server Error
500 => true,

// Bad Gateway
502 => true,

// Service Unavailable
503 => true,

// Gateway Timeout
504 => true,

// Default case
_ => false
};
}

private static bool IsRetriable(PipelineMessage message, Exception exception)
=> IsRetriable(exception) ||
// Retry non-user initiated cancellations
(exception is OperationCanceledException &&
!message.CancellationToken.IsCancellationRequested);

private static bool IsRetriable(Exception exception)
=> (exception is IOException) ||
(exception is ClientResultException ex && ex.Status == 0);

#endregion
}
14 changes: 11 additions & 3 deletions sdk/core/System.ClientModel/src/Pipeline/PipelineTransport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,17 @@ public async ValueTask ProcessAsync(PipelineMessage message)
message.Response.SetIsError(ClassifyResponse(message));
}

private static bool ClassifyResponse(PipelineMessage message) =>
message.MessageClassifier?.IsErrorResponse(message) ??
PipelineMessageClassifier.Default.IsErrorResponse(message);
private static bool ClassifyResponse(PipelineMessage message)
{
if (!message.MessageClassifier.TryClassify(message, out bool isError))
{
bool classified = PipelineMessageClassifier.Default.TryClassify(message, out isError);

Debug.Assert(classified);
}

return isError;
}

protected abstract void ProcessCore(PipelineMessage message);

Expand Down
Loading
Loading