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

[Http.Resilience] Add support of the HTTP resilience for synchronous HttpClient requests #5333

Merged
merged 4 commits into from
Aug 8, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@ namespace Microsoft.Extensions.Http.Resilience.PerformanceTests;

internal sealed class NoRemoteCallHandler : DelegatingHandler
{
private readonly HttpResponseMessage _response;
private readonly Task<HttpResponseMessage> _completedResponse;
private volatile bool _disposed;

public NoRemoteCallHandler()
{
_completedResponse = Task.FromResult(new HttpResponseMessage
_response = new HttpResponseMessage
{
StatusCode = System.Net.HttpStatusCode.OK
});
};

_completedResponse = Task.FromResult(_response);
}

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
Expand All @@ -25,4 +29,20 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques
return _completedResponse;
#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks
}

protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
{
return _response;
}

protected override void Dispose(bool disposing)
{
if (disposing && !_disposed)
{
_disposed = true;
_response.Dispose();
}

base.Dispose(disposing);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,10 @@ public Task<HttpResponseMessage> Retry_Polly_V8()
{
return _v8!.SendAsync(Request, _cancellationToken);
}

[Benchmark]
public HttpResponseMessage Retry_Polly_V8_Sync()
{
return _v8!.Send(Request, _cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,10 @@ public Task<HttpResponseMessage> StandardPipeline_Polly_V8()
{
return _v8!.SendAsync(Request, _cancellationToken);
}

[Benchmark]
public HttpResponseMessage StandardPipeline_Polly_V8_Sync()
{
return _v8!.Send(Request, _cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,28 +54,18 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
{
_ = Throw.IfNull(request);

var pipeline = _pipelineProvider(request);
var created = false;
if (request.GetResilienceContext() is not ResilienceContext context)
{
context = ResilienceContextPool.Shared.Get(cancellationToken);
created = true;
request.SetResilienceContext(context);
}
ResiliencePipeline<HttpResponseMessage> pipeline = _pipelineProvider(request);

if (request.GetRequestMetadata() is RequestMetadata requestMetadata)
{
context.Properties.Set(ResilienceKeys.RequestMetadata, requestMetadata);
}

context.Properties.Set(ResilienceKeys.RequestMessage, request);
ResilienceContext context = GetOrSetResilienceContext(request, cancellationToken, out bool created);
TrySetRequestMetadata(context, request);
SetRequestMessage(context, request);

try
{
var outcome = await pipeline.ExecuteOutcomeAsync(
Outcome<HttpResponseMessage> outcome = await pipeline.ExecuteOutcomeAsync(
static async (context, state) =>
{
var request = context.Properties.GetValue(ResilienceKeys.RequestMessage, state.request);
HttpRequestMessage request = GetRequestMessage(context, state.request);

// Always re-assign the context to this request message before execution.
// This is because for primary actions the context is also cloned and we need to re-assign it
Expand All @@ -84,7 +74,10 @@ static async (context, state) =>

try
{
var response = await state.instance.SendCoreAsync(request, context.CancellationToken).ConfigureAwait(context.ContinueOnCapturedContext);
HttpResponseMessage response = await state.instance
.SendCoreAsync(request, context.CancellationToken)
.ConfigureAwait(context.ContinueOnCapturedContext);

return Outcome.FromResult(response);
}
#pragma warning disable CA1031 // Do not catch general exception types
Expand All @@ -104,19 +97,99 @@ static async (context, state) =>
}
finally
{
if (created)
{
ResilienceContextPool.Shared.Return(context);
request.SetResilienceContext(null);
}
else
{
// Restore the original context
request.SetResilienceContext(context);
}
RestoreResilienceContext(context, request, created);
}
}

#if NET6_0_OR_GREATER
/// <summary>
/// Sends an HTTP request to the inner handler to send to the server as a synchronous operation.
/// </summary>
/// <param name="request">The HTTP request message to send to the server.</param>
/// <param name="cancellationToken">A cancellation token to cancel operation.</param>
/// <returns>An HTTP response received from the server.</returns>
/// <exception cref="ArgumentNullException">If <paramref name="request"/> is <see langword="null"/>.</exception>
protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
{
_ = Throw.IfNull(request);

ResiliencePipeline<HttpResponseMessage> pipeline = _pipelineProvider(request);

ResilienceContext context = GetOrSetResilienceContext(request, cancellationToken, out bool created);
TrySetRequestMetadata(context, request);
SetRequestMessage(context, request);

try
{
return pipeline.Execute(
static (context, state) =>
{
HttpRequestMessage request = GetRequestMessage(context, state.request);

// Always re-assign the context to this request message before execution.
// This is because for primary actions the context is also cloned and we need to re-assign it
// here because Polly doesn't have any other events that we can hook into.
request.SetResilienceContext(context);

return state.instance.SendCore(request, context.CancellationToken);
},
context,
(instance: this, request));
}
finally
{
RestoreResilienceContext(context, request, created);
}
}
#endif

private static ResilienceContext GetOrSetResilienceContext(HttpRequestMessage request, CancellationToken cancellationToken, out bool created)
{
created = false;

if (request.GetResilienceContext() is not ResilienceContext context)
{
context = ResilienceContextPool.Shared.Get(cancellationToken);
created = true;
request.SetResilienceContext(context);
}

return context;
}

private static void TrySetRequestMetadata(ResilienceContext context, HttpRequestMessage request)
{
if (request.GetRequestMetadata() is RequestMetadata requestMetadata)
{
context.Properties.Set(ResilienceKeys.RequestMetadata, requestMetadata);
}
}

private static void SetRequestMessage(ResilienceContext context, HttpRequestMessage request)
=> context.Properties.Set(ResilienceKeys.RequestMessage, request);

private static HttpRequestMessage GetRequestMessage(ResilienceContext context, HttpRequestMessage request)
=> context.Properties.GetValue(ResilienceKeys.RequestMessage, request);

private static void RestoreResilienceContext(ResilienceContext context, HttpRequestMessage request, bool created)
{
if (created)
{
ResilienceContextPool.Shared.Return(context);
request.SetResilienceContext(null);
}
else
{
// Restore the original context
request.SetResilienceContext(context);
}
}

private Task<HttpResponseMessage> SendCoreAsync(HttpRequestMessage requestMessage, CancellationToken cancellationToken)
=> base.SendAsync(requestMessage, cancellationToken);

#if NET6_0_OR_GREATER
private HttpResponseMessage SendCore(HttpRequestMessage requestMessage, CancellationToken cancellationToken)
=> base.Send(requestMessage, cancellationToken);
#endif
}
Loading