diff --git a/src/main/Hangfire.Storage.SQLite/Entities/DistributedLock.cs b/src/main/Hangfire.Storage.SQLite/Entities/DistributedLock.cs index d9f6752..efa5189 100644 --- a/src/main/Hangfire.Storage.SQLite/Entities/DistributedLock.cs +++ b/src/main/Hangfire.Storage.SQLite/Entities/DistributedLock.cs @@ -19,6 +19,7 @@ public class DistributedLock /// /// The name of the resource being held. /// + [Unique] public string Resource { get; set; } /// diff --git a/src/main/Hangfire.Storage.SQLite/SQLiteDistributedLock.cs b/src/main/Hangfire.Storage.SQLite/SQLiteDistributedLock.cs index ad0efd2..89ae4be 100644 --- a/src/main/Hangfire.Storage.SQLite/SQLiteDistributedLock.cs +++ b/src/main/Hangfire.Storage.SQLite/SQLiteDistributedLock.cs @@ -1,5 +1,6 @@ using Hangfire.Logging; using Hangfire.Storage.SQLite.Entities; +using SQLite; using System; using System.Collections.Generic; using System.Text; @@ -134,7 +135,17 @@ private void Acquire(TimeSpan timeout) var rowsAffected = _dbContext.Database.Update(distributedLock); if (rowsAffected == 0) - _dbContext.Database.Insert(distributedLock); + { + try + { + _dbContext.Database.Insert(distributedLock); + } + catch(SQLiteException e) when (e.Result == SQLite3.Result.Constraint) + { + // The lock already exists preventing us from inserting. + continue; + } + } // If result is null, then it means we acquired the lock if (result == null) diff --git a/src/test/Hangfire.Storage.SQLite.Test/SQLiteDistributedLockFacts.cs b/src/test/Hangfire.Storage.SQLite.Test/SQLiteDistributedLockFacts.cs index 5cc655c..c44030e 100644 --- a/src/test/Hangfire.Storage.SQLite.Test/SQLiteDistributedLockFacts.cs +++ b/src/test/Hangfire.Storage.SQLite.Test/SQLiteDistributedLockFacts.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading; using Hangfire.Storage.SQLite.Entities; using Hangfire.Storage.SQLite.Test.Utils; @@ -127,6 +128,50 @@ public void Ctor_WaitForLock_SignaledAtLockRelease() }); } + [Fact, CleanDatabase] + public void Ctor_WaitForLock_OnlySingleLockCanBeAcquired() + { + var connection = ConnectionUtils.CreateConnection(); + var numThreads = 10; + long concurrencyCounter = 0; + var manualResetEvent = new ManualResetEventSlim(); + var success = new bool[numThreads]; + + // Spawn multiple threads to race each other. + var threads = Enumerable.Range(0, numThreads).Select(i => new Thread(() => + { + // Wait for the start signal. + manualResetEvent.Wait(); + + // Attempt to acquire the distributed lock. + using (new SQLiteDistributedLock("resource1", TimeSpan.FromSeconds(5), connection, new SQLiteStorageOptions())) + { + // Find out if any other threads managed to acquire the lock. + var oldConcurrencyCounter = Interlocked.CompareExchange(ref concurrencyCounter, 1, 0); + + // The old concurrency counter should be 0 as only one thread should be allowed to acquire the lock. + success[i] = oldConcurrencyCounter == 0; + + Interlocked.MemoryBarrier(); + + // Hold the lock for some time. + Thread.Sleep(100); + + Interlocked.Decrement(ref concurrencyCounter); + } + })).ToList(); + + threads.ForEach(t => t.Start()); + + manualResetEvent.Set(); + + threads.ForEach(t => Assert.True(t.Join(TimeSpan.FromMinutes(1)), "Thread is hanging unexpected")); + + // All the threads should report success. + Interlocked.MemoryBarrier(); + Assert.DoesNotContain(false, success); + } + [Fact] public void Ctor_ThrowsAnException_WhenOptionsIsNull() {