Skip to content

Commit bf1c618

Browse files
authored
Merge pull request #11 from SumoServer/fix/lock
Rely on SQLite uniqueness constraint to protect locks
2 parents e67c998 + fd2581a commit bf1c618

File tree

3 files changed

+58
-1
lines changed

3 files changed

+58
-1
lines changed

src/main/Hangfire.Storage.SQLite/Entities/DistributedLock.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public class DistributedLock
1919
/// <summary>
2020
/// The name of the resource being held.
2121
/// </summary>
22+
[Unique]
2223
public string Resource { get; set; }
2324

2425
/// <summary>

src/main/Hangfire.Storage.SQLite/SQLiteDistributedLock.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Hangfire.Logging;
22
using Hangfire.Storage.SQLite.Entities;
3+
using SQLite;
34
using System;
45
using System.Collections.Generic;
56
using System.Text;
@@ -134,7 +135,17 @@ private void Acquire(TimeSpan timeout)
134135

135136
var rowsAffected = _dbContext.Database.Update(distributedLock);
136137
if (rowsAffected == 0)
137-
_dbContext.Database.Insert(distributedLock);
138+
{
139+
try
140+
{
141+
_dbContext.Database.Insert(distributedLock);
142+
}
143+
catch(SQLiteException e) when (e.Result == SQLite3.Result.Constraint)
144+
{
145+
// The lock already exists preventing us from inserting.
146+
continue;
147+
}
148+
}
138149

139150
// If result is null, then it means we acquired the lock
140151
if (result == null)

src/test/Hangfire.Storage.SQLite.Test/SQLiteDistributedLockFacts.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Linq;
23
using System.Threading;
34
using Hangfire.Storage.SQLite.Entities;
45
using Hangfire.Storage.SQLite.Test.Utils;
@@ -127,6 +128,50 @@ public void Ctor_WaitForLock_SignaledAtLockRelease()
127128
});
128129
}
129130

131+
[Fact, CleanDatabase]
132+
public void Ctor_WaitForLock_OnlySingleLockCanBeAcquired()
133+
{
134+
var connection = ConnectionUtils.CreateConnection();
135+
var numThreads = 10;
136+
long concurrencyCounter = 0;
137+
var manualResetEvent = new ManualResetEventSlim();
138+
var success = new bool[numThreads];
139+
140+
// Spawn multiple threads to race each other.
141+
var threads = Enumerable.Range(0, numThreads).Select(i => new Thread(() =>
142+
{
143+
// Wait for the start signal.
144+
manualResetEvent.Wait();
145+
146+
// Attempt to acquire the distributed lock.
147+
using (new SQLiteDistributedLock("resource1", TimeSpan.FromSeconds(5), connection, new SQLiteStorageOptions()))
148+
{
149+
// Find out if any other threads managed to acquire the lock.
150+
var oldConcurrencyCounter = Interlocked.CompareExchange(ref concurrencyCounter, 1, 0);
151+
152+
// The old concurrency counter should be 0 as only one thread should be allowed to acquire the lock.
153+
success[i] = oldConcurrencyCounter == 0;
154+
155+
Interlocked.MemoryBarrier();
156+
157+
// Hold the lock for some time.
158+
Thread.Sleep(100);
159+
160+
Interlocked.Decrement(ref concurrencyCounter);
161+
}
162+
})).ToList();
163+
164+
threads.ForEach(t => t.Start());
165+
166+
manualResetEvent.Set();
167+
168+
threads.ForEach(t => Assert.True(t.Join(TimeSpan.FromMinutes(1)), "Thread is hanging unexpected"));
169+
170+
// All the threads should report success.
171+
Interlocked.MemoryBarrier();
172+
Assert.DoesNotContain(false, success);
173+
}
174+
130175
[Fact]
131176
public void Ctor_ThrowsAnException_WhenOptionsIsNull()
132177
{

0 commit comments

Comments
 (0)