2020-07-14 13:24:07 +00:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
2020-11-01 19:03:21 +00:00
using Elsa.DistributedLock ;
2021-03-29 20:11:37 +00:00
using Elsa.DistributedLocking ;
2020-11-01 19:03:21 +00:00
using Microsoft.Azure.Storage ;
using Microsoft.Azure.Storage.Blob ;
using Microsoft.Azure.Storage.RetryPolicies ;
using Microsoft.Extensions.Logging ;
2021-03-31 12:18:37 +00:00
using NodaTime ;
2020-07-14 13:24:07 +00:00
2020-11-01 19:03:21 +00:00
namespace Elsa
2020-07-14 13:24:07 +00:00
{
// See also:
// The lock duration can be 15 to 60 seconds, or can be infinite, Reference: https://docs.microsoft.com/en-us/rest/api/storageservices/lease-blob
public class AzureBlobLockProvider : IDistributedLockProvider
{
private const string Prefix = "elsa" ;
private const string ContainerName = "elsa-lock-container" ;
private const int MaxLeaseTime = 60 ;
private const int MinLeaseTime = 15 ;
2021-03-31 12:18:37 +00:00
private static readonly object SyncRoot = new ( ) ;
private readonly AutoResetEvent _leaseSemaphore = new ( true ) ;
private readonly List < LockedBlob > _lockedBlobs = new ( ) ;
2020-07-14 13:24:07 +00:00
private readonly string _connectionString ;
2020-10-18 08:40:31 +00:00
private readonly TimeSpan _leaseTime ;
2020-11-03 09:58:11 +00:00
private readonly ILogger _logger ;
2020-07-14 13:24:07 +00:00
private readonly TimeSpan _renewInterval ;
2020-11-03 09:58:11 +00:00
private CloudBlobContainer ? _cloudBlobContainer ;
private Timer _renewTimer = default ! ;
2020-07-14 13:24:07 +00:00
2020-10-18 08:40:31 +00:00
public AzureBlobLockProvider (
string connectionString ,
TimeSpan leaseTime ,
TimeSpan renewInterval ,
ILogger < AzureBlobLockProvider > logger )
2020-07-14 13:24:07 +00:00
{
_logger = logger ;
_connectionString = connectionString ;
2020-11-03 09:58:11 +00:00
2020-07-14 13:24:07 +00:00
if ( leaseTime > = TimeSpan . FromSeconds ( MaxLeaseTime ) | | leaseTime < = TimeSpan . FromSeconds ( MinLeaseTime ) )
{
2021-03-31 12:18:37 +00:00
_logger . LogInformation ( "Lease time must be between 15 Seconds and 60 seconds, Found {LeaseTime} seconds. Setting default value of 60 seconds" , leaseTime . TotalSeconds ) ;
2020-07-14 13:24:07 +00:00
_leaseTime = TimeSpan . FromSeconds ( MaxLeaseTime ) ;
}
else
{
_leaseTime = leaseTime ;
}
2020-10-18 08:40:31 +00:00
2020-07-14 13:24:07 +00:00
if ( renewInterval > leaseTime )
{
2021-03-31 12:18:37 +00:00
_logger . LogError ( "Renew Interval can not be greater than LeaseTime {LeaseTime}" , leaseTime . TotalSeconds ) ;
throw new InvalidDataException ( $"Renew Interval can not be greater than LeaseTime {leaseTime.TotalSeconds}." ) ;
2020-07-14 13:24:07 +00:00
}
2020-10-18 08:40:31 +00:00
2020-07-14 13:24:07 +00:00
_renewInterval = renewInterval ;
}
2021-03-31 12:18:37 +00:00
public Task < bool > AcquireLockAsync ( string name , Duration ? timeout = default , CancellationToken cancellationToken = default ) = > CreateLockAsync ( name , timeout , cancellationToken ) ;
2020-07-14 13:24:07 +00:00
2021-03-31 12:18:37 +00:00
private async Task < bool > CreateLockAsync ( string name , Duration ? duration , CancellationToken cancellationToken )
2020-07-14 13:24:07 +00:00
{
2021-03-31 12:18:37 +00:00
// TODO: Figure out how to wait for the amount of time as specified by `duration` before giving up on acquiring a lease.
2020-07-14 13:24:07 +00:00
var resourceName = $"{Prefix}:{name}" ;
var blob = CloudBlobContainer . GetBlockBlobReference ( resourceName ) ;
2021-03-31 12:18:37 +00:00
if ( ! await blob . ExistsAsync ( cancellationToken ) )
await blob . UploadTextAsync ( string . Empty , cancellationToken ) ;
2020-07-14 13:24:07 +00:00
_renewTimer = new Timer ( RenewLeases , null , _renewInterval , _renewInterval ) ;
2020-10-18 08:40:31 +00:00
_logger . LogInformation ( "Lock provider will try to acquire lock for {resourceName}" , resourceName ) ;
2020-07-14 13:24:07 +00:00
if ( _leaseSemaphore . WaitOne ( ) )
{
try
{
2021-03-31 12:18:37 +00:00
var leaseId = await blob . AcquireLeaseAsync ( _leaseTime , null , cancellationToken ) ;
2020-07-14 13:24:07 +00:00
_lockedBlobs . Add ( new LockedBlob { Blob = blob , LeaseId = leaseId , Identifier = resourceName } ) ;
2020-10-18 08:40:31 +00:00
_logger . LogInformation ( "Lock provider acquired lock for {resourceName}" , resourceName ) ;
2020-07-14 13:24:07 +00:00
return true ;
}
catch ( Exception ex )
{
2020-10-18 08:40:31 +00:00
_logger . LogWarning ( "Failed to acquire lock for {resourceName}. Reason > {ex}" , resourceName , ex ) ;
2020-07-14 13:24:07 +00:00
return false ;
}
finally
{
_leaseSemaphore . Set ( ) ;
}
}
2020-10-18 08:40:31 +00:00
2020-07-14 13:24:07 +00:00
return false ;
}
2021-03-31 12:18:37 +00:00
public Task ReleaseLockAsync ( string name , CancellationToken cancellationToken = default ) = > ReleaseLeaseAsync ( name , cancellationToken ) ;
2020-10-18 08:40:31 +00:00
private async Task ReleaseLeaseAsync ( string name , CancellationToken cancellationToken = default )
2020-07-14 13:24:07 +00:00
{
var resourceName = $"{Prefix}:{name}" ;
2020-10-18 08:40:31 +00:00
_logger . LogInformation ( "Lock provider will try to release lock for {resourceName}" , resourceName ) ;
2020-07-14 13:24:07 +00:00
if ( _leaseSemaphore . WaitOne ( ) )
{
try
{
var lockedBlob = _lockedBlobs . FirstOrDefault ( x = > x . Identifier = = resourceName ) ;
if ( lockedBlob ! = null )
{
try
{
await lockedBlob . Blob
2020-10-18 08:40:31 +00:00
. ReleaseLeaseAsync ( AccessCondition . GenerateLeaseCondition ( lockedBlob . LeaseId ) , cancellationToken )
. ConfigureAwait ( false ) ;
2020-07-14 13:24:07 +00:00
2020-10-18 08:40:31 +00:00
_logger . LogInformation ( "Lock provider released lock for {resourceName}" , resourceName ) ;
2020-07-14 13:24:07 +00:00
}
catch ( Exception ex )
{
2020-10-18 08:40:31 +00:00
_logger . LogWarning ( "Failed to release lock for {resourceName}. Reason > {ex}" , resourceName , ex ) ;
2020-07-14 13:24:07 +00:00
}
2020-10-18 08:40:31 +00:00
2020-07-14 13:24:07 +00:00
_lockedBlobs . Remove ( lockedBlob ) ;
}
}
finally
{
_leaseSemaphore . Set ( ) ;
2021-03-31 12:18:37 +00:00
await _renewTimer . DisposeAsync ( ) ;
2020-07-14 13:24:07 +00:00
}
}
}
private CloudBlobContainer CloudBlobContainer
{
get
{
2021-03-31 12:18:37 +00:00
if ( _cloudBlobContainer ! = null )
return _cloudBlobContainer ;
lock ( SyncRoot )
2020-07-14 13:24:07 +00:00
{
2021-03-31 12:18:37 +00:00
if ( _cloudBlobContainer ! = null )
return _cloudBlobContainer ;
var blobClient = CloudStorageAccount . Parse ( _connectionString ) . CreateCloudBlobClient ( ) ;
blobClient . DefaultRequestOptions . RetryPolicy = new ExponentialRetry ( TimeSpan . FromSeconds ( 2.0 ) , 3 ) ;
_cloudBlobContainer = blobClient . GetContainerReference ( ContainerName ) ;
if ( ! _cloudBlobContainer . Exists ( ) )
_cloudBlobContainer . CreateIfNotExists ( BlobContainerPublicAccessType . Off ) ;
2020-07-14 13:24:07 +00:00
}
2020-10-18 08:40:31 +00:00
return _cloudBlobContainer ;
2020-07-14 13:24:07 +00:00
}
}
2020-10-18 08:40:31 +00:00
2020-07-14 13:24:07 +00:00
private async void RenewLeases ( object state )
{
_logger . LogDebug ( "Renew active leases" ) ;
if ( _leaseSemaphore . WaitOne ( ) )
{
try
{
foreach ( var lockedBlobs in _lockedBlobs )
await RenewLock ( lockedBlobs ) . ConfigureAwait ( false ) ;
}
catch ( Exception ex )
{
_logger . LogWarning ( ex , "Error while renewing leases" ) ;
}
finally
{
_leaseSemaphore . Set ( ) ;
}
}
2020-10-18 08:40:31 +00:00
}
2020-07-14 13:24:07 +00:00
private async Task RenewLock ( LockedBlob lockedBlob )
{
try
{
await lockedBlob . Blob . RenewLeaseAsync ( AccessCondition . GenerateLeaseCondition ( lockedBlob . LeaseId ) ) ;
_logger . LogInformation ( "Renewed active leases for {lockedBlob.Identifier}" , lockedBlob . Identifier ) ;
}
catch ( Exception ex )
{
_logger . LogWarning ( ex , "Error while renewing lock" ) ;
}
}
}
2020-10-18 08:40:31 +00:00
}