1- using System . Collections . Concurrent ;
1+ using System . Collections . Concurrent ;
22using Microsoft . Extensions . Logging ;
3+ using Netcorext . Extensions . Threading ;
34
4- namespace Netcorext . Extensions . Threading ;
5-
5+ /// <summary>
6+ /// Provides a key-based locking mechanism for controlling concurrent access to resources.
7+ /// </summary>
68public class KeyLocker : IDisposable
79{
8- private bool _disposed ;
10+ private volatile int _disposed ;
911 private readonly int _maxConcurrent ;
1012 private readonly TimeSpan ? _timeout ;
13+ private readonly TimeSpan _cleanupInterval ;
1114 private readonly bool _throwTimeoutException ;
12- private readonly TimeSpan ? _expired ;
1315 private readonly ILogger _logger ;
1416 private readonly ConcurrentDictionary < string , KeyState > _locks = new ( ) ;
15- private readonly ConcurrentDictionary < string , Timer > _timers = new ( ) ;
16-
17- public KeyLocker ( ILogger logger , TimeSpan ? timeout = null , bool throwTimeoutException = false , TimeSpan ? expired = null , int maxConcurrent = 1 )
17+ private readonly Timer _cleanupTimer ;
18+
19+ /// <summary>
20+ /// Initializes a new instance of the KeyLocker class.
21+ /// </summary>
22+ /// <param name="logger">The logger instance for recording events.</param>
23+ /// <param name="timeout">Optional timeout for wait operations.</param>
24+ /// <param name="throwTimeoutException">Whether to throw an exception on timeout.</param>
25+ /// <param name="maxConcurrent">Maximum number of concurrent operations allowed.</param>
26+ /// <param name="cleanupInterval">Interval for cleaning up expired locks. Default is 5 minutes.</param>
27+ public KeyLocker ( ILogger logger , TimeSpan ? timeout = null , bool throwTimeoutException = false , int maxConcurrent = 1 , TimeSpan ? cleanupInterval = null )
1828 {
19- _maxConcurrent = maxConcurrent ;
29+ _logger = logger ?? throw new ArgumentNullException ( nameof ( logger ) ) ;
2030 _timeout = timeout ;
2131 _throwTimeoutException = throwTimeoutException ;
22- _expired = expired ?? TimeSpan . FromMilliseconds ( 10 * 60 * 1000 ) ;
23- _logger = logger ;
32+ _maxConcurrent = maxConcurrent > 0 ? maxConcurrent : throw new ArgumentException ( "maxConcurrent must be greater than 0" , nameof ( maxConcurrent ) ) ;
33+ _cleanupInterval = cleanupInterval ?? TimeSpan . FromMinutes ( 5 ) ;
34+ _cleanupTimer = new Timer ( CleanupIdleLocks , null , _cleanupInterval , _cleanupInterval ) ;
2435 }
2536
2637 public void Wait ( string key )
2738 {
28- var keyState = _locks . AddOrUpdate ( key , CreateLockItem , ( k , state ) =>
39+ var keyState = _locks . AddOrUpdate ( key , CreateLockItem , ( _ , state ) =>
2940 {
3041 lock ( state )
3142 {
@@ -43,9 +54,6 @@ public void Wait(string key)
4354 if ( keyState . ReleaseAll )
4455 return ;
4556
46- if ( _expired . HasValue && _timers . TryAdd ( key , new Timer ( HandleExpired , key , Timeout . InfiniteTimeSpan , Timeout . InfiniteTimeSpan ) ) )
47- _timers [ key ] . Change ( _expired . Value , _expired . Value ) ;
48-
4957 if ( _timeout . HasValue )
5058 {
5159 if ( keyState . Semaphore . Wait ( _timeout . Value , keyState . Cancellation . Token ) )
@@ -63,7 +71,7 @@ public void Wait(string key)
6371
6472 public async Task WaitAsync ( string key )
6573 {
66- var keyState = _locks . AddOrUpdate ( key , CreateLockItem , ( k , state ) =>
74+ var keyState = _locks . AddOrUpdate ( key , CreateLockItem , ( _ , state ) =>
6775 {
6876 lock ( state )
6977 {
@@ -81,9 +89,6 @@ public async Task WaitAsync(string key)
8189 if ( keyState . ReleaseAll )
8290 return ;
8391
84- if ( _expired . HasValue && _timers . TryAdd ( key , new Timer ( HandleExpired , key , Timeout . InfiniteTimeSpan , Timeout . InfiniteTimeSpan ) ) )
85- _timers [ key ] . Change ( _expired . Value , _expired . Value ) ;
86-
8792 if ( _timeout . HasValue )
8893 {
8994 if ( await keyState . Semaphore . WaitAsync ( _timeout . Value , keyState . Cancellation . Token ) )
@@ -135,8 +140,6 @@ public int ReleaseAll(string key)
135140 if ( keyState . ReleaseAll || keyState . Cancellation . IsCancellationRequested )
136141 return 0 ;
137142
138- keyState . ReleaseAll = true ;
139-
140143 var releaseCount = 0 ;
141144
142145 try
@@ -145,6 +148,7 @@ public int ReleaseAll(string key)
145148 {
146149 try
147150 {
151+ keyState . ReleaseAll = true ;
148152 keyState . LastWaitingTime = DateTimeOffset . UtcNow ;
149153 keyState . Semaphore . Release ( ) ;
150154 keyState . DecrementConcurrent ( ) ;
@@ -167,23 +171,31 @@ public int ReleaseAll(string key)
167171
168172 public void Reset ( string key )
169173 {
170- if ( ! _locks . TryGetValue ( key , out var keyState ) )
174+ if ( ! _locks . TryRemove ( key , out var keyState ) )
171175 return ;
172176
173177 lock ( keyState )
174178 {
175- keyState . LastWaitingTime = DateTimeOffset . UtcNow ;
176- keyState . Cancellation = new CancellationTokenSource ( ) ;
177- keyState . ReleaseAll = false ;
178- }
179+ try
180+ {
181+ while ( true )
182+ {
183+ try
184+ {
185+ keyState . Semaphore . Release ( ) ;
186+ }
187+ catch ( SemaphoreFullException )
188+ {
189+ keyState . Cancellation . Cancel ( true ) ;
179190
180- if ( ! _timers . TryRemove ( key , out var timer ) )
181- return ;
191+ break ;
192+ }
193+ }
194+ }
195+ catch ( ObjectDisposedException ) { }
196+ catch ( ArgumentOutOfRangeException ) { }
182197
183- lock ( timer )
184- {
185- timer . Change ( Timeout . InfiniteTimeSpan , Timeout . InfiniteTimeSpan ) ;
186- timer . Dispose ( ) ;
198+ keyState . Dispose ( ) ;
187199 }
188200 }
189201
@@ -192,43 +204,38 @@ public int GetWaitingCount(string key)
192204 return _locks . TryGetValue ( key , out var keyState ) ? keyState . WaitingConcurrent : 0 ;
193205 }
194206
207+ public bool HasLock ( string key )
208+ {
209+ return _locks . ContainsKey ( key ) ;
210+ }
211+
195212 private void HandleTimeout ( KeyState keyState )
196213 {
197- _logger . LogWarning ( "Lock on key '{Key}' timed out" , keyState . Key ) ;
214+ _logger . LogWarning ( "Lock on key '{Key}' timed out after {Timeout} " , keyState . Key , _timeout ) ;
198215
199216 Release ( keyState . Key ) ;
200217
201218 if ( _throwTimeoutException )
202- throw new TimeoutException ( $ "Lock on key ' { keyState . Key } ' timed out. ") ;
219+ throw new TimeoutException ( $ "Lock operation timed out for key: { keyState . Key } ") ;
203220 }
204221
205- private void HandleExpired ( object ? state )
222+ private void CleanupIdleLocks ( object ? state )
206223 {
207- if ( state is not string key || ! _locks . TryGetValue ( key , out var keyState ) )
208- return ;
209-
210- var elapsed = DateTimeOffset . UtcNow . Subtract ( keyState . LastWaitingTime ) ;
224+ var now = DateTimeOffset . UtcNow ;
211225
212- if ( ! _expired . HasValue || ! ( elapsed >= _expired ) )
213- return ;
226+ var idledKeys = _locks
227+ . Where ( kvp => now - kvp . Value . LastWaitingTime > _cleanupInterval && ! kvp . Value . ReleaseAll )
228+ . Select ( kvp => kvp . Key )
229+ . ToArray ( ) ;
214230
215- lock ( keyState )
231+ foreach ( var key in idledKeys )
216232 {
217- if ( ! _locks . TryRemove ( keyState . Key , out _ ) )
218- return ;
219-
220- _logger . LogWarning ( "Key '{Key}' expired({Elapsed}), has been removed" , keyState . Key , elapsed ) ;
233+ _logger . LogInformation ( "Cleaning up idled lock for key '{Key}'" , key ) ;
221234
222- if ( _timers . TryRemove ( keyState . Key , out var timer ) )
223- {
224- lock ( timer )
225- {
226- timer . Change ( Timeout . InfiniteTimeSpan , Timeout . InfiniteTimeSpan ) ;
227- timer . Dispose ( ) ;
228- }
229- }
235+ if ( ! _locks . TryRemove ( key , out var keyState ) ) continue ;
230236
231- keyState . Dispose ( ) ;
237+ keyState . Cancellation . Cancel ( ) ;
238+ keyState . Semaphore . Dispose ( ) ;
232239 }
233240 }
234241
@@ -248,21 +255,26 @@ private KeyState CreateLockItem(string key)
248255
249256 public void Dispose ( )
250257 {
251- if ( _disposed )
258+ if ( Interlocked . Exchange ( ref _disposed , 1 ) != 0 )
252259 return ;
253260
254- _disposed = true ;
261+ _cleanupTimer . Change ( TimeSpan . Zero , TimeSpan . Zero ) ;
262+ _cleanupTimer . Dispose ( ) ;
255263
256- foreach ( var key in _locks . Keys )
264+ foreach ( var keyState in _locks . Values )
257265 {
258- if ( _locks . TryRemove ( key , out var keyState ) )
259- keyState . Dispose ( ) ;
266+ try
267+ {
268+ keyState . Cancellation . Cancel ( ) ;
269+ keyState . Semaphore . Dispose ( ) ;
270+ }
271+ catch ( Exception ex )
272+ {
273+ _logger . LogError ( ex , "Error during KeyLocker disposal" ) ;
274+ }
260275 }
261276
262- foreach ( var key in _timers . Keys )
263- {
264- if ( _timers . TryRemove ( key , out var timer ) )
265- timer . Dispose ( ) ;
266- }
277+ _locks . Clear ( ) ;
278+ GC . SuppressFinalize ( this ) ;
267279 }
268280}
0 commit comments