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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Autofac; using Autofac;
using NHibernate.Linq;
using NUnit.Framework; using NUnit.Framework;
using Orchard.Data; using Orchard.Data;
using Orchard.Environment; using Orchard.Environment;
@@ -189,7 +187,37 @@ namespace Orchard.Tests.Tasks {
Assert.That(acquired, Is.False); 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 { var record = new DistributedLockRecord {
Name = LockName, Name = LockName,
Count = count, Count = count,
@@ -219,6 +247,11 @@ namespace Orchard.Tests.Tasks {
return CreateLockRecord(1, now, now - TimeSpan.FromHours(1), machineName, threadId); 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() { private string GetMachineName() {
return _machineNameProvider.GetMachineName(); return _machineNameProvider.GetMachineName();
} }

View File

@@ -12,7 +12,7 @@ namespace Orchard.Tasks.Locking.Migrations {
.Column<int>("ThreadId", column => column.Nullable()) .Column<int>("ThreadId", column => column.Nullable())
.Column<int>("Count") .Column<int>("Count")
.Column<DateTime>("CreatedUtc") .Column<DateTime>("CreatedUtc")
.Column<DateTime>("ValidUntilUtc")); .Column<DateTime>("ValidUntilUtc", column => column.Nullable()));
SchemaBuilder.AlterTable("DistributedLockRecord", table => { SchemaBuilder.AlterTable("DistributedLockRecord", table => {
table.CreateIndex("IDX_DistributedLockRecord_Name_ValidUntilUtc_Count", "Name", "ValidUntilUtc", "Count"); 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? ThreadId { get; set; }
public virtual int Count { get; set; } public virtual int Count { get; set; }
public virtual DateTime CreatedUtc { 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; _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); 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); 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); 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()); 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); @lock = AcquireLockInternal(name, maxValidFor, machineName, threadId, timeout ?? TimeSpan.Zero);
if (@lock != null) if (@lock != null)
return true; return true;
@@ -76,7 +76,7 @@ namespace Orchard.Tasks.Locking.Services {
return false; 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); var @lock = AcquireLockInternal(name, maxValidFor, machineName, threadId, timeout);
if (@lock != null) if (@lock != null)
return @lock; 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)); 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 { try {
DistributedLockRecord record = null; DistributedLockRecord record = null;
var acquired = Poll(() => (record = AcquireLockRecord(name, maxValidFor, machineName, threadId)) != null, timeout); var acquired = Poll(() => (record = AcquireLockRecord(name, maxValidFor, machineName, threadId)) != null, timeout);
@@ -109,65 +109,63 @@ namespace Orchard.Tasks.Locking.Services {
return null; return null;
} }
private DistributedLockRecord AcquireLockRecord(string name, TimeSpan maxValidFor, string machineName, int? threadId) { private DistributedLockRecord AcquireLockRecord(string name, TimeSpan? maxValidFor, string machineName, int? threadId) {
//lock (_transactionManagerLock) { var childLifetimeScope = CreateChildLifetimeScope(name);
var childLifetimeScope = CreateChildLifetimeScope(name);
try { try {
var transactionManager = childLifetimeScope.Resolve<ITransactionManager>(); var transactionManager = childLifetimeScope.Resolve<ITransactionManager>();
transactionManager.RequireNew(IsolationLevel.ReadCommitted); transactionManager.RequireNew(IsolationLevel.ReadCommitted);
// This way we can create a nested transaction scope instead of having the unwanted effect // This way we can create a nested transaction scope instead of having the unwanted effect
// of manipulating the transaction of the caller. // of manipulating the transaction of the caller.
var repository = childLifetimeScope.Resolve<IRepository<DistributedLockRecord>>(); var repository = childLifetimeScope.Resolve<IRepository<DistributedLockRecord>>();
// Find an existing, active lock, if any. // Find an existing, active lock, if any.
var record = repository.Table.FirstOrDefault(x => x.Name == name && x.ValidUntilUtc >= _clock.UtcNow && x.Count > 0); 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). // The current owner name (based on machine name and current thread ID).
var canAcquireLock = false; var canAcquireLock = false;
// Check if there's already an active lock. // Check if there's already an active lock.
if (record != null) { if (record != null) {
// Check if the machine name assigned to the lock is the one trying to acquire it. // Check if the machine name assigned to the lock is the one trying to acquire it.
if (record.MachineName == machineName) { if (record.MachineName == machineName) {
if (record.ThreadId != threadId) if (record.ThreadId != threadId)
throw new InvalidOperationException( throw new InvalidOperationException(
threadId == null 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 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."); : "An attempt to acquire a lock for a thread was detected while the requested lock is already assigned to a machine.");
record.Count++; 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);
canAcquireLock = true; 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) if (!canAcquireLock)
return null; return null;
return record; return record;
} }
catch (Exception ex) { catch (Exception ex) {
Logger.Error(ex, "An error occurred while trying to acquire a lock."); Logger.Error(ex, "An error occurred while trying to acquire a lock.");
throw; throw;
} }
finally { finally {
childLifetimeScope.Dispose(); childLifetimeScope.Dispose();
} }
//}
} }
/// <summary> /// <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="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> /// <param name="lock">The acquired lock.</param>
/// <returns>Returns true if a lock was successfully acquired, false otherwise.</returns> /// <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> /// <summary>
/// Acquires a lock with the specified parameters for the current machine. /// Acquires a lock with the specified parameters for the current machine.
/// </summary> /// </summary>
/// <param name="name">The name to use for the lock.</param> /// <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> /// <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> /// <returns>Returns a lock.</returns>
/// <exception cref="TimeoutException">Throws a TimeoutException if no lock could be acquired in time.</exception> /// <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> /// <summary>
/// Tries to acquire a lock on the specified name for the current thread. /// Tries to acquire a lock on the specified name for the current thread.
/// </summary> /// </summary>
/// <param name="name">The name to use for the lock.</param> /// <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="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> /// <param name="lock">The acquired lock.</param>
/// <returns>Returns true if a lock was successfully acquired, false otherwise.</returns> /// <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> /// <summary>
/// Acquires a lock with the specified parameters for the current thread. /// Acquires a lock with the specified parameters for the current thread.
/// </summary> /// </summary>
/// <param name="name">The name to use for the lock.</param> /// <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> /// <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> /// <returns>Returns a lock.</returns>
/// <exception cref="TimeoutException">Throws a TimeoutException if no lock could be acquired in time.</exception> /// <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> /// <summary>
/// Disposes the specified lock. /// 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. /// Tries to acquire a lock on the specified name for the current machine.
/// </summary> /// </summary>
/// <param name="name">The name to use for the lock.</param> /// <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> /// <param name="lock">The acquired lock.</param>
/// <returns>Returns true if a lock was successfully acquired, false otherwise.</returns> /// <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); 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> /// <summary>
/// Acquires a lock with the specified parameters for the current machine. /// Acquires a lock with the specified parameters for the current machine.
/// </summary> /// </summary>
/// <param name="name">The name to use for the lock.</param> /// <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> /// <returns>Returns a lock.</returns>
/// <exception cref="TimeoutException">Throws a TimeoutException if no lock could be acquired in time.</exception> /// <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) { public static DistributedLock AcquireLockForMachine(this IDistributedLockService service, string name, TimeSpan? maxValidFor) {
return service.AcquireLockForMachine(name, maxLifetime, null); 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> /// <summary>
/// Tries to acquire a lock on the specified name for the current thread. /// Tries to acquire a lock on the specified name for the current thread.
/// </summary> /// </summary>
/// <param name="name">The name to use for the lock.</param> /// <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> /// <param name="lock">The acquired lock.</param>
/// <returns>Returns true if a lock was successfully acquired, false otherwise.</returns> /// <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); 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> /// <summary>
/// Acquires a lock with the specified parameters for the current thread. /// Acquires a lock with the specified parameters for the current thread.
/// </summary> /// </summary>
/// <param name="name">The name to use for the lock.</param> /// <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> /// <returns>Returns a lock.</returns>
/// <exception cref="TimeoutException">Throws a TimeoutException if no lock could be acquired in time.</exception> /// <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) { public static DistributedLock AcquireLockForThread(this IDistributedLockService service, string name) {
return service.AcquireLockForThread(name, maxLifetime, null); return service.AcquireLockForThread(name, null, null);
} }
} }
} }