@@ -13,11 +13,19 @@ public class RedisPubSubServer : IRedisPubSubServer
13
13
private DateTime serverTimeAtStart ;
14
14
private Stopwatch startedAt ;
15
15
16
+ public TimeSpan ? HeartbeatInterval = TimeSpan . FromSeconds ( 10 ) ;
17
+ public TimeSpan HeartbeatTimeout = TimeSpan . FromSeconds ( 30 ) ;
18
+ private long lastHeartbeatTicks ;
19
+ private Timer heartbeatTimer ;
20
+
16
21
public Action OnInit { get ; set ; }
17
22
public Action OnStart { get ; set ; }
23
+ public Action OnHeartbeatSent { get ; set ; }
24
+ public Action OnHeartbeatReceived { get ; set ; }
18
25
public Action OnStop { get ; set ; }
19
26
public Action OnDispose { get ; set ; }
20
27
public Action < string , string > OnMessage { get ; set ; }
28
+ public Action < string > OnControlCommand { get ; set ; }
21
29
public Action < string > OnUnSubscribe { get ; set ; }
22
30
public Action < Exception > OnError { get ; set ; }
23
31
public Action < IRedisPubSubServer > OnFailover { get ; set ; }
@@ -34,6 +42,10 @@ public class RedisPubSubServer : IRedisPubSubServer
34
42
private int status ;
35
43
private Thread bgThread ; //Subscription controller thread
36
44
private long bgThreadCount = 0 ;
45
+ private int autoRestart = YES ;
46
+
47
+ private const int NO = 0 ;
48
+ private const int YES = 1 ;
37
49
38
50
public DateTime CurrentServerTime
39
51
{
@@ -62,6 +74,8 @@ public RedisPubSubServer(IRedisClientsManager clientsManager, params string[] ch
62
74
63
75
public IRedisPubSubServer Start ( )
64
76
{
77
+ Interlocked . CompareExchange ( ref autoRestart , 0 , autoRestart ) ;
78
+
65
79
if ( Interlocked . CompareExchange ( ref status , 0 , 0 ) == Status . Started )
66
80
{
67
81
//Start any stopped worker threads
@@ -122,10 +136,63 @@ private void Init()
122
136
startedAt = Stopwatch . StartNew ( ) ;
123
137
}
124
138
139
+ DisposeHeartbeatTimer ( ) ;
140
+
141
+ if ( HeartbeatInterval != null )
142
+ {
143
+ heartbeatTimer = new Timer ( SendHeartbeat , null ,
144
+ TimeSpan . FromMilliseconds ( 0 ) , HeartbeatInterval . Value ) ;
145
+ }
146
+
147
+ Interlocked . CompareExchange ( ref lastHeartbeatTicks , DateTime . UtcNow . Ticks , lastHeartbeatTicks ) ;
148
+
125
149
if ( OnInit != null )
126
150
OnInit ( ) ;
127
151
}
128
152
153
+ void SendHeartbeat ( object state )
154
+ {
155
+ if ( OnHeartbeatSent != null )
156
+ OnHeartbeatSent ( ) ;
157
+
158
+ if ( Interlocked . CompareExchange ( ref status , 0 , 0 ) != Status . Started )
159
+ return ;
160
+
161
+ NotifyAllSubscribers ( ControlCommand . Pulse ) ;
162
+
163
+ if ( DateTime . UtcNow - new DateTime ( lastHeartbeatTicks ) > HeartbeatTimeout )
164
+ {
165
+ if ( Interlocked . CompareExchange ( ref status , 0 , 0 ) == Status . Started )
166
+ {
167
+ Restart ( ) ;
168
+ }
169
+ }
170
+ }
171
+
172
+ void Pulse ( )
173
+ {
174
+ Interlocked . CompareExchange ( ref lastHeartbeatTicks , DateTime . UtcNow . Ticks , lastHeartbeatTicks ) ;
175
+
176
+ if ( OnHeartbeatReceived != null )
177
+ OnHeartbeatReceived ( ) ;
178
+ }
179
+
180
+ private void DisposeHeartbeatTimer ( )
181
+ {
182
+ if ( heartbeatTimer == null )
183
+ return ;
184
+
185
+ try
186
+ {
187
+ heartbeatTimer . Dispose ( ) ;
188
+ }
189
+ catch ( Exception ex )
190
+ {
191
+ if ( this . OnError != null ) this . OnError ( ex ) ;
192
+ }
193
+ heartbeatTimer = null ;
194
+ }
195
+
129
196
private IRedisClient masterClient ;
130
197
private void RunLoop ( )
131
198
{
@@ -150,9 +217,21 @@ private void RunLoop()
150
217
151
218
subscription . OnMessage = ( channel , msg ) =>
152
219
{
153
- if ( msg == Operation . ControlCommand )
220
+ if ( string . IsNullOrEmpty ( msg ) )
221
+ return ;
222
+
223
+ var ctrlMsg = msg . SplitOnFirst ( ':' ) ;
224
+ if ( ctrlMsg [ 0 ] == ControlCommand . Control )
154
225
{
155
226
var op = Interlocked . CompareExchange ( ref doOperation , Operation . NoOp , doOperation ) ;
227
+
228
+ var msgType = ctrlMsg . Length > 1
229
+ ? ctrlMsg [ 1 ]
230
+ : null ;
231
+
232
+ if ( OnControlCommand != null )
233
+ OnControlCommand ( msgType ?? Operation . GetName ( op ) ) ;
234
+
156
235
switch ( op )
157
236
{
158
237
case Operation . Stop :
@@ -169,9 +248,15 @@ private void RunLoop()
169
248
subscription . UnSubscribeFromAllChannels ( ) ; //Un block thread.
170
249
return ;
171
250
}
172
- }
173
251
174
- if ( ! string . IsNullOrEmpty ( msg ) )
252
+ switch ( msgType )
253
+ {
254
+ case ControlCommand . Pulse :
255
+ Pulse ( ) ;
256
+ break ;
257
+ }
258
+ }
259
+ else
175
260
{
176
261
OnMessage ( channel , msg ) ;
177
262
}
@@ -200,19 +285,22 @@ private void RunLoop()
200
285
201
286
if ( this . OnError != null )
202
287
this . OnError ( ex ) ;
288
+ }
203
289
290
+ if ( Interlocked . CompareExchange ( ref autoRestart , 0 , 0 ) == YES
291
+ && Interlocked . CompareExchange ( ref status , 0 , 0 ) != Status . Disposed )
292
+ {
204
293
if ( KeepAliveRetryAfterMs != null )
205
- {
206
294
Thread . Sleep ( KeepAliveRetryAfterMs . Value ) ;
207
295
208
- if ( Interlocked . CompareExchange ( ref status , 0 , 0 ) != Status . Disposed )
209
- Start ( ) ;
210
- }
296
+ Start ( ) ;
211
297
}
212
298
}
213
299
214
300
public void Stop ( )
215
301
{
302
+ Interlocked . CompareExchange ( ref autoRestart , NO , autoRestart ) ;
303
+
216
304
if ( Interlocked . CompareExchange ( ref status , 0 , 0 ) == Status . Disposed )
217
305
throw new ObjectDisposedException ( "RedisPubSubServer has been disposed" ) ;
218
306
@@ -221,20 +309,36 @@ public void Stop()
221
309
Log . Debug ( "Stopping RedisPubSubServer..." ) ;
222
310
223
311
//Unblock current bgthread by issuing StopCommand
224
- try
312
+ SendControlCommand ( Operation . Stop ) ;
313
+ }
314
+ }
315
+
316
+ private void SendControlCommand ( int operation )
317
+ {
318
+ Interlocked . CompareExchange ( ref doOperation , operation , doOperation ) ;
319
+ NotifyAllSubscribers ( ) ;
320
+ }
321
+
322
+ private void NotifyAllSubscribers ( string commandType = null )
323
+ {
324
+ var msg = ControlCommand . Control ;
325
+ if ( commandType != null )
326
+ msg += ":" + commandType ;
327
+
328
+ try
329
+ {
330
+ using ( var redis = ClientsManager . GetClient ( ) )
225
331
{
226
- using ( var redis = ClientsManager . GetClient ( ) )
332
+ foreach ( var channel in Channels )
227
333
{
228
- Interlocked . CompareExchange ( ref doOperation , Operation . Stop , doOperation ) ;
229
- Channels . Each ( x =>
230
- redis . PublishMessage ( x , Operation . ControlCommand ) ) ;
334
+ redis . PublishMessage ( channel , msg ) ;
231
335
}
232
336
}
233
- catch ( Exception ex )
234
- {
235
- if ( this . OnError != null ) this . OnError ( ex ) ;
236
- Log . Warn ( "Could not send STOP message to bg thread: " + ex . Message ) ;
237
- }
337
+ }
338
+ catch ( Exception ex )
339
+ {
340
+ if ( this . OnError != null ) this . OnError ( ex ) ;
341
+ Log . Warn ( "Could not send '{0}' message to bg thread: {1}" . Fmt ( msg , ex . Message ) ) ;
238
342
}
239
343
}
240
344
@@ -251,8 +355,10 @@ private void HandleFailover(IRedisClientsManager clientsManager)
251
355
using ( var currentlySubscribedClient = ( ( RedisClient ) masterClient ) . CloneClient ( ) )
252
356
{
253
357
Interlocked . CompareExchange ( ref doOperation , Operation . Reset , doOperation ) ;
254
- Channels . Each ( x =>
255
- currentlySubscribedClient . PublishMessage ( x , Operation . ControlCommand ) ) ;
358
+ foreach ( var channel in Channels )
359
+ {
360
+ currentlySubscribedClient . PublishMessage ( channel , ControlCommand . Control ) ;
361
+ }
256
362
}
257
363
}
258
364
else
@@ -279,7 +385,7 @@ void HandleUnSubscribe(string channel)
279
385
public void Restart ( )
280
386
{
281
387
Stop ( ) ;
282
- Start ( ) ;
388
+ Interlocked . CompareExchange ( ref autoRestart , YES , autoRestart ) ;
283
389
}
284
390
285
391
private void KillBgThreadIfExists ( )
@@ -319,12 +425,33 @@ private void SleepBackOffMultiplier(int continuousErrorsCount)
319
425
320
426
public static class Operation //dep-free copy of WorkerOperation
321
427
{
322
- public const string ControlCommand = "CTRL" ;
323
-
324
428
public const int NoOp = 0 ;
325
429
public const int Stop = 1 ;
326
430
public const int Reset = 2 ;
327
431
public const int Restart = 3 ;
432
+
433
+ public static string GetName ( int op )
434
+ {
435
+ switch ( op )
436
+ {
437
+ case NoOp :
438
+ return "NoOp" ;
439
+ case Stop :
440
+ return "Stop" ;
441
+ case Reset :
442
+ return "Reset" ;
443
+ case Restart :
444
+ return "Restart" ;
445
+ default :
446
+ return null ;
447
+ }
448
+ }
449
+ }
450
+
451
+ public static class ControlCommand
452
+ {
453
+ public const string Control = "CTRL" ;
454
+ public const string Pulse = "PULSE" ;
328
455
}
329
456
330
457
class Status //dep-free copy of WorkerStatus
@@ -396,6 +523,8 @@ public virtual void Dispose()
396
523
{
397
524
if ( this . OnError != null ) this . OnError ( ex ) ;
398
525
}
526
+
527
+ DisposeHeartbeatTimer ( ) ;
399
528
}
400
529
}
401
530
}
0 commit comments