Skip to content

Commit

Permalink
Merge pull request DbKeeperNet#49 from voloda/Locking
Browse files Browse the repository at this point in the history
Perform db upgrade mutually exlusively when executed on multiple nodes at the same time
  • Loading branch information
voloda authored Nov 4, 2018
2 parents 061ebd2 + 9582910 commit 9d2e605
Show file tree
Hide file tree
Showing 96 changed files with 1,330 additions and 136 deletions.
80 changes: 80 additions & 0 deletions DbKeeperNet.Engine.Tests.Full/DatabaseLockServiceTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using NUnit.Framework;

namespace DbKeeperNet.Engine.Tests.Full
{
/// <summary>
/// Test class for <see cref="DatabaseLockService"/>
/// </summary>
[TestFixture]
public class DatabaseLockServiceTest
{
private Mock<IDatabaseLock> _databaseLock;
private const int LockId = 1;

[SetUp]
public void Setup()
{
_databaseLock = new Mock<IDatabaseLock>(MockBehavior.Strict);
}

[Test]
public void IfDatabaseLockIsNotSupportedLockShouldDoNothing()
{
_databaseLock.Setup(l => l.IsSupported).Returns(false);

var service = CreateDatabaseLockService();

var locker = service.AcquireLock(LockId);
locker.Dispose();
}

[Test]
public void IfDatabaseLockIsSupportedAndAcquiredDisposeShouldRelease()
{
_databaseLock.Setup(l => l.IsSupported).Returns(true);
_databaseLock.Setup(l => l.Acquire(LockId, It.IsAny<string>(), It.IsAny<int>())).Returns(true);
_databaseLock.Setup(l => l.Release(LockId));

var service = CreateDatabaseLockService();

var locker = service.AcquireLock(LockId);

_databaseLock.Verify(l => l.Acquire(LockId, It.IsAny<string>(), It.IsAny<int>()), Times.Once);
_databaseLock.Verify(l => l.Release(LockId), Times.Never);

locker.Dispose();

_databaseLock.Verify(l => l.Release(LockId), Times.Once);
}

[Test]
public void AcquireShouldTryUntilItSucceeds()
{
var sequence = new MockSequence();
_databaseLock.Setup(l => l.IsSupported).Returns(true);
_databaseLock.InSequence(sequence).Setup(l => l.Acquire(LockId, It.IsAny<string>(), It.IsAny<int>())).Returns(false);
_databaseLock.InSequence(sequence).Setup(l => l.Acquire(LockId, It.IsAny<string>(), It.IsAny<int>())).Returns(true);
_databaseLock.Setup(l => l.Release(LockId));

var service = CreateDatabaseLockService();

var locker = service.AcquireLock(LockId);

_databaseLock.Verify(l => l.Acquire(LockId, It.IsAny<string>(), It.IsAny<int>()), Times.Exactly(2));
_databaseLock.Verify(l => l.Release(LockId), Times.Never);

locker.Dispose();

_databaseLock.Verify(l => l.Release(LockId), Times.Once);
}

private DatabaseLockService CreateDatabaseLockService()
{
var service = new DatabaseLockService(NullLogger<DatabaseLockService>.Instance, _databaseLock.Object);

return service;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\packages\NUnit.3.10.1\build\NUnit.props" Condition="Exists('..\packages\NUnit.3.10.1\build\NUnit.props')" />
<Import Project="..\packages\NUnit.3.11.0\build\NUnit.props" Condition="Exists('..\packages\NUnit.3.11.0\build\NUnit.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
Expand Down Expand Up @@ -42,11 +42,12 @@
<Reference Include="Castle.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=407dd0808d44fbdc, processorArchitecture=MSIL">
<HintPath>..\packages\Castle.Core.4.3.1\lib\net45\Castle.Core.dll</HintPath>
</Reference>
<Reference Include="Moq, Version=4.8.0.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
<HintPath>..\packages\Moq.4.8.3\lib\net45\Moq.dll</HintPath>
<Reference Include="Microsoft.Extensions.Logging.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60" />
<Reference Include="Moq, Version=4.10.0.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
<HintPath>..\packages\Moq.4.10.0\lib\net45\Moq.dll</HintPath>
</Reference>
<Reference Include="nunit.framework, Version=3.10.1.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
<HintPath>..\packages\NUnit.3.10.1\lib\net45\nunit.framework.dll</HintPath>
<Reference Include="nunit.framework, Version=3.11.0.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
<HintPath>..\packages\NUnit.3.11.0\lib\net45\nunit.framework.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Configuration" />
Expand All @@ -69,6 +70,8 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="DatabaseLockServiceTest.cs" />
<Compile Include="NullDatabaseLockTest.cs" />
<Compile Include="PreconditionServiceTest.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ScriptDeserializerTest.cs" />
Expand All @@ -88,6 +91,6 @@
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\NUnit.3.10.1\build\NUnit.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\NUnit.3.10.1\build\NUnit.props'))" />
<Error Condition="!Exists('..\packages\NUnit.3.11.0\build\NUnit.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\NUnit.3.11.0\build\NUnit.props'))" />
</Target>
</Project>
38 changes: 38 additions & 0 deletions DbKeeperNet.Engine.Tests.Full/NullDatabaseLockTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using NUnit.Framework;

namespace DbKeeperNet.Engine.Tests.Full
{
[TestFixture]
public class NullDatabaseLockTest
{
[Test]
public void IsSupportedShouldReturnFalse()
{
var databaseLock = CreateNullDatabaseLock();

Assert.That(databaseLock.IsSupported, Is.False);
}

private static NullDatabaseLock CreateNullDatabaseLock()
{
var databaseLock = new NullDatabaseLock();
return databaseLock;
}

[Test]
public void ReleaseShouldDoNothing()
{
var databaseLock = CreateNullDatabaseLock();

databaseLock.Release(0);
}

[Test]
public void AcquireShouldReturnTrue()
{
var databaseLock = CreateNullDatabaseLock();

Assert.That(databaseLock.Acquire(0, null, 0), Is.True);
}
}
}
4 changes: 2 additions & 2 deletions DbKeeperNet.Engine.Tests.Full/packages.config
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Castle.Core" version="4.3.1" targetFramework="net472" />
<package id="Moq" version="4.8.3" targetFramework="net472" />
<package id="NUnit" version="3.10.1" targetFramework="net472" />
<package id="Moq" version="4.10.0" targetFramework="net472" />
<package id="NUnit" version="3.11.0" targetFramework="net472" />
<package id="System.Runtime.CompilerServices.Unsafe" version="4.5.1" targetFramework="net472" />
<package id="System.Threading.Tasks.Extensions" version="4.5.1" targetFramework="net472" />
<package id="System.ValueTuple" version="4.5.0" targetFramework="net472" />
Expand Down
76 changes: 76 additions & 0 deletions DbKeeperNet.Engine.Tests/DatabaseLockTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;

namespace DbKeeperNet.Engine.Tests
{
/// <summary>
/// Base test class to excercise <see cref="IDatabaseLock"/> implementations
/// </summary>
public abstract class DatabaseLockTests : TestBase
{
protected const int TestLockId = 4262;

[SetUp]
public override void Setup()
{
base.Setup();

var upgrader = DefaultScope.ServiceProvider.GetService<IDatabaseUpdater>();
upgrader.ExecuteUpgrade();

Reset();
}

[TearDown]
public override void Shutdown()
{
Reset();

base.Shutdown();
}

protected abstract void Reset();

[Test]
public void SimpleAcquireShouldWorkProperly()
{
var databaseLock = GetService<IDatabaseLock>();

var acquried = databaseLock.Acquire(TestLockId, "Unit test", 5);
Assert.That(acquried, Is.True);
}

[Test]
public void SimpleReleaseShouldWorkProperly()
{
var databaseLock = GetService<IDatabaseLock>();

Assert.DoesNotThrow(() => databaseLock.Release(TestLockId));
}

[Test]
public void NestedLockShouldNotAcquireAlreadyAcquiredLock()
{
var databaseLock = GetService<IDatabaseLock>();

var acquried = databaseLock.Acquire(TestLockId, "Unit test", 5);
Assert.That(acquried, Is.True);

var nestedAcquire = databaseLock.Acquire(TestLockId, "Unit test", 5);
Assert.That(nestedAcquire, Is.False);
}

[Test]
public void SubsequentAcquireAndReleaseShouldWorkProperly()
{
var databaseLock = GetService<IDatabaseLock>();

var acquried = databaseLock.Acquire(TestLockId, "Unit test", 5);
Assert.That(acquried, Is.True);
databaseLock.Release(TestLockId);

var subsequentAcquire = databaseLock.Acquire(TestLockId, "Unit test", 5);
Assert.That(subsequentAcquire, Is.True);
}
}
}
9 changes: 8 additions & 1 deletion DbKeeperNet.Engine.Tests/DbKeeperNet.Engine.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.1" />
<PackageReference Include="NUnit" Version="3.10.1" />
<PackageReference Include="Moq" Version="4.10.0" />
<PackageReference Include="NUnit" Version="3.11.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DbKeeperNet.Engine\DbKeeperNet.Engine.csproj" />
</ItemGroup>

<ItemGroup>
<Reference Include="Moq">
<HintPath>..\packages\Moq.4.8.3\lib\net45\Moq.dll</HintPath>
</Reference>
</ItemGroup>

</Project>
6 changes: 4 additions & 2 deletions DbKeeperNet.Engine.Tests/TestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public virtual void Setup()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddDbKeeperNet(Configure);

ServiceProvider = serviceCollection.BuildServiceProvider(true);

DefaultScope = ServiceProvider.CreateScope();
Expand Down Expand Up @@ -46,12 +46,14 @@ protected static void ExecuteSqlAndIgnoreException(IDatabaseService service, str

Console.WriteLine("Going to run {0}", command);
var connection = service.GetOpenConnection();

using (var transaction = connection.BeginTransaction())
using (var cmd = connection.CreateCommand())
{
cmd.Transaction = transaction;
cmd.CommandText = command;
cmd.CommandType = CommandType.Text;
cmd.ExecuteNonQuery();
transaction.Commit();
}
}
catch (DbException e)
Expand Down
2 changes: 2 additions & 0 deletions DbKeeperNet.Engine/Configuration/DatabaseUpdaterExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public static IServiceCollection AddDbKeeperNet(this IServiceCollection serviceC
.AddTransient<IDatabaseUpdater, DatabaseUpdater>()
.AddTransient<IPreconditionService, PreconditionService>()
.AddTransient<IScriptDeserializer, ScriptDeserializer>()
.AddTransient<IDatabaseLockService, DatabaseLockService>()

.AddScoped<IUpdateScriptManager, UpdateScriptManager>()
;

Expand Down
61 changes: 61 additions & 0 deletions DbKeeperNet.Engine/DatabaseLockService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System;
using System.Threading;
using Microsoft.Extensions.Logging;

namespace DbKeeperNet.Engine
{
public class DatabaseLockService : IDatabaseLockService
{
private readonly ILogger<DatabaseLockService> _logger;
private readonly IDatabaseLock _databaseLock;

public DatabaseLockService(ILogger<DatabaseLockService> logger, IDatabaseLock databaseLock)
{
_logger = logger;
_databaseLock = databaseLock;
}

public IDisposable AcquireLock(int lockId)
{
if (!_databaseLock.IsSupported)
{
_logger.LogInformation("Database lock is not supported, assuming that lock {0} is acquired", lockId);
return new DisposeAction(() => { });
}

const int expirationMinutes = 5;

while (!_databaseLock.Acquire(lockId, Guid.NewGuid().ToString(), expirationMinutes))
{
_logger.LogInformation("Database lock {0} could not be acquired - going to try again after 5 seconds", lockId);
Thread.Sleep(5000);
}

_logger.LogInformation("Database lock {0} was acquired", lockId);

return new DisposeAction(
() =>
{
_databaseLock.Release(lockId);

_logger.LogInformation("Database lock {0} was released", lockId);
}
);
}

private class DisposeAction : IDisposable
{
private readonly Action _action;

public DisposeAction(Action action)
{
_action = action;
}

public void Dispose()
{
_action();
}
}
}
}
Loading

0 comments on commit 9d2e605

Please sign in to comment.