@@ -27,7 +27,7 @@ internal enum LocalSiloHealthCheckKind
2727 MembershipStatus ,
2828 SiloSuspected ,
2929 HealthCheckParticipant ,
30- ThreadPoolQueueDelay ,
30+ ThreadPoolStall ,
3131 ProbeRequests ,
3232 ProbeResponses ,
3333 GarbageCollectionPause ,
@@ -53,6 +53,24 @@ public ImmutableArray<string> Complaints
5353
5454 internal interface ILocalSiloHealthMonitor
5555 {
56+ /// <summary>
57+ /// Returns a timestamp from the stall detector's time source.
58+ /// </summary>
59+ /// <returns>A timestamp suitable for use with <see cref="GetStallDurationAsync"/>.</returns>
60+ long GetTimestamp ( ) ;
61+
62+ /// <summary>
63+ /// Waits for the stall detector to sample past the end of the interval, then returns the detected stall duration.
64+ /// </summary>
65+ /// <param name="startTimestamp">The start of the interval.</param>
66+ /// <param name="endTimestamp">The end of the interval.</param>
67+ /// <param name="cancellationToken">A token which cancels the wait.</param>
68+ /// <returns>The detected stall duration.</returns>
69+ ValueTask < TimeSpan > GetStallDurationAsync (
70+ long startTimestamp ,
71+ long endTimestamp ,
72+ CancellationToken cancellationToken ) ;
73+
5674 /// <summary>
5775 /// Returns the aggregate local health status over the provided interval.
5876 /// </summary>
@@ -101,15 +119,16 @@ void RecordHealthEvent(
101119 /// <item><description>Check that no other silo suspects this silo.</description></item>
102120 /// <item><description>Check for recently received successful ping responses (via <see cref="IProbeHealthMonitor"/>).</description></item>
103121 /// <item><description>Check for recently received ping requests (via <see cref="IProbeHealthMonitor"/>).</description></item>
104- /// <item><description>Check that the .NET Thread Pool is able to process work items within one second .</description></item>
122+ /// <item><description>Check that the .NET Thread Pool executes periodic timer callbacks on schedule .</description></item>
105123 /// <item><description>Check that local async timers have been firing on-time (within 3 seconds of their due time).</description></item>
106124 /// </list>
107125 /// </remarks>
108126 internal partial class LocalSiloHealthMonitor :
109127 ILifecycleParticipant < ISiloLifecycle > ,
110128 ILifecycleObserver ,
111129 ILocalSiloHealthMonitor ,
112- ILocalSiloHealthEventRecorder
130+ ILocalSiloHealthEventRecorder ,
131+ IDisposable
113132 {
114133 internal const int MaxScore = 8 ;
115134 private static readonly TimeSpan HistoryDuration = TimeSpan . FromMinutes ( 1 ) ;
@@ -122,7 +141,7 @@ internal partial class LocalSiloHealthMonitor :
122141 private readonly ILogger < LocalSiloHealthMonitor > _log ;
123142 private readonly ClusterMembershipOptions _clusterMembershipOptions ;
124143 private readonly IAsyncTimer _degradationCheckTimer ;
125- private readonly ThreadPoolMonitor _threadPoolMonitor ;
144+ private readonly ThreadPoolStallDetector _stallDetector ;
126145 private readonly TimeProvider _timeProvider ;
127146#if NET9_0_OR_GREATER
128147 private readonly Lock _historyLock = new ( ) ;
@@ -166,12 +185,32 @@ public LocalSiloHealthMonitor(
166185 MinimumCheckPeriod ,
167186 nameof ( LocalSiloHealthMonitor ) ,
168187 timeProvider ) ;
169- _threadPoolMonitor = new ThreadPoolMonitor ( loggerFactory . CreateLogger < ThreadPoolMonitor > ( ) , timeProvider ) ;
188+ var stallRetentionPeriod = _clusterMembershipOptions . MaxProbeTimeout + ThreadPoolStallDetector . DetectionPeriod ;
189+ if ( stallRetentionPeriod < HistoryDuration )
190+ {
191+ stallRetentionPeriod = HistoryDuration ;
192+ }
193+
194+ _stallDetector = new (
195+ loggerFactory . CreateLogger < ThreadPoolStallDetector > ( ) ,
196+ timeProvider ,
197+ ThreadPoolStallDetector . DetectionPeriod ,
198+ stallRetentionPeriod ) ;
170199 }
171200
172201 /// <inheritdoc />
173202 public ImmutableArray < string > Complaints { get ; private set ; } = [ ] ;
174203
204+ /// <inheritdoc />
205+ public long GetTimestamp ( ) => _timeProvider . GetTimestamp ( ) ;
206+
207+ /// <inheritdoc />
208+ public ValueTask < TimeSpan > GetStallDurationAsync (
209+ long startTimestamp ,
210+ long endTimestamp ,
211+ CancellationToken cancellationToken )
212+ => _stallDetector . GetStallDurationAsync ( startTimestamp , endTimestamp , cancellationToken ) ;
213+
175214 /// <inheritdoc />
176215 public LocalSiloHealthStatus GetLocalHealthStatus (
177216 long startTimestamp ,
@@ -265,7 +304,7 @@ private LocalSiloHealthStatus EnsureHealthCheck(
265304 var events = new List < LocalSiloHealthEvent > ( _healthCheckParticipants . Count + 1 ) ;
266305 var complaints = new List < string > ( ) ;
267306 CheckLocalHealthCheckParticipants ( now . UtcDateTime , timestamp , events , complaints ) ;
268- CheckThreadPoolQueueDelay ( timestamp , events , complaints ) ;
307+ CheckThreadPoolStalls ( timestamp , events , complaints ) ;
269308 AddEvents ( timestamp , events ) ;
270309
271310 var score = GetScore ( events ) ;
@@ -350,28 +389,30 @@ private void AddEvents(long timestamp, List<LocalSiloHealthEvent> events)
350389 }
351390 }
352391
353- private void CheckThreadPoolQueueDelay (
392+ private void CheckThreadPoolStalls (
354393 long timestamp ,
355394 List < LocalSiloHealthEvent > events ,
356395 List < string > complaints )
357396 {
358- var delay = _threadPoolMonitor . MeasureQueueDelay ( ) ;
359- var score = ( int ) delay . TotalSeconds ;
397+ var stallDuration = _stallDetector . GetMaximumStallDuration (
398+ SubtractTimestamp ( timestamp , MinimumCheckPeriod ) ,
399+ timestamp ) ;
400+ var score = ( int ) stallDuration . TotalSeconds ;
360401 string ? complaint = null ;
361402 if ( score >= 1 )
362403 {
363- complaint = $ ".NET Thread Pool is exhibiting delays of { delay . TotalSeconds } s. This can indicate .NET Thread Pool starvation, very long .NET GC pauses, or other runtime or machine pauses.";
404+ complaint = $ ".NET Thread Pool execution stalled for { stallDuration . TotalSeconds } s. This can indicate .NET Thread Pool starvation, very long .NET GC pauses, or other runtime or machine pauses.";
364405 complaints . Add ( complaint ) ;
365406 }
366407
367408 events . Add ( new (
368409 timestamp ,
369- LocalSiloHealthCheckKind . ThreadPoolQueueDelay ,
370- GetCategory ( LocalSiloHealthCheckKind . ThreadPoolQueueDelay ) ,
410+ LocalSiloHealthCheckKind . ThreadPoolStall ,
411+ GetCategory ( LocalSiloHealthCheckKind . ThreadPoolStall ) ,
371412 Source : null ,
372413 score ,
373414 complaint ,
374- delay ,
415+ stallDuration ,
375416 score >= 10 ? LogLevel . Error : LogLevel . Warning ) ) ;
376417 }
377418
@@ -610,9 +651,9 @@ private void LogHealthDetails(ImmutableArray<LocalSiloHealthEvent> events)
610651 continue ;
611652 }
612653
613- if ( healthEvent . Kind == LocalSiloHealthCheckKind . ThreadPoolQueueDelay )
654+ if ( healthEvent . Kind == LocalSiloHealthCheckKind . ThreadPoolStall )
614655 {
615- LogThreadPoolDelay ( healthEvent . LogLevel , healthEvent . Duration ? . TotalSeconds ?? 0 ) ;
656+ LogThreadPoolStall ( healthEvent . LogLevel , healthEvent . Duration ? . TotalSeconds ?? 0 ) ;
616657 }
617658 else
618659 {
@@ -644,79 +685,16 @@ public async Task OnStop(CancellationToken ct)
644685 }
645686 }
646687
647- /// <summary>
648- /// Measures queue delay on the .NET <see cref="ThreadPool"/>.
649- /// </summary>
650- private class ThreadPoolMonitor
688+ public void Dispose ( )
651689 {
652- private static readonly WaitCallback Callback = state => ( ( ThreadPoolMonitor ) state ! ) . Execute ( ) ;
653- #if NET9_0_OR_GREATER
654- private readonly Lock _lockObj = new ( ) ;
655- #else
656- private readonly object _lockObj = new ( ) ;
657- #endif
658- private readonly ILogger < ThreadPoolMonitor > _log ;
659- private readonly TimeProvider _timeProvider ;
660- private bool _scheduled ;
661- private TimeSpan _lastQueueDelay ;
662- private long _queueDelayTimestamp ;
663-
664- public ThreadPoolMonitor ( ILogger < ThreadPoolMonitor > log , TimeProvider timeProvider )
665- {
666- _log = log ;
667- _timeProvider = timeProvider ;
668- }
669-
670- public TimeSpan MeasureQueueDelay ( )
671- {
672- bool shouldSchedule ;
673- TimeSpan delay ;
674- lock ( _lockObj )
675- {
676- var currentQueueDelay = _scheduled ? _timeProvider . GetElapsedTime ( _queueDelayTimestamp ) : TimeSpan . Zero ;
677- delay = currentQueueDelay > _lastQueueDelay ? currentQueueDelay : _lastQueueDelay ;
678-
679- if ( ! _scheduled )
680- {
681- _scheduled = true ;
682- shouldSchedule = true ;
683- _queueDelayTimestamp = _timeProvider . GetTimestamp ( ) ;
684- }
685- else
686- {
687- shouldSchedule = false ;
688- }
689- }
690-
691- if ( shouldSchedule )
692- {
693- _ = ThreadPool . UnsafeQueueUserWorkItem ( Callback , this ) ;
694- }
695-
696- return delay ;
697- }
698-
699- private void Execute ( )
700- {
701- try
702- {
703- lock ( _lockObj )
704- {
705- _scheduled = false ;
706- _lastQueueDelay = _timeProvider . GetElapsedTime ( _queueDelayTimestamp ) ;
707- }
708- }
709- catch ( Exception exception )
710- {
711- LocalSiloHealthMonitor . LogThreadPoolDelayMonitorError ( _log , exception ) ;
712- }
713- }
690+ _degradationCheckTimer . Dispose ( ) ;
691+ _stallDetector . Dispose ( ) ;
714692 }
715693
716694 [ LoggerMessage (
717- Message = ".NET Thread Pool is exhibiting delays of {ThreadPoolQueueDelaySeconds }s. This can indicate .NET Thread Pool starvation, very long .NET GC pauses, or other runtime or machine pauses."
695+ Message = ".NET Thread Pool execution stalled for {ThreadPoolStallSeconds }s. This can indicate .NET Thread Pool starvation, very long .NET GC pauses, or other runtime or machine pauses."
718696 ) ]
719- private partial void LogThreadPoolDelay ( LogLevel logLevel , double threadPoolQueueDelaySeconds ) ;
697+ private partial void LogThreadPoolStall ( LogLevel logLevel , double threadPoolStallSeconds ) ;
720698
721699 [ LoggerMessage (
722700 Message = "{Kind} health check for {Source} reported: {Complaint}"
@@ -763,12 +741,6 @@ private partial void LogRecordedHealthIssue(
763741 ) ]
764742 private partial void LogSelfMonitoringDegraded ( int score , int maxScore , string complaints ) ;
765743
766- [ LoggerMessage (
767- Level = LogLevel . Error ,
768- Message = "Exception monitoring .NET thread pool delay"
769- ) ]
770- private static partial void LogThreadPoolDelayMonitorError ( ILogger logger , Exception exception ) ;
771-
772744 [ LoggerMessage (
773745 Level = LogLevel . Error ,
774746 Message = "Error while monitoring local silo health"
0 commit comments