Skip to content

Commit e3cce33

Browse files
authored
[LLC] Implement RequestOptions.AddPolicy (Azure#25214)
* add test for AddPolicy * share proposed refactoring to HttpPipelineExtensions * ideas in the direction of implementation * mark retry policy * revert mark retry policy * shifting API around * experiment with using MemoryPool * use ArrayPool * use ArrayPool * add tests * update API * throw if didn't use pipeline builder * pr feedback and merge follow-up * pr fb and merge follow-up * pr fb * simplify request pipeline creation * move AddPolicy tests to Core; pr fb * nit * pr fb * pr fb & fix build * pr fb * pr fb & build fix * pr fb * pr fb * pr fb & fix core ci
1 parent f4c3658 commit e3cce33

13 files changed

+421
-12
lines changed

sdk/core/Azure.Core/api/Azure.Core.net461.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ public partial class RequestContext
194194
public RequestContext() { }
195195
public System.Threading.CancellationToken CancellationToken { get { throw null; } set { } }
196196
public Azure.ErrorOptions ErrorOptions { get { throw null; } set { } }
197+
public void AddPolicy(Azure.Core.Pipeline.HttpPipelinePolicy policy, Azure.Core.HttpPipelinePosition position) { }
197198
public static implicit operator Azure.RequestContext (Azure.ErrorOptions options) { throw null; }
198199
}
199200
public partial class RequestFailedException : System.Exception, System.Runtime.Serialization.ISerializable
@@ -764,6 +765,7 @@ public HttpPipeline(Azure.Core.Pipeline.HttpPipelineTransport transport, Azure.C
764765
public static System.IDisposable CreateClientRequestIdScope(string? clientRequestId) { throw null; }
765766
public static System.IDisposable CreateHttpMessagePropertiesScope(System.Collections.Generic.IDictionary<string, object?> messageProperties) { throw null; }
766767
public Azure.Core.HttpMessage CreateMessage() { throw null; }
768+
public Azure.Core.HttpMessage CreateMessage(Azure.RequestContext context) { throw null; }
767769
public Azure.Core.Request CreateRequest() { throw null; }
768770
public void Send(Azure.Core.HttpMessage message, System.Threading.CancellationToken cancellationToken) { }
769771
public System.Threading.Tasks.ValueTask SendAsync(Azure.Core.HttpMessage message, System.Threading.CancellationToken cancellationToken) { throw null; }

sdk/core/Azure.Core/api/Azure.Core.net5.0.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ public partial class RequestContext
194194
public RequestContext() { }
195195
public System.Threading.CancellationToken CancellationToken { get { throw null; } set { } }
196196
public Azure.ErrorOptions ErrorOptions { get { throw null; } set { } }
197+
public void AddPolicy(Azure.Core.Pipeline.HttpPipelinePolicy policy, Azure.Core.HttpPipelinePosition position) { }
197198
public static implicit operator Azure.RequestContext (Azure.ErrorOptions options) { throw null; }
198199
}
199200
public partial class RequestFailedException : System.Exception, System.Runtime.Serialization.ISerializable
@@ -764,6 +765,7 @@ public HttpPipeline(Azure.Core.Pipeline.HttpPipelineTransport transport, Azure.C
764765
public static System.IDisposable CreateClientRequestIdScope(string? clientRequestId) { throw null; }
765766
public static System.IDisposable CreateHttpMessagePropertiesScope(System.Collections.Generic.IDictionary<string, object?> messageProperties) { throw null; }
766767
public Azure.Core.HttpMessage CreateMessage() { throw null; }
768+
public Azure.Core.HttpMessage CreateMessage(Azure.RequestContext context) { throw null; }
767769
public Azure.Core.Request CreateRequest() { throw null; }
768770
public void Send(Azure.Core.HttpMessage message, System.Threading.CancellationToken cancellationToken) { }
769771
public System.Threading.Tasks.ValueTask SendAsync(Azure.Core.HttpMessage message, System.Threading.CancellationToken cancellationToken) { throw null; }

sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ public partial class RequestContext
194194
public RequestContext() { }
195195
public System.Threading.CancellationToken CancellationToken { get { throw null; } set { } }
196196
public Azure.ErrorOptions ErrorOptions { get { throw null; } set { } }
197+
public void AddPolicy(Azure.Core.Pipeline.HttpPipelinePolicy policy, Azure.Core.HttpPipelinePosition position) { }
197198
public static implicit operator Azure.RequestContext (Azure.ErrorOptions options) { throw null; }
198199
}
199200
public partial class RequestFailedException : System.Exception, System.Runtime.Serialization.ISerializable
@@ -764,6 +765,7 @@ public HttpPipeline(Azure.Core.Pipeline.HttpPipelineTransport transport, Azure.C
764765
public static System.IDisposable CreateClientRequestIdScope(string? clientRequestId) { throw null; }
765766
public static System.IDisposable CreateHttpMessagePropertiesScope(System.Collections.Generic.IDictionary<string, object?> messageProperties) { throw null; }
766767
public Azure.Core.HttpMessage CreateMessage() { throw null; }
768+
public Azure.Core.HttpMessage CreateMessage(Azure.RequestContext context) { throw null; }
767769
public Azure.Core.Request CreateRequest() { throw null; }
768770
public void Send(Azure.Core.HttpMessage message, System.Threading.CancellationToken cancellationToken) { }
769771
public System.Threading.Tasks.ValueTask SendAsync(Azure.Core.HttpMessage message, System.Threading.CancellationToken cancellationToken) { throw null; }

sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ public partial class RequestContext
194194
public RequestContext() { }
195195
public System.Threading.CancellationToken CancellationToken { get { throw null; } set { } }
196196
public Azure.ErrorOptions ErrorOptions { get { throw null; } set { } }
197+
public void AddPolicy(Azure.Core.Pipeline.HttpPipelinePolicy policy, Azure.Core.HttpPipelinePosition position) { }
197198
public static implicit operator Azure.RequestContext (Azure.ErrorOptions options) { throw null; }
198199
}
199200
public partial class RequestFailedException : System.Exception, System.Runtime.Serialization.ISerializable
@@ -764,6 +765,7 @@ public HttpPipeline(Azure.Core.Pipeline.HttpPipelineTransport transport, Azure.C
764765
public static System.IDisposable CreateClientRequestIdScope(string? clientRequestId) { throw null; }
765766
public static System.IDisposable CreateHttpMessagePropertiesScope(System.Collections.Generic.IDictionary<string, object?> messageProperties) { throw null; }
766767
public Azure.Core.HttpMessage CreateMessage() { throw null; }
768+
public Azure.Core.HttpMessage CreateMessage(Azure.RequestContext context) { throw null; }
767769
public Azure.Core.Request CreateRequest() { throw null; }
768770
public void Send(Azure.Core.HttpMessage message, System.Threading.CancellationToken cancellationToken) { }
769771
public System.Threading.Tasks.ValueTask SendAsync(Azure.Core.HttpMessage message, System.Threading.CancellationToken cancellationToken) { throw null; }

sdk/core/Azure.Core/src/HttpMessage.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System;
55
using System.Collections.Generic;
66
using System.IO;
7-
using System.Net.Http;
87
using System.Threading;
98
using Azure.Core.Pipeline;
109

@@ -13,7 +12,7 @@ namespace Azure.Core
1312
/// <summary>
1413
/// Represents a context flowing through the <see cref="HttpPipeline"/>.
1514
/// </summary>
16-
public sealed class HttpMessage: IDisposable
15+
public sealed class HttpMessage : IDisposable
1716
{
1817
private Dictionary<string, object>? _properties;
1918

@@ -81,6 +80,19 @@ public Response Response
8180
/// </summary>
8281
public TimeSpan? NetworkTimeout { get; set; }
8382

83+
internal void AddPolicies(RequestContext context)
84+
{
85+
if (context == null || context.Policies == null || context.Policies.Count == 0)
86+
{
87+
return;
88+
}
89+
90+
Policies ??= new(context.Policies.Count);
91+
Policies.AddRange(context.Policies);
92+
}
93+
94+
internal List<(HttpPipelinePosition Position, HttpPipelinePolicy Policy)>? Policies { get; set; }
95+
8496
/// <summary>
8597
/// Gets a property that modifies the pipeline behavior. Please refer to individual policies documentation on what properties it supports.
8698
/// </summary>
@@ -115,7 +127,7 @@ public void SetProperty(string name, object value)
115127
{
116128
case ResponseShouldNotBeUsedStream responseContent:
117129
return responseContent.Original;
118-
case Stream stream :
130+
case Stream stream:
119131
_response.ContentStream = new ResponseShouldNotBeUsedStream(_response.ContentStream);
120132
return stream;
121133
default:
@@ -132,7 +144,7 @@ public void Dispose()
132144
_response?.Dispose();
133145
}
134146

135-
private class ResponseShouldNotBeUsedStream: Stream
147+
private class ResponseShouldNotBeUsedStream : Stream
136148
{
137149
public Stream Original { get; }
138150

sdk/core/Azure.Core/src/Pipeline/DisposableHttpPipeline.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ public sealed class DisposableHttpPipeline : HttpPipeline, IDisposable
1414
/// Creates a new instance of <see cref="HttpPipeline"/> with the provided transport, policies and response classifier.
1515
/// </summary>
1616
/// <param name="transport">The <see cref="HttpPipelineTransport"/> to use for sending the requests.</param>
17+
/// <param name="perCallIndex"></param>
18+
/// <param name="perRetryIndex"></param>
1719
/// <param name="policies">Policies to be invoked as part of the pipeline in order.</param>
1820
/// <param name="responseClassifier">The response classifier to be used in invocations.</param>
19-
internal DisposableHttpPipeline(HttpPipelineTransport transport, HttpPipelinePolicy[]? policies = null, ResponseClassifier? responseClassifier = null)
20-
: base(transport, policies, responseClassifier)
21-
{ }
21+
internal DisposableHttpPipeline(HttpPipelineTransport transport, int perCallIndex, int perRetryIndex, HttpPipelinePolicy[]? policies = null, ResponseClassifier? responseClassifier = null)
22+
: base(transport, perCallIndex, perRetryIndex, policies, responseClassifier)
23+
{
24+
}
2225

2326
/// <summary>
2427
/// Calls Dispose on the underlying <see cref="HttpPipelineTransport"/>.

sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs

Lines changed: 135 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.Buffers;
56
using System.Collections.Generic;
67
using System.Threading;
78
using System.Threading.Tasks;
@@ -19,6 +20,25 @@ public class HttpPipeline
1920

2021
private readonly ReadOnlyMemory<HttpPipelinePolicy> _pipeline;
2122

23+
/// <summary>
24+
/// Indicates whether or not the pipeline was created using its internal constructor.
25+
/// If it was, we know the indices where we can add per-request policies at positions
26+
/// <see cref="HttpPipelinePosition.PerCall"/> and <see cref="HttpPipelinePosition.PerRetry"/>.
27+
/// </summary>
28+
private readonly bool _internallyConstructed;
29+
30+
/// <summary>
31+
/// The pipeline index where <see cref="HttpPipelinePosition.PerCall"/> policies will be added,
32+
/// if any are specified using <see cref="RequestContext.AddPolicy(HttpPipelinePolicy, HttpPipelinePosition)"/>.
33+
/// </summary>
34+
private readonly int _perCallIndex;
35+
36+
/// <summary>
37+
/// The pipeline index where <see cref="HttpPipelinePosition.PerRetry"/> policies will be added,
38+
/// if any are specified using <see cref="RequestContext.AddPolicy(HttpPipelinePolicy, HttpPipelinePosition)"/>.
39+
/// </summary>
40+
private readonly int _perRetryIndex;
41+
2242
/// <summary>
2343
/// Creates a new instance of <see cref="HttpPipeline"/> with the provided transport, policies and response classifier.
2444
/// </summary>
@@ -39,6 +59,13 @@ public HttpPipeline(HttpPipelineTransport transport, HttpPipelinePolicy[]? polic
3959
_pipeline = all;
4060
}
4161

62+
internal HttpPipeline(HttpPipelineTransport transport, int perCallIndex, int perRetryIndex, HttpPipelinePolicy[]? policies = null, ResponseClassifier? responseClassifier = null) : this(transport, policies, responseClassifier)
63+
{
64+
_perCallIndex = perCallIndex;
65+
_perRetryIndex = perRetryIndex;
66+
_internallyConstructed = true;
67+
}
68+
4269
/// <summary>
4370
/// Creates a new <see cref="Request"/> instance.
4471
/// </summary>
@@ -55,6 +82,18 @@ public HttpMessage CreateMessage()
5582
return new HttpMessage(CreateRequest(), ResponseClassifier);
5683
}
5784

85+
/// <summary>
86+
/// Creates a new <see cref="HttpMessage"/> instance.
87+
/// </summary>
88+
/// <param name="context">Context specifying the message options.</param>
89+
/// <returns>The message.</returns>
90+
public HttpMessage CreateMessage(RequestContext context)
91+
{
92+
var message = CreateMessage();
93+
message.AddPolicies(context);
94+
return message;
95+
}
96+
5897
/// <summary>
5998
/// The <see cref="ResponseClassifier"/> instance used in this pipeline invocations.
6099
/// </summary>
@@ -70,7 +109,28 @@ public ValueTask SendAsync(HttpMessage message, CancellationToken cancellationTo
70109
{
71110
message.CancellationToken = cancellationToken;
72111
AddHttpMessageProperties(message);
73-
return _pipeline.Span[0].ProcessAsync(message, _pipeline.Slice(1));
112+
113+
if (message.Policies == null || message.Policies.Count == 0)
114+
{
115+
return _pipeline.Span[0].ProcessAsync(message, _pipeline.Slice(1));
116+
}
117+
118+
return SendAsync(message);
119+
}
120+
121+
private async ValueTask SendAsync(HttpMessage message)
122+
{
123+
var length = _pipeline.Length + message.Policies!.Count;
124+
var policies = ArrayPool<HttpPipelinePolicy>.Shared.Rent(length);
125+
try
126+
{
127+
var pipeline = CreateRequestPipeline(policies, message.Policies);
128+
await pipeline.Span[0].ProcessAsync(message, pipeline.Slice(1)).ConfigureAwait(false);
129+
}
130+
finally
131+
{
132+
ArrayPool<HttpPipelinePolicy>.Shared.Return(policies);
133+
}
74134
}
75135

76136
/// <summary>
@@ -82,8 +142,27 @@ public void Send(HttpMessage message, CancellationToken cancellationToken)
82142
{
83143
message.CancellationToken = cancellationToken;
84144
AddHttpMessageProperties(message);
85-
_pipeline.Span[0].Process(message, _pipeline.Slice(1));
145+
146+
if (message.Policies == null || message.Policies.Count == 0)
147+
{
148+
_pipeline.Span[0].Process(message, _pipeline.Slice(1));
149+
}
150+
else
151+
{
152+
var length = _pipeline.Length + message.Policies.Count;
153+
var policies = ArrayPool<HttpPipelinePolicy>.Shared.Rent(length);
154+
try
155+
{
156+
var pipeline = CreateRequestPipeline(policies, message.Policies);
157+
pipeline.Span[0].Process(message, pipeline.Slice(1));
158+
}
159+
finally
160+
{
161+
ArrayPool<HttpPipelinePolicy>.Shared.Return(policies);
162+
}
163+
}
86164
}
165+
87166
/// <summary>
88167
/// Invokes the pipeline asynchronously with the provided request.
89168
/// </summary>
@@ -144,6 +223,60 @@ public static IDisposable CreateHttpMessagePropertiesScope(IDictionary<string, o
144223
return CurrentHttpMessagePropertiesScope.Value;
145224
}
146225

226+
private ReadOnlyMemory<HttpPipelinePolicy> CreateRequestPipeline(HttpPipelinePolicy[] policies, List<(HttpPipelinePosition Position, HttpPipelinePolicy Policy)> customPolicies)
227+
{
228+
if (!_internallyConstructed)
229+
{
230+
throw new InvalidOperationException("Cannot send messages with per-request policies if the pipeline wasn't constructed with HttpPipelineBuilder.");
231+
}
232+
233+
// Copy over client policies and splice in custom policies at designated indices
234+
var pipeline = _pipeline.Span;
235+
int transportIndex = pipeline.Length - 1;
236+
237+
pipeline.Slice(0, _perCallIndex).CopyTo(policies);
238+
239+
int index = _perCallIndex;
240+
int count = AddCustomPolicies(customPolicies, policies, HttpPipelinePosition.PerCall, index);
241+
242+
index += count;
243+
count = _perRetryIndex - _perCallIndex;
244+
pipeline.Slice(_perCallIndex, count).CopyTo(policies.AsSpan(index, count));
245+
246+
index += count;
247+
count = AddCustomPolicies(customPolicies, policies, HttpPipelinePosition.PerRetry, index);
248+
249+
index += count;
250+
count = transportIndex - _perRetryIndex;
251+
pipeline.Slice(_perRetryIndex, count).CopyTo(policies.AsSpan(index, count));
252+
253+
index += count;
254+
count = AddCustomPolicies(customPolicies, policies, HttpPipelinePosition.BeforeTransport, index);
255+
256+
index += count;
257+
policies[index] = pipeline[transportIndex];
258+
259+
return new ReadOnlyMemory<HttpPipelinePolicy>(policies, 0, index + 1);
260+
}
261+
262+
private static int AddCustomPolicies(List<(HttpPipelinePosition Position, HttpPipelinePolicy Policy)> source, HttpPipelinePolicy[] target, HttpPipelinePosition position, int start)
263+
{
264+
int count = 0;
265+
if (source != null)
266+
{
267+
foreach (var policy in source)
268+
{
269+
if (policy.Position == position)
270+
{
271+
target[start + count] = policy.Policy;
272+
count++;
273+
}
274+
}
275+
}
276+
277+
return count;
278+
}
279+
147280
private static void AddHttpMessageProperties(HttpMessage message)
148281
{
149282
if (CurrentHttpMessagePropertiesScope.Value != null)

sdk/core/Azure.Core/src/Pipeline/HttpPipelineBuilder.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ public static HttpPipeline Build(
5353
/// <returns>A new instance of <see cref="HttpPipeline"/></returns>
5454
public static HttpPipeline Build(ClientOptions options, HttpPipelinePolicy[] perCallPolicies, HttpPipelinePolicy[] perRetryPolicies, ResponseClassifier responseClassifier, HttpPipelineTransportOptions? defaultTransportOptions)
5555
{
56+
int perCallIndex;
57+
int perRetryIndex;
5658
if (perCallPolicies == null)
5759
{
5860
throw new ArgumentNullException(nameof(perCallPolicies));
@@ -94,6 +96,9 @@ void AddCustomerPolicies(HttpPipelinePosition position)
9496

9597
AddCustomerPolicies(HttpPipelinePosition.PerCall);
9698

99+
policies.RemoveAll(static policy => policy == null);
100+
perCallIndex = policies.Count;
101+
97102
policies.Add(ClientRequestIdPolicy.Shared);
98103

99104
if (diagnostics.IsTelemetryEnabled)
@@ -110,6 +115,9 @@ void AddCustomerPolicies(HttpPipelinePosition position)
110115

111116
AddCustomerPolicies(HttpPipelinePosition.PerRetry);
112117

118+
policies.RemoveAll(static policy => policy == null);
119+
perRetryIndex = policies.Count;
120+
113121
if (diagnostics.IsLoggingEnabled)
114122
{
115123
string assemblyName = options.GetType().Assembly!.GetName().Name!;
@@ -122,7 +130,6 @@ void AddCustomerPolicies(HttpPipelinePosition position)
122130
policies.Add(new RequestActivityPolicy(isDistributedTracingEnabled, ClientDiagnostics.GetResourceProviderNamespace(options.GetType().Assembly), sanitizer));
123131

124132
AddCustomerPolicies(HttpPipelinePosition.BeforeTransport);
125-
126133
policies.RemoveAll(static policy => policy == null);
127134

128135
// Override the provided Transport with the provided transport options if the transport has not been set after default construction and options are not null.
@@ -141,12 +148,16 @@ void AddCustomerPolicies(HttpPipelinePosition position)
141148
{
142149
transport = HttpPipelineTransport.Create(defaultTransportOptions);
143150
return new DisposableHttpPipeline(transport,
151+
perCallIndex,
152+
perRetryIndex,
144153
policies.ToArray(),
145154
responseClassifier);
146155
}
147156
}
148157

149158
return new HttpPipeline(transport,
159+
perCallIndex,
160+
perRetryIndex,
150161
policies.ToArray(),
151162
responseClassifier);
152163
}

sdk/core/Azure.Core/src/Pipeline/HttpPipelineTransport.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public abstract class HttpPipelineTransport
2525

2626
/// <summary>
2727
/// Creates a new transport specific instance of <see cref="Request"/>. This should not be called directly, <see cref="HttpPipeline.CreateRequest"/> or
28-
/// <see cref="HttpPipeline.CreateMessage"/> should be used instead.
28+
/// <see cref="HttpPipeline.CreateMessage()"/> should be used instead.
2929
/// </summary>
3030
/// <returns></returns>
3131
public abstract Request CreateRequest();

0 commit comments

Comments
 (0)