Skip to content

Commit 9a6bafa

Browse files
authored
Merge pull request #183 from mgoodfellow/add-failsafe-to-redislock
Add failsafe to RedisLockExtension to resolve #181
2 parents 4e2388c + a4f6f01 commit 9a6bafa

2 files changed

Lines changed: 50 additions & 1 deletion

File tree

src/CacheTower.Extensions.Redis/RedisLockExtension.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Concurrent;
33
using System.Collections.Generic;
44
using System.Linq;
5+
using System.Threading;
56
using System.Threading.Tasks;
67
using StackExchange.Redis;
78

@@ -87,7 +88,13 @@ public async ValueTask<CacheEntry<T>> WithRefreshAsync<T>(string cacheKey, Func<
8788
}
8889
else
8990
{
90-
var completionSource = LockedOnKeyRefresh.GetOrAdd(cacheKey, key => new TaskCompletionSource<bool>());
91+
var completionSource = LockedOnKeyRefresh.GetOrAdd(cacheKey, key =>
92+
{
93+
var tcs = new TaskCompletionSource<bool>();
94+
var cts = new CancellationTokenSource(Options.LockTimeout);
95+
cts.Token.Register(tcs => ((TaskCompletionSource<bool>)tcs).TrySetCanceled(), tcs, useSynchronizationContext: false);
96+
return tcs;
97+
});
9198

9299
//Last minute check to confirm whether waiting is required (in case the notification is missed)
93100
var currentEntry = await RegisteredStack!.GetAsync<T>(cacheKey);

tests/CacheTower.Tests/Extensions/Redis/RedisLockExtensionTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,5 +207,47 @@ public async Task ObservedLockMultiple()
207207

208208
cacheStackMock.Verify(c => c.GetAsync<int>("TestKey"), Times.Exactly(4), "Two checks to the cache stack are expected");
209209
}
210+
211+
[TestMethod]
212+
public async Task FailsafeOnSubscriberFailure()
213+
{
214+
RedisHelper.ResetState();
215+
216+
var connection = RedisHelper.GetConnection();
217+
218+
var cacheStackMock = new Mock<ICacheStack>();
219+
var extension = new RedisLockExtension(connection, new RedisLockOptions(lockTimeout: TimeSpan.FromSeconds(1)));
220+
extension.Register(cacheStackMock.Object);
221+
222+
var cacheEntry = new CacheEntry<int>(13, TimeSpan.FromDays(1));
223+
224+
//Establish lock
225+
await connection.GetDatabase().StringSetAsync("Lock:TestKey", RedisValue.EmptyString);
226+
227+
var refreshTask = extension.WithRefreshAsync("TestKey",
228+
() =>
229+
{
230+
return new ValueTask<CacheEntry<int>>(cacheEntry);
231+
},
232+
new CacheSettings(TimeSpan.FromDays(1))
233+
).AsTask();
234+
235+
//Delay to allow for Redis check and self-entry into lock
236+
await Task.Delay(TimeSpan.FromSeconds(1));
237+
238+
Assert.IsTrue(extension.LockedOnKeyRefresh.ContainsKey("TestKey"), "Lock was not established");
239+
240+
//We don't publish to end lock
241+
242+
//However, we still expect to succeed
243+
var succeedingTask = await Task.WhenAny(refreshTask, Task.Delay(TimeSpan.FromSeconds(10)));
244+
if (!succeedingTask.Equals(refreshTask))
245+
{
246+
RedisHelper.DebugInfo(connection);
247+
Assert.Fail("Refresh has timed out - something has gone very wrong");
248+
}
249+
250+
cacheStackMock.Verify(c => c.GetAsync<int>("TestKey"), Times.Exactly(1), "One checks to the cache stack are expected as it will fail to resolve lock");
251+
}
210252
}
211253
}

0 commit comments

Comments
 (0)