Refactored distributed locks.

This commit is contained in:
Daniel Stolt
2015-09-06 21:40:45 +02:00
parent 1da2d7e3e9
commit 86b9f35454
3 changed files with 153 additions and 155 deletions

View File

@@ -87,7 +87,6 @@ namespace Orchard.Tests.Tasks {
[Test] [Test]
public void TryAcquiringLockTwiceFails() { public void TryAcquiringLockTwiceFails() {
_distributedLockService.DisableMonitorLock = true;
IDistributedLock @lock; IDistributedLock @lock;
_machineNameProvider.MachineName = "Orchard Test Machine 1"; _machineNameProvider.MachineName = "Orchard Test Machine 1";
var attempt1 = _distributedLockService.TryAcquireLock(LockName, TimeSpan.FromSeconds(60), out @lock); var attempt1 = _distributedLockService.TryAcquireLock(LockName, TimeSpan.FromSeconds(60), out @lock);
@@ -152,11 +151,10 @@ namespace Orchard.Tests.Tasks {
[Test] [Test]
public void MultipleAcquisitionsFromDifferentMachinesShouldFail() { public void MultipleAcquisitionsFromDifferentMachinesShouldFail() {
IDistributedLock @lock; IDistributedLock @lock;
_distributedLockService.DisableMonitorLock = true;
_machineNameProvider.MachineName = "Orchard Test Machine 1"; _machineNameProvider.MachineName = "Orchard Test Machine 1";
var attempt1 = _distributedLockService.TryAcquireLock(LockName, TimeSpan.FromSeconds(60), out @lock); var attempt1 = _distributedLockService.TryAcquireLock(LockName, TimeSpan.FromMinutes(60), out @lock);
_machineNameProvider.MachineName = "Orchard Test Machine 2"; _machineNameProvider.MachineName = "Orchard Test Machine 2";
var attempt2 = _distributedLockService.TryAcquireLock(LockName, TimeSpan.FromSeconds(60), out @lock); var attempt2 = _distributedLockService.TryAcquireLock(LockName, TimeSpan.FromMinutes(60), out @lock);
Assert.That(attempt1, Is.True); Assert.That(attempt1, Is.True);
Assert.That(attempt2, Is.False); Assert.That(attempt2, Is.False);
@@ -165,7 +163,6 @@ namespace Orchard.Tests.Tasks {
[Test] [Test]
public void MultipleAcquisitionsFromDifferentMachinesOnDifferentTenantShouldSucceed() { public void MultipleAcquisitionsFromDifferentMachinesOnDifferentTenantShouldSucceed() {
IDistributedLock @lock; IDistributedLock @lock;
_distributedLockService.DisableMonitorLock = true;
_machineNameProvider.MachineName = "Orchard Test Machine 1"; _machineNameProvider.MachineName = "Orchard Test Machine 1";
var attempt1 = _distributedLockService.TryAcquireLock(LockName, TimeSpan.FromSeconds(60), out @lock); var attempt1 = _distributedLockService.TryAcquireLock(LockName, TimeSpan.FromSeconds(60), out @lock);
_machineNameProvider.MachineName = "Orchard Test Machine 2"; _machineNameProvider.MachineName = "Orchard Test Machine 2";
@@ -194,8 +191,6 @@ namespace Orchard.Tests.Tasks {
[Test] [Test]
public void TryAcquireActiveLockWithNullTimeoutReturnsFalseImmediately() { public void TryAcquireActiveLockWithNullTimeoutReturnsFalseImmediately() {
// Disable monitor locking to simulate concurrent requests
_distributedLockService.DisableMonitorLock = true;
CreateNonExpiredActiveLock("Other Machine"); CreateNonExpiredActiveLock("Other Machine");
IDistributedLock @lock; IDistributedLock @lock;
@@ -206,8 +201,6 @@ namespace Orchard.Tests.Tasks {
[Test] [Test]
public void ActiveLockWithUndefinedValidUntilNeverExpires() { public void ActiveLockWithUndefinedValidUntilNeverExpires() {
// Disable monitor locking to simulate concurrent requests
_distributedLockService.DisableMonitorLock = true;
CreateNonExpiredActiveLockThatNeverExpires("Other Machine"); CreateNonExpiredActiveLockThatNeverExpires("Other Machine");
_clock.Advance(DateTime.MaxValue - _clock.UtcNow); // Fast forward to the End of Time. _clock.Advance(DateTime.MaxValue - _clock.UtcNow); // Fast forward to the End of Time.
@@ -226,7 +219,7 @@ namespace Orchard.Tests.Tasks {
var attempt1 = _distributedLockService.TryAcquireLock(LockName, maxValidFor: null, timeout: null, dLock: out @lock); var attempt1 = _distributedLockService.TryAcquireLock(LockName, maxValidFor: null, timeout: null, dLock: out @lock);
// Release the lock. // Release the lock.
_distributedLockService.ReleaseDistributedLock((DistributedLock)@lock); @lock.Dispose();
// Acquire the lock from another machine. // Acquire the lock from another machine.
_machineNameProvider.MachineName = "Orchard Test Machine 2"; _machineNameProvider.MachineName = "Orchard Test Machine 2";
@@ -239,7 +232,7 @@ namespace Orchard.Tests.Tasks {
private DistributedLockRecord CreateLockRecord(DateTime createdUtc, DateTime? validUntilUtc, string machineName) { private DistributedLockRecord CreateLockRecord(DateTime createdUtc, DateTime? validUntilUtc, string machineName) {
var record = new DistributedLockRecord { var record = new DistributedLockRecord {
Name = ShellSettings.DefaultName + ":" + LockName, Name = String.Format("DistributedLock:{0}:{1}", ShellSettings.DefaultName, LockName),
CreatedUtc = createdUtc, CreatedUtc = createdUtc,
ValidUntilUtc = validUntilUtc, ValidUntilUtc = validUntilUtc,
MachineName = machineName, MachineName = machineName,

View File

@@ -1,36 +1,41 @@
using System; using System;
using System.Threading;
namespace Orchard.Tasks.Locking.Services { namespace Orchard.Tasks.Locking.Services {
public class DistributedLock : IDistributedLock { public class DistributedLock : IDistributedLock {
private DistributedLockService _service; private readonly string _name;
private string _name; private readonly string _internalName;
private readonly Action _releaseLockAction;
private int _count; private int _count;
public DistributedLock(DistributedLockService service, string name) { internal DistributedLock(string name, string internalName, Action releaseLockAction) {
_service = service;
_name = name; _name = name;
_internalName = internalName;
_releaseLockAction = releaseLockAction;
_count = 1; _count = 1;
} }
public string Name { string IDistributedLock.Name {
get { get {
return _name; return _name;
} }
} }
public void Increment() { internal string InternalName {
get {
return _internalName;
}
}
internal void Increment() {
_count++; _count++;
} }
public void Dispose() { public void Dispose() {
_count--; _count--;
if (_count == 0) { if (_count == 0)
Monitor.Exit(String.Intern(_name)); _releaseLockAction();
_service.ReleaseDistributedLock(this);
}
} }
} }
} }

View File

@@ -1,5 +1,5 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Generic;
using System.Data; using System.Data;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@@ -21,7 +21,8 @@ namespace Orchard.Tasks.Locking.Services {
private readonly ILifetimeScope _lifetimeScope; private readonly ILifetimeScope _lifetimeScope;
private readonly IClock _clock; private readonly IClock _clock;
private readonly ShellSettings _shellSettings; private readonly ShellSettings _shellSettings;
private readonly ConcurrentDictionary<string, DistributedLock> _locks; private readonly Dictionary<string, DistributedLock> _locks;
private readonly TimeSpan _defaultRepeatInterval;
public DistributedLockService( public DistributedLockService(
IMachineNameProvider machineNameProvider, IMachineNameProvider machineNameProvider,
@@ -32,183 +33,182 @@ namespace Orchard.Tasks.Locking.Services {
_lifetimeScope = lifetimeScope; _lifetimeScope = lifetimeScope;
_shellSettings = shellSettings; _shellSettings = shellSettings;
_machineNameProvider = machineNameProvider; _machineNameProvider = machineNameProvider;
_locks = new ConcurrentDictionary<string, DistributedLock>(); _locks = new Dictionary<string, DistributedLock>();
_defaultRepeatInterval = TimeSpan.FromMilliseconds(500);
} }
public bool DisableMonitorLock { get; set; }
public bool TryAcquireLock(string name, TimeSpan? maxValidFor, TimeSpan? timeout, out IDistributedLock dLock) { public bool TryAcquireLock(string name, TimeSpan? maxValidFor, TimeSpan? timeout, out IDistributedLock dLock) {
try { try {
dLock = AcquireLock(name, maxValidFor, timeout); dLock = AcquireLockInternal(name, maxValidFor, timeout, throwOnTimeout: false);
return dLock != null;
if (dLock != null) {
Logger.Debug("Successfully acquired lock '{0}'.", name);
return true;
} }
catch {
Logger.Warning("Failed to acquire lock '{0}' within the specified timeout ({1}).", name, timeout);
}
catch (Exception ex) {
Logger.Error(ex, "Error while trying to acquire lock '{0}'.", name);
// TODO: Is it correct to not throw here? Should we instead ONLY swallow TimeoutException?
}
dLock = null; dLock = null;
return false; return false;
} }
}
public IDistributedLock AcquireLock(string name, TimeSpan? maxValidFor, TimeSpan? timeout) { public IDistributedLock AcquireLock(string name, TimeSpan? maxValidFor, TimeSpan? timeout) {
DistributedLock dLock = null;
try { try {
var acquired = Poll(() => (dLock = AcquireLockInternal(name, maxValidFor)) != null, timeout); DistributedLock result = AcquireLockInternal(name, maxValidFor, timeout, throwOnTimeout: true);
if (acquired)
Logger.Debug("Successfully acquired lock '{0}'.", name); Logger.Debug("Successfully acquired lock '{0}'.", name);
else return result;
Logger.Debug("Failed to acquire lock '{0}' within the specified timeout.", name);
} }
catch (Exception ex) { catch (Exception ex) {
Logger.Error(ex, "Error while trying to acquire lock '{0}'.", name); Logger.Error(ex, "Error while trying to acquire lock '{0}'.", name);
throw; throw;
} }
}
if (dLock == null && timeout != null) private DistributedLock AcquireLockInternal(string name, TimeSpan? maxValidFor, TimeSpan? timeout, bool throwOnTimeout) {
throw new TimeoutException(String.Format("Failed to acquire lock '{0}' within the specified timeout ({1}).", name, timeout)); var internalName = GetInternalLockName(name);
var monitorTimeout = timeout.HasValue ? timeout.Value : TimeSpan.FromMilliseconds(-1); // -1 ms is .NET magic number for "infinite".
var monitorObj = String.Intern(String.Format("{0}:{1}", _machineNameProvider.GetMachineName(), internalName));
if (!Monitor.TryEnter(monitorObj, monitorTimeout)) {
Logger.Debug("Could not enter local monitor for lock '{0}' within the specified timeout ({1}).", internalName, timeout);
if (throwOnTimeout)
throw new TimeoutException(String.Format("Failed to acquire lock '{0}' within the specified timeout ({1}).", internalName, timeout));
return null;
}
Logger.Debug("Successfully entered local monitor for lock '{0}'.", internalName);
try {
DistributedLock dLock = null;
// If there's already a distributed lock object in our dictionary, that means
// this acquisition is a reentrance. Use the existing lock object from the
// dictionary but increment its count.
if (_locks.TryGetValue(monitorObj, out dLock)) {
Logger.Debug("Current thread is re-entering lock '{0}'; incrementing count.", internalName);
dLock.Increment();
}
else {
// No distributed lock object existed in our dictionary. Try to take ownership
// of database record until timeout expires, and if successful create a distributed
// lock object and add it to our dictionary.
var success = RepeatUntilTimeout(timeout, _defaultRepeatInterval, () => {
if (EnsureDistributedLockRecord(internalName, maxValidFor)) {
Logger.Debug("Record for lock '{0}' already owned by current machine or was successfully created; creating lock object.", internalName);
dLock = new DistributedLock(name, internalName, releaseLockAction: () => {
Monitor.Exit(monitorObj);
DeleteDistributedLockRecord(internalName);
});
_locks.Add(monitorObj, dLock);
return true;
}
return false;
});
if (!success) {
Logger.Debug("Record for lock '{0}' could not be created for current machine within the specified timeout ({1}).", internalName, timeout);
if (throwOnTimeout)
throw new TimeoutException(String.Format("Failed to acquire lock '{0}' within the specified timeout ({1}).", internalName, timeout));
return null;
}
}
return dLock; return dLock;
} }
catch (Exception ex) {
Monitor.Exit(monitorObj);
public void ReleaseDistributedLock(DistributedLock dLock) { Logger.Error(ex, "An error occurred while trying to acquire lock '{0}'.", internalName);
throw;
}
}
private bool EnsureDistributedLockRecord(string internalName, TimeSpan? maxValidFor) {
var localMachineName = _machineNameProvider.GetMachineName();
var hasLockRecord = false;
ExecuteOnSeparateTransaction(repository => {
// Try to find a valid lock record in the database.
var record = repository.Table.FirstOrDefault(x => x.Name == internalName && (x.ValidUntilUtc == null || x.ValidUntilUtc >= _clock.UtcNow));
if (record == null) {
// No record existed, so we're good to create a new one.
Logger.Debug("No valid record was found for lock '{0}'; creating a new record.", internalName);
repository.Create(new DistributedLockRecord {
Name = internalName,
MachineName = localMachineName,
CreatedUtc = _clock.UtcNow,
ValidUntilUtc = maxValidFor.HasValue ? _clock.UtcNow + maxValidFor.Value : default(DateTime?)
});
hasLockRecord = true;
}
else if (record.MachineName == localMachineName) {
// Existing lock was for correct machine name => lock record exists.
Logger.Debug("Found a valid record for lock '{0}' and current local machine name '{1}'.", internalName, localMachineName);
hasLockRecord = true;
}
});
return hasLockRecord;
}
private void DeleteDistributedLockRecord(string internalName) {
try { try {
var record = GetDistributedLockRecordByName(dLock.Name); ExecuteOnSeparateTransaction(repository => {
var record = repository.Table.FirstOrDefault(x => x.Name == internalName);
if (record == null) if (record == null)
throw new OrchardException(T("No lock record could be found in the database for lock '{0}'.", dLock.Name)); throw new Exception(String.Format("No record could be found in the database for lock '{0}'.", internalName));
repository.Delete(record);
TryCommitNewTransaction(repository => repository.Delete(record)); Logger.Debug("Successfully deleted record for lock '{0}'.", internalName);
});
} }
catch (Exception ex) { catch (Exception ex) {
if (ex.IsFatal()) if (ex.IsFatal())
throw; throw;
Logger.Error(ex, "An non-fatal error occurred while releasing lock '{0}'.", dLock.Name); Logger.Warning(ex, "An error occurred while deleting record for lock '{0}'.", internalName);
} }
} }
private DistributedLockRecord GetDistributedLockRecordByName(string name) { private bool RepeatUntilTimeout(TimeSpan? timeout, TimeSpan repeatInterval, Func<bool> action) {
DistributedLockRecord result = null; bool success;
TryCommitNewTransaction(repository => {
result = repository.Table.FirstOrDefault(x =>
x.Name == name
);
});
return result;
}
private DistributedLockRecord GetValidDistributedLockRecordByName(string name) {
DistributedLockRecord result = null;
TryCommitNewTransaction(repository => {
result = repository.Table.FirstOrDefault(x =>
x.Name == name && (x.ValidUntilUtc == null || x.ValidUntilUtc >= _clock.UtcNow)
);
});
return result;
}
private DistributedLock AcquireLockInternal(string name, TimeSpan? maxValidFor) {
try {
name = GetTenantLockName(name);
if (!DisableMonitorLock && !Monitor.TryEnter(String.Intern(name)))
return null;
DistributedLock dLock = null;
// Return the existing lock in case of reentrancy.
if(!DisableMonitorLock && _locks.TryGetValue(name, out dLock)) {
dLock.Increment();
return dLock;
}
// Find an existing active lock, if any.
var record = GetValidDistributedLockRecordByName(name);
// The current owner name (based on machine name).
var canAcquireLock = false;
// Check if there's already an active lock.
if (record != null) {
// Check if the machine name assigned to the lock is the one trying to acquire it.
if (record.MachineName == _machineNameProvider.GetMachineName()) {
canAcquireLock = true;
}
}
else {
// No one has an active lock yet, so good to go.
record = new DistributedLockRecord {
Name = name,
MachineName = _machineNameProvider.GetMachineName(),
CreatedUtc = _clock.UtcNow,
ValidUntilUtc = maxValidFor != null ? _clock.UtcNow + maxValidFor : null
};
canAcquireLock = TryCommitNewTransaction(repository => {
repository.Create(record);
});
}
if (!canAcquireLock) {
return null;
}
dLock = new DistributedLock(this, name);
if (!DisableMonitorLock) {
_locks.TryAdd(name, dLock);
}
return dLock;
}
catch (Exception ex) {
Monitor.Exit(String.Intern(name));
Logger.Error(ex, "An error occurred while trying to acquire a lock.");
throw;
}
}
private string GetTenantLockName(string name) {
return _shellSettings.Name + ":" + name;
}
/// <summary>
/// Executes the specified function until it returns true, for the specified amount of time, or indefinitely if no timeout was given.
/// </summary>
/// <param name="operation">The operation to repeatedly execute until it returns true.</param>
/// <param name="timeout">The amount of time to retry executing the function. If null is specified, the specified function is executed indefinitely until it returns true.</param>
/// <returns>Returns true if the specified function returned true within the specified timeout, false otherwise.</returns>
private bool Poll(Func<bool> operation, TimeSpan? timeout) {
var waitedTime = TimeSpan.Zero; var waitedTime = TimeSpan.Zero;
var waitTime = TimeSpan.FromMilliseconds(timeout.GetValueOrDefault().TotalMilliseconds / 10); while (!(success = action()) && (!timeout.HasValue || waitedTime < timeout.Value)) {
bool acquired; Task.Delay(repeatInterval).Wait();
waitedTime += repeatInterval;
while (!(acquired = operation()) && (timeout == null || waitedTime < timeout.Value)) {
Task.Delay(waitTime).ContinueWith(t => {
waitedTime += waitTime;
}).Wait();
} }
return acquired; return success;
} }
private bool TryCommitNewTransaction(Action<IRepository<DistributedLockRecord>> action) { private void ExecuteOnSeparateTransaction(Action<IRepository<DistributedLockRecord>> action) {
if (action == null) if (action == null)
throw new ArgumentNullException(); throw new ArgumentNullException();
try {
using (var childLifetimeScope = _lifetimeScope.BeginLifetimeScope()) { using (var childLifetimeScope = _lifetimeScope.BeginLifetimeScope()) {
var repository = childLifetimeScope.Resolve<IRepository<DistributedLockRecord>>(); var repository = childLifetimeScope.Resolve<IRepository<DistributedLockRecord>>();
var transactionManager = childLifetimeScope.Resolve<ITransactionManager>(); var transactionManager = childLifetimeScope.Resolve<ITransactionManager>();
transactionManager.RequireNew(IsolationLevel.ReadCommitted); transactionManager.RequireNew(IsolationLevel.ReadCommitted);
action(repository); action(repository);
} }
}
return true; private string GetInternalLockName(string name) {
} // Prefix the requested lock name by a constant and the tenant name.
catch { return String.Format("DistributedLock:{0}:{1}", _shellSettings.Name, name);
return false;
}
} }
} }
} }