-
Notifications
You must be signed in to change notification settings - Fork 753
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 support for processing requests with StreamContent to AddStandardHedgingHandler() #5112
Changes from 5 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,9 @@ | |
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Net.Http; | ||
using System.Threading.Tasks; | ||
using Microsoft.Extensions.ObjectPool; | ||
using Microsoft.Shared.Diagnostics; | ||
using Microsoft.Shared.Pools; | ||
|
@@ -22,21 +24,40 @@ internal sealed class RequestMessageSnapshot : IResettable, IDisposable | |
private Version? _version; | ||
private HttpContent? _content; | ||
|
||
public static RequestMessageSnapshot Create(HttpRequestMessage request) | ||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Resilience", "EA0014:The async method doesn't support cancellation", Justification = "Past the point of no cancellation.")] | ||
public static async Task<RequestMessageSnapshot> CreateAsync(HttpRequestMessage request) | ||
{ | ||
_ = Throw.IfNull(request); | ||
|
||
var snapshot = _snapshots.Get(); | ||
snapshot.Initialize(request); | ||
await snapshot.InitializeAsync(request).ConfigureAwait(false); | ||
return snapshot; | ||
} | ||
|
||
public HttpRequestMessage CreateRequestMessage() | ||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Resilience", "EA0014:The async method doesn't support cancellation", Justification = "Past the point of no cancellation.")] | ||
public async Task<HttpRequestMessage> CreateRequestMessageAsync() | ||
adamhammond marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
var clone = new HttpRequestMessage(_method!, _requestUri) | ||
if (!IsInitialized()) | ||
{ | ||
throw new InvalidOperationException($"{nameof(CreateRequestMessageAsync)}() cannot be called on a snapshot object that has been reset and/or has not been initialized"); | ||
} | ||
|
||
var clone = new HttpRequestMessage(_method!, _requestUri?.OriginalString) | ||
{ | ||
Content = _content, | ||
Version = _version! | ||
}; | ||
|
||
if (_content is StreamContent) | ||
adamhammond marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
(HttpContent? content, HttpContent? clonedContent) = await CloneContentAsync(_content).ConfigureAwait(false); | ||
_content = content; | ||
clone.Content = clonedContent; | ||
} | ||
else | ||
{ | ||
clone.Content = _content; | ||
} | ||
|
||
#if NET5_0_OR_GREATER | ||
foreach (var prop in _properties) | ||
{ | ||
|
@@ -56,6 +77,7 @@ public HttpRequestMessage CreateRequestMessage() | |
return clone; | ||
} | ||
|
||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Critical Bug", "S2952:Classes should \"Dispose\" of members from the classes' own \"Dispose\" methods", Justification = "Handled by ObjectPool")] | ||
bool IResettable.TryReset() | ||
{ | ||
_properties.Clear(); | ||
|
@@ -64,24 +86,76 @@ bool IResettable.TryReset() | |
_method = null; | ||
_version = null; | ||
_requestUri = null; | ||
if (_content is StreamContent) | ||
{ | ||
// a snapshot's StreamContent is always a unique copy (deep clone) | ||
// therefore, it is safe to dispose when snapshot is no longer needed | ||
_content.Dispose(); | ||
} | ||
|
||
_content = null; | ||
|
||
return true; | ||
} | ||
|
||
void IDisposable.Dispose() => _snapshots.Return(this); | ||
|
||
private void Initialize(HttpRequestMessage request) | ||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Resilience", "EA0014:The async method doesn't support cancellation", Justification = "Past the point of no cancellation.")] | ||
private static async Task<(HttpContent? content, HttpContent? clonedContent)> CloneContentAsync(HttpContent? content) | ||
{ | ||
if (request.Content is StreamContent) | ||
HttpContent? clonedContent = null; | ||
if (content is not null) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This introduces performance regression for non-streamed content. Create an extra branch for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @martintmk can you expand on why you believe this will cause a performance regression? The cloning of the Also, if you are referring to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see, thanks for detailed explanation. Can you still try to avoid the extra branches by condensing the code into a single There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ahh I see what you mean. I understand where you are coming from; however, I don't think encapsulating the content operations behind a single method will achieve the additional clarity or eliminate the required mutation tests as you are suggesting. Below are my thoughts:
|
||
{ | ||
Throw.InvalidOperationException($"{nameof(StreamContent)} content cannot by cloned."); | ||
HttpContent originalContent = content; | ||
Stream originalRequestBody = await content.ReadAsStreamAsync().ConfigureAwait(false); | ||
MemoryStream clonedRequestBody = new MemoryStream(); | ||
await originalRequestBody.CopyToAsync(clonedRequestBody).ConfigureAwait(false); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The streamed content can point to a large file which can have > GBs in size. Since we are copying to memory we should constraint it. Let's say < 10MB There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if it's a good idea to impose a size limit. For example, I don't think the framework imposes any such limit when cloning request content for redirected requests. IMO, we should allow this to be limitless, and require users to enforce their own request content size limits, if they have them, via server configurations and/or their own custom handlers, filters, middleware, etc. Thoughts? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change can buffer potentially endless stream into memory and crash the process. I think it's something we should guard against. This thing should be used for relatively small streamed payloads. Btw, you can try to call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do understand the concern; however, the max size that a given stream can grow before it crashes the process is bound only by the memory resources available on the given server that it's executing on. Therefore, by choosing a static max limit, we are creating a cap that might unnecessarily hinder users of the API that have both requirements and the necessary server resources available to process requests with exceptionally large stream content. Further, I feel strongly that we should not impose a size limit when cloning the StreamContent. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is not true, the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The only way what you are saying is true is if the content is a |
||
clonedRequestBody.Position = 0; | ||
if (originalRequestBody.CanSeek) | ||
{ | ||
originalRequestBody.Position = 0; | ||
} | ||
else | ||
{ | ||
originalRequestBody = new MemoryStream(); | ||
await clonedRequestBody.CopyToAsync(originalRequestBody).ConfigureAwait(false); | ||
originalRequestBody.Position = 0; | ||
clonedRequestBody.Position = 0; | ||
} | ||
|
||
clonedContent = new StreamContent(clonedRequestBody); | ||
content = new StreamContent(originalRequestBody); | ||
foreach (KeyValuePair<string, IEnumerable<string>> header in originalContent.Headers) | ||
{ | ||
_ = clonedContent.Headers.TryAddWithoutValidation(header.Key, header.Value); | ||
_ = content.Headers.TryAddWithoutValidation(header.Key, header.Value); | ||
} | ||
} | ||
|
||
return (content, clonedContent); | ||
} | ||
|
||
private bool IsInitialized() | ||
{ | ||
return _method is not null; | ||
} | ||
|
||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Resilience", "EA0014:The async method doesn't support cancellation", Justification = "Past the point of no cancellation.")] | ||
private async Task InitializeAsync(HttpRequestMessage request) | ||
{ | ||
_method = request.Method; | ||
_version = request.Version; | ||
_requestUri = request.RequestUri; | ||
_content = request.Content; | ||
if (request.Content is StreamContent) | ||
{ | ||
(HttpContent? requestContent, HttpContent? clonedRequestContent) = await CloneContentAsync(request.Content).ConfigureAwait(false); | ||
_content = clonedRequestContent; | ||
request.Content = requestContent; | ||
} | ||
else | ||
{ | ||
_content = request.Content; | ||
} | ||
|
||
// headers | ||
_headers.AddRange(request.Headers); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a breaking change here :/
The
OnHedging
callback does not have access to request message anymore. This is because OnHedging is called after the action is created and before it is invoked.https://github.com/App-vNext/Polly/blob/f85029c6d14ad20fd36e4fcdde7a32f33409137a/src/Polly.Core/Hedging/Controller/TaskExecution.cs#L127