Skip to content

Commit b77ce82

Browse files
authored
Fix potential grain timer deadlock during disposal (#8949)
1 parent 0276f18 commit b77ce82

File tree

3 files changed

+92
-39
lines changed

3 files changed

+92
-39
lines changed

src/Orleans.Core/Timers/SafeTimerBase.cs

+8
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ public void Start(TimeSpan due, TimeSpan period)
5858
timer.Change(due, Constants.INFINITE_TIMESPAN);
5959
}
6060

61+
public void Stop()
62+
{
63+
timerFrequency = Constants.INFINITE_TIMESPAN;
64+
dueTime = Constants.INFINITE_TIMESPAN;
65+
timerStarted = false;
66+
timer.Change(Constants.INFINITE_TIMESPAN, Constants.INFINITE_TIMESPAN);
67+
}
68+
6169
private void Init(ILogger logger, Func<object, Task> asynCallback, TimerCallback synCallback, object state, TimeSpan due, TimeSpan period)
6270
{
6371
if (synCallback == null && asynCallback == null) throw new ArgumentNullException("synCallback", "Cannot use null for both sync and asyncTask timer callbacks.");

src/Orleans.Runtime/Catalog/ActivationData.cs

+2
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,9 @@ public void PrepareForDeactivation()
222222
SetState(ActivationState.Deactivating);
223223
deactivationStartTime = DateTime.UtcNow;
224224
if (!IsCurrentlyExecuting)
225+
{
225226
StopAllTimers();
227+
}
226228
}
227229

228230
/// <summary>

src/Orleans.Runtime/Timers/GrainTimer.cs

+82-39
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#nullable enable
12
using System;
23
using System.Reflection;
34
using System.Threading;
@@ -9,30 +10,28 @@ namespace Orleans.Runtime
910
{
1011
internal class GrainTimer : IGrainTimer
1112
{
12-
private Func<object, Task> asyncCallback;
13-
private AsyncTaskSafeTimer timer;
13+
private readonly Func<object?, Task> asyncCallback;
1414
private readonly TimeSpan dueTime;
1515
private readonly TimeSpan timerFrequency;
16-
private DateTime previousTickTime;
17-
private int totalNumTicks;
1816
private readonly ILogger logger;
19-
private volatile Task currentlyExecutingTickTask;
20-
private object currentlyExecutingTickTaskLock = new();
17+
private readonly object currentlyExecutingTickTaskLock = new();
2118
private readonly OrleansTaskScheduler scheduler;
22-
private readonly IActivationData activationData;
19+
private readonly IActivationData? activationData;
20+
private DateTime previousTickTime;
21+
private int totalNumTicks;
22+
private volatile AsyncTaskSafeTimer? timer;
23+
private volatile Task? currentlyExecutingTickTask;
2324

2425
public string Name { get; }
25-
26-
private bool TimerAlreadyStopped { get { return timer == null || asyncCallback == null; } }
2726

28-
private GrainTimer(OrleansTaskScheduler scheduler, IActivationData activationData, ILogger logger, Func<object, Task> asyncCallback, object state, TimeSpan dueTime, TimeSpan period, string name)
27+
private GrainTimer(OrleansTaskScheduler scheduler, IActivationData? activationData, ILogger logger, Func<object?, Task> asyncCallback, object? state, TimeSpan dueTime, TimeSpan period, string? name)
2928
{
3029
var ctxt = RuntimeContext.CurrentGrainContext;
3130
scheduler.CheckSchedulingContextValidity(ctxt);
3231
this.scheduler = scheduler;
3332
this.activationData = activationData;
3433
this.logger = logger;
35-
this.Name = name;
34+
this.Name = name ?? string.Empty;
3635
this.asyncCallback = asyncCallback;
3736
timer = new AsyncTaskSafeTimer(logger,
3837
stateObj => TimerTick(stateObj, ctxt),
@@ -46,33 +45,43 @@ private GrainTimer(OrleansTaskScheduler scheduler, IActivationData activationDat
4645
internal static IGrainTimer FromTaskCallback(
4746
OrleansTaskScheduler scheduler,
4847
ILogger logger,
49-
Func<object, Task> asyncCallback,
48+
Func<object?, Task> asyncCallback,
5049
object state,
5150
TimeSpan dueTime,
5251
TimeSpan period,
53-
string name = null,
54-
IActivationData activationData = null)
52+
string? name = null,
53+
IActivationData? activationData = null)
5554
{
5655
return new GrainTimer(scheduler, activationData, logger, asyncCallback, state, dueTime, period, name);
5756
}
5857

5958
public void Start()
6059
{
61-
if (TimerAlreadyStopped)
60+
if (timer is not { } asyncTimer)
61+
{
6262
throw new ObjectDisposedException(String.Format("The timer {0} was already disposed.", GetFullName()));
63+
}
6364

64-
timer.Start(dueTime, timerFrequency);
65+
asyncTimer.Start(dueTime, timerFrequency);
6566
}
6667

6768
public void Stop()
6869
{
69-
asyncCallback = null;
70+
// Stop the timer from ticking, but don't dispose it yet.
71+
if (timer is { } asyncTimer)
72+
{
73+
asyncTimer.Stop();
74+
}
7075
}
7176

7277
private async Task TimerTick(object state, IGrainContext context)
7378
{
74-
if (TimerAlreadyStopped)
79+
if (timer is null)
80+
{
81+
// The timer has been disposed already.
7582
return;
83+
}
84+
7685
try
7786
{
7887
// Schedule call back to grain context
@@ -90,25 +99,25 @@ private async Task TimerTick(object state, IGrainContext context)
9099
private async Task ForwardToAsyncCallback(object state)
91100
{
92101
// AsyncSafeTimer ensures that calls to this method are serialized.
93-
if (TimerAlreadyStopped) return;
102+
if (timer is null)
103+
{
104+
return;
105+
}
94106

95107
try
96108
{
97109
RequestContext.Clear(); // Clear any previous RC, so it does not leak into this call by mistake.
98110
lock (this.currentlyExecutingTickTaskLock)
99111
{
100-
if (TimerAlreadyStopped) return;
112+
if (timer is null)
113+
{
114+
return;
115+
}
101116

102-
totalNumTicks++;
103-
104-
if (logger.IsEnabled(LogLevel.Trace))
105-
logger.Trace(ErrorCode.TimerBeforeCallback, "About to make timer callback for timer {0}", GetFullName());
106-
107-
currentlyExecutingTickTask = asyncCallback(state);
117+
currentlyExecutingTickTask = InvokeAsyncCallback(state);
108118
}
119+
109120
await currentlyExecutingTickTask;
110-
111-
if (logger.IsEnabled(LogLevel.Trace)) logger.Trace(ErrorCode.TimerAfterCallback, "Completed timer callback for timer {0}", GetFullName());
112121
}
113122
catch (Exception exc)
114123
{
@@ -124,10 +133,34 @@ private async Task ForwardToAsyncCallback(object state)
124133
{
125134
previousTickTime = DateTime.UtcNow;
126135
currentlyExecutingTickTask = null;
136+
127137
// if this is not a repeating timer, then we can
128138
// dispose of the timer.
129139
if (timerFrequency == Constants.INFINITE_TIMESPAN)
130-
DisposeTimer();
140+
{
141+
DisposeTimer();
142+
}
143+
}
144+
}
145+
146+
private async Task InvokeAsyncCallback(object state)
147+
{
148+
// This is called under a lock, so ensure that the method yields before invoking a callback
149+
// which could take a different lock and potentially cause a deadlock.
150+
await Task.Yield();
151+
152+
totalNumTicks++;
153+
154+
if (logger.IsEnabled(LogLevel.Trace))
155+
{
156+
logger.Trace(ErrorCode.TimerBeforeCallback, "About to make timer callback for timer {0}", GetFullName());
157+
}
158+
159+
await asyncCallback(state);
160+
161+
if (logger.IsEnabled(LogLevel.Trace))
162+
{
163+
logger.Trace(ErrorCode.TimerAfterCallback, "Completed timer callback for timer {0}", GetFullName());
131164
}
132165
}
133166

@@ -149,9 +182,13 @@ private string GetFullName()
149182
// may not execute and starve this GrainTimer callback.
150183
public bool CheckTimerFreeze(DateTime lastCheckTime)
151184
{
152-
if (TimerAlreadyStopped) return true;
185+
if (timer is not { } asyncTimer)
186+
{
187+
return true;
188+
}
189+
153190
// check underlying SafeTimer (checking that .NET thread pool does not starve this timer)
154-
if (!timer.CheckTimerFreeze(lastCheckTime, () => Name)) return false;
191+
if (!asyncTimer.CheckTimerFreeze(lastCheckTime, () => Name)) return false;
155192
// if SafeTimer failed the check, no need to check GrainTimer too, since it will fail as well.
156193

157194
// check myself (checking that scheduler.QueueWorkItem does not starve this timer)
@@ -176,22 +213,28 @@ public void Dispose()
176213
protected virtual void Dispose(bool disposing)
177214
{
178215
if (disposing)
216+
{
179217
DisposeTimer();
180-
181-
asyncCallback = null;
218+
}
182219
}
183220

184221
private void DisposeTimer()
185222
{
186-
var tmp = timer;
187-
if (tmp == null) return;
223+
var asyncTimer = Interlocked.CompareExchange(ref timer, null, timer);
224+
if (asyncTimer == null)
225+
{
226+
return;
227+
}
188228

189-
Utils.SafeExecute(tmp.Dispose);
190-
timer = null;
191-
lock (this.currentlyExecutingTickTaskLock)
229+
try
192230
{
193-
asyncCallback = null;
231+
asyncTimer.Dispose();
194232
}
233+
catch (Exception ex)
234+
{
235+
logger.LogError(ex, "Error disposing timer {TimerName}", GetFullName());
236+
}
237+
195238
activationData?.OnTimerDisposed(this);
196239
}
197240
}

0 commit comments

Comments
 (0)