Skip to content

Commit 0cd4e2e

Browse files
[Http.Resilience] Add support of the HTTP resilience for synchronous HttpClient requests (#5333)
* Add support of resilience for synchronous HttpClient requests * Added one more test * Mention a variable type explicitly if it is not obvious from the assignment * Fixes #5236 Adds StandardPipeline and Retry benchmarks for synchronous HttpClient requests
1 parent b841602 commit 0cd4e2e

File tree

13 files changed

+462
-125
lines changed

13 files changed

+462
-125
lines changed

bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/NoRemoteCallHandler.cs

+22-2
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,18 @@ namespace Microsoft.Extensions.Http.Resilience.PerformanceTests;
99

1010
internal sealed class NoRemoteCallHandler : DelegatingHandler
1111
{
12+
private readonly HttpResponseMessage _response;
1213
private readonly Task<HttpResponseMessage> _completedResponse;
14+
private volatile bool _disposed;
1315

1416
public NoRemoteCallHandler()
1517
{
16-
_completedResponse = Task.FromResult(new HttpResponseMessage
18+
_response = new HttpResponseMessage
1719
{
1820
StatusCode = System.Net.HttpStatusCode.OK
19-
});
21+
};
22+
23+
_completedResponse = Task.FromResult(_response);
2024
}
2125

2226
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
@@ -25,4 +29,20 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques
2529
return _completedResponse;
2630
#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks
2731
}
32+
33+
protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
34+
{
35+
return _response;
36+
}
37+
38+
protected override void Dispose(bool disposing)
39+
{
40+
if (disposing && !_disposed)
41+
{
42+
_disposed = true;
43+
_response.Dispose();
44+
}
45+
46+
base.Dispose(disposing);
47+
}
2848
}

bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/RetryBenchmark.cs

+6
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,10 @@ public Task<HttpResponseMessage> Retry_Polly_V8()
7676
{
7777
return _v8!.SendAsync(Request, _cancellationToken);
7878
}
79+
80+
[Benchmark]
81+
public HttpResponseMessage Retry_Polly_V8_Sync()
82+
{
83+
return _v8!.Send(Request, _cancellationToken);
84+
}
7985
}

bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/StandardResilienceBenchmark.cs

+6
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,10 @@ public Task<HttpResponseMessage> StandardPipeline_Polly_V8()
7878
{
7979
return _v8!.SendAsync(Request, _cancellationToken);
8080
}
81+
82+
[Benchmark]
83+
public HttpResponseMessage StandardPipeline_Polly_V8_Sync()
84+
{
85+
return _v8!.Send(Request, _cancellationToken);
86+
}
8187
}

src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/ResilienceHandler.cs

+100-27
Original file line numberDiff line numberDiff line change
@@ -54,28 +54,18 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
5454
{
5555
_ = Throw.IfNull(request);
5656

57-
var pipeline = _pipelineProvider(request);
58-
var created = false;
59-
if (request.GetResilienceContext() is not ResilienceContext context)
60-
{
61-
context = ResilienceContextPool.Shared.Get(cancellationToken);
62-
created = true;
63-
request.SetResilienceContext(context);
64-
}
57+
ResiliencePipeline<HttpResponseMessage> pipeline = _pipelineProvider(request);
6558

66-
if (request.GetRequestMetadata() is RequestMetadata requestMetadata)
67-
{
68-
context.Properties.Set(ResilienceKeys.RequestMetadata, requestMetadata);
69-
}
70-
71-
context.Properties.Set(ResilienceKeys.RequestMessage, request);
59+
ResilienceContext context = GetOrSetResilienceContext(request, cancellationToken, out bool created);
60+
TrySetRequestMetadata(context, request);
61+
SetRequestMessage(context, request);
7262

7363
try
7464
{
75-
var outcome = await pipeline.ExecuteOutcomeAsync(
65+
Outcome<HttpResponseMessage> outcome = await pipeline.ExecuteOutcomeAsync(
7666
static async (context, state) =>
7767
{
78-
var request = context.Properties.GetValue(ResilienceKeys.RequestMessage, state.request);
68+
HttpRequestMessage request = GetRequestMessage(context, state.request);
7969

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

8575
try
8676
{
87-
var response = await state.instance.SendCoreAsync(request, context.CancellationToken).ConfigureAwait(context.ContinueOnCapturedContext);
77+
HttpResponseMessage response = await state.instance
78+
.SendCoreAsync(request, context.CancellationToken)
79+
.ConfigureAwait(context.ContinueOnCapturedContext);
80+
8881
return Outcome.FromResult(response);
8982
}
9083
#pragma warning disable CA1031 // Do not catch general exception types
@@ -104,19 +97,99 @@ static async (context, state) =>
10497
}
10598
finally
10699
{
107-
if (created)
108-
{
109-
ResilienceContextPool.Shared.Return(context);
110-
request.SetResilienceContext(null);
111-
}
112-
else
113-
{
114-
// Restore the original context
115-
request.SetResilienceContext(context);
116-
}
100+
RestoreResilienceContext(context, request, created);
101+
}
102+
}
103+
104+
#if NET6_0_OR_GREATER
105+
/// <summary>
106+
/// Sends an HTTP request to the inner handler to send to the server as a synchronous operation.
107+
/// </summary>
108+
/// <param name="request">The HTTP request message to send to the server.</param>
109+
/// <param name="cancellationToken">A cancellation token to cancel operation.</param>
110+
/// <returns>An HTTP response received from the server.</returns>
111+
/// <exception cref="ArgumentNullException">If <paramref name="request"/> is <see langword="null"/>.</exception>
112+
protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
113+
{
114+
_ = Throw.IfNull(request);
115+
116+
ResiliencePipeline<HttpResponseMessage> pipeline = _pipelineProvider(request);
117+
118+
ResilienceContext context = GetOrSetResilienceContext(request, cancellationToken, out bool created);
119+
TrySetRequestMetadata(context, request);
120+
SetRequestMessage(context, request);
121+
122+
try
123+
{
124+
return pipeline.Execute(
125+
static (context, state) =>
126+
{
127+
HttpRequestMessage request = GetRequestMessage(context, state.request);
128+
129+
// Always re-assign the context to this request message before execution.
130+
// This is because for primary actions the context is also cloned and we need to re-assign it
131+
// here because Polly doesn't have any other events that we can hook into.
132+
request.SetResilienceContext(context);
133+
134+
return state.instance.SendCore(request, context.CancellationToken);
135+
},
136+
context,
137+
(instance: this, request));
138+
}
139+
finally
140+
{
141+
RestoreResilienceContext(context, request, created);
142+
}
143+
}
144+
#endif
145+
146+
private static ResilienceContext GetOrSetResilienceContext(HttpRequestMessage request, CancellationToken cancellationToken, out bool created)
147+
{
148+
created = false;
149+
150+
if (request.GetResilienceContext() is not ResilienceContext context)
151+
{
152+
context = ResilienceContextPool.Shared.Get(cancellationToken);
153+
created = true;
154+
request.SetResilienceContext(context);
155+
}
156+
157+
return context;
158+
}
159+
160+
private static void TrySetRequestMetadata(ResilienceContext context, HttpRequestMessage request)
161+
{
162+
if (request.GetRequestMetadata() is RequestMetadata requestMetadata)
163+
{
164+
context.Properties.Set(ResilienceKeys.RequestMetadata, requestMetadata);
165+
}
166+
}
167+
168+
private static void SetRequestMessage(ResilienceContext context, HttpRequestMessage request)
169+
=> context.Properties.Set(ResilienceKeys.RequestMessage, request);
170+
171+
private static HttpRequestMessage GetRequestMessage(ResilienceContext context, HttpRequestMessage request)
172+
=> context.Properties.GetValue(ResilienceKeys.RequestMessage, request);
173+
174+
private static void RestoreResilienceContext(ResilienceContext context, HttpRequestMessage request, bool created)
175+
{
176+
if (created)
177+
{
178+
ResilienceContextPool.Shared.Return(context);
179+
request.SetResilienceContext(null);
180+
}
181+
else
182+
{
183+
// Restore the original context
184+
request.SetResilienceContext(context);
117185
}
118186
}
119187

120188
private Task<HttpResponseMessage> SendCoreAsync(HttpRequestMessage requestMessage, CancellationToken cancellationToken)
121189
=> base.SendAsync(requestMessage, cancellationToken);
190+
191+
#if NET6_0_OR_GREATER
192+
private HttpResponseMessage SendCore(HttpRequestMessage requestMessage, CancellationToken cancellationToken)
193+
=> base.Send(requestMessage, cancellationToken);
194+
#endif
122195
}

0 commit comments

Comments
 (0)