mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-10-15 11:44:58 +08:00
Added support for eternal distributed locks.
Eternal locks never expire until they are explicitly released.
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
@@ -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");
|
||||
|
@@ -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; }
|
||||
}
|
||||
}
|
@@ -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>
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user