diff --git a/src/ActionCache.csproj b/src/ActionCache.csproj index 93ee78e..639c1b0 100644 --- a/src/ActionCache.csproj +++ b/src/ActionCache.csproj @@ -52,6 +52,7 @@ + diff --git a/src/Common/Concurrency/DistributedCacheLocker.cs b/src/Common/Concurrency/DistributedCacheLocker.cs deleted file mode 100644 index 80ad783..0000000 --- a/src/Common/Concurrency/DistributedCacheLocker.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Microsoft.Extensions.Caching.Distributed; - -namespace ActionCache.Common.Concurrency; - -/// -/// Provides a mechanism for acquiring and releasing locks in a distributed cache system. -/// -public class DistributedCacheLocker : CacheLockerBase -{ - /// - /// The distributed cache instance used to store lock information. - /// - protected readonly IDistributedCache Cache; - - /// - /// Initializes a new instance of the class. - /// - /// The distributed cache instance to be used for managing locks. - /// The duration of a acquired lock. - /// The timeout period before assuming a lock acquisition failure. - public DistributedCacheLocker(IDistributedCache cache, TimeSpan lockDuration, TimeSpan lockTimeout) - : base(lockDuration, lockTimeout) - { - Cache = cache; - } - - /// - public override async Task TryAcquireLockAsync(string resource) - { - var cacheLock = new DistributedCacheLock(resource, LockDuration, LockTimeout); - var existingCacheLock = await Cache.GetStringAsync(cacheLock.Key); - if (existingCacheLock is null) - { - await Cache.SetStringAsync(cacheLock.Key, cacheLock.Value, new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = cacheLock.Duration - }); - - // Perform one more get to ensure this lock value is the one - // in the cache and we didn't hit a race condition. Even so, this does - // not guarantee atomicity but it's better than nothing. - var existingCacheLockValue = await Cache.GetStringAsync(cacheLock.Key); - - cacheLock.IsAcquired = existingCacheLockValue == cacheLock.Value; - } - - return cacheLock; - } - - /// - public override async Task ReleaseLockAsync(DistributedCacheLock cacheLock) - { - var currentValue = await Cache.GetStringAsync(cacheLock.Key); - if (currentValue == cacheLock.Value) - { - await Cache.RemoveAsync(cacheLock.Key); - } - } - - /// - public override async Task WaitForLockAsync(string resource) - { - var cacheLock = new DistributedCacheLock(resource, LockDuration, LockTimeout); - while (cacheLock.ShouldTryAcquire()) - { - var acquiredLock = await TryAcquireLockAsync(cacheLock.Resource); - if (acquiredLock.IsAcquired) - { - cacheLock = acquiredLock; - break; - } - - await Task.Delay(100); - } - - return cacheLock; - } -} \ No newline at end of file diff --git a/src/Common/Concurrency/Locks/DistributedCacheLock.cs b/src/Common/Concurrency/Locks/DistributedCacheLock.cs deleted file mode 100644 index 3351eeb..0000000 --- a/src/Common/Concurrency/Locks/DistributedCacheLock.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace ActionCache.Common.Concurrency; - -/// -/// Represents a distributed lock mechanism that can be used with a distributed cache. -/// -public class DistributedCacheLock : CacheLock -{ - /// - /// Initializes a new instance of the class with a resource, duration, and timeout. - /// - /// The unique resource identifier to lock. - /// The duration for which the lock is held. - /// The timeout duration for attempting to acquire the lock. - public DistributedCacheLock(string resource, TimeSpan duration, TimeSpan timeout) : base(resource) - { - Duration = duration; - Timeout = timeout; - } - - /// - /// Gets the cache key used for storing the lock in the distributed cache. - /// - public string Key => $"Lock:{Resource}"; - - /// - /// Gets or sets the unique value associated with the lock, typically a GUID. - /// - public string Value { get; set; } = Guid.NewGuid().ToString(); - - /// - /// Determines whether the lock acquisition should be attempted. - /// - /// - /// true if the lock is not already acquired and the timeout has not been exceeded; otherwise, false. - /// - public bool ShouldTryAcquire() => !(IsAcquired || HasExceededTimeout()); - - /// - /// Checks whether the timeout period for acquiring the lock has been exceeded. - /// - /// - /// true if the current time is greater than or equal to the requested time plus the timeout duration; otherwise, false. - /// - public bool HasExceededTimeout() => DateTime.UtcNow >= DateRequested.Add(Timeout); -} \ No newline at end of file diff --git a/src/Common/Concurrency/Locks/NullCacheLock.cs b/src/Common/Concurrency/Locks/NullCacheLock.cs index 8ccdf28..f35aedc 100644 --- a/src/Common/Concurrency/Locks/NullCacheLock.cs +++ b/src/Common/Concurrency/Locks/NullCacheLock.cs @@ -12,5 +12,6 @@ public class NullCacheLock : CacheLock /// The resource name associated with this lock. public NullCacheLock(string resource) : base(resource) { + IsAcquired = true; } } \ No newline at end of file diff --git a/src/Common/Concurrency/Locks/SemaphoreSlimLock.cs b/src/Common/Concurrency/Locks/SemaphoreSlimLock.cs deleted file mode 100644 index f4a87e1..0000000 --- a/src/Common/Concurrency/Locks/SemaphoreSlimLock.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace ActionCache.Common.Concurrency; - -/// -/// Represents the lock state for a specific resource using SemaphoreSlim. -/// -public class SemaphoreSlimLock : CacheLock -{ - /// - /// Initializes a new instance of the class with the specified resource, duration, and timeout. - /// - /// The unique identifier of the resource to lock. - /// The duration for which the lock should be held. - /// The maximum time allowed for attempting to acquire the lock. - public SemaphoreSlimLock(string resource, TimeSpan duration, TimeSpan timeout) : base(resource) - { - Duration = duration; - Timeout = timeout; - IsAcquired = false; - } -} \ No newline at end of file diff --git a/src/Common/Concurrency/SemaphoreSlimLocker.cs b/src/Common/Concurrency/SemaphoreSlimLocker.cs deleted file mode 100644 index 2209617..0000000 --- a/src/Common/Concurrency/SemaphoreSlimLocker.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Collections.Concurrent; - -namespace ActionCache.Common.Concurrency; - -/// -/// Provides a locking mechanism using for resource synchronization in a distributed cache system. -/// -public class SemaphoreSlimLocker : CacheLockerBase -{ - /// - /// A thread-safe dictionary to manage semaphores for each resource. - /// - protected readonly ConcurrentDictionary Semaphores = new(); - - /// - /// Initializes a new instance of the class with the specified lock duration and timeout. - /// - /// The duration for which the lock should be held before it expires. - /// The maximum time to wait for acquiring the lock. - public SemaphoreSlimLocker(TimeSpan lockDuration, TimeSpan lockTimeout) - : base(lockDuration, lockTimeout) - { - } - - /// - public override async Task TryAcquireLockAsync(string resource) - { - var cacheLock = new SemaphoreSlimLock(resource, LockDuration, LockTimeout); - var semaphore = Semaphores.GetOrAdd(resource, _ => new SemaphoreSlim(1, 1)); - - cacheLock.IsAcquired = await semaphore.WaitAsync(cacheLock.Timeout); - return cacheLock; - } - - /// - public override Task ReleaseLockAsync(SemaphoreSlimLock cacheLock) - { - if (cacheLock.IsAcquired) - { - if (Semaphores.TryGetValue(cacheLock.Resource, out var semaphore)) - { - semaphore.Release(); - } - } - - return Task.CompletedTask; - } - - /// - public override Task WaitForLockAsync(string resource) => TryAcquireLockAsync(resource); -} \ No newline at end of file diff --git a/src/Memory/Extensions/Internal/IMemoryCacheExtensions.cs b/src/Memory/Extensions/Internal/IMemoryCacheExtensions.cs index c4a20f6..c5a8e2c 100644 --- a/src/Memory/Extensions/Internal/IMemoryCacheExtensions.cs +++ b/src/Memory/Extensions/Internal/IMemoryCacheExtensions.cs @@ -59,7 +59,7 @@ internal static void SetKey(this IMemoryCache cache, Namespace @namespace, strin var keys = cache.GetKeys(@namespace, entryOptions); if (keys.TryAdd(key, entryOptions.AbsoluteExpiration)) { - cache.Set(key, keys, entryOptions); + cache.Set(@namespace, keys, entryOptions); } } diff --git a/src/Memory/MemoryActionCache.cs b/src/Memory/MemoryActionCache.cs index e4bb75d..d6649fb 100644 --- a/src/Memory/MemoryActionCache.cs +++ b/src/Memory/MemoryActionCache.cs @@ -1,5 +1,5 @@ using ActionCache.Common.Caching; -using ActionCache.Common.Concurrency; +using ActionCache.Common.Concurrency.Locks; using ActionCache.Memory.Extensions.Internal; using ActionCache.Utilities; using Microsoft.Extensions.Caching.Memory; @@ -10,7 +10,7 @@ namespace ActionCache.Memory; /// /// Represents a memory action cache implementation. /// -public class MemoryActionCache : ActionCacheBase +public class MemoryActionCache : ActionCacheBase { /// /// A memory cache implementation. @@ -31,7 +31,7 @@ public class MemoryActionCache : ActionCacheBase public MemoryActionCache( IMemoryCache cache, CancellationTokenSource cancellationTokenSource, - ActionCacheContext context + ActionCacheContext context ) : base(context) { Cache = cache; diff --git a/src/Memory/MemoryActionCacheFactory.cs b/src/Memory/MemoryActionCacheFactory.cs index 0a9478f..cff0c30 100644 --- a/src/Memory/MemoryActionCacheFactory.cs +++ b/src/Memory/MemoryActionCacheFactory.cs @@ -1,6 +1,7 @@ using ActionCache.Common; using ActionCache.Common.Caching; using ActionCache.Common.Concurrency; +using ActionCache.Common.Concurrency.Locks; using ActionCache.Utilities; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; @@ -45,15 +46,12 @@ IActionCacheRefreshProvider refreshProvider { if (ExpirationTokens.TryGetOrAdd(@namespace, out var expirationTokenSource)) { - var context = new ActionCacheContext + var context = new ActionCacheContext { Namespace = @namespace, EntryOptions = EntryOptions, RefreshProvider = RefreshProvider, - CacheLocker = new SemaphoreSlimLocker( - EntryOptions.LockDuration, - EntryOptions.LockTimeout - ) + CacheLocker = new NullCacheLocker() }; return new MemoryActionCache(Cache, expirationTokenSource, context); @@ -69,7 +67,7 @@ IActionCacheRefreshProvider refreshProvider { if (ExpirationTokens.TryGetOrAdd(@namespace, out var expirationTokenSource)) { - var context = new ActionCacheContext + var context = new ActionCacheContext { Namespace = @namespace, EntryOptions = new ActionCacheEntryOptions @@ -78,10 +76,7 @@ IActionCacheRefreshProvider refreshProvider SlidingExpiration = slidingExpiration }, RefreshProvider = RefreshProvider, - CacheLocker = new SemaphoreSlimLocker( - EntryOptions.LockDuration, - EntryOptions.LockTimeout - ) + CacheLocker = new NullCacheLocker() }; return new MemoryActionCache(Cache, expirationTokenSource, context); diff --git a/src/Redis/Concurrency/Locks/RedisCacheLock.cs b/src/Redis/Concurrency/Locks/RedisCacheLock.cs new file mode 100644 index 0000000..db2a8e0 --- /dev/null +++ b/src/Redis/Concurrency/Locks/RedisCacheLock.cs @@ -0,0 +1,31 @@ +using ActionCache.Common.Concurrency; + +namespace ActionCache.Redis.Concurrency.Locks; + +/// +/// Represents a Redis-backed distributed lock acquired via SET NX PX. +/// Holds the lock key and a unique fencing token used by the release script +/// to ensure only the owner can release the lock. +/// +public class RedisCacheLock : CacheLock +{ + /// + /// Initializes a new instance of the class. + /// + /// The logical resource being locked (used to build ). + /// How long the lock is held in Redis (TTL on the key). + /// Maximum time the locker will poll before giving up. + public RedisCacheLock(string resource, TimeSpan lockDuration, TimeSpan lockTimeout) : base(resource) + { + Duration = lockDuration; + Timeout = lockTimeout; + Key = $"Lock:{resource}"; + Token = Guid.NewGuid().ToString("N"); + } + + /// Redis key under which the lock is stored. + public string Key { get; } + + /// Unique fencing token stored as the Redis value; used by the release script. + public string Token { get; } +} diff --git a/src/Redis/Concurrency/RedisCacheLocker.cs b/src/Redis/Concurrency/RedisCacheLocker.cs new file mode 100644 index 0000000..9857d9c --- /dev/null +++ b/src/Redis/Concurrency/RedisCacheLocker.cs @@ -0,0 +1,81 @@ +using ActionCache.Common.Concurrency; +using ActionCache.Redis.Concurrency.Locks; +using StackExchange.Redis; + +namespace ActionCache.Redis.Concurrency; + +/// +/// A distributed cache locker backed by Redis using the SET NX PX (SETNX) pattern. +/// Acquisition and release are each a single atomic Lua script, so no race conditions +/// can occur between check-and-set steps. +/// +public class RedisCacheLocker : CacheLockerBase +{ + private readonly IDatabase _database; + + // SET key token NX PX ttlMs — returns "OK" on success, null on failure. + private const string AcquireScript = + "return redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2])"; + + // DEL key only when the stored value matches our token (fencing). + private const string ReleaseScript = + "if redis.call('GET', KEYS[1]) == ARGV[1] then " + + " return redis.call('DEL', KEYS[1]) " + + "else " + + " return 0 " + + "end"; + + /// + /// Initializes a new instance of the class. + /// + /// The Redis database used for lock operations. + /// TTL applied to the lock key in Redis. + /// Maximum time will poll. + public RedisCacheLocker(IDatabase database, TimeSpan lockDuration, TimeSpan lockTimeout) + : base(lockDuration, lockTimeout) + { + _database = database; + } + + /// + public override async Task TryAcquireLockAsync(string resource) + { + var cacheLock = new RedisCacheLock(resource, LockDuration, LockTimeout); + + var result = await _database.ScriptEvaluateAsync( + AcquireScript, + [(RedisKey)cacheLock.Key], + [cacheLock.Token, (long)LockDuration.TotalMilliseconds]); + + cacheLock.IsAcquired = result.ToString() == "OK"; + return cacheLock; + } + + /// + public override async Task ReleaseLockAsync(RedisCacheLock cacheLock) + { + if (!cacheLock.IsAcquired) + return; + + await _database.ScriptEvaluateAsync( + ReleaseScript, + [(RedisKey)cacheLock.Key], + [cacheLock.Token]); + } + + /// + public override async Task WaitForLockAsync(string resource) + { + var deadline = DateTime.UtcNow.Add(LockTimeout); + while (DateTime.UtcNow < deadline) + { + var cacheLock = await TryAcquireLockAsync(resource); + if (cacheLock.IsAcquired) + return cacheLock; + + await Task.Delay(100); + } + + return new RedisCacheLock(resource, LockDuration, LockTimeout); + } +} diff --git a/src/Redis/RedisActionCacheFactory.cs b/src/Redis/RedisActionCacheFactory.cs index e0d20ff..bf57c01 100644 --- a/src/Redis/RedisActionCacheFactory.cs +++ b/src/Redis/RedisActionCacheFactory.cs @@ -15,15 +15,15 @@ public class RedisActionCacheFactory : ActionCacheFactoryBase { /// /// An IDatabase representation of a Redis cache. - /// + /// protected readonly IDatabase Cache; - + /// /// Constructor for RedisActionCacheFactory. /// /// ConnectionMultiplexer for Redis. - /// The global entry options used for creation when expiration times are not supplied. - /// The refresh provider to handle cache refreshes. + /// The global entry options used for creation when expiration times are not supplied. + /// The refresh provider to handle cache refreshes. public RedisActionCacheFactory( IConnectionMultiplexer connectionMultiplexer, IOptions entryOptions, @@ -32,7 +32,7 @@ IActionCacheRefreshProvider refreshProvider { Cache = connectionMultiplexer.GetDatabase(); } - + /// public override IActionCache? Create(Namespace @namespace) { @@ -64,4 +64,4 @@ IActionCacheRefreshProvider refreshProvider return new RedisActionCache(Cache, context); } -} \ No newline at end of file +} diff --git a/src/SqlServer/Concurrency/Locks/SqlServerCacheLock.cs b/src/SqlServer/Concurrency/Locks/SqlServerCacheLock.cs new file mode 100644 index 0000000..19df3d5 --- /dev/null +++ b/src/SqlServer/Concurrency/Locks/SqlServerCacheLock.cs @@ -0,0 +1,30 @@ +using ActionCache.Common.Concurrency; +using Microsoft.Data.SqlClient; + +namespace ActionCache.SqlServer.Concurrency.Locks; + +/// +/// Represents a lock backed by a SQL Server session-level application lock (sp_getapplock). +/// The open is held until +/// calls sp_releaseapplock and disposes it. +/// +public class SqlServerCacheLock : CacheLock +{ + /// + /// Initializes a new instance of the class. + /// + /// The resource identifier passed to sp_getapplock. + /// Duration hint stored on the lock. + /// Maximum time to wait for acquisition. + public SqlServerCacheLock(string resource, TimeSpan lockDuration, TimeSpan lockTimeout) : base(resource) + { + Duration = lockDuration; + Timeout = lockTimeout; + } + + /// + /// The open SQL connection that holds the session-level application lock. + /// Kept alive until disposes it. + /// + internal SqlConnection? Connection { get; set; } +} diff --git a/src/SqlServer/Concurrency/SqlServerCacheLocker.cs b/src/SqlServer/Concurrency/SqlServerCacheLocker.cs new file mode 100644 index 0000000..3836eda --- /dev/null +++ b/src/SqlServer/Concurrency/SqlServerCacheLocker.cs @@ -0,0 +1,109 @@ +using ActionCache.Common.Concurrency; +using ActionCache.SqlServer.Concurrency.Locks; +using Microsoft.Data.SqlClient; +using System.Data; + +namespace ActionCache.SqlServer.Concurrency; + +/// +/// A cache locker that uses SQL Server session-level application locks (sp_getapplock / +/// sp_releaseapplock) for true atomic, cross-process mutual exclusion. +/// +public class SqlServerCacheLocker : CacheLockerBase +{ + private readonly string _connectionString; + + /// + /// Initializes a new instance of the class. + /// + /// Connection string used to open a dedicated lock connection. + /// Duration hint stored on acquired locks. + /// Maximum time sp_getapplock will wait before returning -1. + public SqlServerCacheLocker(string connectionString, TimeSpan lockDuration, TimeSpan lockTimeout) + : base(lockDuration, lockTimeout) + { + _connectionString = connectionString; + } + + /// + public override Task TryAcquireLockAsync(string resource) => + AcquireLockAsync(resource, timeoutMs: 0); + + /// + public override Task WaitForLockAsync(string resource) => + AcquireLockAsync(resource, timeoutMs: (int)LockTimeout.TotalMilliseconds); + + /// + public override async Task ReleaseLockAsync(SqlServerCacheLock cacheLock) + { + if (cacheLock.Connection is null) + return; + + try + { + using var cmd = new SqlCommand("sp_releaseapplock", cacheLock.Connection) + { + CommandType = CommandType.StoredProcedure + }; + cmd.Parameters.AddWithValue("@Resource", cacheLock.Resource); + cmd.Parameters.AddWithValue("@LockOwner", "Session"); + + await cmd.ExecuteNonQueryAsync(); + } + finally + { + await cacheLock.Connection.DisposeAsync(); + } + } + + private async Task AcquireLockAsync(string resource, int timeoutMs) + { + var cacheLock = new SqlServerCacheLock(resource, LockDuration, LockTimeout); + var connection = new SqlConnection(_connectionString); + + try + { + await connection.OpenAsync(); + + using var cmd = new SqlCommand("sp_getapplock", connection) + { + CommandType = CommandType.StoredProcedure + }; + + cmd.Parameters.AddWithValue("@Resource", resource); + cmd.Parameters.AddWithValue("@LockMode", "Exclusive"); + cmd.Parameters.AddWithValue("@LockOwner", "Session"); + cmd.Parameters.AddWithValue("@LockTimeout", timeoutMs); + + var returnParam = new SqlParameter + { + ParameterName = "@ReturnValue", + SqlDbType = SqlDbType.Int, + Direction = ParameterDirection.ReturnValue + }; + cmd.Parameters.Add(returnParam); + + await cmd.ExecuteNonQueryAsync(); + + // sp_getapplock return codes: 0 = granted immediately, 1 = granted after wait. + // Negative values indicate failure (-1 = timeout, -2 = cancelled, -3 = deadlock victim). + cacheLock.IsAcquired = (int)returnParam.Value is 0 or 1; + + if (cacheLock.IsAcquired) + { + cacheLock.Connection = connection; + } + else + { + await connection.DisposeAsync(); + } + } + catch + { + await connection.DisposeAsync(); + throw; + } + + return cacheLock; + } +} diff --git a/src/SqlServer/SqlServerActionCache.cs b/src/SqlServer/SqlServerActionCache.cs index d5410a3..87f1312 100644 --- a/src/SqlServer/SqlServerActionCache.cs +++ b/src/SqlServer/SqlServerActionCache.cs @@ -1,5 +1,5 @@ using ActionCache.Common.Caching; -using ActionCache.Common.Concurrency; +using ActionCache.SqlServer.Concurrency.Locks; using ActionCache.Common.Serialization; using ActionCache.Memory.Extensions.Internal; using ActionCache.Utilities; @@ -10,7 +10,7 @@ namespace ActionCache.SqlServer; /// /// A cache implementation for SQL Server-based action caching with distributed locking support. /// -public class SqlServerActionCache : ActionCacheBase +public class SqlServerActionCache : ActionCacheBase { /// /// The distributed cache used for storing and retrieving cache entries. @@ -22,7 +22,7 @@ public class SqlServerActionCache : ActionCacheBase /// /// The distributed cache to be used. /// The cache context. - public SqlServerActionCache(IDistributedCache cache, ActionCacheContext context) + public SqlServerActionCache(IDistributedCache cache, ActionCacheContext context) : base(context) => Cache = cache; /// diff --git a/src/SqlServer/SqlServerActionCacheFactory.cs b/src/SqlServer/SqlServerActionCacheFactory.cs index d5a05aa..8462c60 100644 --- a/src/SqlServer/SqlServerActionCacheFactory.cs +++ b/src/SqlServer/SqlServerActionCacheFactory.cs @@ -1,50 +1,58 @@ using ActionCache.Common; using ActionCache.Common.Caching; -using ActionCache.Common.Concurrency; +using ActionCache.SqlServer.Concurrency; +using ActionCache.SqlServer.Concurrency.Locks; using ActionCache.Utilities; using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.SqlServer; using Microsoft.Extensions.Options; namespace ActionCache.SqlServer; /// -/// Represents a factory for creating SqlServer action caches. +/// Factory for creating instances backed by +/// (sp_getapplock / sp_releaseapplock). /// public class SqlServerActionCacheFactory : ActionCacheFactoryBase { - /// - /// A SqlServer cache implementation. - /// + /// The distributed cache used for storing cache entries. protected readonly IDistributedCache Cache; + /// Connection string extracted from for the locker. + protected readonly string ConnectionString; + /// /// Initializes a new instance of the class. /// - /// The SqlServer cache to use. - /// The global entry options used for creation when expiration times are not supplied. - /// The refresh provider responsible for invoking cached controller actions. + /// The distributed cache to be used. + /// SQL Server cache options; the connection string is used by the locker. + /// Global entry options used when expiration is not specified per namespace. + /// Provider responsible for refreshing cached action results. public SqlServerActionCacheFactory( IDistributedCache cache, + IOptions sqlServerCacheOptions, IOptions entryOptions, IActionCacheRefreshProvider refreshProvider ) : base(entryOptions, refreshProvider) { Cache = cache; + ConnectionString = sqlServerCacheOptions.Value.ConnectionString + ?? throw new InvalidOperationException( + "SqlServerCacheOptions.ConnectionString must be set to use SqlServerCacheLocker."); } /// public override IActionCache? Create(Namespace @namespace) { - var context = new ActionCacheContext + var context = new ActionCacheContext { Namespace = @namespace, EntryOptions = EntryOptions, RefreshProvider = RefreshProvider, - CacheLocker = new DistributedCacheLocker( - Cache, + CacheLocker = new SqlServerCacheLocker( + ConnectionString, EntryOptions.LockDuration, - EntryOptions.LockTimeout - ) + EntryOptions.LockTimeout) }; return new SqlServerActionCache(Cache, context); @@ -53,7 +61,7 @@ IActionCacheRefreshProvider refreshProvider /// public override IActionCache? Create(Namespace @namespace, TimeSpan? absoluteExpiration = null, TimeSpan? slidingExpiration = null) { - var context = new ActionCacheContext + var context = new ActionCacheContext { Namespace = @namespace, EntryOptions = new ActionCacheEntryOptions @@ -62,13 +70,12 @@ IActionCacheRefreshProvider refreshProvider SlidingExpiration = slidingExpiration }, RefreshProvider = RefreshProvider, - CacheLocker = new DistributedCacheLocker( - Cache, - EntryOptions.LockDuration, - EntryOptions.LockTimeout - ) + CacheLocker = new SqlServerCacheLocker( + ConnectionString, + EntryOptions.LockDuration, + EntryOptions.LockTimeout) }; - + return new SqlServerActionCache(Cache, context); } -} \ No newline at end of file +} diff --git a/test/Integration/Integration.csproj b/test/Integration/Integration.csproj index b222cf5..0a582ad 100644 --- a/test/Integration/Integration.csproj +++ b/test/Integration/Integration.csproj @@ -7,7 +7,7 @@ false true - $(MSBuildThisFileDirectory)integration.runsettings + $(MSBuildThisFileDirectory)Integration.runsettings diff --git a/test/Integration/integration.runsettings b/test/Integration/Integration.runsettings similarity index 100% rename from test/Integration/integration.runsettings rename to test/Integration/Integration.runsettings diff --git a/test/Unit/Common/Caching/ActionCacheBaseRefreshTests.cs b/test/Unit/Common/Caching/ActionCacheBaseRefreshTests.cs index 6097ebf..d1c0336 100644 --- a/test/Unit/Common/Caching/ActionCacheBaseRefreshTests.cs +++ b/test/Unit/Common/Caching/ActionCacheBaseRefreshTests.cs @@ -1,8 +1,8 @@ using ActionCache.Common; using ActionCache.Common.Caching; using ActionCache.Common.Concurrency; -using ActionCache.Common.Concurrency.Locks; using ActionCache.SqlServer; +using ActionCache.SqlServer.Concurrency.Locks; using ActionCache.Utilities; using Microsoft.Extensions.Caching.Distributed; using Moq; @@ -16,7 +16,7 @@ namespace Unit.Common.Caching; public class ActionCacheBaseRefreshTests { private Mock _cacheMock; - private Mock> _lockerMock; + private Mock> _lockerMock; private Mock _refreshProviderMock; private SqlServerActionCache _sut; @@ -24,7 +24,7 @@ public class ActionCacheBaseRefreshTests public void SetUp() { _cacheMock = new Mock(); - _lockerMock = new Mock>(); + _lockerMock = new Mock>(); _refreshProviderMock = new Mock(); _lockerMock @@ -38,7 +38,7 @@ public void SetUp() .Returns>>>( async (_, func) => await func()); - var context = new ActionCacheContext + var context = new ActionCacheContext { Namespace = new Namespace("TestNs"), EntryOptions = new ActionCacheEntryOptions(), diff --git a/test/Unit/Common/Concurrency/CacheLockTests.cs b/test/Unit/Common/Concurrency/CacheLockTests.cs index c5b053c..d076a00 100644 --- a/test/Unit/Common/Concurrency/CacheLockTests.cs +++ b/test/Unit/Common/Concurrency/CacheLockTests.cs @@ -1,5 +1,7 @@ using ActionCache.Common.Concurrency; using ActionCache.Common.Concurrency.Locks; +using ActionCache.Redis.Concurrency.Locks; +using ActionCache.SqlServer.Concurrency.Locks; namespace Unit.Common; @@ -7,92 +9,94 @@ namespace Unit.Common; public class CacheLockTests { [Test] - public void CacheLock_DefaultIsAcquired_IsFalse() + public void NullCacheLock_Resource_IsSet() { - var cacheLock = new SemaphoreSlimLock("res", TimeSpan.FromMilliseconds(200), TimeSpan.FromMilliseconds(200)); - cacheLock.IsAcquired.Should().BeFalse(); + var cacheLock = new NullCacheLock("resource"); + cacheLock.Resource.Should().Be("resource"); } [Test] - public void CacheLock_Resource_IsSet() + public void NullCacheLock_IsAcquired_IsTrue() { - var cacheLock = new SemaphoreSlimLock("myResource", TimeSpan.Zero, TimeSpan.Zero); - cacheLock.Resource.Should().Be("myResource"); + var cacheLock = new NullCacheLock("r"); + cacheLock.IsAcquired.Should().BeTrue(); } [Test] - public void CacheLock_DateRequested_IsApproximatelyNow() + public void RedisCacheLock_Resource_IsSet() { - var before = DateTime.UtcNow; - var cacheLock = new SemaphoreSlimLock("r", TimeSpan.Zero, TimeSpan.Zero); - var after = DateTime.UtcNow; - cacheLock.DateRequested.Should().BeOnOrAfter(before).And.BeOnOrBefore(after); + var cacheLock = new RedisCacheLock("myns", TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5)); + cacheLock.Resource.Should().Be("myns"); } [Test] - public void SemaphoreSlimLock_Duration_IsSet() + public void RedisCacheLock_Key_PrefixedWithLock() { - var cacheLock = new SemaphoreSlimLock("r", TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2)); - cacheLock.Duration.Should().Be(TimeSpan.FromSeconds(1)); - cacheLock.Timeout.Should().Be(TimeSpan.FromSeconds(2)); + var cacheLock = new RedisCacheLock("myns", TimeSpan.Zero, TimeSpan.Zero); + cacheLock.Key.Should().StartWith("Lock:"); + cacheLock.Key.Should().Contain("myns"); } [Test] - public void NullCacheLock_Resource_IsSet() + public void RedisCacheLock_Token_IsNonEmptyGuid() { - var cacheLock = new NullCacheLock("resource"); - cacheLock.Resource.Should().Be("resource"); + var cacheLock = new RedisCacheLock("r", TimeSpan.Zero, TimeSpan.Zero); + cacheLock.Token.Should().NotBeNullOrWhiteSpace(); + Guid.TryParse(cacheLock.Token, out _).Should().BeTrue(); } [Test] - public void NullCacheLock_IsAcquired_DefaultsFalse() + public void RedisCacheLock_TwoInstances_HaveDifferentTokens() { - var cacheLock = new NullCacheLock("r"); - cacheLock.IsAcquired.Should().BeFalse(); + var a = new RedisCacheLock("r", TimeSpan.Zero, TimeSpan.Zero); + var b = new RedisCacheLock("r", TimeSpan.Zero, TimeSpan.Zero); + a.Token.Should().NotBe(b.Token); } [Test] - public void DistributedCacheLock_Key_PrefixedWithLock() + public void RedisCacheLock_IsAcquired_DefaultsFalse() { - var cacheLock = new DistributedCacheLock("myns", TimeSpan.Zero, TimeSpan.Zero); - cacheLock.Key.Should().StartWith("Lock:"); - cacheLock.Key.Should().Contain("myns"); + var cacheLock = new RedisCacheLock("r", TimeSpan.Zero, TimeSpan.Zero); + cacheLock.IsAcquired.Should().BeFalse(); } [Test] - public void DistributedCacheLock_Value_IsGuid() + public void RedisCacheLock_DateRequested_IsApproximatelyNow() { - var cacheLock = new DistributedCacheLock("r", TimeSpan.Zero, TimeSpan.Zero); - cacheLock.Value.Should().NotBeNullOrWhiteSpace(); - Guid.TryParse(cacheLock.Value, out _).Should().BeTrue(); + var before = DateTime.UtcNow; + var cacheLock = new RedisCacheLock("r", TimeSpan.Zero, TimeSpan.Zero); + var after = DateTime.UtcNow; + cacheLock.DateRequested.Should().BeOnOrAfter(before).And.BeOnOrBefore(after); } [Test] - public void DistributedCacheLock_ShouldTryAcquire_WhenNotAcquiredAndNotTimedOut_ReturnsTrue() + public void SqlServerCacheLock_Resource_IsSet() { - var cacheLock = new DistributedCacheLock("r", TimeSpan.Zero, TimeSpan.FromSeconds(30)); - cacheLock.ShouldTryAcquire().Should().BeTrue(); + var cacheLock = new SqlServerCacheLock("myns", TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5)); + cacheLock.Resource.Should().Be("myns"); } [Test] - public void DistributedCacheLock_ShouldTryAcquire_WhenAlreadyAcquired_ReturnsFalse() + public void SqlServerCacheLock_Duration_IsSet() { - var cacheLock = new DistributedCacheLock("r", TimeSpan.Zero, TimeSpan.FromSeconds(30)); - cacheLock.IsAcquired = true; - cacheLock.ShouldTryAcquire().Should().BeFalse(); + var cacheLock = new SqlServerCacheLock("r", TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2)); + cacheLock.Duration.Should().Be(TimeSpan.FromSeconds(1)); + cacheLock.Timeout.Should().Be(TimeSpan.FromSeconds(2)); } [Test] - public void DistributedCacheLock_HasExceededTimeout_WhenTimeoutIsZero_ReturnsTrue() + public void SqlServerCacheLock_IsAcquired_DefaultsFalse() { - var cacheLock = new DistributedCacheLock("r", TimeSpan.Zero, TimeSpan.Zero); - cacheLock.HasExceededTimeout().Should().BeTrue(); + var cacheLock = new SqlServerCacheLock("r", TimeSpan.Zero, TimeSpan.Zero); + cacheLock.IsAcquired.Should().BeFalse(); } [Test] - public void DistributedCacheLock_HasExceededTimeout_WhenTimeoutIsFuture_ReturnsFalse() + public void CacheLock_DateRequested_IsApproximatelyNow() { - var cacheLock = new DistributedCacheLock("r", TimeSpan.Zero, TimeSpan.FromSeconds(30)); - cacheLock.HasExceededTimeout().Should().BeFalse(); + var before = DateTime.UtcNow; + var cacheLock = new NullCacheLock("r"); + var after = DateTime.UtcNow; + cacheLock.DateRequested.Should().BeOnOrAfter(before).And.BeOnOrBefore(after); } } diff --git a/test/Unit/Common/Concurrency/DistributedCacheLockerTests.cs b/test/Unit/Common/Concurrency/DistributedCacheLockerTests.cs deleted file mode 100644 index 3f35230..0000000 --- a/test/Unit/Common/Concurrency/DistributedCacheLockerTests.cs +++ /dev/null @@ -1,137 +0,0 @@ -using ActionCache.Common.Concurrency; -using ActionCache.Common.Concurrency.Locks; -using Microsoft.Extensions.Caching.Distributed; -using Moq; -using System.Text; - -namespace Unit.Common.Concurrency; - -[TestFixture] -public class DistributedCacheLockerTests -{ - private Mock _cacheMock; - private DistributedCacheLocker _sut; - - [SetUp] - public void SetUp() - { - _cacheMock = new Mock(); - _sut = new DistributedCacheLocker( - _cacheMock.Object, - lockDuration: TimeSpan.FromMilliseconds(500), - lockTimeout: TimeSpan.FromMilliseconds(200)); - } - - [Test] - public async Task TryAcquireLockAsync_WhenNoExistingLock_SetsLockInCache() - { - string? storedValue = null; - _cacheMock.Setup(cache => cache.GetAsync(It.IsAny(), default)) - .ReturnsAsync((byte[]?)null); - _cacheMock.Setup(cache => cache.SetAsync(It.IsAny(), It.IsAny(), It.IsAny(), default)) - .Callback( - (key, val, opts, ct) => storedValue = Encoding.UTF8.GetString(val)) - .Returns(Task.CompletedTask); - - await _sut.TryAcquireLockAsync("my-resource"); - - _cacheMock.Verify( - cache => cache.SetAsync(It.IsAny(), It.IsAny(), It.IsAny(), default), - Times.Once); - } - - [Test] - public async Task TryAcquireLockAsync_WhenLockAlreadyExists_DoesNotSetLock() - { - var existingValue = Encoding.UTF8.GetBytes("some-existing-lock"); - _cacheMock.Setup(cache => cache.GetAsync(It.IsAny(), default)) - .ReturnsAsync(existingValue); - - var result = await _sut.TryAcquireLockAsync("my-resource"); - - result.IsAcquired.Should().BeFalse(); - _cacheMock.Verify( - cache => cache.SetAsync(It.IsAny(), It.IsAny(), It.IsAny(), default), - Times.Never); - } - - [Test] - public async Task ReleaseLockAsync_WhenCurrentValueMatchesLock_RemovesFromCache() - { - var cacheLock = new DistributedCacheLock("my-resource", - TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); - - _cacheMock.Setup(cache => cache.GetAsync(cacheLock.Key, default)) - .ReturnsAsync(Encoding.UTF8.GetBytes(cacheLock.Value)); - - await _sut.ReleaseLockAsync(cacheLock); - - _cacheMock.Verify(cache => cache.RemoveAsync(cacheLock.Key, default), Times.Once); - } - - [Test] - public async Task ReleaseLockAsync_WhenCurrentValueDiffers_DoesNotRemove() - { - var cacheLock = new DistributedCacheLock("my-resource", - TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); - - _cacheMock.Setup(cache => cache.GetAsync(cacheLock.Key, default)) - .ReturnsAsync(Encoding.UTF8.GetBytes("different-value")); - - await _sut.ReleaseLockAsync(cacheLock); - - _cacheMock.Verify(cache => cache.RemoveAsync(It.IsAny(), default), Times.Never); - } - - [Test] - public async Task WaitForLockThenAsync_WhenLockTimesOut_DoesNotExecuteAction() - { - var sutWithShortTimeout = new DistributedCacheLocker( - _cacheMock.Object, - lockDuration: TimeSpan.FromMilliseconds(100), - lockTimeout: TimeSpan.Zero); - - var existingValue = Encoding.UTF8.GetBytes("existing-lock"); - _cacheMock.Setup(cache => cache.GetAsync(It.IsAny(), default)) - .ReturnsAsync(existingValue); - - var actionExecuted = false; - await sutWithShortTimeout.WaitForLockThenAsync("resource", () => { actionExecuted = true; }); - - actionExecuted.Should().BeFalse(); - } - - [Test] - public async Task WaitForLockAsync_WhenLockCantBeAcquired_ReturnsUnacquiredLock() - { - var sutWithShortTimeout = new DistributedCacheLocker( - _cacheMock.Object, - lockDuration: TimeSpan.FromMilliseconds(100), - lockTimeout: TimeSpan.Zero); - - var existingValue = Encoding.UTF8.GetBytes("existing-lock"); - _cacheMock.Setup(cache => cache.GetAsync(It.IsAny(), default)) - .ReturnsAsync(existingValue); - - var result = await sutWithShortTimeout.WaitForLockAsync("resource"); - - result.IsAcquired.Should().BeFalse(); - } - - [Test] - public async Task WaitForLockAsync_WhenLockIsAcquired_ReturnsAcquiredLock() - { - byte[]? storedValue = null; - _cacheMock.Setup(cache => cache.GetAsync(It.IsAny(), default)) - .ReturnsAsync(() => storedValue); - - _cacheMock.Setup(cache => cache.SetAsync(It.IsAny(), It.IsAny(), It.IsAny(), default)) - .Callback( - (key, val, opts, ct) => storedValue = val) - .Returns(Task.CompletedTask); - - var result = await _sut.WaitForLockAsync("resource"); - - result.IsAcquired.Should().BeTrue(); - } -} diff --git a/test/Unit/Common/Concurrency/NullCacheLockerTests.cs b/test/Unit/Common/Concurrency/NullCacheLockerTests.cs index 7bb50f0..689817d 100644 --- a/test/Unit/Common/Concurrency/NullCacheLockerTests.cs +++ b/test/Unit/Common/Concurrency/NullCacheLockerTests.cs @@ -16,6 +16,7 @@ public async Task TryAcquireLockAsync_ReturnsAcquiredLock() var cacheLock = await _locker.TryAcquireLockAsync("resource"); cacheLock.Should().NotBeNull(); cacheLock.Resource.Should().Be("resource"); + cacheLock.IsAcquired.Should().BeTrue(); } [Test] @@ -23,6 +24,7 @@ public async Task WaitForLockAsync_ReturnsAcquiredLock() { var cacheLock = await _locker.WaitForLockAsync("resource"); cacheLock.Should().NotBeNull(); + cacheLock.IsAcquired.Should().BeTrue(); } [Test] @@ -33,23 +35,22 @@ public async Task ReleaseLockAsync_CompletesWithoutError() } [Test] - public async Task WaitForLockThenAsync_Action_DoesNotExecute_BecauseLockNeverAcquired() + public async Task WaitForLockThenAsync_Action_AlwaysExecutes() { - // NullCacheLock.IsAcquired defaults false, so the base WaitForLockThenAsync never runs the action var executed = false; await _locker.WaitForLockThenAsync("resource", () => { executed = true; }); - executed.Should().BeFalse(); + executed.Should().BeTrue(); } [Test] - public async Task WaitForLockThenAsync_Func_ReturnsDefault_BecauseLockNeverAcquired() + public async Task WaitForLockThenAsync_AsyncFunc_ReturnsResult() { var result = await _locker.WaitForLockThenAsync("resource", () => Task.FromResult(42)); - result.Should().Be(default(int)); + result.Should().Be(42); } [Test] - public async Task WaitForLockThenAsync_AsyncAction_DoesNotExecute_BecauseLockNeverAcquired() + public async Task WaitForLockThenAsync_AsyncAction_AlwaysExecutes() { var executed = false; await _locker.WaitForLockThenAsync("resource", () => @@ -57,14 +58,13 @@ await _locker.WaitForLockThenAsync("resource", () => executed = true; return Task.CompletedTask; }); - executed.Should().BeFalse(); + executed.Should().BeTrue(); } [Test] - public async Task WaitForLockThenAsync_SyncFunc_ReturnsDefault_BecauseLockNeverAcquired() + public async Task WaitForLockThenAsync_SyncFunc_ReturnsResult() { var result = await _locker.WaitForLockThenAsync("resource", () => 99); - result.Should().Be(default(int)); + result.Should().Be(99); } - } diff --git a/test/Unit/Common/Concurrency/SemaphoreSlimLockerTests.cs b/test/Unit/Common/Concurrency/SemaphoreSlimLockerTests.cs deleted file mode 100644 index a1185a2..0000000 --- a/test/Unit/Common/Concurrency/SemaphoreSlimLockerTests.cs +++ /dev/null @@ -1,91 +0,0 @@ -using ActionCache.Common.Concurrency; - -namespace Unit.Common; - -[TestFixture] -public class SemaphoreSlimLockerTests -{ - private SemaphoreSlimLocker _locker; - - [SetUp] - public void SetUp() => - _locker = new SemaphoreSlimLocker(TimeSpan.FromMilliseconds(200), TimeSpan.FromMilliseconds(500)); - - [Test] - public async Task TryAcquireLockAsync_AcquiresLock() - { - var cacheLock = await _locker.TryAcquireLockAsync("res"); - cacheLock.IsAcquired.Should().BeTrue(); - cacheLock.Resource.Should().Be("res"); - await _locker.ReleaseLockAsync(cacheLock); - } - - [Test] - public async Task WaitForLockAsync_AcquiresLock() - { - var cacheLock = await _locker.WaitForLockAsync("res"); - cacheLock.IsAcquired.Should().BeTrue(); - await _locker.ReleaseLockAsync(cacheLock); - } - - [Test] - public async Task ReleaseLockAsync_WhenAcquired_ReleasesSemaphore() - { - var first = await _locker.TryAcquireLockAsync("res"); - await _locker.ReleaseLockAsync(first); - - var second = await _locker.TryAcquireLockAsync("res"); - second.IsAcquired.Should().BeTrue(); - await _locker.ReleaseLockAsync(second); - } - - [Test] - public async Task ReleaseLockAsync_WhenNotAcquired_IsNoOp() - { - var cacheLock = await _locker.TryAcquireLockAsync("res"); - cacheLock.IsAcquired = false; - await _locker.Invoking(l => l.ReleaseLockAsync(cacheLock)).Should().NotThrowAsync(); - } - - [Test] - public async Task TryAcquireLockAsync_WhenAlreadyHeld_TimesOutAndReturnsNotAcquired() - { - var shortTimeout = new SemaphoreSlimLocker(TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(50)); - var first = await shortTimeout.TryAcquireLockAsync("res"); - first.IsAcquired.Should().BeTrue(); - - var second = await shortTimeout.TryAcquireLockAsync("res"); - second.IsAcquired.Should().BeFalse(); - - await shortTimeout.ReleaseLockAsync(first); - } - - [Test] - public async Task WaitForLockThenAsync_Action_ExecutesWhenLockAcquired() - { - var executed = false; - await _locker.WaitForLockThenAsync("res", () => { executed = true; }); - executed.Should().BeTrue(); - } - - [Test] - public async Task WaitForLockThenAsync_AsyncFunc_ReturnsResult() - { - var result = await _locker.WaitForLockThenAsync("res", () => Task.FromResult("hello")); - result.Should().Be("hello"); - } - - [Test] - public async Task WaitForLockThenAsync_WhenLockNotAcquired_DoesNotExecuteAction() - { - var shortTimeout = new SemaphoreSlimLocker(TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(50)); - var held = await shortTimeout.TryAcquireLockAsync("blocked"); - held.IsAcquired.Should().BeTrue(); - - var executed = false; - await shortTimeout.WaitForLockThenAsync("blocked", () => { executed = true; }); - executed.Should().BeFalse(); - - await shortTimeout.ReleaseLockAsync(held); - } -} diff --git a/test/Unit/Redis/RedisActionCacheExpirationTests.cs b/test/Unit/Redis/RedisActionCacheExpirationTests.cs index a025977..501cc82 100644 --- a/test/Unit/Redis/RedisActionCacheExpirationTests.cs +++ b/test/Unit/Redis/RedisActionCacheExpirationTests.cs @@ -32,7 +32,7 @@ public void SetUp() [Test] public async Task GetAsync_WhenAbsoluteExpirationHasPassed_DeletesAndReturnsDefault() { - var pastTimestamp = 1L; // 1 ms after Unix epoch = year 1970, definitely in the past + var pastTimestamp = 1L; var entries = new HashEntry[] { new HashEntry(RedisHashEntry.Value, "\"value\""), diff --git a/test/Unit/SqlServer/SqlServerActionCacheFactoryTests.cs b/test/Unit/SqlServer/SqlServerActionCacheFactoryTests.cs index 23d308f..3a9342d 100644 --- a/test/Unit/SqlServer/SqlServerActionCacheFactoryTests.cs +++ b/test/Unit/SqlServer/SqlServerActionCacheFactoryTests.cs @@ -4,6 +4,7 @@ using ActionCache.SqlServer; using ActionCache.Utilities; using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.SqlServer; using Microsoft.Extensions.Options; using Moq; @@ -15,6 +16,7 @@ public class SqlServerActionCacheFactoryTests private Mock _cacheMock; private Mock _refreshProviderMock; private IOptions _entryOptions; + private IOptions _sqlServerOptions; private SqlServerActionCacheFactory _sut; [SetUp] @@ -23,7 +25,15 @@ public void SetUp() _cacheMock = new Mock(); _refreshProviderMock = new Mock(); _entryOptions = Options.Create(new ActionCacheEntryOptions()); - _sut = new SqlServerActionCacheFactory(_cacheMock.Object, _entryOptions, _refreshProviderMock.Object); + _sqlServerOptions = Options.Create(new SqlServerCacheOptions + { + ConnectionString = "Server=localhost;Database=Cache;Integrated Security=true;" + }); + _sut = new SqlServerActionCacheFactory( + _cacheMock.Object, + _sqlServerOptions, + _entryOptions, + _refreshProviderMock.Object); } [Test] diff --git a/test/Unit/SqlServer/SqlServerActionCacheTests.cs b/test/Unit/SqlServer/SqlServerActionCacheTests.cs index 279fdeb..918a52b 100644 --- a/test/Unit/SqlServer/SqlServerActionCacheTests.cs +++ b/test/Unit/SqlServer/SqlServerActionCacheTests.cs @@ -1,8 +1,8 @@ using ActionCache.Common; using ActionCache.Common.Caching; using ActionCache.Common.Concurrency; -using ActionCache.Common.Concurrency.Locks; using ActionCache.SqlServer; +using ActionCache.SqlServer.Concurrency.Locks; using ActionCache.Utilities; using Microsoft.Extensions.Caching.Distributed; using Moq; @@ -16,14 +16,14 @@ namespace Unit.SqlServer; public class SqlServerActionCacheTests { private Mock _cacheMock; - private Mock> _lockerMock; + private Mock> _lockerMock; private SqlServerActionCache _sut; [SetUp] public void SetUp() { _cacheMock = new Mock(); - _lockerMock = new Mock>(); + _lockerMock = new Mock>(); _lockerMock .Setup(locker => locker.WaitForLockThenAsync(It.IsAny(), It.IsAny>())) @@ -36,7 +36,7 @@ public void SetUp() .Returns>>>( async (_, func) => await func()); - var context = new ActionCacheContext + var context = new ActionCacheContext { Namespace = new Namespace("TestNs"), EntryOptions = new ActionCacheEntryOptions(), @@ -112,7 +112,7 @@ public async Task RemoveAsync_WithKey_RemovesFromCache() Times.AtLeastOnce); } -[Test] + [Test] public async Task GetKeysAsync_WhenNoKeys_ReturnsEmpty() { _cacheMock.Setup(cache => cache.Get(It.IsAny()))