2020-07-14 13:24:07 +00:00
using System ;
2020-01-14 22:11:50 +00:00
using System.Collections.Generic ;
using System.Data ;
using System.Data.SqlClient ;
using System.Threading ;
using System.Threading.Tasks ;
2020-07-14 13:24:07 +00:00
using Elsa.DistributedLock ;
2020-01-14 22:11:50 +00:00
using Microsoft.Extensions.Logging ;
2020-11-01 19:03:21 +00:00
namespace Elsa
2020-01-14 22:11:50 +00:00
{
// CREDITS:
// Implementation taken & adapted from Workflow Core: https://github.com/danielgerlag/workflow-core/blob/master/src/providers/WorkflowCore.LockProviders.SqlServer/SqlLockProvider.cs
public class SqlLockProvider : IDistributedLockProvider
{
2020-10-09 17:52:54 +00:00
private readonly ILogger _logger ;
2020-01-14 22:11:50 +00:00
private const string Prefix = "elsa" ;
2020-10-09 17:52:54 +00:00
private readonly string _connectionString ;
private readonly IDictionary < string , SqlConnection > _locks = new Dictionary < string , SqlConnection > ( ) ;
2020-12-20 19:05:09 +00:00
private readonly AutoResetEvent _mutex = new ( true ) ;
2020-01-14 22:11:50 +00:00
public SqlLockProvider ( string connectionString , ILogger < SqlLockProvider > logger )
{
2020-10-10 15:13:52 +00:00
_logger = logger ;
2020-01-14 22:11:50 +00:00
var connectionStringBuilder = new SqlConnectionStringBuilder ( connectionString )
{
Pooling = true ,
ApplicationName = "Elsa Distributed Lock Provider"
} ;
2020-10-10 15:13:52 +00:00
_connectionString = connectionStringBuilder . ToString ( ) ;
2020-01-14 22:11:50 +00:00
}
public async Task < bool > AcquireLockAsync ( string name , CancellationToken cancellationToken )
{
2020-10-09 17:52:54 +00:00
if ( ! _mutex . WaitOne ( ) )
2020-01-14 22:11:50 +00:00
return false ;
try
{
2020-10-09 17:52:54 +00:00
var connection = new SqlConnection ( _connectionString ) ;
2020-01-14 22:11:50 +00:00
await connection . OpenAsync ( cancellationToken ) ;
try
{
var command = connection . CreateCommand ( ) ;
command . CommandText = "sp_getapplock" ;
command . CommandType = CommandType . StoredProcedure ;
command . Parameters . AddWithValue ( "@Resource" , $"{Prefix}:{name}" ) ;
command . Parameters . AddWithValue ( "@LockOwner" , $"Session" ) ;
command . Parameters . AddWithValue ( "@LockMode" , $"Exclusive" ) ;
command . Parameters . AddWithValue ( "@LockTimeout" , 0 ) ;
var returnParameter = command . Parameters . Add ( "RetVal" , SqlDbType . Int ) ;
returnParameter . Direction = ParameterDirection . ReturnValue ;
await command . ExecuteNonQueryAsync ( cancellationToken ) ;
var result = Convert . ToInt32 ( returnParameter . Value ) ;
switch ( result )
{
case - 1 :
2020-10-09 17:52:54 +00:00
_logger . LogDebug ( $"The lock request timed out for {name}" ) ;
2020-01-14 22:11:50 +00:00
break ;
2020-07-14 13:24:07 +00:00
2020-01-14 22:11:50 +00:00
case - 2 :
2020-10-09 17:52:54 +00:00
_logger . LogDebug ( $"The lock request was canceled for {name}" ) ;
2020-01-14 22:11:50 +00:00
break ;
2020-07-14 13:24:07 +00:00
2020-01-14 22:11:50 +00:00
case - 3 :
2020-10-09 17:52:54 +00:00
_logger . LogDebug ( $"The lock request was chosen as a deadlock victim for {name}" ) ;
2020-01-14 22:11:50 +00:00
break ;
2020-07-14 13:24:07 +00:00
2020-01-14 22:11:50 +00:00
case - 999 :
2020-10-09 17:52:54 +00:00
_logger . LogError ( $"Lock provider error for {name}" ) ;
2020-01-14 22:11:50 +00:00
break ;
}
if ( result > = 0 )
{
2020-10-09 17:52:54 +00:00
_locks [ name ] = connection ;
2020-01-14 22:11:50 +00:00
return true ;
}
else
{
connection . Close ( ) ;
return false ;
}
}
catch ( Exception )
{
connection . Close ( ) ;
throw ;
}
}
finally
{
2020-10-09 17:52:54 +00:00
_mutex . Set ( ) ;
2020-01-14 22:11:50 +00:00
}
}
public async Task ReleaseLockAsync ( string name , CancellationToken cancellationToken )
{
2020-10-09 17:52:54 +00:00
if ( ! _mutex . WaitOne ( ) )
2020-01-14 22:11:50 +00:00
return ;
2020-07-14 13:24:07 +00:00
2020-01-14 22:11:50 +00:00
try
{
2020-10-09 17:52:54 +00:00
if ( ! _locks . ContainsKey ( name ) )
2020-01-14 22:11:50 +00:00
return ;
2020-07-14 13:24:07 +00:00
2020-10-09 17:52:54 +00:00
var connection = _locks [ name ] ;
2020-01-14 22:11:50 +00:00
if ( connection = = null )
return ;
try
{
var command = connection . CreateCommand ( ) ;
command . CommandText = "sp_releaseapplock" ;
command . CommandType = CommandType . StoredProcedure ;
command . Parameters . AddWithValue ( "@Resource" , $"{Prefix}:{name}" ) ;
command . Parameters . AddWithValue ( "@LockOwner" , $"Session" ) ;
2020-07-14 13:24:07 +00:00
2020-01-14 22:11:50 +00:00
var returnParameter = command . Parameters . Add ( "RetVal" , SqlDbType . Int ) ;
returnParameter . Direction = ParameterDirection . ReturnValue ;
await command . ExecuteNonQueryAsync ( cancellationToken ) ;
var result = Convert . ToInt32 ( returnParameter . Value ) ;
if ( result < 0 )
2020-10-09 17:52:54 +00:00
_logger . LogError ( $"Unable to release lock for {name}" ) ;
2020-01-14 22:11:50 +00:00
}
finally
{
connection . Close ( ) ;
2020-10-09 17:52:54 +00:00
_locks . Remove ( name ) ;
2020-01-14 22:11:50 +00:00
}
}
finally
{
2020-10-09 17:52:54 +00:00
_mutex . Set ( ) ;
2020-01-14 22:11:50 +00:00
}
}
}
}