DistributedLock icon indicating copy to clipboard operation
DistributedLock copied to clipboard

Single-node in-memory implementation

Open srogovtsev opened this issue 1 year ago • 9 comments

Consider the following scenario: we have a system that might run in a distributed mode and in a single-node mode. There are routines that require cross-node synchronization, so we use distributed locks. We use DI and IDistributedLockProvider to separate ourselves from the implementation details (and use different stores depending on the deployment. But when we deploy the same system in a single-node, we don't need any kind of backing store, and I would assume that the same functionality can be reasonable implemented simply in-memory, based on existing .net synchronization primitives, and just injected instead (e.g., same way as AddDistributedMemoryCache implements in-memory cache for IDistributedCache).

But I would suppose that this is a very obvious idea, and it's either that I simply missed this implementation, or there's some hidden complexity/issue that I miss, and it would be recommended to use a backing store even on single-node installations.

Will be very glad to get any pointers.

srogovtsev avatar Jul 10 '24 20:07 srogovtsev

Hi @srogovtsev thanks for your interest in the library.

This use-case makes sense and is something I’ve considered in the past. As far as why I haven’t built it (yet):

  1. I didn’t have a real use-case outside of unit tests, and for that there are other reasonable options such as…
  2. File-based and wait handle-based locks, which are effectively local machine scoped. Of course, the latter is mostly windows-only.
  3. The implementation is simple but not entirely trivial because I’d want to retain name-based identity, meaning that 2 different lock objects with the same name need to synchronize with each other (via underlying static data structures that can’t leak memory). Another challenge is the lack of (last time I checked) a native async RW lock implementation, which id want to support.

I would call this “process-scoped synchronization”.

If this is something you’d be interested in contributing, let me know and I can offer some guidance.

madelson avatar Jul 11 '24 22:07 madelson

Very useful for monolithic applications that do not want to introduce external components

mingyangzhu avatar Jul 15 '24 05:07 mingyangzhu

For single node situations like local dev and unit tests, you can do this (non-async bc it'd be in Program.cs) using TestContainers.Redis:

    var redisContainer = new RedisBuilder()
        .WithImage("redis:7.0") // Pin to a specific Redis version for stability
        .Build();
    Task.Run(async () => await redisContainer.StartAsync()).Wait();
    // Connect to Redis using the container's connection string
    var redisConnection = ConnectionMultiplexer.Connect(redisContainer.GetConnectionString());
    // Get the IDatabase instance
    return redisConnection.GetDatabase();

netdragonboberb avatar Jun 29 '25 04:06 netdragonboberb

The issue with redis like this is that it adds massive overhead to your testing, because you can't actually test anything if you use a shared instance of redis so you're spinning containers for every single test.

Also, it's not possible to use Garnet in memory because:


 One or more errors occurred. (ERR This instance has Lua scripting support disabled)
       ---> StackExchange.Redis.RedisServerException: ERR This instance has Lua scripting support disabled
         at StackExchange.Redis.RedisDatabase.ScriptEvaluateAsync(String script, RedisKey[] keys, RedisValue[] values, CommandFlags flags) in /_/src/StackExchange.Redis/RedisDatabase.cs:line 1645
         at Medallion.Threading.Redis.RedLock.RedLockRelease.ReleaseAsync() in /_/src/DistributedLock.Redis/RedLock/RedLockRelease.cs:line 57
         --- End of inner exception stack trace ---
         at Medallion.Threading.Redis.RedLock.RedLockRelease.ReleaseAsync() in /_/src/DistributedLock.Redis/RedLock/RedLockRelease.cs:line 66
         at Medallion.Threading.Redis.RedLock.RedLockHandle.DisposeAsync() in /_/src/DistributedLock.Redis/RedLock/RedLockHandle.cs:line 46
         at Shared.Locking.DistributedSynchronizationHandle`1.DisposeAsync() in E:\Repos\ProjectFulcrum\Api\Shared\Locking\IDistributedSynchronizationHandle.cs:line 20 

Which was removed from IDistributedCache functionality because Redis itself is depreciating it so nothing else supports it that is new for replacements.

JohnGalt1717 avatar Jul 01 '25 19:07 JohnGalt1717

Another challenge is the lack of (last time I checked) a native async RW lock implementation, which id want to support.

@madelson Sorry if its ignorant, but wouldn't SemaphoreSlim work? It has WaitAsync and supports timeouts.

ntark avatar Oct 20 '25 17:10 ntark

@ntark SemaphoreSlim gives you an easy mutex (lock) or of course a semaphore.

However, I don’t think it gives us reader-writer lock semantics out of the box. Let me know if I’m missing something.

madelson avatar Oct 20 '25 22:10 madelson

@madelson Yep I didn't correctly get the RW abbreviation. While not out of the box, there is an option of configuring to have N Readers and running Wait/WaitAsync N times on Write Lock, although it does seem janky and non-optimal.

ntark avatar Oct 21 '25 06:10 ntark

There is the https://learn.microsoft.com/en-us/dotnet/api/system.threading.readerwriterlock?view=net-9.0 that could be used for RW

hoerup avatar Oct 21 '25 07:10 hoerup

@ntark yeah you can do that but it has a few issues:

  • no upgradable write functionality
  • does not support “any number of readers” (and if you make the semaphore count huge then taking the write lock takes forever
  • if 2 threads take the write lock at the same time each may get some semaphore tickets, resulting in a deadlock

@hoerup yeah there’s even ReaderWriterLockSlim but last I checked it doesn’t support async

madelson avatar Oct 21 '25 22:10 madelson