Skip to content

Commit

Permalink
Merge pull request #105 from madelson/mysql
Browse files Browse the repository at this point in the history
2.2 release (MySQL support)
  • Loading branch information
madelson authored Oct 2, 2021
2 parents 0e146cc + ae60a27 commit b82e2b4
Show file tree
Hide file tree
Showing 38 changed files with 1,169 additions and 50 deletions.
1 change: 1 addition & 0 deletions DistributedLock.Core/AssemblyAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@
[assembly: InternalsVisibleTo("DistributedLock.FileSystem, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")]
[assembly: InternalsVisibleTo("DistributedLock.Redis, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")]
[assembly: InternalsVisibleTo("DistributedLock.ZooKeeper, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")]
[assembly: InternalsVisibleTo("DistributedLock.MySql, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")]
#endif
2 changes: 1 addition & 1 deletion DistributedLock.Core/DistributedLock.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</PropertyGroup>

<PropertyGroup>
<Version>1.0.2</Version>
<Version>1.0.3</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<Authors>Michael Adelson</Authors>
<Description>Core interfaces and utilities that support the DistributedLock.* family of packages</Description>
Expand Down
2 changes: 1 addition & 1 deletion DistributedLock.Core/Internal/Data/DatabaseConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public DatabaseCommand CreateCommand()
// without closing the connection. However, we don't currently have any use-cases for that
public async ValueTask BeginTransactionAsync()
{
Invariant.Require(this._transaction == null);
Invariant.Require(!this.HasTransaction);

using var _ = await this.ConnectionMonitor.AcquireConnectionLockAsync(CancellationToken.None).ConfigureAwait(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ sealed class DedicatedConnectionOrTransactionDbDistributedLock : IDbDistributedL
{
private readonly string _name;
private readonly Func<DatabaseConnection> _connectionFactory;
private readonly bool _scopeToOwnedTransaction;
private readonly bool _transactionScopedIfPossible;
private readonly TimeoutValue _keepaliveCadence;

/// <summary>
/// Constructs an instance using the given EXTERNALLY OWNED <paramref name="externalConnectionFactory"/>.
/// </summary>
public DedicatedConnectionOrTransactionDbDistributedLock(string name, Func<DatabaseConnection> externalConnectionFactory)
: this(name, externalConnectionFactory, useTransaction: false, keepaliveCadence: Timeout.InfiniteTimeSpan)
: this(name, externalConnectionFactory, useTransaction: true, keepaliveCadence: Timeout.InfiniteTimeSpan)
{
}

Expand All @@ -34,7 +37,7 @@ public DedicatedConnectionOrTransactionDbDistributedLock(
{
this._name = name;
this._connectionFactory = connectionFactory;
this._scopeToOwnedTransaction = useTransaction;
this._transactionScopedIfPossible = useTransaction;
this._keepaliveCadence = keepaliveCadence;
}

Expand All @@ -50,39 +53,34 @@ public DedicatedConnectionOrTransactionDbDistributedLock(
try
{
DatabaseConnection connection;
bool transactionScoped;
if (contextHandle != null)
{
connection = GetContextHandleConnection<TLockCookie>(contextHandle);
transactionScoped = false;
}
else
{
connectionResource = connection = this._connectionFactory();
if (connection.IsExernallyOwned)
{
Invariant.Require(!this._scopeToOwnedTransaction);
if (!connection.CanExecuteQueries)
{
throw new InvalidOperationException("The connection and/or transaction are disposed or closed");
}
transactionScoped = false;
}
else
{
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
if (this._scopeToOwnedTransaction)
if (this._transactionScopedIfPossible) // for an internally-owned connection, we must create the transaction
{
await connection.BeginTransactionAsync().ConfigureAwait(false);
}
transactionScoped = this._scopeToOwnedTransaction;
}
}

var lockCookie = await strategy.TryAcquireAsync(connection, this._name, timeout, cancellationToken).ConfigureAwait(false);
if (lockCookie != null)
{
result = new Handle<TLockCookie>(connection, strategy, this._name, lockCookie, transactionScoped, connectionResource);
result = new Handle<TLockCookie>(connection, strategy, this._name, lockCookie, transactionScoped: this._transactionScopedIfPossible && connection.HasTransaction, connectionResource);
if (!this._keepaliveCadence.IsInfinite)
{
connection.SetKeepaliveCadence(this._keepaliveCadence);
Expand Down Expand Up @@ -143,12 +141,12 @@ public ValueTask DisposeAsync()

private sealed class InnerHandle : IAsyncDisposable
{
private static readonly object DisposedSentinel = new object();
private static readonly object DisposedSentinel = new();

private readonly IDbSynchronizationStrategy<TLockCookie> _strategy;
private readonly string _name;
private readonly TLockCookie _lockCookie;
private readonly bool _scopedToOwnedTransaction;
private readonly bool _transactionScoped;
private readonly IAsyncDisposable? _connectionResource;
private object? _connectionMonitoringHandleOrDisposedSentinel;

Expand All @@ -157,14 +155,14 @@ public InnerHandle(
IDbSynchronizationStrategy<TLockCookie> strategy,
string name,
TLockCookie lockCookie,
bool scopedToOwnTransaction,
bool transactionScoped,
IAsyncDisposable? connectionResource)
{
this.Connection = connection;
this._strategy = strategy;
this._name = name;
this._lockCookie = lockCookie;
this._scopedToOwnedTransaction = scopedToOwnTransaction;
this._transactionScoped = transactionScoped;
this._connectionResource = connectionResource;
}

Expand Down Expand Up @@ -215,13 +213,12 @@ public async ValueTask DisposeAsync()

try
{
// If we're not scoped to a transaction, explicit release is required regardless of whether
// we are about to dispose the connection due to connection pooling. For a pooled connection,
// simply calling Dispose() will not release the lock: it just returns the connection to the pool.
if (!(this._scopedToOwnedTransaction
// For external transaction-scoped locks, we're not about to dispose the transaction but if the transaction is
// dead (e. g. completed or rolled back) then we know the lock has been released.
|| (this.Connection.IsExernallyOwned && this.Connection.HasTransaction && !this.Connection.CanExecuteQueries)))
// For transaction-scoped locks, we can sometimes skip the explicit release step. This comes up when either
// (a) We own the connection and therefore the transaction. In this case we're about to dispose the transaction and release that way
// (b) The transaction is dead (e. g. completed or rolled back) in which case the lock has already been released
var canSkipExplicitRelease =
this._transactionScoped && (!this.Connection.IsExernallyOwned || !this.Connection.CanExecuteQueries);
if (!canSkipExplicitRelease)
{
await this._strategy.ReleaseAsync(this.Connection, this._name, this._lockCookie).ConfigureAwait(false);
}
Expand Down
3 changes: 3 additions & 0 deletions DistributedLock.MySql/AssemblyAttributes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("DistributedLock.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")]
55 changes: 55 additions & 0 deletions DistributedLock.MySql/DistributedLock.MySql.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;net461</TargetFrameworks>
<RootNamespace>Medallion.Threading.MySql</RootNamespace>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<WarningLevel>4</WarningLevel>
<LangVersion>Latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>

<PropertyGroup>
<Version>1.0.0</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<Authors>Michael Adelson</Authors>
<Description>Provides a distributed lock implementation based on MySql</Description>
<Copyright>Copyright © 2021 Michael Adelson</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageTags>distributed lock async mutex sql mysql</PackageTags>
<PackageProjectUrl>https://github.com/madelson/DistributedLock</PackageProjectUrl>
<RepositoryUrl>https://github.com/madelson/DistributedLock</RepositoryUrl>
<FileVersion>1.0.0.0</FileVersion>
<PackageReleaseNotes>See https://github.com/madelson/DistributedLock#release-notes</PackageReleaseNotes>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>..\DistributedLock.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<Optimize>True</Optimize>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
<TreatSpecificWarningsAsErrors />
<!-- see https://github.com/dotnet/sdk/issues/2679 -->
<DebugType>embedded</DebugType>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<Optimize>False</Optimize>
<NoWarn>1591</NoWarn>
<DefineConstants>TRACE;DEBUG</DefineConstants>
</PropertyGroup>

<ItemGroup>
<!-- Used as the default over MySql.Data because of better licensing -->
<PackageReference Include="MySqlConnector" Version="1.3.11" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DistributedLock.Core\DistributedLock.Core.csproj" />
</ItemGroup>

<Import Project="..\CopyPackageToPublishDirectory.targets" />
<Import Project="..\FixDistributedLockCoreDependencyVersion.targets" />
</Project>
76 changes: 76 additions & 0 deletions DistributedLock.MySql/MySqlConnectionOptionsBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using Medallion.Threading.Internal;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace Medallion.Threading.MySql
{
/// <summary>
/// Specifies options for connecting to and locking against a MySQL database
/// </summary>
public sealed class MySqlConnectionOptionsBuilder
{
private TimeoutValue? _keepaliveCadence;
private bool? _useMultiplexing;

internal MySqlConnectionOptionsBuilder() { }

/// <summary>
/// MySQL's wait_timeout system variable determines how long the server will allow a connection to be idle before killing it.
/// For more information, see https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_wait_timeout.
///
/// To prevent this, this option sets the cadence at which we run a no-op "keepalive" query on a connection that is holding a lock.
///
/// Because MySQL's default for this setting is 8 hours, the default <paramref name="keepaliveCadence"/> is 3.5 hours.
///
/// Setting a value of <see cref="Timeout.InfiniteTimeSpan"/> disables keepalive.
/// </summary>
public MySqlConnectionOptionsBuilder KeepaliveCadence(TimeSpan keepaliveCadence)
{
this._keepaliveCadence = new TimeoutValue(keepaliveCadence, nameof(keepaliveCadence));
return this;
}

/// <summary>
/// This mode takes advantage of the fact that while "holding" a lock (or other synchronization primitive)
/// a connection is essentially idle. Thus, rather than creating a new connection for each held lock it is
/// often possible to multiplex a shared connection so that that connection can hold multiple locks at the same time.
///
/// Multiplexing is on by default.
///
/// This is implemented in such a way that releasing a lock held on such a connection will never be blocked by an
/// Acquire() call that is waiting to acquire a lock on that same connection. For this reason, the multiplexing
/// strategy is "optimistic": if the lock can't be acquired instantaneously on the shared connection, a new (shareable)
/// connection will be allocated.
///
/// This option can improve performance and avoid connection pool starvation in high-load scenarios. It is also
/// particularly applicable to cases where <see cref="IDistributedLock.TryAcquire(TimeSpan, System.Threading.CancellationToken)"/>
/// semantics are used with a zero-length timeout.
/// </summary>
public MySqlConnectionOptionsBuilder UseMultiplexing(bool useMultiplexing = true)
{
this._useMultiplexing = useMultiplexing;
return this;
}

internal static (TimeoutValue keepaliveCadence, bool useMultiplexing) GetOptions(Action<MySqlConnectionOptionsBuilder>? optionsBuilder)
{
MySqlConnectionOptionsBuilder? options;
if (optionsBuilder != null)
{
options = new MySqlConnectionOptionsBuilder();
optionsBuilder(options);
}
else
{
options = null;
}

var keepaliveCadence = options?._keepaliveCadence ?? TimeSpan.FromHours(3.5);
var useMultiplexing = options?._useMultiplexing ?? true;

return (keepaliveCadence, useMultiplexing);
}
}
}
49 changes: 49 additions & 0 deletions DistributedLock.MySql/MySqlDatabaseConnection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Medallion.Threading.Internal.Data;
using MySqlConnector;
using System;
using System.Collections.Generic;
using System.Data;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Medallion.Threading.MySql
{
internal class MySqlDatabaseConnection : DatabaseConnection
{
public MySqlDatabaseConnection(IDbConnection connection)
: base(connection, isExternallyOwned: true)
{
}

public MySqlDatabaseConnection(IDbTransaction transaction)
: base(transaction, isExternallyOwned: true)
{
}

public MySqlDatabaseConnection(string connectionString)
: base(new MySqlConnection(connectionString), isExternallyOwned: false)
{
}

// Seems like this only helps when executing a statement multiple times on the
// same connection (unclear since there's limited documentation)
public override bool ShouldPrepareCommands => false;

public override bool IsCommandCancellationException(Exception exception)
{
// see https://mysqlconnector.net/overview/command-cancellation/
return exception is MySqlException ex && ex.ErrorCode == MySqlErrorCode.QueryInterrupted;
}

public override async Task SleepAsync(TimeSpan sleepTime, CancellationToken cancellationToken, Func<DatabaseCommand, CancellationToken, ValueTask<int>> executor)
{
using var sleepCommand = this.CreateCommand();
sleepCommand.SetCommandText("SELECT SLEEP(@durationSeconds)");
sleepCommand.AddParameter("durationSeconds", sleepTime.TotalSeconds);
sleepCommand.SetTimeout(sleepTime);

await executor(sleepCommand, cancellationToken).ConfigureAwait(false);
}
}
}
Loading

0 comments on commit b82e2b4

Please sign in to comment.