-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Ensure Http Telemetry correctness #38876
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
Changes from all 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 |
|---|---|---|
|
|
@@ -7,16 +7,18 @@ | |
| using System.Text; | ||
| using System.Threading; | ||
| using System.Collections.Generic; | ||
| using System.Diagnostics; | ||
|
|
||
| namespace System.Net.Http | ||
| { | ||
| public class HttpRequestMessage : IDisposable | ||
| { | ||
| private const int MessageNotYetSent = 0; | ||
| private const int MessageAlreadySent = 1; | ||
| private const int MessageShouldEmitTelemetry = 2; | ||
|
|
||
| // Track whether the message has been sent. | ||
| // The message shouldn't be sent again if this field is equal to MessageAlreadySent. | ||
| // The message should only be sent if this field is equal to MessageNotYetSent. | ||
| private int _sendStatus = MessageNotYetSent; | ||
|
|
||
| private HttpMethod _method; | ||
|
|
@@ -26,6 +28,7 @@ public class HttpRequestMessage : IDisposable | |
| private HttpContent? _content; | ||
| private bool _disposed; | ||
| private IDictionary<string, object?>? _properties; | ||
| private HttpRequestMessageFinalizer? _finalizer; | ||
|
|
||
| public Version Version | ||
| { | ||
|
|
@@ -197,7 +200,40 @@ private void InitializeValues(HttpMethod method, Uri? requestUri) | |
|
|
||
| internal bool MarkAsSent() | ||
| { | ||
| return Interlocked.Exchange(ref _sendStatus, MessageAlreadySent) == MessageNotYetSent; | ||
| return Interlocked.CompareExchange(ref _sendStatus, MessageAlreadySent, MessageNotYetSent) == MessageNotYetSent; | ||
| } | ||
|
|
||
| internal void MarkAsTrackedByTelemetry() | ||
| { | ||
| if (_finalizer is null) | ||
| { | ||
| _finalizer = new HttpRequestMessageFinalizer(); | ||
| } | ||
| else | ||
| { | ||
| GC.ReRegisterForFinalize(_finalizer); | ||
| } | ||
|
|
||
| Debug.Assert(_sendStatus != MessageShouldEmitTelemetry); | ||
| _sendStatus = MessageShouldEmitTelemetry; | ||
|
Member
Author
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. Currently assumes that an
Contributor
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. If this is the case, does
Member
Author
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. It does if we want to keep the guarantee of the request only being used once by HttpClient. As this restriction is only imposed by HttpClient, other HttpMessageInvokers are free to reuse the request right now, just not in parallel. |
||
| } | ||
|
|
||
| internal void OnAborted() => OnStopped(aborted: true); | ||
|
|
||
| internal void OnStopped(bool aborted = false) | ||
| { | ||
| if (_sendStatus == MessageShouldEmitTelemetry && Interlocked.Exchange(ref _sendStatus, MessageAlreadySent) == MessageShouldEmitTelemetry) | ||
| { | ||
| if (aborted) | ||
| { | ||
| HttpTelemetry.Log.RequestAborted(); | ||
| } | ||
|
|
||
| HttpTelemetry.Log.RequestStop(); | ||
|
|
||
| Debug.Assert(_finalizer != null); | ||
| GC.SuppressFinalize(_finalizer); | ||
| } | ||
| } | ||
|
|
||
| #region IDisposable Members | ||
|
|
@@ -214,6 +250,8 @@ protected virtual void Dispose(bool disposing) | |
| _content.Dispose(); | ||
| } | ||
| } | ||
|
|
||
| OnStopped(); | ||
| } | ||
|
|
||
| public void Dispose() | ||
|
|
@@ -231,5 +269,16 @@ private void CheckDisposed() | |
| throw new ObjectDisposedException(this.GetType().ToString()); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// This class will only be allocated if Telemetry is enabled. | ||
| /// We can't use HttpRequestMessage's own finalizer because it is not sealed. | ||
| /// The finalizer will run iff OnStopped/OnAborted/Dispose were never called. | ||
| /// This way we ensure that RequestStop is always called if we call RequestStart. | ||
| /// </summary> | ||
| private sealed class HttpRequestMessageFinalizer | ||
| { | ||
| ~HttpRequestMessageFinalizer() => HttpTelemetry.Log.RequestStop(); | ||
| } | ||
|
Contributor
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. Shouldn't this run as part of the response/content cleanup, not the request? We have some other finalizers already (HttpConnection etc.) to catch when a response isn't disposed -- to avoid the overhead of finalizer here, can we merge this into that? Or, is the goal to have this work for any handler? |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| namespace System.Net.Http | ||
| { | ||
| internal class HttpTelemetry | ||
| { | ||
| public static HttpTelemetry Log => new HttpTelemetry(); | ||
|
|
||
| public void RequestStop() { } | ||
|
|
||
| public void RequestAborted() { } | ||
| } | ||
| } |
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.
Is there another place we can track this? Maybe as a bit flag in
_sendStatus? It would be nice to not increase the size ofHttpRequestMessage.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.
bonus points: merge
_disposedinto_sendStatustoo.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.
Yes, please don't increase the size of the object for this. We should also avoid adding a finalizer. Even if finalization is suppressed, it makes object creation measurably more expensive.
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 believe we could avoid it by instead using Http(2)Connection's finalizer - it would mean having to remove this optimization when Telemetry is enabled + adding a finalizer to Http2Connection if Telemetry is enabled.
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 familiar with this code, but I will add +1 to @stephentoub's comment. We should not add a finalizable object for this. It's definitely much more expensive, always gets promoted, and if you only do it when tracing is on, then it more noticeably changes the behavior with tracing on. I also worry about adding too much "just in tracing" logic, since it often magnifies the observer effect, meaning that behavior changes when you're watching with the profiler attached.
In general, we just accept when we get missing start or stop events and make tools that know how to handle them, but with counters, that changes things a bit, since you have a running total that you're trying to track vs. just missing a start or stop for a pair of events. But do remember that it's still possible to be missing starts and stops even if you do everything perfectly, because there's always a race between emitting events and enabling or disabling tracing.