Skip to content

Commit

Permalink
Add threading projects.
Browse files Browse the repository at this point in the history
  • Loading branch information
Codespilot committed Jul 5, 2023
1 parent 7097f33 commit 8839263
Show file tree
Hide file tree
Showing 68 changed files with 5,055 additions and 4 deletions.
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<PackageVersion Include="Autofac.Extensions.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Autofac.Extras.DynamicProxy" Version="7.1.0" />
<PackageVersion Include="AutoMapper" Version="12.0.1" />
<PackageVersion Include="Azure.Storage.Blobs" Version="12.16.0" />
<PackageVersion Include="Castle.Core" Version="5.1.1" />
<PackageVersion Include="Consul" Version="1.6.10.9" />
<PackageVersion Include="FluentValidation" Version="11.6.0" />
Expand Down Expand Up @@ -52,6 +53,7 @@
<PackageVersion Include="System.Runtime.Loader" Version="4.3.0" />
<PackageVersion Include="System.Text.Json" Version="7.0.3" />
<PackageVersion Include="Winton.Extensions.Configuration.Consul" Version="3.2.0" />
<PackageVersion Include="ZooKeeperNetEx" Version="3.4.12.4" />
</ItemGroup>
<!-- Microsoft.Extensions -->
<ItemGroup>
Expand Down
49 changes: 47 additions & 2 deletions Euonia.sln
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,19 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Caching", "Caching", "{2673
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Euonia.Caching.Tests.Shared", "Tests\Euonia.Caching.Tests.Shared\Euonia.Caching.Tests.Shared.shproj", "{4A28CD6B-0C75-4D39-B613-66DE6B693675}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Euonia.Caching.Runtime.Tests", "Tests\Euonia.Caching.Runtime.Tests\Euonia.Caching.Runtime.Tests.csproj", "{EFA1CD9D-4B53-483C-BF9D-F21B9B2C6FDE}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Euonia.Caching.Runtime.Tests", "Tests\Euonia.Caching.Runtime.Tests\Euonia.Caching.Runtime.Tests.csproj", "{EFA1CD9D-4B53-483C-BF9D-F21B9B2C6FDE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Euonia.Caching.Default.Tests", "Tests\Euonia.Caching.Default.Tests\Euonia.Caching.Default.Tests.csproj", "{ACACF41C-B5CB-4C9F-B378-3F15ED15F1D1}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Euonia.Caching.Default.Tests", "Tests\Euonia.Caching.Default.Tests\Euonia.Caching.Default.Tests.csproj", "{ACACF41C-B5CB-4C9F-B378-3F15ED15F1D1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Euonia.Threading", "Source\Euonia.Threading\Euonia.Threading.csproj", "{D71E9924-333D-4417-8FB6-BAB1533C420A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Euonia.Threading.Redis", "Source\Euonia.Threading.Redis\Euonia.Threading.Redis.csproj", "{5FF78A05-D3B8-4E4E-891C-C4F14E9D2BCD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Euonia.Threading.Azure", "Source\Euonia.Threading.Azure\Euonia.Threading.Azure.csproj", "{667DC49A-C0A9-46B3-9C0B-6BE2E9EFD4D2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Euonia.Threading.FileSystem", "Source\Euonia.Threading.FileSystem\Euonia.Threading.FileSystem.csproj", "{AC00CE1D-A54D-4243-9762-49A33CF4548B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Euonia.Threading.ZooKeeper", "Source\Euonia.Threading.ZooKeeper\Euonia.Threading.ZooKeeper.csproj", "{0DADDE7F-28D9-43B3-8CC5-88BBE887CBD9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -271,6 +281,36 @@ Global
{ACACF41C-B5CB-4C9F-B378-3F15ED15F1D1}.Product|Any CPU.Build.0 = Debug|Any CPU
{ACACF41C-B5CB-4C9F-B378-3F15ED15F1D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ACACF41C-B5CB-4C9F-B378-3F15ED15F1D1}.Release|Any CPU.Build.0 = Release|Any CPU
{D71E9924-333D-4417-8FB6-BAB1533C420A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D71E9924-333D-4417-8FB6-BAB1533C420A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D71E9924-333D-4417-8FB6-BAB1533C420A}.Product|Any CPU.ActiveCfg = Debug|Any CPU
{D71E9924-333D-4417-8FB6-BAB1533C420A}.Product|Any CPU.Build.0 = Debug|Any CPU
{D71E9924-333D-4417-8FB6-BAB1533C420A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D71E9924-333D-4417-8FB6-BAB1533C420A}.Release|Any CPU.Build.0 = Release|Any CPU
{5FF78A05-D3B8-4E4E-891C-C4F14E9D2BCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5FF78A05-D3B8-4E4E-891C-C4F14E9D2BCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5FF78A05-D3B8-4E4E-891C-C4F14E9D2BCD}.Product|Any CPU.ActiveCfg = Debug|Any CPU
{5FF78A05-D3B8-4E4E-891C-C4F14E9D2BCD}.Product|Any CPU.Build.0 = Debug|Any CPU
{5FF78A05-D3B8-4E4E-891C-C4F14E9D2BCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5FF78A05-D3B8-4E4E-891C-C4F14E9D2BCD}.Release|Any CPU.Build.0 = Release|Any CPU
{667DC49A-C0A9-46B3-9C0B-6BE2E9EFD4D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{667DC49A-C0A9-46B3-9C0B-6BE2E9EFD4D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{667DC49A-C0A9-46B3-9C0B-6BE2E9EFD4D2}.Product|Any CPU.ActiveCfg = Debug|Any CPU
{667DC49A-C0A9-46B3-9C0B-6BE2E9EFD4D2}.Product|Any CPU.Build.0 = Debug|Any CPU
{667DC49A-C0A9-46B3-9C0B-6BE2E9EFD4D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{667DC49A-C0A9-46B3-9C0B-6BE2E9EFD4D2}.Release|Any CPU.Build.0 = Release|Any CPU
{AC00CE1D-A54D-4243-9762-49A33CF4548B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AC00CE1D-A54D-4243-9762-49A33CF4548B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AC00CE1D-A54D-4243-9762-49A33CF4548B}.Product|Any CPU.ActiveCfg = Debug|Any CPU
{AC00CE1D-A54D-4243-9762-49A33CF4548B}.Product|Any CPU.Build.0 = Debug|Any CPU
{AC00CE1D-A54D-4243-9762-49A33CF4548B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AC00CE1D-A54D-4243-9762-49A33CF4548B}.Release|Any CPU.Build.0 = Release|Any CPU
{0DADDE7F-28D9-43B3-8CC5-88BBE887CBD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0DADDE7F-28D9-43B3-8CC5-88BBE887CBD9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0DADDE7F-28D9-43B3-8CC5-88BBE887CBD9}.Product|Any CPU.ActiveCfg = Debug|Any CPU
{0DADDE7F-28D9-43B3-8CC5-88BBE887CBD9}.Product|Any CPU.Build.0 = Debug|Any CPU
{0DADDE7F-28D9-43B3-8CC5-88BBE887CBD9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0DADDE7F-28D9-43B3-8CC5-88BBE887CBD9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -314,6 +354,11 @@ Global
{4A28CD6B-0C75-4D39-B613-66DE6B693675} = {2673555B-2E7C-417B-8D1A-25F3FE05A141}
{EFA1CD9D-4B53-483C-BF9D-F21B9B2C6FDE} = {2673555B-2E7C-417B-8D1A-25F3FE05A141}
{ACACF41C-B5CB-4C9F-B378-3F15ED15F1D1} = {2673555B-2E7C-417B-8D1A-25F3FE05A141}
{D71E9924-333D-4417-8FB6-BAB1533C420A} = {5EEB0F25-5A4E-4A68-975C-53E42A959621}
{5FF78A05-D3B8-4E4E-891C-C4F14E9D2BCD} = {5EEB0F25-5A4E-4A68-975C-53E42A959621}
{667DC49A-C0A9-46B3-9C0B-6BE2E9EFD4D2} = {5EEB0F25-5A4E-4A68-975C-53E42A959621}
{AC00CE1D-A54D-4243-9762-49A33CF4548B} = {5EEB0F25-5A4E-4A68-975C-53E42A959621}
{0DADDE7F-28D9-43B3-8CC5-88BBE887CBD9} = {5EEB0F25-5A4E-4A68-975C-53E42A959621}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {84CDDCF4-F3D0-45FC-87C5-557845F58F55}
Expand Down
5 changes: 3 additions & 2 deletions Source/Euonia.Core/Threading/ILockProvider.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
namespace Nerosoft.Euonia.Threading;

/*
/// <summary>
/// Specifies contract of lock provider.
/// </summary>
Expand Down Expand Up @@ -217,4 +217,5 @@ public interface ILockProvider
Task ReleaseAsync(string resource);
#endregion
}
}
*/
228 changes: 228 additions & 0 deletions Source/Euonia.Threading.Azure/AzureLockProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
using Azure;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Specialized;

namespace Nerosoft.Euonia.Threading.Azure;

/// <summary>
/// Implements a <see cref="ILockProvider"/> based on Azure blob leases
/// </summary>
public sealed partial class AzureLockProvider : ILockProvider<AzureSynchronizationHandle>
{
/// <summary>
/// Metadata marker used to indicate that a blob was created for distributed locking and therefore
/// should be destroyed upon release
/// </summary>
private const string CREATED_METADATA_KEY = "__EUONIA_LOCK__";

internal readonly BlobClientWrapper BlobClient;
internal readonly AzureSynchronizationOptions Options;

/// <summary>
/// Constructs a lock that will lease the provided <paramref name="blobClient"/>
/// </summary>
public AzureLockProvider(BlobBaseClient blobClient, Action<AzureSynchronizationOptionsBuilder> options = null)
{
BlobClient = new BlobClientWrapper(blobClient ?? throw new ArgumentNullException(nameof(blobClient)));
Options = AzureSynchronizationOptionsBuilder.GetOptions(options);
}

/// <summary>
/// Constructs a lock that will lease a blob based on <paramref name="name"/> within the provided <paramref name="blobContainerClient"/>.
/// </summary>
public AzureLockProvider(BlobContainerClient blobContainerClient, string name, Action<AzureSynchronizationOptionsBuilder> options = null)
{
if (blobContainerClient == null)
{
throw new ArgumentNullException(nameof(blobContainerClient));
}

if (name == null)
{
throw new ArgumentNullException(nameof(name));
}

BlobClient = new BlobClientWrapper(blobContainerClient.GetBlobClient(GetSafeName(name, blobContainerClient)));
Options = AzureSynchronizationOptionsBuilder.GetOptions(options);
}

/// <summary>
/// Implements <see cref="ILockProvider.Name"/>
/// </summary>
public string Name => BlobClient.Name;

// implementation based on https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#blob-names
private static string GetSafeName(string name, BlobContainerClient blobContainerClient)
{
var maxLength = IsStorageEmulator() ? 256 : 1024;

return Helpers.ToSafeName(name, maxLength, s => ConvertToValidName(s));

// check based on
// https://docs.microsoft.com/en-us/azure/storage/common/storage-use-emulator#connect-to-the-emulator-account-using-the-well-known-account-name-and-key
bool IsStorageEmulator() => blobContainerClient.Uri.IsAbsoluteUri
&& blobContainerClient.Uri.AbsoluteUri.StartsWith("http://127.0.0.1:10000/devstoreaccount1", StringComparison.Ordinal);

static string ConvertToValidName(string name)
{
const int maxSlashes = 253; // allowed to have up to 254 segments, which means 253 slashes

if (name.Length == 0)
{
return "__EMPTY__";
}

StringBuilder builder = null;
var slashCount = 0;
for (var i = 0; i < name.Length; ++i)
{
var @char = name[i];

// enforce cap on # path segments and note that trailing slash or DOT are
// discouraged

if ((@char == '/' || @char == '\\')
&& (++slashCount > maxSlashes || i == name.Length - 1))
{
EnsureBuilder().Append("SLASH");
}
else if (@char == '.' && i == name.Length - 1)
{
EnsureBuilder().Append("DOT");
}
else
{
builder?.Append(@char);
}

StringBuilder EnsureBuilder() => builder ??= new StringBuilder().Append(name, startIndex: 0, count: i);
}

return builder?.ToString() ?? name;
}
}

private async ValueTask<AzureSynchronizationHandle> TryAcquireAsync(BlobLeaseClientWrapper leaseClient, bool isRetryAfterCreate, CancellationToken cancellationToken)
{
try
{
await leaseClient.AcquireAsync(Options.Duration, cancellationToken).ConfigureAwait(false);
}
catch (RequestFailedException acquireException)
{
switch (acquireException.ErrorCode)
{
case AzureErrors.LeaseAlreadyPresent:
// if we just created and it already doesn't exist again, just return null and retry later
case AzureErrors.BlobNotFound when isRetryAfterCreate:
return null;
// create the blob
case AzureErrors.BlobNotFound:
{
var metadata = new Dictionary<string, string> { [CREATED_METADATA_KEY] = DateTime.UtcNow.ToString("o") }; // date value is just for debugging
try
{
await BlobClient.CreateIfNotExistsAsync(metadata, cancellationToken).ConfigureAwait(false);
}
catch (RequestFailedException createException)
{
// handle the race condition where we try to create and someone else creates it first
return createException.ErrorCode == AzureErrors.LeaseIdMissing
? default(AzureSynchronizationHandle)
: throw new AggregateException($"Blob {BlobClient.Name} does not exist and could not be created. See inner exceptions for details", acquireException, createException);
}

try
{
return await TryAcquireAsync(leaseClient, isRetryAfterCreate: true, cancellationToken: cancellationToken).ConfigureAwait(false);
}
catch (Exception retryException)
{
// if the retry fails and we created, attempt deletion to clean things up
try
{
await BlobClient.DeleteIfExistsAsync().ConfigureAwait(false);
}
catch (Exception deletionException)
{
throw new AggregateException(retryException, deletionException);
}

throw;
}
}
default:
throw;
}
}

var shouldDeleteBlob = isRetryAfterCreate
|| (await BlobClient.GetMetadataAsync(leaseClient.LeaseId, cancellationToken).ConfigureAwait(false)).ContainsKey(CREATED_METADATA_KEY);

var internalHandle = new InternalHandle(leaseClient, ownsBlob: shouldDeleteBlob, @lock: this);
return new AzureSynchronizationHandle(internalHandle);
}
}

public sealed partial class AzureLockProvider
{
public AzureSynchronizationHandle Acquire(TimeSpan? timeout = null, CancellationToken cancellationToken = default)
{
return Helpers.Acquire(this, timeout, cancellationToken);
}

public ValueTask<AzureSynchronizationHandle> AcquireAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default)
{
return Helpers.AcquireAsync(this, timeout, cancellationToken);
}

public AzureSynchronizationHandle TryAcquire(TimeSpan timeout = default, CancellationToken cancellationToken = default)
{
return Helpers.TryAcquire(this, timeout, cancellationToken);
}

public ValueTask<AzureSynchronizationHandle> TryAcquireAsync(TimeSpan timeout = default, CancellationToken cancellationToken = default)
{
return this.As<ILockProvider<AzureSynchronizationHandle>>().TryAcquireAsync(timeout, cancellationToken);
}

public ValueTask<AzureSynchronizationHandle> TryAcquireAsync(TimeoutValue timeout, CancellationToken cancellationToken)
{
return BusyWaitHelper.WaitAsync(
(@lock: this, leaseClient: BlobClient.GetBlobLeaseClient()),
(state, token) => state.@lock.TryAcquireAsync(state.leaseClient, isRetryAfterCreate: false, cancellationToken: token),
timeout,
minSleepTime: Options.MinBusyWaitSleepTime,
maxSleepTime: Options.MaxBusyWaitSleepTime,
cancellationToken
);
}
}

public sealed partial class AzureLockProvider
{
ISynchronizationHandle ILockProvider.TryAcquire(TimeSpan timeout, CancellationToken cancellationToken)
{
return TryAcquire(timeout, cancellationToken);
}

ISynchronizationHandle ILockProvider.Acquire(TimeSpan? timeout, CancellationToken cancellationToken)
{
return Acquire(timeout, cancellationToken);
}

ValueTask<ISynchronizationHandle> ILockProvider.TryAcquireAsync(TimeSpan timeout, CancellationToken cancellationToken)
{
return TryAcquireAsync(timeout, cancellationToken).Convert(TaskConversion<ISynchronizationHandle>.ValueTask);
}

ValueTask<ISynchronizationHandle> ILockProvider.AcquireAsync(TimeSpan? timeout, CancellationToken cancellationToken)
{
return AcquireAsync(timeout, cancellationToken).Convert(TaskConversion<ISynchronizationHandle>.ValueTask);
}

ValueTask<ISynchronizationHandle> ILockProvider.TryAcquireAsync(TimeoutValue timeout, CancellationToken cancellationToken)
{
return TryAcquireAsync(timeout, cancellationToken).Convert(TaskConversion<ISynchronizationHandle>.ValueTask);
}
}
28 changes: 28 additions & 0 deletions Source/Euonia.Threading.Azure/AzureSynchronizationFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Azure.Storage.Blobs;

namespace Nerosoft.Euonia.Threading.Azure;

/// <summary>
/// Implements <see cref="ILockFactory"/> for <see cref="AzureLockProvider"/>
/// </summary>
public sealed class AzureSynchronizationFactory : ILockFactory
{
private readonly BlobContainerClient _blobContainerClient;
private readonly Action<AzureSynchronizationOptionsBuilder> _options;

/// <summary>
/// Constructs a provider that scopes blobs within the provided <paramref name="blobContainerClient"/> and uses the provided <paramref name="options"/>.
/// </summary>
public AzureSynchronizationFactory(BlobContainerClient blobContainerClient, Action<AzureSynchronizationOptionsBuilder> options = null)
{
_blobContainerClient = blobContainerClient ?? throw new ArgumentNullException(nameof(blobContainerClient));
_options = options;
}

/// <summary>
/// Constructs an <see cref="AzureLockProvider"/> with the given <paramref name="name"/>.
/// </summary>
private AzureLockProvider Create(string name) => new(_blobContainerClient, name, _options);

ILockProvider ILockFactory.Create(string name) => Create(name);
}
Loading

0 comments on commit 8839263

Please sign in to comment.