Skip to content

Commit 6aa13dd

Browse files
authored
Use a dedicated thread for timers in rather than a Timer (#21490)
* Use a dedicated thread for timers in rather than a Timer - This make it possible to still timeout various operations when there's thread pool starvation occurring. - Print heartbeat slow if duration > interval
1 parent a8b8c19 commit 6aa13dd

File tree

3 files changed

+51
-44
lines changed

3 files changed

+51
-44
lines changed

src/Servers/Kestrel/Core/src/Internal/Infrastructure/Heartbeat.cs

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@ internal class Heartbeat : IDisposable
1616
private readonly IDebugger _debugger;
1717
private readonly IKestrelTrace _trace;
1818
private readonly TimeSpan _interval;
19-
private Timer _timer;
20-
private int _executingOnHeartbeat;
21-
private long _lastHeartbeatTicks;
19+
private Thread _timerThread;
20+
private volatile bool _stopped;
2221

2322
public Heartbeat(IHeartbeatHandler[] callbacks, ISystemClock systemClock, IDebugger debugger, IKestrelTrace trace)
2423
{
@@ -27,58 +26,62 @@ public Heartbeat(IHeartbeatHandler[] callbacks, ISystemClock systemClock, IDebug
2726
_debugger = debugger;
2827
_trace = trace;
2928
_interval = Interval;
29+
_timerThread = new Thread(state => ((Heartbeat)state).TimerLoop())
30+
{
31+
Name = "Kestrel Timer",
32+
IsBackground = true
33+
};
3034
}
3135

3236
public void Start()
3337
{
3438
OnHeartbeat();
35-
_timer = new Timer(OnHeartbeat, state: this, dueTime: _interval, period: _interval);
36-
}
37-
38-
private static void OnHeartbeat(object state)
39-
{
40-
((Heartbeat)state).OnHeartbeat();
39+
_timerThread.Start(this);
4140
}
4241

43-
// Called by the Timer (background) thread
4442
internal void OnHeartbeat()
4543
{
4644
var now = _systemClock.UtcNow;
4745

48-
if (Interlocked.Exchange(ref _executingOnHeartbeat, 1) == 0)
46+
try
4947
{
50-
Volatile.Write(ref _lastHeartbeatTicks, now.Ticks);
48+
foreach (var callback in _callbacks)
49+
{
50+
callback.OnHeartbeat(now);
51+
}
5152

52-
try
53+
if (!_debugger.IsAttached)
5354
{
54-
foreach (var callback in _callbacks)
55+
var after = _systemClock.UtcNow;
56+
57+
var duration = TimeSpan.FromTicks(after.Ticks - now.Ticks);
58+
59+
if (duration > _interval)
5560
{
56-
callback.OnHeartbeat(now);
61+
_trace.HeartbeatSlow(duration, _interval, now);
5762
}
5863
}
59-
catch (Exception ex)
60-
{
61-
_trace.LogError(0, ex, $"{nameof(Heartbeat)}.{nameof(OnHeartbeat)}");
62-
}
63-
finally
64-
{
65-
Interlocked.Exchange(ref _executingOnHeartbeat, 0);
66-
}
6764
}
68-
else
65+
catch (Exception ex)
6966
{
70-
if (!_debugger.IsAttached)
71-
{
72-
var lastHeartbeatTicks = Volatile.Read(ref _lastHeartbeatTicks);
73-
74-
_trace.HeartbeatSlow(TimeSpan.FromTicks(now.Ticks - lastHeartbeatTicks), _interval, now);
75-
}
67+
_trace.LogError(0, ex, $"{nameof(Heartbeat)}.{nameof(OnHeartbeat)}");
68+
}
69+
}
70+
71+
private void TimerLoop()
72+
{
73+
while (!_stopped)
74+
{
75+
Thread.Sleep(_interval);
76+
77+
OnHeartbeat();
7678
}
7779
}
7880

7981
public void Dispose()
8082
{
81-
_timer?.Dispose();
83+
_stopped = true;
84+
// Don't block waiting for the thread to exit
8285
}
8386
}
8487
}

src/Servers/Kestrel/Core/test/DateHeaderValueManagerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public void GetDateHeaderValue_ReturnsUpdatedValueAfterHeartbeat()
8585
heartbeat.OnHeartbeat();
8686

8787
Assert.Equal(future.ToString(Rfc1123DateFormat), dateHeaderValueManager.GetDateHeaderValues().String);
88-
Assert.Equal(2, systemClock.UtcNowCalled);
88+
Assert.Equal(4, systemClock.UtcNowCalled);
8989
}
9090
}
9191

src/Servers/Kestrel/Core/test/HeartbeatTests.cs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,17 @@ public void HeartbeatIntervalIsOneSecond()
2222
}
2323

2424
[Fact]
25-
public async Task BlockedHeartbeatDoesntCauseOverlapsAndIsLoggedAsError()
25+
public async Task HeartbeatTakingLongerThanIntervalIsLoggedAsError()
2626
{
2727
var systemClock = new MockSystemClock();
2828
var heartbeatHandler = new Mock<IHeartbeatHandler>();
2929
var debugger = new Mock<IDebugger>();
3030
var kestrelTrace = new Mock<IKestrelTrace>();
3131
var handlerMre = new ManualResetEventSlim();
3232
var handlerStartedTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
33+
var now = systemClock.UtcNow;
3334

34-
heartbeatHandler.Setup(h => h.OnHeartbeat(systemClock.UtcNow)).Callback(() =>
35+
heartbeatHandler.Setup(h => h.OnHeartbeat(now)).Callback(() =>
3536
{
3637
handlerStartedTcs.SetResult(null);
3738
handlerMre.Wait();
@@ -45,29 +46,31 @@ public async Task BlockedHeartbeatDoesntCauseOverlapsAndIsLoggedAsError()
4546
blockedHeartbeatTask = Task.Run(() => heartbeat.OnHeartbeat());
4647

4748
await handlerStartedTcs.Task.DefaultTimeout();
48-
49-
heartbeat.OnHeartbeat();
5049
}
5150

51+
// 2 seconds passes...
52+
systemClock.UtcNow = systemClock.UtcNow.AddSeconds(2);
53+
5254
handlerMre.Set();
5355

5456
await blockedHeartbeatTask.DefaultTimeout();
5557

56-
heartbeatHandler.Verify(h => h.OnHeartbeat(systemClock.UtcNow), Times.Once());
57-
kestrelTrace.Verify(t => t.HeartbeatSlow(TimeSpan.Zero, Heartbeat.Interval, systemClock.UtcNow), Times.Once());
58+
heartbeatHandler.Verify(h => h.OnHeartbeat(now), Times.Once());
59+
kestrelTrace.Verify(t => t.HeartbeatSlow(TimeSpan.FromSeconds(2), Heartbeat.Interval, now), Times.Once());
5860
}
5961

6062
[Fact]
61-
public async Task BlockedHeartbeatIsNotLoggedAsErrorIfDebuggerAttached()
63+
public async Task HeartbeatTakingLongerThanIntervalIsNotLoggedAsErrorIfDebuggerAttached()
6264
{
6365
var systemClock = new MockSystemClock();
6466
var heartbeatHandler = new Mock<IHeartbeatHandler>();
6567
var debugger = new Mock<IDebugger>();
6668
var kestrelTrace = new Mock<IKestrelTrace>();
6769
var handlerMre = new ManualResetEventSlim();
6870
var handlerStartedTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
71+
var now = systemClock.UtcNow;
6972

70-
heartbeatHandler.Setup(h => h.OnHeartbeat(systemClock.UtcNow)).Callback(() =>
73+
heartbeatHandler.Setup(h => h.OnHeartbeat(now)).Callback(() =>
7174
{
7275
handlerStartedTcs.SetResult(null);
7376
handlerMre.Wait();
@@ -82,16 +85,17 @@ public async Task BlockedHeartbeatIsNotLoggedAsErrorIfDebuggerAttached()
8285
blockedHeartbeatTask = Task.Run(() => heartbeat.OnHeartbeat());
8386

8487
await handlerStartedTcs.Task.DefaultTimeout();
85-
86-
heartbeat.OnHeartbeat();
8788
}
8889

90+
// 2 seconds passes...
91+
systemClock.UtcNow = systemClock.UtcNow.AddSeconds(2);
92+
8993
handlerMre.Set();
9094

9195
await blockedHeartbeatTask.DefaultTimeout();
9296

93-
heartbeatHandler.Verify(h => h.OnHeartbeat(systemClock.UtcNow), Times.Once());
94-
kestrelTrace.Verify(t => t.HeartbeatSlow(TimeSpan.Zero, Heartbeat.Interval, systemClock.UtcNow), Times.Never());
97+
heartbeatHandler.Verify(h => h.OnHeartbeat(now), Times.Once());
98+
kestrelTrace.Verify(t => t.HeartbeatSlow(TimeSpan.FromSeconds(2), Heartbeat.Interval, now), Times.Never());
9599
}
96100

97101
[Fact]

0 commit comments

Comments
 (0)