Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 8f1aa8e

Browse files
authored
Fix a deadlock in EventCounter/EventSource (#28089)
1 parent c88336e commit 8f1aa8e

File tree

1 file changed

+46
-10
lines changed
  • src/System.Private.CoreLib/shared/System/Diagnostics/Tracing

1 file changed

+46
-10
lines changed

src/System.Private.CoreLib/shared/System/Diagnostics/Tracing/CounterGroup.cs

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -204,22 +204,47 @@ private void ResetCounters()
204204

205205
private void OnTimer()
206206
{
207-
Debug.Assert(Monitor.IsEntered(s_counterGroupLock));
208207
if (_eventSource.IsEnabled())
209208
{
210-
DateTime now = DateTime.UtcNow;
211-
TimeSpan elapsed = now - _timeStampSinceCollectionStarted;
209+
DateTime now;
210+
TimeSpan elapsed;
211+
int pollingIntervalInMilliseconds;
212+
DiagnosticCounter[] counters;
213+
lock (s_counterGroupLock)
214+
{
215+
now = DateTime.UtcNow;
216+
elapsed = now - _timeStampSinceCollectionStarted;
217+
pollingIntervalInMilliseconds = _pollingIntervalInMilliseconds;
218+
counters = new DiagnosticCounter[_counters.Count];
219+
_counters.CopyTo(counters);
220+
}
221+
222+
// MUST keep out of the scope of s_counterGroupLock because this will cause WritePayload
223+
// callback can be re-entrant to CounterGroup (i.e. it's possible it calls back into EnableTimer()
224+
// above, since WritePayload callback can contain user code that can invoke EventSource constructor
225+
// and lead to a deadlock. (See https://github.com/dotnet/runtime/issues/40190 for details)
212226

213227
foreach (var counter in _counters)
214228
{
215-
counter.WritePayload((float)elapsed.TotalSeconds, _pollingIntervalInMilliseconds);
229+
// NOTE: It is still possible for a race condition to occur here. An example is if the session
230+
// that subscribed to these batch of counters was disabled and it was immediately enabled in
231+
// a different session, some of the counter data that was supposed to be written to the old
232+
// session can now "overflow" into the new session.
233+
// This problem pre-existed to this change (when we used to hold lock in the call to WritePayload):
234+
// the only difference being the old behavior caused the entire batch of counters to be either
235+
// written to the old session or the new session. The behavior change is not being treated as a
236+
// significant problem to address for now, but we can come back and address it if it turns out to
237+
// be an actual issue.
238+
counter.WritePayload((float)elapsed.TotalSeconds, pollingIntervalInMilliseconds);
216239
}
217-
_timeStampSinceCollectionStarted = now;
218-
219-
do
240+
lock (s_counterGroupLock)
220241
{
221-
_nextPollingTimeStamp += new TimeSpan(0, 0, 0, 0, _pollingIntervalInMilliseconds);
222-
} while (_nextPollingTimeStamp <= now);
242+
_timeStampSinceCollectionStarted = now;
243+
do
244+
{
245+
_nextPollingTimeStamp += new TimeSpan(0, 0, 0, 0, _pollingIntervalInMilliseconds);
246+
} while (_nextPollingTimeStamp <= now);
247+
}
223248
}
224249
}
225250

@@ -234,8 +259,15 @@ private void OnTimer()
234259
private static void PollForValues()
235260
{
236261
AutoResetEvent? sleepEvent = null;
262+
263+
// Cache of onTimer callbacks for each CounterGroup.
264+
// We cache these outside of the scope of s_counterGroupLock because
265+
// calling into the callbacks can cause a re-entrancy into CounterGroup.Enable()
266+
// and result in a deadlock. (See https://github.com/dotnet/runtime/issues/40190 for details)
267+
List<Action> onTimers = new List<Action>();
237268
while (true)
238269
{
270+
onTimers.Clear();
239271
int sleepDurationInMilliseconds = Int32.MaxValue;
240272
lock (s_counterGroupLock)
241273
{
@@ -245,14 +277,18 @@ private static void PollForValues()
245277
DateTime now = DateTime.UtcNow;
246278
if (counterGroup._nextPollingTimeStamp < now + new TimeSpan(0, 0, 0, 0, 1))
247279
{
248-
counterGroup.OnTimer();
280+
onTimers.Add(() => counterGroup.OnTimer());
249281
}
250282

251283
int millisecondsTillNextPoll = (int)((counterGroup._nextPollingTimeStamp - now).TotalMilliseconds);
252284
millisecondsTillNextPoll = Math.Max(1, millisecondsTillNextPoll);
253285
sleepDurationInMilliseconds = Math.Min(sleepDurationInMilliseconds, millisecondsTillNextPoll);
254286
}
255287
}
288+
foreach (Action onTimer in onTimers)
289+
{
290+
onTimer.Invoke();
291+
}
256292
if (sleepDurationInMilliseconds == Int32.MaxValue)
257293
{
258294
sleepDurationInMilliseconds = -1; // WaitOne uses -1 to mean infinite

0 commit comments

Comments
 (0)