From f363ac2b5fedfa7b9267a446755e6adfee8fd9aa Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 10 Aug 2015 11:56:00 +0100 Subject: [PATCH] Initial work on a distributed locking service. --- .../Modules/Orchard.TaskLease/Migrations.cs | 11 +- .../Models/DatabaseLockRecord.cs | 9 ++ .../Orchard.TaskLease.csproj | 5 + .../Services/DatabaseLock.cs | 100 ++++++++++++++++++ .../Services/ITaskLeaseService.cs | 1 + .../Services/TaskLeaseService.cs | 1 + src/Orchard/Orchard.Framework.csproj | 4 + src/Orchard/Tasks/Locking/DefaultLock.cs | 14 +++ src/Orchard/Tasks/Locking/ILock.cs | 16 +++ src/Orchard/Tasks/Locking/ILockService.cs | 27 +++++ src/Orchard/Tasks/Locking/LockService.cs | 48 +++++++++ 11 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 src/Orchard.Web/Modules/Orchard.TaskLease/Models/DatabaseLockRecord.cs create mode 100644 src/Orchard.Web/Modules/Orchard.TaskLease/Services/DatabaseLock.cs create mode 100644 src/Orchard/Tasks/Locking/DefaultLock.cs create mode 100644 src/Orchard/Tasks/Locking/ILock.cs create mode 100644 src/Orchard/Tasks/Locking/ILockService.cs create mode 100644 src/Orchard/Tasks/Locking/LockService.cs diff --git a/src/Orchard.Web/Modules/Orchard.TaskLease/Migrations.cs b/src/Orchard.Web/Modules/Orchard.TaskLease/Migrations.cs index 4f43eb7a5..464894eb5 100644 --- a/src/Orchard.Web/Modules/Orchard.TaskLease/Migrations.cs +++ b/src/Orchard.Web/Modules/Orchard.TaskLease/Migrations.cs @@ -1,6 +1,4 @@ using System; -using Orchard.ContentManagement.MetaData; -using Orchard.Core.Contents.Extensions; using Orchard.Data.Migration; namespace Orchard.TaskLease { @@ -19,5 +17,14 @@ namespace Orchard.TaskLease { return 1; } + + public int UpdateFrom1() { + SchemaBuilder.CreateTable("DatabaseLockRecord", table => table + .Column("Id", column => column.PrimaryKey().Identity()) + .Column("Name", column => column.NotNull().WithLength(256)) + .Column("AcquiredUtc")); + + return 2; + } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.TaskLease/Models/DatabaseLockRecord.cs b/src/Orchard.Web/Modules/Orchard.TaskLease/Models/DatabaseLockRecord.cs new file mode 100644 index 000000000..039e5d0d8 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.TaskLease/Models/DatabaseLockRecord.cs @@ -0,0 +1,9 @@ +using System; + +namespace Orchard.TaskLease.Models { + public class DatabaseLockRecord { + public virtual int Id { get; set; } + public virtual string Name { get; set; } + public virtual DateTime? AcquiredUtc { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.TaskLease/Orchard.TaskLease.csproj b/src/Orchard.Web/Modules/Orchard.TaskLease/Orchard.TaskLease.csproj index 7273eb307..7d5361a83 100644 --- a/src/Orchard.Web/Modules/Orchard.TaskLease/Orchard.TaskLease.csproj +++ b/src/Orchard.Web/Modules/Orchard.TaskLease/Orchard.TaskLease.csproj @@ -49,6 +49,9 @@ false + + ..\..\..\..\lib\autofac\Autofac.dll + @@ -73,6 +76,7 @@ + @@ -82,6 +86,7 @@ + diff --git a/src/Orchard.Web/Modules/Orchard.TaskLease/Services/DatabaseLock.cs b/src/Orchard.Web/Modules/Orchard.TaskLease/Services/DatabaseLock.cs new file mode 100644 index 000000000..3ca738c28 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.TaskLease/Services/DatabaseLock.cs @@ -0,0 +1,100 @@ +using System; +using System.Data; +using System.Linq; +using Autofac; +using Orchard.Data; +using Orchard.Environment.Extensions; +using Orchard.Exceptions; +using Orchard.Services; +using Orchard.TaskLease.Models; +using Orchard.Tasks.Locking; +using Orchard.Validation; + +namespace Orchard.TaskLease.Services { + + /// + /// Provides a database driven implementation of + /// + [OrchardSuppressDependency("Orchard.Tasks.Locking.DefaultLock")] + public class DatabaseLock : ILock { + private readonly ILifetimeScope _lifetimeScope; + private readonly IClock _clock; + private string _name; + private bool _isAcquired; + private int _id; + private bool _isDisposed; + + public DatabaseLock(ILifetimeScope lifetimeScope, IClock clock) { + _lifetimeScope = lifetimeScope; + _clock = clock; + } + + public bool TryAcquire(string name, TimeSpan maxLifetime) { + Argument.ThrowIfNullOrEmpty(name, "name"); + + if (name.Length > 256) + throw new ArgumentException("The lock's name can't be longer than 256 characters."); + + // This way we can create a nested transaction scope instead of having the unwanted effect + // of manipulating the transaction of the caller. + using (var scope = BeginLifeTimeScope(name)) { + var repository = scope.Resolve>(); + var record = repository.Table.FirstOrDefault(x => x.Name == name); + + if (record != null) { + // There is a nexisting lock, but check if it has expired. + var isExpired = record.AcquiredUtc + maxLifetime < _clock.UtcNow; + if (isExpired) { + repository.Delete(record); + record = null; + } + } + + var canAcquire = record == null; + + if (canAcquire) { + record = new DatabaseLockRecord { + Name = name, + AcquiredUtc = _clock.UtcNow + }; + repository.Create(record); + repository.Flush(); + + _name = name; + _isAcquired = true; + _id = record.Id; + } + + return canAcquire; + } + } + + // This will be called at least by the IoC container when the request ends. + public void Dispose() { + if (_isDisposed || !_isAcquired) return; + + _isDisposed = true; + + using (var scope = BeginLifeTimeScope(_name)) { + try { + var repository = scope.Resolve>(); + var record = repository.Get(_id); + + if (record != null) { + repository.Delete(record); + repository.Flush(); + } + } + catch (Exception ex) { + if (ex.IsFatal()) throw; + } + } + } + + private ILifetimeScope BeginLifeTimeScope(string name) { + var scope = _lifetimeScope.BeginLifetimeScope("Orchard.Tasks.Locking.Database." + name); + scope.Resolve().RequireNew(IsolationLevel.ReadCommitted); + return scope; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.TaskLease/Services/ITaskLeaseService.cs b/src/Orchard.Web/Modules/Orchard.TaskLease/Services/ITaskLeaseService.cs index bf3abfb2c..d9b5effd5 100644 --- a/src/Orchard.Web/Modules/Orchard.TaskLease/Services/ITaskLeaseService.cs +++ b/src/Orchard.Web/Modules/Orchard.TaskLease/Services/ITaskLeaseService.cs @@ -7,6 +7,7 @@ namespace Orchard.TaskLease.Services { /// Describes a service to save and acquire task leases. A task lease can't be acquired by two different machines, /// for a specific amount of time. Optionnally a State can be saved along with the lease. /// + [Obsolete("Use Orchard.Tasks.Locking.ILockService instead.")] public interface ITaskLeaseService : IDependency { /// diff --git a/src/Orchard.Web/Modules/Orchard.TaskLease/Services/TaskLeaseService.cs b/src/Orchard.Web/Modules/Orchard.TaskLease/Services/TaskLeaseService.cs index b8f17306c..0d58f1c28 100644 --- a/src/Orchard.Web/Modules/Orchard.TaskLease/Services/TaskLeaseService.cs +++ b/src/Orchard.Web/Modules/Orchard.TaskLease/Services/TaskLeaseService.cs @@ -9,6 +9,7 @@ namespace Orchard.TaskLease.Services { /// /// Provides a database driven implementation of /// + [Obsolete("Use Orchard.Tasks.Locking.ILockService instead.")] public class TaskLeaseService : ITaskLeaseService { private readonly IRepository _repository; diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index 216323343..9693a7400 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -397,6 +397,10 @@ + + + + diff --git a/src/Orchard/Tasks/Locking/DefaultLock.cs b/src/Orchard/Tasks/Locking/DefaultLock.cs new file mode 100644 index 000000000..9ece47036 --- /dev/null +++ b/src/Orchard/Tasks/Locking/DefaultLock.cs @@ -0,0 +1,14 @@ +using System; + +namespace Orchard.Tasks.Locking { + public class DefaultLock : ILock { + + public bool TryAcquire(string name, TimeSpan maxLifetime) { + return true; + } + + public void Dispose() { + // Noop. + } + } +} \ No newline at end of file diff --git a/src/Orchard/Tasks/Locking/ILock.cs b/src/Orchard/Tasks/Locking/ILock.cs new file mode 100644 index 000000000..a80237d85 --- /dev/null +++ b/src/Orchard/Tasks/Locking/ILock.cs @@ -0,0 +1,16 @@ +using System; + +namespace Orchard.Tasks.Locking { + /// + /// Provides a lock on a provided name. + /// + public interface ILock : ITransientDependency, IDisposable { + /// + /// Tries to acquire a lock on the specified name. + /// + /// The name to use for the lock. + /// The maximum amount of time the lock is allowed. This is a safety net in case the caller fails to release the lock. + /// Returns true if a lock was acquired, false otherwise. + bool TryAcquire(string name, TimeSpan maxLifetime); + } +} \ No newline at end of file diff --git a/src/Orchard/Tasks/Locking/ILockService.cs b/src/Orchard/Tasks/Locking/ILockService.cs new file mode 100644 index 000000000..fbb0928cb --- /dev/null +++ b/src/Orchard/Tasks/Locking/ILockService.cs @@ -0,0 +1,27 @@ +using System; + +namespace Orchard.Tasks.Locking { + /// + /// Provides distributed locking functionality. + /// + public interface ILockService : IDependency { + /// + /// Tries to acquire a lock on the specified name. + /// + /// The name to use for the lock. + /// The maximum amount of time the lock is allowed. This is a safety net in case the caller fails to release the lock. + /// The amount of time to wait for the lock to be acquired before timing out + /// Returns a lock if one was successfully acquired, null otherwise. + ILock TryAcquireLock(string name, TimeSpan maxLifetime, TimeSpan timeout); + + /// + /// Acquires a lock with the specified parameters. + /// + /// The name to use for the lock. + /// The maximum amount of time the lock is allowed. This is a safety net in case the caller fails to release the lock. + /// The amount of time to wait for the lock to be acquired before timing out + /// Returns a lock if one was successfully acquired. + /// Thrown if the lock couldn't be acquired. + ILock AcquireLock(string name, TimeSpan maxLifetime, TimeSpan timeout); + } +} \ No newline at end of file diff --git a/src/Orchard/Tasks/Locking/LockService.cs b/src/Orchard/Tasks/Locking/LockService.cs new file mode 100644 index 000000000..395408926 --- /dev/null +++ b/src/Orchard/Tasks/Locking/LockService.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading.Tasks; +using Orchard.Environment; +using Orchard.Logging; + +namespace Orchard.Tasks.Locking { + public class LockService : ILockService { + private readonly Work _lock; + private readonly IMachineNameProvider _machineNameProvider; + + public LockService(Work @lock, IMachineNameProvider machineNameProvider) { + _lock = @lock; + _machineNameProvider = machineNameProvider; + Logger = NullLogger.Instance; + } + + public ILogger Logger { get; set; } + + public ILock TryAcquireLock(string name, TimeSpan maxLifetime, TimeSpan timeout) { + var waitedTime = TimeSpan.Zero; + var waitTime = TimeSpan.FromMilliseconds(timeout.TotalMilliseconds / 10); + var @lock = _lock.Value; + bool acquired; + + while (!(acquired = @lock.TryAcquire(name, maxLifetime)) && waitedTime < timeout) { + Task.Delay(timeout).ContinueWith(t => { + waitedTime += waitTime; + }).Wait(); + } + + var machineName = _machineNameProvider.GetMachineName(); + + if (acquired) { + Logger.Debug(String.Format("Successfully acquired a lock named {0} on machine {1}.", name, machineName)); + return @lock; + } + + Logger.Debug(String.Format("Failed to acquire a lock named {0} on machine {1}.", name, machineName)); + return null; + } + + public ILock AcquireLock(string name, TimeSpan maxLifetime, TimeSpan timeout) { + var lockResult = TryAcquireLock(name, maxLifetime, timeout); + if (lockResult != null) return lockResult; + throw new TimeoutException(String.Format("No lock for \"{0}\" could not be acquired within {1} milliseconds.", name, timeout.TotalMilliseconds)); + } + } +} \ No newline at end of file