1
+ #nullable enable
1
2
using System ;
2
3
using System . Reflection ;
3
4
using System . Threading ;
@@ -9,30 +10,28 @@ namespace Orleans.Runtime
9
10
{
10
11
internal class GrainTimer : IGrainTimer
11
12
{
12
- private Func < object , Task > asyncCallback ;
13
- private AsyncTaskSafeTimer timer ;
13
+ private readonly Func < object ? , Task > asyncCallback ;
14
14
private readonly TimeSpan dueTime ;
15
15
private readonly TimeSpan timerFrequency ;
16
- private DateTime previousTickTime ;
17
- private int totalNumTicks ;
18
16
private readonly ILogger logger ;
19
- private volatile Task currentlyExecutingTickTask ;
20
- private object currentlyExecutingTickTaskLock = new ( ) ;
17
+ private readonly object currentlyExecutingTickTaskLock = new ( ) ;
21
18
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 ;
23
24
24
25
public string Name { get ; }
25
-
26
- private bool TimerAlreadyStopped { get { return timer == null || asyncCallback == null ; } }
27
26
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 )
29
28
{
30
29
var ctxt = RuntimeContext . CurrentGrainContext ;
31
30
scheduler . CheckSchedulingContextValidity ( ctxt ) ;
32
31
this . scheduler = scheduler ;
33
32
this . activationData = activationData ;
34
33
this . logger = logger ;
35
- this . Name = name ;
34
+ this . Name = name ?? string . Empty ;
36
35
this . asyncCallback = asyncCallback ;
37
36
timer = new AsyncTaskSafeTimer ( logger ,
38
37
stateObj => TimerTick ( stateObj , ctxt ) ,
@@ -46,33 +45,43 @@ private GrainTimer(OrleansTaskScheduler scheduler, IActivationData activationDat
46
45
internal static IGrainTimer FromTaskCallback (
47
46
OrleansTaskScheduler scheduler ,
48
47
ILogger logger ,
49
- Func < object , Task > asyncCallback ,
48
+ Func < object ? , Task > asyncCallback ,
50
49
object state ,
51
50
TimeSpan dueTime ,
52
51
TimeSpan period ,
53
- string name = null ,
54
- IActivationData activationData = null )
52
+ string ? name = null ,
53
+ IActivationData ? activationData = null )
55
54
{
56
55
return new GrainTimer ( scheduler , activationData , logger , asyncCallback , state , dueTime , period , name ) ;
57
56
}
58
57
59
58
public void Start ( )
60
59
{
61
- if ( TimerAlreadyStopped )
60
+ if ( timer is not { } asyncTimer )
61
+ {
62
62
throw new ObjectDisposedException ( String . Format ( "The timer {0} was already disposed." , GetFullName ( ) ) ) ;
63
+ }
63
64
64
- timer . Start ( dueTime , timerFrequency ) ;
65
+ asyncTimer . Start ( dueTime , timerFrequency ) ;
65
66
}
66
67
67
68
public void Stop ( )
68
69
{
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
+ }
70
75
}
71
76
72
77
private async Task TimerTick ( object state , IGrainContext context )
73
78
{
74
- if ( TimerAlreadyStopped )
79
+ if ( timer is null )
80
+ {
81
+ // The timer has been disposed already.
75
82
return ;
83
+ }
84
+
76
85
try
77
86
{
78
87
// Schedule call back to grain context
@@ -90,25 +99,25 @@ private async Task TimerTick(object state, IGrainContext context)
90
99
private async Task ForwardToAsyncCallback ( object state )
91
100
{
92
101
// AsyncSafeTimer ensures that calls to this method are serialized.
93
- if ( TimerAlreadyStopped ) return ;
102
+ if ( timer is null )
103
+ {
104
+ return ;
105
+ }
94
106
95
107
try
96
108
{
97
109
RequestContext . Clear ( ) ; // Clear any previous RC, so it does not leak into this call by mistake.
98
110
lock ( this . currentlyExecutingTickTaskLock )
99
111
{
100
- if ( TimerAlreadyStopped ) return ;
112
+ if ( timer is null )
113
+ {
114
+ return ;
115
+ }
101
116
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 ) ;
108
118
}
119
+
109
120
await currentlyExecutingTickTask ;
110
-
111
- if ( logger . IsEnabled ( LogLevel . Trace ) ) logger . Trace ( ErrorCode . TimerAfterCallback , "Completed timer callback for timer {0}" , GetFullName ( ) ) ;
112
121
}
113
122
catch ( Exception exc )
114
123
{
@@ -124,10 +133,34 @@ private async Task ForwardToAsyncCallback(object state)
124
133
{
125
134
previousTickTime = DateTime . UtcNow ;
126
135
currentlyExecutingTickTask = null ;
136
+
127
137
// if this is not a repeating timer, then we can
128
138
// dispose of the timer.
129
139
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 ( ) ) ;
131
164
}
132
165
}
133
166
@@ -149,9 +182,13 @@ private string GetFullName()
149
182
// may not execute and starve this GrainTimer callback.
150
183
public bool CheckTimerFreeze ( DateTime lastCheckTime )
151
184
{
152
- if ( TimerAlreadyStopped ) return true ;
185
+ if ( timer is not { } asyncTimer )
186
+ {
187
+ return true ;
188
+ }
189
+
153
190
// 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 ;
155
192
// if SafeTimer failed the check, no need to check GrainTimer too, since it will fail as well.
156
193
157
194
// check myself (checking that scheduler.QueueWorkItem does not starve this timer)
@@ -176,22 +213,28 @@ public void Dispose()
176
213
protected virtual void Dispose ( bool disposing )
177
214
{
178
215
if ( disposing )
216
+ {
179
217
DisposeTimer ( ) ;
180
-
181
- asyncCallback = null ;
218
+ }
182
219
}
183
220
184
221
private void DisposeTimer ( )
185
222
{
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
+ }
188
228
189
- Utils . SafeExecute ( tmp . Dispose ) ;
190
- timer = null ;
191
- lock ( this . currentlyExecutingTickTaskLock )
229
+ try
192
230
{
193
- asyncCallback = null ;
231
+ asyncTimer . Dispose ( ) ;
194
232
}
233
+ catch ( Exception ex )
234
+ {
235
+ logger . LogError ( ex , "Error disposing timer {TimerName}" , GetFullName ( ) ) ;
236
+ }
237
+
195
238
activationData ? . OnTimerDisposed ( this ) ;
196
239
}
197
240
}
0 commit comments