4
4
// Pending dotnet API review
5
5
6
6
using System . Collections . Generic ;
7
+ using System . Diagnostics ;
8
+ using System . Diagnostics . CodeAnalysis ;
7
9
using System . Threading . Tasks ;
8
10
9
11
namespace System . Threading . RateLimiting
10
12
{
13
+ /// <summary>
14
+ /// <see cref="RateLimiter"/> implementation that helps manage concurrent access to a resource.
15
+ /// </summary>
11
16
public sealed class ConcurrencyLimiter : RateLimiter
12
17
{
13
18
private int _permitCount ;
14
19
private int _queueCount ;
15
20
16
- private readonly object _lock = new object ( ) ;
17
21
private readonly ConcurrencyLimiterOptions _options ;
18
22
private readonly Deque < RequestRegistration > _queue = new Deque < RequestRegistration > ( ) ;
19
23
20
24
private static readonly ConcurrencyLease SuccessfulLease = new ConcurrencyLease ( true , null , 0 ) ;
21
25
private static readonly ConcurrencyLease FailedLease = new ConcurrencyLease ( false , null , 0 ) ;
26
+ private static readonly ConcurrencyLease QueueLimitLease = new ConcurrencyLease ( false , null , 0 , "Queue limit reached" ) ;
22
27
28
+ // Use the queue as the lock field so we don't need to allocate another object for a lock and have another field in the object
29
+ private object Lock => _queue ;
30
+
31
+ /// <summary>
32
+ /// Initializes the <see cref="ConcurrencyLimiter"/>.
33
+ /// </summary>
34
+ /// <param name="options">Options to specify the behavior of the <see cref="ConcurrencyLimiter"/>.</param>
23
35
public ConcurrencyLimiter ( ConcurrencyLimiterOptions options )
24
36
{
25
37
_options = options ;
26
38
_permitCount = _options . PermitLimit ;
27
39
}
28
40
41
+ /// <inheritdoc/>
29
42
public override int GetAvailablePermits ( ) => _permitCount ;
30
43
44
+ /// <inheritdoc/>
31
45
protected override RateLimitLease AcquireCore ( int permitCount )
32
46
{
33
47
// These amounts of resources can never be acquired
34
48
if ( permitCount > _options . PermitLimit )
35
49
{
36
- throw new InvalidOperationException ( $ "{ permitCount } permits exceeds the permit limit of { _options . PermitLimit } .") ;
50
+ throw new ArgumentOutOfRangeException ( nameof ( permitCount ) , $ "{ permitCount } permits exceeds the permit limit of { _options . PermitLimit } .") ;
37
51
}
38
52
39
- // Return SuccessfulAcquisition or FailedAcquisition depending to indicate limiter state
53
+ // Return SuccessfulLease or FailedLease to indicate limiter state
40
54
if ( permitCount == 0 )
41
55
{
42
- return GetAvailablePermits ( ) > 0 ? SuccessfulLease : FailedLease ;
56
+ return _permitCount > 0 ? SuccessfulLease : FailedLease ;
43
57
}
44
58
45
59
// Perf: Check SemaphoreSlim implementation instead of locking
46
- if ( GetAvailablePermits ( ) >= permitCount )
60
+ if ( _permitCount >= permitCount )
47
61
{
48
- lock ( _lock )
62
+ lock ( Lock )
49
63
{
50
- if ( GetAvailablePermits ( ) >= permitCount )
64
+ if ( TryLeaseUnsynchronized ( permitCount , out RateLimitLease ? lease ) )
51
65
{
52
- _permitCount -= permitCount ;
53
- return new ConcurrencyLease ( true , this , permitCount ) ;
66
+ return lease ;
54
67
}
55
68
}
56
69
}
57
70
58
71
return FailedLease ;
59
72
}
60
73
74
+ /// <inheritdoc/>
61
75
protected override ValueTask < RateLimitLease > WaitAsyncCore ( int permitCount , CancellationToken cancellationToken = default )
62
76
{
77
+ cancellationToken . ThrowIfCancellationRequested ( ) ;
78
+
63
79
// These amounts of resources can never be acquired
64
- if ( permitCount < 0 || permitCount > _options . PermitLimit )
80
+ if ( permitCount > _options . PermitLimit )
65
81
{
66
- throw new ArgumentOutOfRangeException ( ) ;
82
+ throw new ArgumentOutOfRangeException ( nameof ( permitCount ) , $ " { permitCount } permits exceeds the permit limit of { _options . PermitLimit } ." ) ;
67
83
}
68
84
69
85
// Return SuccessfulAcquisition if requestedCount is 0 and resources are available
70
- if ( permitCount == 0 && GetAvailablePermits ( ) > 0 )
86
+ if ( permitCount == 0 && _permitCount > 0 )
71
87
{
72
- // Perf: static failed/successful value tasks?
73
88
return new ValueTask < RateLimitLease > ( SuccessfulLease ) ;
74
89
}
75
90
76
91
// Perf: Check SemaphoreSlim implementation instead of locking
77
- lock ( _lock ) // Check lock check
92
+ lock ( Lock )
78
93
{
79
- if ( GetAvailablePermits ( ) >= permitCount )
94
+ if ( TryLeaseUnsynchronized ( permitCount , out RateLimitLease ? lease ) )
80
95
{
81
- _permitCount -= permitCount ;
82
- return new ValueTask < RateLimitLease > ( new ConcurrencyLease ( true , this , permitCount ) ) ;
96
+ return new ValueTask < RateLimitLease > ( lease ) ;
83
97
}
84
98
85
99
// Don't queue if queue limit reached
86
100
if ( _queueCount + permitCount > _options . QueueLimit )
87
101
{
88
102
// Perf: static failed/successful value tasks?
89
- return new ValueTask < RateLimitLease > ( FailedLease ) ;
103
+ return new ValueTask < RateLimitLease > ( QueueLimitLease ) ;
104
+ }
105
+
106
+ TaskCompletionSource < RateLimitLease > tcs = new TaskCompletionSource < RateLimitLease > ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
107
+ CancellationTokenRegistration ctr ;
108
+ if ( cancellationToken . CanBeCanceled )
109
+ {
110
+ ctr = cancellationToken . Register ( obj =>
111
+ {
112
+ ( ( TaskCompletionSource < RateLimitLease > ) obj ) . TrySetException ( new OperationCanceledException ( cancellationToken ) ) ;
113
+ } , tcs ) ;
90
114
}
91
115
92
- var request = new RequestRegistration ( permitCount ) ;
116
+ RequestRegistration request = new RequestRegistration ( permitCount , tcs , ctr ) ;
93
117
_queue . EnqueueTail ( request ) ;
94
118
_queueCount += permitCount ;
119
+ Debug . Assert ( _queueCount <= _options . QueueLimit ) ;
95
120
96
- // TODO: handle cancellation
97
- return new ValueTask < RateLimitLease > ( request . TCS . Task ) ;
121
+ return new ValueTask < RateLimitLease > ( request . Tcs . Task ) ;
98
122
}
99
123
}
100
124
125
+ private bool TryLeaseUnsynchronized ( int permitCount , [ NotNullWhen ( true ) ] out RateLimitLease ? lease )
126
+ {
127
+ // if permitCount is 0 we want to queue it if there are no available permits
128
+ if ( _permitCount >= permitCount && _permitCount != 0 )
129
+ {
130
+ if ( permitCount == 0 )
131
+ {
132
+ // Edge case where the check before the lock showed 0 available permits but when we got the lock some permits were now available
133
+ lease = SuccessfulLease ;
134
+ return true ;
135
+ }
136
+
137
+ // a. if there are no items queued we can lease
138
+ // b. if there are items queued but the processing order is newest first, then we can lease the incoming request since it is the newest
139
+ if ( _queueCount == 0 || ( _queueCount > 0 && _options . QueueProcessingOrder == QueueProcessingOrder . NewestFirst ) )
140
+ {
141
+ _permitCount -= permitCount ;
142
+ Debug . Assert ( _permitCount >= 0 ) ;
143
+ lease = new ConcurrencyLease ( true , this , permitCount ) ;
144
+ return true ;
145
+ }
146
+ }
147
+
148
+ lease = null ;
149
+ return false ;
150
+ }
151
+
101
152
private void Release ( int releaseCount )
102
153
{
103
- lock ( _lock ) // Check lock check
154
+ lock ( Lock )
104
155
{
105
156
_permitCount += releaseCount ;
157
+ Debug . Assert ( _permitCount <= _options . PermitLimit ) ;
106
158
107
159
while ( _queue . Count > 0 )
108
160
{
109
- var nextPendingRequest =
161
+ RequestRegistration nextPendingRequest =
110
162
_options . QueueProcessingOrder == QueueProcessingOrder . OldestFirst
111
163
? _queue . PeekHead ( )
112
164
: _queue . PeekTail ( ) ;
113
165
114
- if ( GetAvailablePermits ( ) >= nextPendingRequest . Count )
166
+ if ( _permitCount >= nextPendingRequest . Count )
115
167
{
116
- var request =
168
+ nextPendingRequest =
117
169
_options . QueueProcessingOrder == QueueProcessingOrder . OldestFirst
118
170
? _queue . DequeueHead ( )
119
171
: _queue . DequeueTail ( ) ;
120
172
121
- _permitCount -= request . Count ;
122
- _queueCount -= request . Count ;
123
-
124
- // requestToFulfill == request
125
- request . TCS . SetResult ( new ConcurrencyLease ( true , this , request . Count ) ) ;
173
+ _permitCount -= nextPendingRequest . Count ;
174
+ _queueCount -= nextPendingRequest . Count ;
175
+ Debug . Assert ( _queueCount >= 0 ) ;
176
+ Debug . Assert ( _permitCount >= 0 ) ;
177
+
178
+ ConcurrencyLease lease = nextPendingRequest . Count == 0 ? SuccessfulLease : new ConcurrencyLease ( true , this , nextPendingRequest . Count ) ;
179
+ // Check if request was canceled
180
+ if ( ! nextPendingRequest . Tcs . TrySetResult ( lease ) )
181
+ {
182
+ // Queued item was canceled so add count back
183
+ _permitCount += nextPendingRequest . Count ;
184
+ }
185
+ nextPendingRequest . CancellationTokenRegistration . Dispose ( ) ;
126
186
}
127
187
else
128
188
{
@@ -134,25 +194,41 @@ private void Release(int releaseCount)
134
194
135
195
private class ConcurrencyLease : RateLimitLease
136
196
{
137
- private static readonly IEnumerable < string > Empty = new string [ 0 ] ;
138
-
139
197
private bool _disposed ;
140
198
private readonly ConcurrencyLimiter ? _limiter ;
141
199
private readonly int _count ;
200
+ private readonly string ? _reason ;
142
201
143
- public ConcurrencyLease ( bool isAcquired , ConcurrencyLimiter ? limiter , int count )
202
+ public ConcurrencyLease ( bool isAcquired , ConcurrencyLimiter ? limiter , int count , string ? reason = null )
144
203
{
145
204
IsAcquired = isAcquired ;
146
205
_limiter = limiter ;
147
206
_count = count ;
207
+ _reason = reason ;
208
+
209
+ // No need to set the limiter if count is 0, Dispose will noop
210
+ Debug . Assert ( count == 0 ? limiter is null : true ) ;
148
211
}
149
212
150
213
public override bool IsAcquired { get ; }
151
214
152
- public override IEnumerable < string > MetadataNames => Empty ;
215
+ public override IEnumerable < string > MetadataNames => Enumerable ( ) ;
216
+
217
+ private IEnumerable < string > Enumerable ( )
218
+ {
219
+ if ( _reason is not null )
220
+ {
221
+ yield return MetadataName . ReasonPhrase . Name ;
222
+ }
223
+ }
153
224
154
225
public override bool TryGetMetadata ( string metadataName , out object ? metadata )
155
226
{
227
+ if ( _reason is not null && metadataName == MetadataName . ReasonPhrase . Name )
228
+ {
229
+ metadata = _reason ;
230
+ return true ;
231
+ }
156
232
metadata = default ;
157
233
return false ;
158
234
}
@@ -170,18 +246,22 @@ protected override void Dispose(bool disposing)
170
246
}
171
247
}
172
248
173
- private struct RequestRegistration
249
+ private readonly struct RequestRegistration
174
250
{
175
- public RequestRegistration ( int requestedCount )
251
+ public RequestRegistration ( int requestedCount , TaskCompletionSource < RateLimitLease > tcs ,
252
+ CancellationTokenRegistration cancellationTokenRegistration )
176
253
{
177
254
Count = requestedCount ;
178
255
// Perf: Use AsyncOperation<TResult> instead
179
- TCS = new TaskCompletionSource < RateLimitLease > ( ) ;
256
+ Tcs = tcs ;
257
+ CancellationTokenRegistration = cancellationTokenRegistration ;
180
258
}
181
259
182
260
public int Count { get ; }
183
261
184
- public TaskCompletionSource < RateLimitLease > TCS { get ; }
262
+ public TaskCompletionSource < RateLimitLease > Tcs { get ; }
263
+
264
+ public CancellationTokenRegistration CancellationTokenRegistration { get ; }
185
265
}
186
266
}
187
267
}
0 commit comments