Skip to content

Commit 311f959

Browse files
Improve remove behavior (#17)
1 parent f5f2da0 commit 311f959

File tree

3 files changed

+75
-55
lines changed

3 files changed

+75
-55
lines changed

src/Immediate.Cache.Shared/ApplicationCacheBase.cs

Lines changed: 44 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.Diagnostics;
21
using System.Diagnostics.CodeAnalysis;
32
using Immediate.Handlers.Shared;
43
using Microsoft.Extensions.Caching.Memory;
@@ -128,20 +127,8 @@ Owned<IHandler<TRequest, TResponse>> handler
128127
private TaskCompletionSource<TResponse>? _responseSource;
129128
private readonly Lock _lock = new();
130129

131-
public async ValueTask<TResponse> GetValue(CancellationToken cancellationToken)
132-
{
133-
try
134-
{
135-
return await GetHandlerTask().WaitAsync(cancellationToken).ConfigureAwait(false);
136-
}
137-
catch (OperationCanceledException) when (
138-
!cancellationToken.IsCancellationRequested
139-
&& _responseSource?.Task is { IsCompletedSuccessfully: true } task
140-
)
141-
{
142-
return await task.ConfigureAwait(false);
143-
}
144-
}
130+
public async ValueTask<TResponse> GetValue(CancellationToken cancellationToken) =>
131+
await GetHandlerTask().WaitAsync(cancellationToken).ConfigureAwait(false);
145132

146133
[SuppressMessage("Maintainability", "CA1508:Avoid dead conditional code", Justification = "Double-checked lock pattern")]
147134
private Task<TResponse> GetHandlerTask()
@@ -154,43 +141,57 @@ private Task<TResponse> GetHandlerTask()
154141
if (_responseSource is not null)
155142
return _responseSource.Task;
156143

157-
_tokenSource = new();
158-
_responseSource = new TaskCompletionSource<TResponse>();
144+
var ts = _tokenSource = new();
145+
_responseSource = new();
159146

160-
return Task.Run(() => RunHandler(_tokenSource, _responseSource));
147+
return Task.Run(() => RunHandler(ts));
161148
}
162149
}
163150

164-
private async Task<TResponse> RunHandler(
165-
CancellationTokenSource tokenSource,
166-
TaskCompletionSource<TResponse> responseSource
167-
)
151+
private async Task<TResponse> RunHandler(CancellationTokenSource tokenSource)
168152
{
169-
try
153+
while (true)
170154
{
171-
var scope = handler.GetScope();
155+
if (_responseSource?.Task is { IsCompletedSuccessfully: true } task)
156+
return await task.ConfigureAwait(false);
172157

173-
await using (scope.ConfigureAwait(false))
158+
try
174159
{
175-
var response = await scope.Service
176-
.HandleAsync(
177-
request,
178-
tokenSource.Token
179-
)
180-
.ConfigureAwait(false);
181-
182-
lock (_lock)
183-
{
184-
Debug.Assert(_responseSource == responseSource);
185-
responseSource.SetResult(response);
160+
var token = tokenSource.Token;
161+
var scope = handler.GetScope();
186162

187-
return response;
163+
await using (scope.ConfigureAwait(false))
164+
{
165+
var response = await scope.Service
166+
.HandleAsync(
167+
request,
168+
token
169+
)
170+
.ConfigureAwait(false);
171+
172+
lock (_lock)
173+
{
174+
if (!token.IsCancellationRequested)
175+
{
176+
var rs = _responseSource ??= new();
177+
rs.SetResult(response);
178+
179+
return response;
180+
}
181+
}
188182
}
189183
}
190-
}
191-
catch (OperationCanceledException) when (_responseSource is not null)
192-
{
193-
return await _responseSource.Task.ConfigureAwait(false);
184+
catch (OperationCanceledException) when (tokenSource.IsCancellationRequested)
185+
{
186+
}
187+
188+
lock (_lock)
189+
{
190+
if (_tokenSource is null or { IsCancellationRequested: true })
191+
_tokenSource = new();
192+
193+
tokenSource = _tokenSource;
194+
}
194195
}
195196
}
196197

@@ -201,6 +202,7 @@ public void SetValue(TResponse response)
201202
_responseSource = new TaskCompletionSource<TResponse>();
202203
_responseSource.SetResult(response);
203204
_tokenSource?.Cancel();
205+
_tokenSource = null;
204206
}
205207
}
206208

@@ -210,6 +212,7 @@ public void RemoveValue()
210212
{
211213
_responseSource = null;
212214
_tokenSource?.Cancel();
215+
_tokenSource = null;
213216
}
214217
}
215218
}

tests/Immediate.Cache.FunctionalTests/ApplicationCacheTests.cs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -260,13 +260,18 @@ public async Task RemovingValueCancelsExistingOperation()
260260

261261
cache.RemoveValue(request);
262262

263-
_ = await Assert.ThrowsAnyAsync<OperationCanceledException>(
264-
async () => await responseTask
265-
);
263+
// allow IC task to be run
264+
await Task.Delay(10);
266265

267-
Assert.True(responseTask.IsCanceled);
268-
Assert.Equal(0, request.TimesExecuted);
269-
Assert.True(request.CancellationToken.IsCancellationRequested);
266+
request.CompletionSource.SetResult();
267+
268+
var response = await responseTask;
269+
270+
Assert.Equal(1, request.TimesExecuted);
271+
Assert.Equal(1, request.TimesCancelled);
272+
273+
Assert.Equal(1, response.Value);
274+
Assert.True(response.ExecutedHandler);
270275
}
271276

272277
[Test]
@@ -279,9 +284,11 @@ public async Task SettingValueCancelsExistingOperation()
279284
CompletionSource = new(),
280285
};
281286

282-
using var tcs = new CancellationTokenSource();
283287
var cache = _serviceProvider.GetRequiredService<DelayGetValueCache>();
284-
var responseTask = cache.GetValue(request, tcs.Token);
288+
var responseTask = cache.GetValue(request, default);
289+
290+
// allow IC task to be run
291+
await Task.Delay(10);
285292

286293
cache.SetValue(request, new(5, ExecutedHandler: false, Guid.NewGuid()));
287294

@@ -290,6 +297,6 @@ public async Task SettingValueCancelsExistingOperation()
290297
Assert.False(response.ExecutedHandler);
291298

292299
Assert.Equal(0, request.TimesExecuted);
293-
Assert.True(request.CancellationToken.IsCancellationRequested);
300+
Assert.Equal(1, request.TimesCancelled);
294301
}
295302
}

tests/Immediate.Cache.FunctionalTests/DelayGetValue.cs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public sealed class Query
1111
public required string Name { get; init; }
1212
public required TaskCompletionSource CompletionSource { get; init; }
1313
public int TimesExecuted { get; set; }
14+
public int TimesCancelled { get; set; }
1415
public CancellationToken CancellationToken { get; set; }
1516
}
1617

@@ -23,12 +24,21 @@ private static async ValueTask<Response> HandleAsync(
2324
CancellationToken token
2425
)
2526
{
26-
query.CancellationToken = token;
27-
await query.CompletionSource.Task.WaitAsync(token);
27+
try
28+
{
29+
query.CancellationToken = token;
30+
await query.CompletionSource.Task.WaitAsync(token);
2831

29-
lock (s_lock)
30-
query.TimesExecuted++;
32+
lock (s_lock)
33+
query.TimesExecuted++;
3134

32-
return new(query.Value, ExecutedHandler: true, RandomValue: Guid.NewGuid());
35+
return new(query.Value, ExecutedHandler: true, RandomValue: Guid.NewGuid());
36+
}
37+
catch
38+
{
39+
lock (s_lock)
40+
query.TimesCancelled++;
41+
throw;
42+
}
3343
}
3444
}

0 commit comments

Comments
 (0)