Skip to content
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

Fix a deadlock in EventSource and CounterGroup #40259

Merged
merged 5 commits into from
Aug 15, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -200,22 +200,47 @@ private void ResetCounters()

private void OnTimer()
{
Debug.Assert(Monitor.IsEntered(s_counterGroupLock));
if (_eventSource.IsEnabled())
{
DateTime now = DateTime.UtcNow;
TimeSpan elapsed = now - _timeStampSinceCollectionStarted;
DateTime now;
TimeSpan elapsed;
int pollingIntervalInMilliseconds;
DiagnosticCounter[] counters;
lock (s_counterGroupLock)
{
now = DateTime.UtcNow;
elapsed = now - _timeStampSinceCollectionStarted;
pollingIntervalInMilliseconds = _pollingIntervalInMilliseconds;
counters = new DiagnosticCounter[_counters.Count];
_counters.CopyTo(counters);
}

foreach (DiagnosticCounter counter in _counters)
// MUST keep out of the scope of s_counterGroupLock because this will cause WritePayload
noahfalk marked this conversation as resolved.
Show resolved Hide resolved
// callback can be re-entrant to CounterGroup (i.e. it's possible it calls back into EnableTimer()
// above, since WritePayload callback can contain user code that can invoke EventSource constructor
// and lead to a deadlock. (See https://github.com/dotnet/runtime/issues/40190 for details)
foreach (DiagnosticCounter counter in counters)
{
counter.WritePayload((float)elapsed.TotalSeconds, _pollingIntervalInMilliseconds);
// NOTE: It is still possible for a race condition to occur here. An example is if the session
// that subscribed to these batch of counters was disabled and it was immediately enabled in
// a different session, some of the counter data that was supposed to be written to the old
// session can now "overflow" into the new session.
// This problem pre-existed to this change (when we used to hold lock in the call to WritePayload):
// the only difference being the old behavior caused the entire batch of counters to be either
// written to the old session or the new session. The behavior change is not being treated as a
// significant problem to address for now, but we can come back and address it if it turns out to
// be an actual issue.
counter.WritePayload((float)elapsed.TotalSeconds, pollingIntervalInMilliseconds);
}
_timeStampSinceCollectionStarted = now;

do
lock (s_counterGroupLock)
{
_nextPollingTimeStamp += new TimeSpan(0, 0, 0, 0, _pollingIntervalInMilliseconds);
} while (_nextPollingTimeStamp <= now);
_timeStampSinceCollectionStarted = now;
do
{
_nextPollingTimeStamp += new TimeSpan(0, 0, 0, 0, _pollingIntervalInMilliseconds);
} while (_nextPollingTimeStamp <= now);
}
}
}

Expand All @@ -228,8 +253,15 @@ private void OnTimer()
private static void PollForValues()
{
AutoResetEvent? sleepEvent = null;

// Cache of onTimer callbacks for each CounterGroup.
// We cache these outside of the scope of s_counterGroupLock because
// calling into the callbacks can cause a re-entrancy into CounterGroup.Enable()
// and result in a deadlock. (See https://github.com/dotnet/runtime/issues/40190 for details)
List<Action> onTimers = new List<Action>();
while (true)
{
onTimers.Clear();
int sleepDurationInMilliseconds = int.MaxValue;
lock (s_counterGroupLock)
{
Expand All @@ -239,14 +271,18 @@ private static void PollForValues()
DateTime now = DateTime.UtcNow;
if (counterGroup._nextPollingTimeStamp < now + new TimeSpan(0, 0, 0, 0, 1))
{
counterGroup.OnTimer();
onTimers.Add(() => counterGroup.OnTimer());
}

int millisecondsTillNextPoll = (int)((counterGroup._nextPollingTimeStamp - now).TotalMilliseconds);
millisecondsTillNextPoll = Math.Max(1, millisecondsTillNextPoll);
sleepDurationInMilliseconds = Math.Min(sleepDurationInMilliseconds, millisecondsTillNextPoll);
}
}
foreach (Action onTimer in onTimers)
{
onTimer.Invoke();
}
if (sleepDurationInMilliseconds == int.MaxValue)
{
sleepDurationInMilliseconds = -1; // WaitOne uses -1 to mean infinite
Expand Down