-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Convert PictureBox WebRequest methods to HttpClient #1952
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1952 +/- ##
==================================================
+ Coverage 26.93408% 26.93898% +0.0049%
==================================================
Files 864 864
Lines 266755 266751 -4
Branches 37915 37917 +2
==================================================
+ Hits 71848 71860 +12
+ Misses 189788 189765 -23
- Partials 5119 5126 +7
|
}); | ||
using HttpClient client = new HttpClient(); | ||
|
||
client.GetAsync(CalculateUri(_imageLocation)) |
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.
The calling site should probably get a try/catch handler since the exception patterns likely have changed between what BeginGetResponse
and GetAsync
calls throw synchronously. Better be on the safe side instead of relying on that GetAsync behaves exactly as BeginGetResponse and never throws synchronously here.
_totalBytesRead = 0; | ||
|
||
Stream responseStream = webResponse.GetResponseStream(); | ||
Stream responseStream = await resp.Content.ReadAsStreamAsync(); | ||
|
||
// Continue on with asynchronous reading. | ||
responseStream.BeginRead( |
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.
How about translating stream reading to async/await as well? Considering that you already are making the method async its probably not too hard. But if you want to keep those changes separated for clarity I won't object, the current diff is nicely isolated.
@@ -609,7 +607,7 @@ private void LoadCompletedDelegate(object arg) | |||
|
|||
private void LoadProgressDelegate(object arg) => OnLoadProgressChanged((ProgressChangedEventArgs)arg); | |||
|
|||
private void GetResponseCallback(IAsyncResult result) | |||
private async Task GetResponseCallback(IAsyncResult result) |
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.
You probably want to change the argument, ContinueWith passes a Task
Also by making the method async you are introducing a new problem, nobody will observe exceptions thrown here since the original ContinueWith
used to invoke it isn't followed up by anything. Due to the try/catch in the method this is probably safe, but you should add a comment to the method for future editors that it must not throw, otherwise an unhandled async exception will default to terminating the application.
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.
I agree completely. This is my first attempt at contributing to OSS so I was unclear if these guidelines implied not changing APM to TAP in the case of the callback. I originally made the change in that direction before I realized it might not be accepted due to divergence from the current pattern. I will change that and push code. That should take care of several of the other comments you have as well.
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.
I'm not a team member so if there are open questions feel free to wait for official responses before adressing this.
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.
I understood that, but it's still a valid concern/criticism.
@@ -619,13 +617,12 @@ private void GetResponseCallback(IAsyncResult result) | |||
|
|||
try | |||
{ | |||
WebRequest req = (WebRequest)result.AsyncState; | |||
WebResponse webResponse = req.EndGetResponse(result); | |||
HttpResponseMessage resp = (HttpResponseMessage) result.AsyncState; |
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.
I think you want to use Task.Result
for proper exception handling? Not sure how AsyncState works on a Task object, maybe it does the right thing, but its very nonstandard code and hard to read.
// Invoke BeginGetResponse on a threadpool thread, as it has unpredictable latency | ||
req.BeginGetResponse(new AsyncCallback(GetResponseCallback), req); | ||
}); | ||
using HttpClient client = new HttpClient(); |
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.
❗️ This is an antipattern, HttpClient
mustn't be used like this. This was a very unfortunate design decision made by the team.
More info:
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.
The linked article just explains a problem (socket exhaustion) but does not explain at all why calling Dispose
prevents the sockets from being closed and why not calling Dispose would close them faster. It just says "not disposing is better" without saying why it makes a difference. I can believe that using a pooled HttpClient instead of many instances is more performant, but I don't see why using Dispose is an antipattern which breaks things.
Anyways, I just want to raise a word of caution making decisions based on that article alone, if you have technical background on the problem or know more about it, I'll not object changing this, but if its just based on this article it might be wise to actually ask someone from corefx for advice.
either way the current code looks broken because it disposes the HttpClient before its done with its request. I don't think the HttpClient should be disposed this early, if at all?
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.
@RussKie Code has been updated and pushed.
For posterity, the using was not disposed of prior to doing work. The scope is implicit with C# 8.0
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.
@neldridg not sure where C# 8 scopes make a difference, usually you await within the using scope to prevent the disposal happening before you are done. This was not the case here because the caller is not async and calling ContinueWith is not compatible with using scopes (regardless of what syntax they use) because it returns immediately. You'd be disposing the HttpClient while the GetAsync Task was still incomplete. (Which may or may not be a problem, I don't know the semantics of disposing HttpClient and the linked article doesn't go into details either, it just claims there are problems associated without explaining them)
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.
Here's the official doc for HttpClient
, specifically the remarks sections notes the following:
HttpClient is intended to be instantiated once and re-used throughout the life of an application. Instantiating an HttpClient class for every request will exhaust the number of sockets available under heavy loads. This will result in SocketException errors. Below is an example using HttpClient correctly.
So it would be logical to add an instance of HttpClient
as a field to the PictureBox
class, and then reuse it.
However an app is likely to have more than one instance of the PictureBox
, and thus the instance of the HttpClient
should be shared across all of them.
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.
Yeah thats more like I was suspecting, the article is misleading. The dispose is irrelevant (and probably not implemented to dispose everything), so the antipattern is constructing multiple HttpClients in the first place. Static field is probably the way to go, couldn't find any other place in WinForms which wants to do web requests.
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.
Adding more reference for anyone coming along this discussion. You definitely want to reuse HttpClient and not create/dispose them per request to avoid performance regressions: https://github.com/dotnet/corefx/issues/23401#issuecomment-561671803
We will also need some tests. A good place would be to start with manual verification tests and enhance |
Fixes #1756
This change follows the suggestion from #1756 to change the
WebRequest
methods inPictureBox.cs
toHttpClient
.Proposed changes
WebRequest
methods to asynchronousHttpClient
Get methods.Customer Impact
Regression?
Risk
Test methodology
Test environment(s)
GetResponseCallback
method to be madeasync
to deal with the requirement ofHttpClient
to useasync
methods.HttpClient
is to cache all results. No changes were made to headers as this covers the task.Microsoft Reviewers: Open in CodeFlow