Added support for eternal distributed locks.

Eternal locks never expire until they are explicitly released.
This commit is contained in:
Sipke Schoorstra
2015-08-22 22:55:00 +01:00
parent c5b0cac24a
commit 86bae087fa
5 changed files with 138 additions and 78 deletions

View File

@@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Autofac;
using NHibernate.Linq;
using NUnit.Framework;
using Orchard.Data;
using Orchard.Environment;
@@ -189,7 +187,37 @@ namespace Orchard.Tests.Tasks {
Assert.That(acquired, Is.False);
}
private DistributedLockRecord CreateLockRecord(int count, DateTime createdUtc, DateTime validUntilUtc, string machineName, int? threadId) {
[Test]
public void ActiveLockWithUndefinedValidUntilNeverExpires() {
CreateNonExpiredActiveLockThatNeverExpires("Other Machine", null);
_clock.Advance(DateTime.MaxValue - _clock.UtcNow); // Fast forward to the End of Time.
DistributedLock @lock;
var acquired = _distributedLockService.TryAcquireLockForThread(LockName, TimeSpan.FromMinutes(1), null, out @lock);
Assert.That(acquired, Is.False);
}
[Test]
public void ActiveLockWithUndefinedValidUntilNeverExpiresUntilReleased() {
DistributedLock @lock;
// Create a never expiring lock.
_machineNameProvider.MachineName = "Orchard Test Machine 1";
var attempt1 = _distributedLockService.TryAcquireLockForThread(LockName, maxValidFor: null, timeout: null, @lock: out @lock);
// Release the lock.
_distributedLockService.ReleaseLock(@lock);
// Acquire the lock from another machine.
_machineNameProvider.MachineName = "Orchard Test Machine 2";
var attempt2 = _distributedLockService.TryAcquireLockForThread(LockName, maxValidFor: null, timeout: null, @lock: out @lock);
// Validate the results.
Assert.That(attempt1, Is.True);
Assert.That(attempt2, Is.True);
}
private DistributedLockRecord CreateLockRecord(int count, DateTime createdUtc, DateTime? validUntilUtc, string machineName, int? threadId) {
var record = new DistributedLockRecord {
Name = LockName,
Count = count,
@@ -219,6 +247,11 @@ namespace Orchard.Tests.Tasks {
return CreateLockRecord(1, now, now - TimeSpan.FromHours(1), machineName, threadId);
}
private DistributedLockRecord CreateNonExpiredActiveLockThatNeverExpires(string machineName, int? threadId) {
var now = _clock.UtcNow;
return CreateLockRecord(1, now, null, machineName, threadId);
}
private string GetMachineName() {
return _machineNameProvider.GetMachineName();
}

View File

@@ -12,7 +12,7 @@ namespace Orchard.Tasks.Locking.Migrations {
.Column<int>("ThreadId", column => column.Nullable())
.Column<int>("Count")
.Column<DateTime>("CreatedUtc")
.Column<DateTime>("ValidUntilUtc"));
.Column<DateTime>("ValidUntilUtc", column => column.Nullable()));
SchemaBuilder.AlterTable("DistributedLockRecord", table => {
table.CreateIndex("IDX_DistributedLockRecord_Name_ValidUntilUtc_Count", "Name", "ValidUntilUtc", "Count");

View File

@@ -8,6 +8,6 @@ namespace Orchard.Tasks.Locking.Records {
public virtual int? ThreadId { get; set; }
public virtual int Count { get; set; }
public virtual DateTime CreatedUtc { get; set; }
public virtual DateTime ValidUntilUtc { get; set; }
public virtual DateTime? ValidUntilUtc { get; set; }
}
}

View File

@@ -25,19 +25,19 @@ namespace Orchard.Tasks.Locking.Services {
_threadProvider = threadProvider;
}
public bool TryAcquireLockForMachine(string name, TimeSpan maxValidFor, TimeSpan? timeout, out DistributedLock @lock) {
public bool TryAcquireLockForMachine(string name, TimeSpan? maxValidFor, TimeSpan? timeout, out DistributedLock @lock) {
return TryAcquireLock(name, maxValidFor, timeout, GetMachineName(), null, out @lock);
}
public DistributedLock AcquireLockForMachine(string name, TimeSpan maxValidFor, TimeSpan? timeout) {
public DistributedLock AcquireLockForMachine(string name, TimeSpan? maxValidFor, TimeSpan? timeout) {
return AcquireLock(name, maxValidFor, timeout, GetMachineName(), null);
}
public bool TryAcquireLockForThread(string name, TimeSpan maxValidFor, TimeSpan? timeout, out DistributedLock @lock) {
public bool TryAcquireLockForThread(string name, TimeSpan? maxValidFor, TimeSpan? timeout, out DistributedLock @lock) {
return TryAcquireLock(name, maxValidFor, timeout, GetMachineName(), GetThreadId(), out @lock);
}
public DistributedLock AcquireLockForThread(string name, TimeSpan maxValidFor, TimeSpan? timeout) {
public DistributedLock AcquireLockForThread(string name, TimeSpan? maxValidFor, TimeSpan? timeout) {
return AcquireLock(name, maxValidFor, timeout, GetMachineName(), GetThreadId());
}
@@ -68,7 +68,7 @@ namespace Orchard.Tasks.Locking.Services {
}
}
private bool TryAcquireLock(string name, TimeSpan maxValidFor, TimeSpan? timeout, string machineName, int? threadId, out DistributedLock @lock) {
private bool TryAcquireLock(string name, TimeSpan? maxValidFor, TimeSpan? timeout, string machineName, int? threadId, out DistributedLock @lock) {
@lock = AcquireLockInternal(name, maxValidFor, machineName, threadId, timeout ?? TimeSpan.Zero);
if (@lock != null)
return true;
@@ -76,7 +76,7 @@ namespace Orchard.Tasks.Locking.Services {
return false;
}
private DistributedLock AcquireLock(string name, TimeSpan maxValidFor, TimeSpan? timeout, string machineName, int? threadId) {
private DistributedLock AcquireLock(string name, TimeSpan? maxValidFor, TimeSpan? timeout, string machineName, int? threadId) {
var @lock = AcquireLockInternal(name, maxValidFor, machineName, threadId, timeout);
if (@lock != null)
return @lock;
@@ -84,7 +84,7 @@ namespace Orchard.Tasks.Locking.Services {
throw new TimeoutException(String.Format("Failed to acquire a lock named '{0}' within the specified timeout ('{1}').", name, timeout));
}
private DistributedLock AcquireLockInternal(string name, TimeSpan maxValidFor, string machineName, int? threadId, TimeSpan? timeout = null) {
private DistributedLock AcquireLockInternal(string name, TimeSpan? maxValidFor, string machineName, int? threadId, TimeSpan? timeout = null) {
try {
DistributedLockRecord record = null;
var acquired = Poll(() => (record = AcquireLockRecord(name, maxValidFor, machineName, threadId)) != null, timeout);
@@ -109,65 +109,63 @@ namespace Orchard.Tasks.Locking.Services {
return null;
}
private DistributedLockRecord AcquireLockRecord(string name, TimeSpan maxValidFor, string machineName, int? threadId) {
//lock (_transactionManagerLock) {
var childLifetimeScope = CreateChildLifetimeScope(name);
private DistributedLockRecord AcquireLockRecord(string name, TimeSpan? maxValidFor, string machineName, int? threadId) {
var childLifetimeScope = CreateChildLifetimeScope(name);
try {
var transactionManager = childLifetimeScope.Resolve<ITransactionManager>();
transactionManager.RequireNew(IsolationLevel.ReadCommitted);
try {
var transactionManager = childLifetimeScope.Resolve<ITransactionManager>();
transactionManager.RequireNew(IsolationLevel.ReadCommitted);
// This way we can create a nested transaction scope instead of having the unwanted effect
// of manipulating the transaction of the caller.
var repository = childLifetimeScope.Resolve<IRepository<DistributedLockRecord>>();
// This way we can create a nested transaction scope instead of having the unwanted effect
// of manipulating the transaction of the caller.
var repository = childLifetimeScope.Resolve<IRepository<DistributedLockRecord>>();
// Find an existing, active lock, if any.
var record = repository.Table.FirstOrDefault(x => x.Name == name && x.ValidUntilUtc >= _clock.UtcNow && x.Count > 0);
// Find an existing, active lock, if any.
var record = repository.Table.FirstOrDefault(x => x.Name == name && (x.ValidUntilUtc == null || x.ValidUntilUtc >= _clock.UtcNow) && x.Count > 0);
// The current owner name (based on machine name and current thread ID).
var canAcquireLock = false;
// The current owner name (based on machine name and current thread ID).
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 == machineName) {
if (record.ThreadId != threadId)
throw new InvalidOperationException(
threadId == null
? "An attempt to acquire a lock for a machine was detected while the requested lock is already assigned to a specific thread."
: "An attempt to acquire a lock for a thread was detected while the requested lock is already assigned to a machine.");
// 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 == machineName) {
if (record.ThreadId != threadId)
throw new InvalidOperationException(
threadId == null
? "An attempt to acquire a lock for a machine was detected while the requested lock is already assigned to a specific thread."
: "An attempt to acquire a lock for a thread was detected while the requested lock is already assigned to a machine.");
record.Count++;
canAcquireLock = true;
}
}
else {
// No one has an active lock yet, so good to go.
record = new DistributedLockRecord {
Name = name,
MachineName = machineName,
ThreadId = threadId,
Count = 1,
CreatedUtc = _clock.UtcNow,
ValidUntilUtc = _clock.UtcNow + maxValidFor
};
repository.Create(record);
record.Count++;
canAcquireLock = true;
}
}
else {
// No one has an active lock yet, so good to go.
record = new DistributedLockRecord {
Name = name,
MachineName = machineName,
ThreadId = threadId,
Count = 1,
CreatedUtc = _clock.UtcNow,
ValidUntilUtc = maxValidFor != null ? _clock.UtcNow + maxValidFor : null
};
repository.Create(record);
canAcquireLock = true;
}
if (!canAcquireLock)
return null;
if (!canAcquireLock)
return null;
return record;
}
catch (Exception ex) {
Logger.Error(ex, "An error occurred while trying to acquire a lock.");
throw;
}
finally {
childLifetimeScope.Dispose();
}
//}
return record;
}
catch (Exception ex) {
Logger.Error(ex, "An error occurred while trying to acquire a lock.");
throw;
}
finally {
childLifetimeScope.Dispose();
}
}
/// <summary>

View File

@@ -13,37 +13,37 @@ namespace Orchard.Tasks.Locking.Services {
/// <param name="timeout">The amount of time to wait for the lock to be acquired before timing out. A null value will cause the method to return immedieately if no lock could be acquired.</param>
/// <param name="lock">The acquired lock.</param>
/// <returns>Returns true if a lock was successfully acquired, false otherwise.</returns>
bool TryAcquireLockForMachine(string name, TimeSpan maxValidFor, TimeSpan? timeout, out DistributedLock @lock);
bool TryAcquireLockForMachine(string name, TimeSpan? maxValidFor, TimeSpan? timeout, out DistributedLock @lock);
/// <summary>
/// Acquires a lock with the specified parameters for the current machine.
/// </summary>
/// <param name="name">The name to use for the 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>
/// <param name="maxValidFor">The maximum amount of time the lock is allowed. This is a safety net in case the caller fails to release the lock. If null is specified, the lock never expires until it's released by the owner.</param>
/// <param name="timeout">The amount of time to wait for the lock to be acquired before timing out. A null value will cause the method to block indefinitely until a lock can be acquired.</param>
/// <returns>Returns a lock.</returns>
/// <exception cref="TimeoutException">Throws a TimeoutException if no lock could be acquired in time.</exception>
DistributedLock AcquireLockForMachine(string name, TimeSpan maxLifetime, TimeSpan? timeout);
DistributedLock AcquireLockForMachine(string name, TimeSpan? maxValidFor, TimeSpan? timeout);
/// <summary>
/// Tries to acquire a lock on the specified name for the current thread.
/// </summary>
/// <param name="name">The name to use for the lock.</param>
/// <param name="maxValidFor">The maximum amount of time the lock is allowed. This is a safety net in case the caller fails to release the lock.</param>
/// <param name="maxValidFor">The maximum amount of time the lock is allowed. This is a safety net in case the caller fails to release the lock. If null is specified, the lock never expires until it's released by the owner.</param>
/// <param name="timeout">The amount of time to wait for the lock to be acquired before timing out. A null value will cause the method to return immedieately if no lock could be acquired.</param>
/// <param name="lock">The acquired lock.</param>
/// <returns>Returns true if a lock was successfully acquired, false otherwise.</returns>
bool TryAcquireLockForThread(string name, TimeSpan maxValidFor, TimeSpan? timeout, out DistributedLock @lock);
bool TryAcquireLockForThread(string name, TimeSpan? maxValidFor, TimeSpan? timeout, out DistributedLock @lock);
/// <summary>
/// Acquires a lock with the specified parameters for the current thread.
/// </summary>
/// <param name="name">The name to use for the 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>
/// <param name="maxValidFor">The maximum amount of time the lock is allowed. This is a safety net in case the caller fails to release the lock. If null is specified, the lock never expires until it's released by the owner.</param>
/// <param name="timeout">The amount of time to wait for the lock to be acquired before timing out. A null value will cause the method to block indefinitely until a lock can be acquired.</param>
/// <returns>Returns a lock.</returns>
/// <exception cref="TimeoutException">Throws a TimeoutException if no lock could be acquired in time.</exception>
DistributedLock AcquireLockForThread(string name, TimeSpan maxLifetime, TimeSpan? timeout);
DistributedLock AcquireLockForThread(string name, TimeSpan? maxValidFor, TimeSpan? timeout);
/// <summary>
/// Disposes the specified lock.
@@ -56,44 +56,73 @@ namespace Orchard.Tasks.Locking.Services {
/// Tries to acquire a lock on the specified name for the current machine.
/// </summary>
/// <param name="name">The name to use for the lock.</param>
/// <param name="maxValidFor">The maximum amount of time the lock is allowed. This is a safety net in case the caller fails to release the lock.</param>
/// <param name="maxValidFor">The maximum amount of time the lock is allowed. This is a safety net in case the caller fails to release the lock. If null is specified, the lock never expires until it's released by the owner.</param>
/// <param name="lock">The acquired lock.</param>
/// <returns>Returns true if a lock was successfully acquired, false otherwise.</returns>
public static bool TryAcquireLockForMachine(this IDistributedLockService service, string name, TimeSpan maxValidFor, out DistributedLock @lock) {
public static bool TryAcquireLockForMachine(this IDistributedLockService service, string name, TimeSpan? maxValidFor, out DistributedLock @lock) {
return service.TryAcquireLockForMachine(name, maxValidFor, null, out @lock);
}
/// <summary>
/// Tries to acquire a lock on the specified name for the current machine.
/// </summary>
/// <param name="name">The name to use for the lock.</param>
/// <param name="lock">The acquired lock.</param>
/// <returns>Returns true if a lock was successfully acquired, false otherwise.</returns>
public static bool TryAcquireLockForMachine(this IDistributedLockService service, string name, out DistributedLock @lock) {
return service.TryAcquireLockForMachine(name, null, null, out @lock);
}
/// <summary>
/// Acquires a lock with the specified parameters for the current machine.
/// </summary>
/// <param name="name">The name to use for the 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>
/// <param name="maxValidFor">The maximum amount of time the lock is allowed. This is a safety net in case the caller fails to release the lock. If null is specified, the lock never expires until it's released by the owner.</param>
/// <returns>Returns a lock.</returns>
/// <exception cref="TimeoutException">Throws a TimeoutException if no lock could be acquired in time.</exception>
public static DistributedLock AcquireLockForMachine(this IDistributedLockService service, string name, TimeSpan maxLifetime) {
return service.AcquireLockForMachine(name, maxLifetime, null);
public static DistributedLock AcquireLockForMachine(this IDistributedLockService service, string name, TimeSpan? maxValidFor) {
return service.AcquireLockForMachine(name, maxValidFor, null);
}
/// <summary>
/// Acquires a lock with the specified parameters for the current machine.
/// </summary>
/// <param name="name">The name to use for the lock.</param>
/// <returns>Returns a lock.</returns>
/// <exception cref="TimeoutException">Throws a TimeoutException if no lock could be acquired in time.</exception>
public static DistributedLock AcquireLockForMachine(this IDistributedLockService service, string name) {
return service.AcquireLockForMachine(name, null, null);
}
/// <summary>
/// Tries to acquire a lock on the specified name for the current thread.
/// </summary>
/// <param name="name">The name to use for the lock.</param>
/// <param name="maxValidFor">The maximum amount of time the lock is allowed. This is a safety net in case the caller fails to release the lock.</param>
/// <param name="maxValidFor">The maximum amount of time the lock is allowed. This is a safety net in case the caller fails to release the lock. If null is specified, the lock never expires until it's released by the owner.</param>
/// <param name="lock">The acquired lock.</param>
/// <returns>Returns true if a lock was successfully acquired, false otherwise.</returns>
public static bool TryAcquireLockForThread(this IDistributedLockService service, string name, TimeSpan maxValidFor, out DistributedLock @lock) {
public static bool TryAcquireLockForThread(this IDistributedLockService service, string name, TimeSpan? maxValidFor, out DistributedLock @lock) {
return service.TryAcquireLockForThread(name, maxValidFor, null, out @lock);
}
/// <summary>
/// Tries to acquire a lock on the specified name for the current thread.
/// </summary>
/// <param name="name">The name to use for the lock.</param>
/// <param name="lock">The acquired lock.</param>
/// <returns>Returns true if a lock was successfully acquired, false otherwise.</returns>
public static bool TryAcquireLockForThread(this IDistributedLockService service, string name, out DistributedLock @lock) {
return service.TryAcquireLockForThread(name, null, null, out @lock);
}
/// <summary>
/// Acquires a lock with the specified parameters for the current thread.
/// </summary>
/// <param name="name">The name to use for the 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 a lock.</returns>
/// <exception cref="TimeoutException">Throws a TimeoutException if no lock could be acquired in time.</exception>
public static DistributedLock AcquireLockForThread(this IDistributedLockService service, string name, TimeSpan maxLifetime) {
return service.AcquireLockForThread(name, maxLifetime, null);
public static DistributedLock AcquireLockForThread(this IDistributedLockService service, string name) {
return service.AcquireLockForThread(name, null, null);
}
}
}