2
2
using Hangfire . Storage . SQLite . Entities ;
3
3
using SQLite ;
4
4
using System ;
5
- using System . Collections . Generic ;
5
+ using System . Diagnostics ;
6
6
using System . Threading ;
7
7
8
8
namespace Hangfire . Storage . SQLite
@@ -14,9 +14,6 @@ public class SQLiteDistributedLock : IDisposable
14
14
{
15
15
private static readonly ILog Logger = LogProvider . For < SQLiteDistributedLock > ( ) ;
16
16
17
- private static readonly ThreadLocal < Dictionary < string , int > > AcquiredLocks
18
- = new ThreadLocal < Dictionary < string , int > > ( ( ) => new Dictionary < string , int > ( StringComparer . OrdinalIgnoreCase ) ) ;
19
-
20
17
private readonly string _resource ;
21
18
private readonly string _resourceKey ;
22
19
@@ -30,15 +27,17 @@ private static readonly ThreadLocal<Dictionary<string, int>> AcquiredLocks
30
27
31
28
private string EventWaitHandleName => string . Intern ( $@ "{ GetType ( ) . FullName } .{ _resource } ") ;
32
29
30
+ public event Action < bool > Heartbeat ;
31
+
33
32
/// <summary>
34
33
/// Creates SQLite distributed lock
35
34
/// </summary>
36
35
/// <param name="resource">Lock resource</param>
37
- /// <param name="timeout">Lock timeout</param>
38
36
/// <param name="database">Lock database</param>
39
37
/// <param name="storageOptions">Database options</param>
40
38
/// <exception cref="DistributedLockTimeoutException">Thrown if lock is not acuired within the timeout</exception>
41
- public SQLiteDistributedLock ( string resource , TimeSpan timeout , HangfireDbContext database ,
39
+ private SQLiteDistributedLock ( string resource ,
40
+ HangfireDbContext database ,
42
41
SQLiteStorageOptions storageOptions )
43
42
{
44
43
_resource = resource ?? throw new ArgumentNullException ( nameof ( resource ) ) ;
@@ -50,22 +49,25 @@ public SQLiteDistributedLock(string resource, TimeSpan timeout, HangfireDbContex
50
49
{
51
50
throw new ArgumentException ( $@ "The { nameof ( resource ) } cannot be empty", nameof ( resource ) ) ;
52
51
}
52
+ }
53
+
54
+ public static SQLiteDistributedLock Acquire (
55
+ string resource ,
56
+ TimeSpan timeout ,
57
+ HangfireDbContext database ,
58
+ SQLiteStorageOptions storageOptions )
59
+ {
53
60
if ( timeout . TotalSeconds > int . MaxValue )
54
61
{
55
62
throw new ArgumentException ( $ "The timeout specified is too large. Please supply a timeout equal to or less than { int . MaxValue } seconds", nameof ( timeout ) ) ;
56
63
}
57
64
58
- if ( ! AcquiredLocks . Value . ContainsKey ( _resource ) || AcquiredLocks . Value [ _resource ] == 0 )
59
- {
60
- Cleanup ( ) ;
61
- Acquire ( timeout ) ;
62
- AcquiredLocks . Value [ _resource ] = 1 ;
63
- StartHeartBeat ( ) ;
64
- }
65
- else
66
- {
67
- AcquiredLocks . Value [ _resource ] ++ ;
68
- }
65
+ var slock = new SQLiteDistributedLock ( resource , database , storageOptions ) ;
66
+
67
+ slock . Acquire ( timeout ) ;
68
+ slock . StartHeartBeat ( ) ;
69
+
70
+ return slock ;
69
71
}
70
72
71
73
/// <summary>
@@ -78,96 +80,52 @@ public void Dispose()
78
80
{
79
81
return ;
80
82
}
83
+
81
84
_completed = true ;
85
+ _heartbeatTimer ? . Dispose ( ) ;
86
+ Release ( ) ;
87
+ }
82
88
83
- if ( ! AcquiredLocks . Value . ContainsKey ( _resource ) )
89
+ private bool TryAcquireLock ( )
90
+ {
91
+ Cleanup ( ) ;
92
+ try
84
93
{
85
- return ;
86
- }
87
-
88
- AcquiredLocks . Value [ _resource ] -- ;
94
+ var distributedLock = new DistributedLock
95
+ {
96
+ Id = Guid . NewGuid ( ) . ToString ( ) ,
97
+ Resource = _resource ,
98
+ ResourceKey = _resourceKey ,
99
+ ExpireAt = DateTime . UtcNow . Add ( _storageOptions . DistributedLockLifetime )
100
+ } ;
89
101
90
- if ( AcquiredLocks . Value [ _resource ] > 0 )
91
- {
92
- return ;
102
+ return _dbContext . Database . Insert ( distributedLock ) == 1 ;
93
103
}
94
-
95
- // Timer callback may be invoked after the Dispose method call,
96
- // but since we use the resource key, we will not disturb other owners.
97
- AcquiredLocks . Value . Remove ( _resource ) ;
98
-
99
- if ( _heartbeatTimer != null )
104
+ catch ( SQLiteException e ) when ( e . Result == SQLite3 . Result . Constraint )
100
105
{
101
- _heartbeatTimer . Dispose ( ) ;
102
- _heartbeatTimer = null ;
106
+ return false ;
103
107
}
104
-
105
- Release ( ) ;
106
-
107
- Cleanup ( ) ;
108
108
}
109
109
110
110
private void Acquire ( TimeSpan timeout )
111
111
{
112
- try
112
+ var sw = Stopwatch . StartNew ( ) ;
113
+ do
113
114
{
114
- var isLockAcquired = false ;
115
- var now = DateTime . UtcNow ;
116
- var lockTimeoutTime = now . Add ( timeout ) ;
117
-
118
- while ( lockTimeoutTime >= now )
115
+ if ( TryAcquireLock ( ) )
119
116
{
120
- Cleanup ( ) ;
121
-
122
- lock ( EventWaitHandleName )
123
- {
124
- var result = _dbContext . DistributedLockRepository . FirstOrDefault ( _ => _ . Resource == _resource ) ;
125
-
126
- if ( result == null )
127
- {
128
- try
129
- {
130
- var distributedLock = new DistributedLock ( ) ;
131
- distributedLock . Id = Guid . NewGuid ( ) . ToString ( ) ;
132
- distributedLock . Resource = _resource ;
133
- distributedLock . ResourceKey = _resourceKey ;
134
- distributedLock . ExpireAt = DateTime . UtcNow . Add ( _storageOptions . DistributedLockLifetime ) ;
135
-
136
- _dbContext . Database . Insert ( distributedLock ) ;
137
-
138
- // we were able to acquire the lock - break the loop
139
- isLockAcquired = true ;
140
- break ;
141
- }
142
- catch ( SQLiteException e ) when ( e . Result == SQLite3 . Result . Constraint )
143
- {
144
- // The lock already exists preventing us from inserting.
145
- continue ;
146
- }
147
- }
148
- }
149
-
150
- // we couldn't acquire the lock - wait a bit and try again
151
- var waitTime = ( int ) timeout . TotalMilliseconds / 10 ;
152
- lock ( EventWaitHandleName )
153
- Monitor . Wait ( EventWaitHandleName , waitTime ) ;
154
-
155
- now = DateTime . UtcNow ;
117
+ return ;
156
118
}
157
119
158
- if ( ! isLockAcquired )
120
+ var waitTime = ( int ) timeout . TotalMilliseconds / 10 ;
121
+ // either wait for the event to be raised, or timeout
122
+ lock ( EventWaitHandleName )
159
123
{
160
- throw new DistributedLockTimeoutException ( _resource ) ;
124
+ Monitor . Wait ( EventWaitHandleName , waitTime ) ;
161
125
}
162
- }
163
- catch ( DistributedLockTimeoutException ex )
164
- {
165
- throw ex ;
166
- }
167
- catch ( Exception ex )
168
- {
169
- throw ex ;
170
- }
126
+ } while ( sw . Elapsed <= timeout ) ;
127
+
128
+ throw new DistributedLockTimeoutException ( _resource ) ;
171
129
}
172
130
173
131
/// <summary>
@@ -179,9 +137,12 @@ private void Release()
179
137
Retry . Twice ( ( retry ) => {
180
138
181
139
// Remove resource lock (if it's still ours)
182
- _dbContext . DistributedLockRepository . Delete ( _ => _ . Resource == _resource && _ . ResourceKey == _resourceKey ) ;
183
- lock ( EventWaitHandleName )
184
- Monitor . Pulse ( EventWaitHandleName ) ;
140
+ var count = _dbContext . DistributedLockRepository . Delete ( _ => _ . Resource == _resource && _ . ResourceKey == _resourceKey ) ;
141
+ if ( count != 0 )
142
+ {
143
+ lock ( EventWaitHandleName )
144
+ Monitor . Pulse ( EventWaitHandleName ) ;
145
+ }
185
146
} ) ;
186
147
}
187
148
@@ -192,7 +153,7 @@ private void Cleanup()
192
153
Retry . Twice ( ( _ ) => {
193
154
// Delete expired locks (of any owner)
194
155
_dbContext . DistributedLockRepository .
195
- Delete ( x => x . Resource == _resource && x . ExpireAt < DateTime . UtcNow ) ;
156
+ Delete ( x => x . Resource == _resource && x . ExpireAt < DateTime . UtcNow ) ;
196
157
} ) ;
197
158
}
198
159
catch ( Exception ex )
@@ -210,27 +171,48 @@ private void StartHeartBeat()
210
171
211
172
_heartbeatTimer = new Timer ( state =>
212
173
{
174
+ // stop timer
175
+ _heartbeatTimer ? . Change ( Timeout . Infinite , Timeout . Infinite ) ;
213
176
// Timer callback may be invoked after the Dispose method call,
214
177
// but since we use the resource key, we will not disturb other owners.
215
178
try
216
179
{
217
- var distributedLock = _dbContext . DistributedLockRepository . FirstOrDefault ( x => x . Resource == _resource && x . ResourceKey == _resourceKey ) ;
218
- if ( distributedLock != null )
219
- {
220
- distributedLock . ExpireAt = DateTime . UtcNow . Add ( _storageOptions . DistributedLockLifetime ) ;
221
-
222
- _dbContext . Database . Update ( distributedLock ) ;
223
- }
224
- else
180
+ var didUpdate = UpdateExpiration ( _dbContext . DistributedLockRepository , DateTime . UtcNow . Add ( _storageOptions . DistributedLockLifetime ) ) ;
181
+ Heartbeat ? . Invoke ( didUpdate ) ;
182
+ if ( ! didUpdate )
225
183
{
226
184
Logger . ErrorFormat ( "Unable to update heartbeat on the resource '{0}'. The resource is not locked or is locked by another owner." , _resource ) ;
185
+
186
+ // if we no longer have a lock, stop the heartbeat immediately
187
+ _heartbeatTimer ? . Dispose ( ) ;
188
+ return ;
227
189
}
228
190
}
229
191
catch ( Exception ex )
230
192
{
231
193
Logger . ErrorFormat ( "Unable to update heartbeat on the resource '{0}'. {1}" , _resource , ex ) ;
232
194
}
195
+ // restart timer
196
+ _heartbeatTimer ? . Change ( timerInterval , timerInterval ) ;
233
197
} , null , timerInterval , timerInterval ) ;
234
198
}
199
+
200
+ private bool UpdateExpiration ( TableQuery < DistributedLock > tableQuery , DateTime expireAt )
201
+ {
202
+ var expireColumn = tableQuery . Table . FindColumnWithPropertyName ( nameof ( DistributedLock . ExpireAt ) ) . Name ;
203
+ var resourceColumn = tableQuery . Table . FindColumnWithPropertyName ( nameof ( DistributedLock . Resource ) ) . Name ;
204
+ var resourceKeyColumn = tableQuery . Table . FindColumnWithPropertyName ( nameof ( DistributedLock . ResourceKey ) ) . Name ;
205
+ var table = tableQuery . Table . TableName ;
206
+
207
+ var command = tableQuery . Connection . CreateCommand ( $@ "UPDATE ""{ table } ""
208
+ SET ""{ expireColumn } "" = ?
209
+ WHERE ""{ resourceColumn } "" = ?
210
+ AND ""{ resourceKeyColumn } "" = ?" ,
211
+ expireAt ,
212
+ _resource ,
213
+ _resourceKey ) ;
214
+
215
+ return command . ExecuteNonQuery ( ) != 0 ;
216
+ }
235
217
}
236
- }
218
+ }
0 commit comments