mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2026-02-09 09:16:41 +08:00
Initial work on a distributed locking service.
This commit is contained in:
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using Orchard.ContentManagement.MetaData;
|
|
||||||
using Orchard.Core.Contents.Extensions;
|
|
||||||
using Orchard.Data.Migration;
|
using Orchard.Data.Migration;
|
||||||
|
|
||||||
namespace Orchard.TaskLease {
|
namespace Orchard.TaskLease {
|
||||||
@@ -19,5 +17,14 @@ namespace Orchard.TaskLease {
|
|||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int UpdateFrom1() {
|
||||||
|
SchemaBuilder.CreateTable("DatabaseLockRecord", table => table
|
||||||
|
.Column<int>("Id", column => column.PrimaryKey().Identity())
|
||||||
|
.Column<string>("Name", column => column.NotNull().WithLength(256))
|
||||||
|
.Column<DateTime>("AcquiredUtc"));
|
||||||
|
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,9 @@
|
|||||||
<Prefer32Bit>false</Prefer32Bit>
|
<Prefer32Bit>false</Prefer32Bit>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Reference Include="Autofac">
|
||||||
|
<HintPath>..\..\..\..\lib\autofac\Autofac.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
<Reference Include="Microsoft.CSharp" />
|
<Reference Include="Microsoft.CSharp" />
|
||||||
<Reference Include="System" />
|
<Reference Include="System" />
|
||||||
<Reference Include="System.Data" />
|
<Reference Include="System.Data" />
|
||||||
@@ -73,6 +76,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="Web.config" />
|
<Content Include="Web.config" />
|
||||||
|
<Compile Include="Models\DatabaseLockRecord.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Content Include="Module.txt" />
|
<Content Include="Module.txt" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -82,6 +86,7 @@
|
|||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="Models\TaskLeaseRecord.cs" />
|
<Compile Include="Models\TaskLeaseRecord.cs" />
|
||||||
<Compile Include="Services\ITaskLeaseService.cs" />
|
<Compile Include="Services\ITaskLeaseService.cs" />
|
||||||
|
<Compile Include="Services\DatabaseLock.cs" />
|
||||||
<Compile Include="Services\TaskLeaseService.cs" />
|
<Compile Include="Services\TaskLeaseService.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup />
|
<ItemGroup />
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a database driven implementation of <see cref="ILock" />
|
||||||
|
/// </summary>
|
||||||
|
[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<IRepository<DatabaseLockRecord>>();
|
||||||
|
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<IRepository<DatabaseLockRecord>>();
|
||||||
|
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<ITransactionManager>().RequireNew(IsolationLevel.ReadCommitted);
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
/// 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.
|
/// for a specific amount of time. Optionnally a State can be saved along with the lease.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete("Use Orchard.Tasks.Locking.ILockService instead.")]
|
||||||
public interface ITaskLeaseService : IDependency {
|
public interface ITaskLeaseService : IDependency {
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ namespace Orchard.TaskLease.Services {
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides a database driven implementation of <see cref="ITaskLeaseService" />
|
/// Provides a database driven implementation of <see cref="ITaskLeaseService" />
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete("Use Orchard.Tasks.Locking.ILockService instead.")]
|
||||||
public class TaskLeaseService : ITaskLeaseService {
|
public class TaskLeaseService : ITaskLeaseService {
|
||||||
|
|
||||||
private readonly IRepository<TaskLeaseRecord> _repository;
|
private readonly IRepository<TaskLeaseRecord> _repository;
|
||||||
|
|||||||
@@ -397,6 +397,10 @@
|
|||||||
<Compile Include="Services\IJsonConverter.cs" />
|
<Compile Include="Services\IJsonConverter.cs" />
|
||||||
<Compile Include="Settings\CurrentSiteWorkContext.cs" />
|
<Compile Include="Settings\CurrentSiteWorkContext.cs" />
|
||||||
<Compile Include="Settings\ResourceDebugMode.cs" />
|
<Compile Include="Settings\ResourceDebugMode.cs" />
|
||||||
|
<Compile Include="Tasks\Locking\DefaultLock.cs" />
|
||||||
|
<Compile Include="Tasks\Locking\ILockService.cs" />
|
||||||
|
<Compile Include="Tasks\Locking\ILock.cs" />
|
||||||
|
<Compile Include="Tasks\Locking\LockService.cs" />
|
||||||
<Compile Include="Themes\CurrentThemeWorkContext.cs" />
|
<Compile Include="Themes\CurrentThemeWorkContext.cs" />
|
||||||
<Compile Include="Themes\ThemeManager.cs" />
|
<Compile Include="Themes\ThemeManager.cs" />
|
||||||
<Compile Include="Time\CurrentTimeZoneWorkContext.cs" />
|
<Compile Include="Time\CurrentTimeZoneWorkContext.cs" />
|
||||||
|
|||||||
14
src/Orchard/Tasks/Locking/DefaultLock.cs
Normal file
14
src/Orchard/Tasks/Locking/DefaultLock.cs
Normal file
@@ -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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/Orchard/Tasks/Locking/ILock.cs
Normal file
16
src/Orchard/Tasks/Locking/ILock.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Orchard.Tasks.Locking {
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a lock on a provided name.
|
||||||
|
/// </summary>
|
||||||
|
public interface ILock : ITransientDependency, IDisposable {
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to acquire a lock on the specified name.
|
||||||
|
/// </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 true if a lock was acquired, false otherwise.</returns>
|
||||||
|
bool TryAcquire(string name, TimeSpan maxLifetime);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/Orchard/Tasks/Locking/ILockService.cs
Normal file
27
src/Orchard/Tasks/Locking/ILockService.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Orchard.Tasks.Locking {
|
||||||
|
/// <summary>
|
||||||
|
/// Provides distributed locking functionality.
|
||||||
|
/// </summary>
|
||||||
|
public interface ILockService : IDependency {
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to acquire a lock on the specified name.
|
||||||
|
/// </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="timeout">The amount of time to wait for the lock to be acquired before timing out</param>
|
||||||
|
/// <returns>Returns a lock if one was successfully acquired, null otherwise.</returns>
|
||||||
|
ILock TryAcquireLock(string name, TimeSpan maxLifetime, TimeSpan timeout);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Acquires a lock with the specified parameters.
|
||||||
|
/// </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="timeout">The amount of time to wait for the lock to be acquired before timing out</param>
|
||||||
|
/// <returns>Returns a lock if one was successfully acquired.</returns>
|
||||||
|
/// <exception cref="TimeoutException">Thrown if the lock couldn't be acquired.</exception>
|
||||||
|
ILock AcquireLock(string name, TimeSpan maxLifetime, TimeSpan timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/Orchard/Tasks/Locking/LockService.cs
Normal file
48
src/Orchard/Tasks/Locking/LockService.cs
Normal file
@@ -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<ILock> _lock;
|
||||||
|
private readonly IMachineNameProvider _machineNameProvider;
|
||||||
|
|
||||||
|
public LockService(Work<ILock> @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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user