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()
{