File tree Expand file tree Collapse file tree
tests/MADE.Threading.Tests/Tests Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -20,7 +20,7 @@ namespace MADE.Threading;
2020/// }
2121///
2222/// // Reduce concurrency on backpressure.
23- /// semaphore.TryShrink ();
23+ /// await semaphore.TryShrinkAsync ();
2424/// </code>
2525/// </remarks>
2626public sealed class AdaptiveSemaphore : IDisposable
@@ -96,7 +96,20 @@ public async Task<int> TryShrinkAsync(CancellationToken cancellationToken = defa
9696 this . limit -- ;
9797 }
9898
99- await this . semaphore . WaitAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
99+ try
100+ {
101+ await this . semaphore . WaitAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
102+ }
103+ catch
104+ {
105+ lock ( this . adjustLock )
106+ {
107+ this . limit ++ ;
108+ }
109+
110+ throw ;
111+ }
112+
100113 return this . limit ;
101114 }
102115
@@ -122,7 +135,17 @@ public int TryGrow()
122135 }
123136
124137 this . limit ++ ;
125- this . semaphore . Release ( ) ;
138+
139+ try
140+ {
141+ this . semaphore . Release ( ) ;
142+ }
143+ catch
144+ {
145+ this . limit -- ;
146+ throw ;
147+ }
148+
126149 return this . limit ;
127150 }
128151 }
@@ -156,13 +179,16 @@ public IDisposable Wait(CancellationToken cancellationToken = default)
156179 /// <inheritdoc />
157180 public void Dispose ( )
158181 {
159- if ( this . disposed )
182+ lock ( this . adjustLock )
160183 {
161- return ;
162- }
184+ if ( this . disposed )
185+ {
186+ return ;
187+ }
163188
164- this . semaphore . Dispose ( ) ;
165- this . disposed = true ;
189+ this . semaphore . Dispose ( ) ;
190+ this . disposed = true ;
191+ }
166192 }
167193
168194 private sealed class SemaphoreReleaser : IDisposable
Original file line number Diff line number Diff line change @@ -151,6 +151,28 @@ public async Task ShouldReduceAvailablePermits()
151151 semaphore . Limit . ShouldBe ( 1 ) ;
152152 semaphore . Available . ShouldBe ( 1 ) ;
153153 }
154+
155+ [ Test ]
156+ public async Task ShouldRollBackLimitWhenCancelled ( )
157+ {
158+ // Arrange
159+ using var semaphore = new AdaptiveSemaphore ( initial : 1 , minimum : 1 , maximum : 2 ) ;
160+ semaphore . TryGrow ( ) ; // limit = 2, available = 2
161+
162+ // Exhaust both permits so the next WaitAsync will block.
163+ using var hold1 = await semaphore . WaitAsync ( ) ;
164+ using var hold2 = await semaphore . WaitAsync ( ) ;
165+
166+ using var cts = new CancellationTokenSource ( ) ;
167+ cts . Cancel ( ) ;
168+
169+ // Act - shrink should decrement limit then fail to acquire, rolling back.
170+ await Should . ThrowAsync < OperationCanceledException > (
171+ async ( ) => await semaphore . TryShrinkAsync ( cts . Token ) ) ;
172+
173+ // Assert - limit should be restored to 2.
174+ semaphore . Limit . ShouldBe ( 2 ) ;
175+ }
154176 }
155177
156178 public class WhenGrowing
You can’t perform that action at this time.
0 commit comments