Improved locking code.

- Moved DatabaseLock into Framework and making it the default lock provider.
- Removed unnecessary thread synchronization code.
- Allowing multiple lock acquisitions from the same machine (simulating a "lease").
- Updated unit tests.
This commit is contained in:
Sipke Schoorstra
2015-08-11 16:43:48 +01:00
parent cb6835aba3
commit 77b8df516a
19 changed files with 106 additions and 105 deletions

View File

@@ -182,7 +182,6 @@
<Compile Include="Stubs\MessageChannelSelectorStub.cs" />
<Compile Include="Stubs\ShapeDisplayStub.cs" />
<Compile Include="Tags\Services\TagsServiceTests.cs" />
<Compile Include="TaskLease\DatabaseLockTests.cs" />
<Compile Include="Themes\Services\ThemeServiceTests.cs" />
<Compile Include="Users\Controllers\AccountControllerTests.cs" />
<Compile Include="Users\Services\UserServiceTests.cs" />

View File

@@ -292,6 +292,7 @@
<Compile Include="Stubs\StubVirtualPathMonitor.cs" />
<Compile Include="Stubs\StubCacheManager.cs" />
<Compile Include="Stubs\StubWebSiteFolder.cs" />
<Compile Include="Tasks\DatabaseLockTests.cs" />
<Compile Include="Tasks\DistributedLockServiceTests.cs" />
<Compile Include="Time\TimeZoneSelectorTests.cs" />
<Compile Include="UI\Resources\ResourceManagerTests.cs" />

View File

@@ -1,23 +1,27 @@
using System;
using Orchard.Tasks.Locking;
using Orchard.Tasks.Locking.Services;
namespace Orchard.Tests.Stubs {
public class StubDistributedLock : IDistributedLock {
public static bool IsAcquired { get; private set; }
public static string AcquiredByMachineName { get; private set; }
public bool IsDisposed { get; private set; }
public bool TryAcquire(string name, TimeSpan maxLifetime) {
if (IsAcquired)
public bool TryAcquire(string name, string machineName, TimeSpan maxLifetime) {
if (IsAcquired && machineName != AcquiredByMachineName)
return false;
return IsAcquired = true;
IsAcquired = true;
AcquiredByMachineName = machineName;
return true;
}
public void Dispose() {
IsDisposed = true;
IsAcquired = false;
AcquiredByMachineName = null;
}
}
}

View File

@@ -2,8 +2,12 @@
namespace Orchard.Tests.Stubs {
public class StubMachineNameProvider : IMachineNameProvider {
public StubMachineNameProvider() {
MachineName = "Orchard Machine";
}
public string MachineName { get; set; }
public string GetMachineName() {
return "Orchard Machine";
return MachineName;
}
}
}

View File

@@ -4,12 +4,15 @@ using System.Linq;
using Autofac;
using NUnit.Framework;
using Orchard.Data;
using Orchard.TaskLease.Models;
using Orchard.TaskLease.Services;
using Orchard.Tasks.Locking.Providers;
using Orchard.Tasks.Locking.Records;
namespace Orchard.Tests.Modules.TaskLease {
namespace Orchard.Tests.Tasks {
[TestFixture]
public class DatabaseLockTests : DatabaseEnabledTestsBase {
private const string MachineName1 = "Orchard Testing Machine 1";
private const string MachineName2 = "Orchard Testing Machine 2";
private const string LockName = "Orchard Test Lock";
private IRepository<DatabaseLockRecord> _databaseLockRepository;
private DatabaseLock _lock;
@@ -32,7 +35,7 @@ namespace Orchard.Tests.Modules.TaskLease {
[Test]
public void AcquiringLockSucceeds() {
var lockAcquired = _lock.TryAcquire("Test", TimeSpan.FromSeconds(60));
var lockAcquired = _lock.TryAcquire(LockName, MachineName1, TimeSpan.FromSeconds(60));
Assert.That(lockAcquired, Is.True);
Assert.That(_databaseLockRepository.Table.Count(), Is.EqualTo(1));
@@ -40,15 +43,15 @@ namespace Orchard.Tests.Modules.TaskLease {
[Test]
public void DisposingRemovesLockRecord() {
_lock.TryAcquire("Test", TimeSpan.FromSeconds(60));
_lock.TryAcquire(LockName, MachineName1, TimeSpan.FromSeconds(60));
_lock.Dispose();
Assert.That(_databaseLockRepository.Table.Count(), Is.EqualTo(0));
}
[Test]
public void AcquiringLockTwiceFails() {
var attempt1 = _lock.TryAcquire("Test", TimeSpan.FromSeconds(60));
var attempt2 = _lock.TryAcquire("Test", TimeSpan.FromSeconds(60));
var attempt1 = _lock.TryAcquire(LockName, MachineName1, TimeSpan.FromSeconds(60));
var attempt2 = _lock.TryAcquire(LockName, MachineName2, TimeSpan.FromSeconds(60));
Assert.That(attempt1, Is.True);
Assert.That(attempt2, Is.False);
@@ -56,8 +59,8 @@ namespace Orchard.Tests.Modules.TaskLease {
[Test]
public void AcquiringExpiredLockSucceeds() {
var attempt1 = _lock.TryAcquire("Test", TimeSpan.FromSeconds(60));
var attempt2 = _lock.TryAcquire("Test", TimeSpan.FromSeconds(-1)); // Treat the previosuly stored lock as immediately expired.
var attempt1 = _lock.TryAcquire(LockName, MachineName1, TimeSpan.FromSeconds(60));
var attempt2 = _lock.TryAcquire(LockName, MachineName2, TimeSpan.FromSeconds(-1)); // Treat the previosuly stored lock as immediately expired.
Assert.That(attempt1, Is.True);
Assert.That(attempt2, Is.True);

View File

@@ -5,17 +5,19 @@ using System.Threading.Tasks;
using Autofac;
using NUnit.Framework;
using Orchard.Environment;
using Orchard.Tasks.Locking;
using Orchard.Tasks.Locking.Services;
using Orchard.Tests.Stubs;
namespace Orchard.Tests.Tasks {
[TestFixture]
public class DistributedLockServiceTests : ContainerTestBase {
private const string LockName = "Orchard Test Lock";
private DistributedLockService _distributedLockService;
private StubMachineNameProvider _stubMachineNameProvider;
protected override void Register(ContainerBuilder builder) {
builder.RegisterType<StubWorkContextAccessor>().As<IWorkContextAccessor>();
builder.RegisterType<StubMachineNameProvider>().As<IMachineNameProvider>();
builder.RegisterType<StubMachineNameProvider>().As<IMachineNameProvider>().SingleInstance();
builder.RegisterType<StubDistributedLock>().As<IDistributedLock>();
builder.RegisterType<DistributedLockService>().AsSelf();
builder.RegisterInstance(new Work<IDistributedLock>(resolve => _container.Resolve<IDistributedLock>())).AsSelf();
@@ -23,22 +25,35 @@ namespace Orchard.Tests.Tasks {
protected override void Resolve(ILifetimeScope container) {
_distributedLockService = container.Resolve<DistributedLockService>();
_stubMachineNameProvider = (StubMachineNameProvider)container.Resolve<IMachineNameProvider>();
}
[Test]
public void AcquiringLockSucceeds() {
IDistributedLock @lock;
var lockAcquired = _distributedLockService.TryAcquireLock("Test", TimeSpan.FromSeconds(60), TimeSpan.Zero, out @lock);
var lockAcquired = _distributedLockService.TryAcquireLock(LockName, TimeSpan.FromSeconds(60), TimeSpan.Zero, out @lock);
Assert.That(lockAcquired, Is.True);
}
[Test]
public void AcquiringLockTwiceOnSameMachineSucceeds() {
IDistributedLock @lock;
var attempt1 = _distributedLockService.TryAcquireLock(LockName, TimeSpan.FromSeconds(60), TimeSpan.Zero, out @lock);
var attempt2 = _distributedLockService.TryAcquireLock(LockName, TimeSpan.FromSeconds(60), TimeSpan.Zero, out @lock);
Assert.That(attempt1, Is.True);
Assert.That(attempt2, Is.True);
}
[Test]
public void AcquiringLockTwiceFails() {
IDistributedLock @lock;
var attempt1 = _distributedLockService.TryAcquireLock("Test", TimeSpan.FromSeconds(60), TimeSpan.Zero, out @lock);
var attempt2 = _distributedLockService.TryAcquireLock("Test", TimeSpan.FromSeconds(60), TimeSpan.Zero, out @lock);
_stubMachineNameProvider.MachineName = "Orchard Test Machine 1";
var attempt1 = _distributedLockService.TryAcquireLock(LockName, TimeSpan.FromSeconds(60), TimeSpan.Zero, out @lock);
_stubMachineNameProvider.MachineName = "Orchard Test Machine 2";
var attempt2 = _distributedLockService.TryAcquireLock(LockName, TimeSpan.FromSeconds(60), TimeSpan.Zero, out @lock);
Assert.That(attempt1, Is.True);
Assert.That(attempt2, Is.False);
@@ -52,7 +67,8 @@ namespace Orchard.Tests.Tasks {
foreach (var index in Enumerable.Range(0, 10).AsParallel()) {
var task = Task.Factory.StartNew(() => {
IDistributedLock @lock;
var attempt = _distributedLockService.TryAcquireLock("Test", TimeSpan.FromSeconds(60), TimeSpan.Zero, out @lock);
_stubMachineNameProvider.MachineName = "Orchard Test Machine " + (index + 1);
var attempt = _distributedLockService.TryAcquireLock(LockName, TimeSpan.FromSeconds(60), TimeSpan.Zero, out @lock);
attempts.Add(attempt);
});

View File

@@ -17,14 +17,5 @@ namespace Orchard.TaskLease {
return 1;
}
public int UpdateFrom1() {
SchemaBuilder.CreateTable("DatabaseLockRecord", table => table
.Column<int>("Id", column => column.PrimaryKey().Identity())
.Column<string>("Name", column => column.NotNull().WithLength(256))
.Column<DateTime>("AcquiredUtc"));
return 2;
}
}
}

View File

@@ -4,5 +4,6 @@ Author: The Orchard Team
Website: http://orchardtasklease.codeplex.com
Version: 1.9.1
OrchardVersion: 1.9
Description: Provides services to synchronize tasks in a farm environment
LifecycleStatus: Deprecated
Description: Provides services to synchronize tasks in a farm environment.
Category: Hosting

View File

@@ -76,7 +76,6 @@
</ItemGroup>
<ItemGroup>
<Content Include="Web.config" />
<Compile Include="Models\DatabaseLockRecord.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Content Include="Module.txt" />
</ItemGroup>
@@ -86,7 +85,6 @@
</Compile>
<Compile Include="Models\TaskLeaseRecord.cs" />
<Compile Include="Services\ITaskLeaseService.cs" />
<Compile Include="Services\DatabaseLock.cs" />
<Compile Include="Services\TaskLeaseService.cs" />
</ItemGroup>
<ItemGroup />

View File

@@ -1133,7 +1133,7 @@ Global
{642A49D7-8752-4177-80D6-BFBBCFAD3DE0} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{966EC390-3C7F-4D98-92A6-F0F30D02E9B1} = {902528F6-1444-42A3-8B75-A54B775B539C}
{3158C928-888C-4A84-8BC1-4A8257489538} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{3F72A4E9-7B72-4260-B010-C16EC54F9BAF} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{3F72A4E9-7B72-4260-B010-C16EC54F9BAF} = {902528F6-1444-42A3-8B75-A54B775B539C}
{E07AFA7E-7B36-44C3-A537-AFCCAA93EA7A} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA}
{5531E894-D259-45A3-AA61-26DBE720C1CE} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{2969635F-D9C3-4D01-890D-437B46659690} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA}

View File

@@ -3,7 +3,7 @@ using System.Linq;
using Orchard.Environment;
using Orchard.Environment.Features;
using Orchard.Logging;
using Orchard.Tasks.Locking;
using Orchard.Tasks.Locking.Services;
namespace Orchard.Data.Migration {
/// <summary>

View File

@@ -397,10 +397,12 @@
<Compile Include="Services\IJsonConverter.cs" />
<Compile Include="Settings\CurrentSiteWorkContext.cs" />
<Compile Include="Settings\ResourceDebugMode.cs" />
<Compile Include="Tasks\Locking\DefaultLock.cs" />
<Compile Include="Tasks\Locking\IDistributedLockService.cs" />
<Compile Include="Tasks\Locking\IDistributedLock.cs" />
<Compile Include="Tasks\Locking\DistributedLockService.cs" />
<Compile Include="Tasks\Locking\Migrations\FrameworkMigrations.cs" />
<Compile Include="Tasks\Locking\Providers\DatabaseLock.cs" />
<Compile Include="Tasks\Locking\Services\IDistributedLockService.cs" />
<Compile Include="Tasks\Locking\Services\IDistributedLock.cs" />
<Compile Include="Tasks\Locking\Services\DistributedLockService.cs" />
<Compile Include="Tasks\Locking\Records\DatabaseLockRecord.cs" />
<Compile Include="Themes\CurrentThemeWorkContext.cs" />
<Compile Include="Themes\ThemeManager.cs" />
<Compile Include="Time\CurrentTimeZoneWorkContext.cs" />

View File

@@ -1,31 +0,0 @@
using System;
namespace Orchard.Tasks.Locking {
public class DefaultLock : IDistributedLock {
private readonly IStaticLockSemaphore _semaphore;
public DefaultLock(IStaticLockSemaphore semaphore) {
_semaphore = semaphore;
}
public bool TryAcquire(string name, TimeSpan maxLifetime) {
if (_semaphore.IsAcquired)
return false;
return _semaphore.IsAcquired = true;
}
public void Dispose() {
_semaphore.IsAcquired = false;
}
public interface IStaticLockSemaphore : ISingletonDependency {
bool IsAcquired { get; set; }
}
public class StaticLockSemaphore : IStaticLockSemaphore {
public bool IsAcquired { get; set; }
}
}
}

View File

@@ -0,0 +1,17 @@
using System;
using Orchard.Data.Migration;
namespace Orchard.Tasks.Locking.Migrations {
public class FrameworkMigrations : DataMigrationImpl {
public int Create() {
SchemaBuilder.CreateTable("DatabaseLockRecord", table => table
.Column<int>("Id", column => column.PrimaryKey().Identity())
.Column<string>("Name", column => column.NotNull().WithLength(256))
.Column<string>("MachineName", column => column.WithLength(256))
.Column<DateTime>("AcquiredUtc"));
return 1;
}
}
}

View File

@@ -3,20 +3,18 @@ using System.Data;
using System.Linq;
using Autofac;
using Orchard.Data;
using Orchard.Environment.Extensions;
using Orchard.Exceptions;
using Orchard.Logging;
using Orchard.Services;
using Orchard.TaskLease.Models;
using Orchard.Tasks.Locking;
using Orchard.Tasks.Locking.Records;
using Orchard.Tasks.Locking.Services;
using Orchard.Validation;
namespace Orchard.TaskLease.Services {
namespace Orchard.Tasks.Locking.Providers {
/// <summary>
/// Provides a database driven implementation of <see cref="IDistributedLock" />
/// </summary>
[OrchardSuppressDependency("Orchard.Tasks.Locking.DefaultLock")]
public class DatabaseLock : IDistributedLock {
private readonly ILifetimeScope _lifetimeScope;
private readonly IClock _clock;
@@ -34,7 +32,7 @@ namespace Orchard.TaskLease.Services {
public ILogger Logger { get; set; }
public bool TryAcquire(string name, TimeSpan maxLifetime) {
public bool TryAcquire(string name, string machineName, TimeSpan maxLifetime) {
if(_isDisposed)
throw new ObjectDisposedException("DatabaseLock");
@@ -53,9 +51,10 @@ namespace Orchard.TaskLease.Services {
var record = repository.Table.FirstOrDefault(x => x.Name == name);
if (record != null) {
// There is a nexisting lock, but check if it has expired.
// There is an existing lock, but check if it has expired or if the current machine is requesting the lock.
var isExpired = record.AcquiredUtc + maxLifetime < _clock.UtcNow;
if (isExpired) {
var isCurrentMachine = record.MachineName == machineName;
if (isExpired || isCurrentMachine) {
repository.Delete(record);
record = null;
}
@@ -66,6 +65,7 @@ namespace Orchard.TaskLease.Services {
if (canAcquire) {
record = new DatabaseLockRecord {
Name = name,
MachineName = machineName,
AcquiredUtc = _clock.UtcNow
};
repository.Create(record);

View File

@@ -1,9 +1,10 @@
using System;
namespace Orchard.TaskLease.Models {
namespace Orchard.Tasks.Locking.Records {
public class DatabaseLockRecord {
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual string MachineName { get; set; }
public virtual DateTime? AcquiredUtc { get; set; }
}
}

View File

@@ -4,9 +4,8 @@ using System.Threading.Tasks;
using Orchard.Environment;
using Orchard.Logging;
namespace Orchard.Tasks.Locking {
namespace Orchard.Tasks.Locking.Services {
public class DistributedLockService : IDistributedLockService {
private readonly ReaderWriterLockSlim _rwl = new ReaderWriterLockSlim();
private readonly IWorkContextAccessor _wca;
private readonly IMachineNameProvider _machineNameProvider;
@@ -22,13 +21,12 @@ namespace Orchard.Tasks.Locking {
var machineName = _machineNameProvider.GetMachineName();
@lock = Resolve<IDistributedLock>();
if (_rwl.TryEnterWriteLock(0)) {
try {
var waitedTime = TimeSpan.Zero;
var waitTime = TimeSpan.FromMilliseconds(timeout.TotalMilliseconds / 10);
bool acquired;
while (!(acquired = @lock.TryAcquire(name, maxLifetime)) && waitedTime < timeout) {
while (!(acquired = @lock.TryAcquire(name, machineName, maxLifetime)) && waitedTime < timeout) {
Task.Delay(timeout).ContinueWith(t => {
waitedTime += waitTime;
}).Wait();
@@ -43,10 +41,6 @@ namespace Orchard.Tasks.Locking {
Logger.Error(ex, "Error while trying to acquire a lock named {0} on machine {1}.", name, machineName);
throw;
}
finally {
_rwl.ExitWriteLock();
}
}
Logger.Debug("Could not acquire a lock named {0} on machine {1}.", name, machineName);
return false;

View File

@@ -1,6 +1,6 @@
using System;
namespace Orchard.Tasks.Locking {
namespace Orchard.Tasks.Locking.Services {
/// <summary>
/// Provides a lock on a provided name.
/// </summary>
@@ -9,8 +9,9 @@ namespace Orchard.Tasks.Locking {
/// Tries to acquire a lock on the specified name.
/// </summary>
/// <param name="name">The name to use for the lock.</param>
/// <param name="machineName">The machine name trying to acquire a lock.</param>
/// <param name="maxLifetime">The maximum amount of time the lock is allowed. This is a safety net in case the caller fails to release the lock.</param>
/// <returns>Returns true if a lock was acquired, false otherwise.</returns>
bool TryAcquire(string name, TimeSpan maxLifetime);
bool TryAcquire(string name, string machineName, TimeSpan maxLifetime);
}
}

View File

@@ -1,6 +1,6 @@
using System;
namespace Orchard.Tasks.Locking {
namespace Orchard.Tasks.Locking.Services {
/// <summary>
/// Provides distributed locking functionality.
/// </summary>