diff --git a/.gitignore b/.gitignore index d4b26077..17f162d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ #OS junk files Thumbs.db *.DS_Store - +#Rider files +**/.idea/** + #Visual Studio files *.obj *.exe @@ -47,6 +49,10 @@ StyleCop.Cache #Project files [Bb]uild/ +# vs code files +.vscode/ +*.code-workspace + #Nuget Files *.nupkg #MA: using nuget package restore! diff --git a/docs/Developing DistributedLock.md b/docs/Developing DistributedLock.md index 380b8484..b6fc84d6 100644 --- a/docs/Developing DistributedLock.md +++ b/docs/Developing DistributedLock.md @@ -78,3 +78,23 @@ Extract the zip archive, and within it copy `zoo_sample.cfg` to `zoo.cfg`. Add the full path of the extracted directory (the one containing README.md, bin, conf, etc) to `DistributedLock.Tests/credentials/zookeeper.txt` as a single line. Also, install Java Development Kit (JDK) because ZooKeeper runs on Java. + +### Etcd + +The easiest way to run etcd tests locally is to install [Docker](https://docs.docker.com/desktop/): + +```bash +# Run a single-node etcd cluster +docker run -d --name etcd \ + -p 2379:2379 \ + -p 2380:2380 \ + gcr.io/etcd-development/etcd:v3.6.1 \ + /usr/local/bin/etcd \ + --advertise-client-urls http://0.0.0.0:2379 \ + --listen-client-urls http://0.0.0.0:2379 \ + --data-dir /etcd-data + +# Or run a 3-node cluster for more robust testing, this is already setup in EtcdClusterSetup +``` + +The tests will automatically start and stop etcd instances using Docker containers, so you don't need to have etcd running as a service for testing. \ No newline at end of file diff --git a/docs/DistributedLock.Etcd.md b/docs/DistributedLock.Etcd.md new file mode 100644 index 00000000..c89061e2 --- /dev/null +++ b/docs/DistributedLock.Etcd.md @@ -0,0 +1,74 @@ +# DistributedLock.Etcd + +[Download the NuGet package](https://www.nuget.org/packages/DistributedLock.Etcd) [![NuGet Status](http://img.shields.io/nuget/v/DistributedLock.Etcd.svg?style=flat)](https://www.nuget.org/packages/DistributedLock.Etcd/) + +The DistributedLock.Etcd package offers distributed locks based on [etcd](https://etcd.io/). For example: + +```C# +using dotnet_etcd; +using Medallion.Threading.Etcd; + +var client = new EtcdClient("http://localhost:2379"); +var @lock = new EtcdLeaseDistributedLock(client, "MyLockName"); +await using (await @lock.AcquireAsync()) +{ + // I have the lock +} +``` + +## APIs + +- The `EtcdLeaseDistributedLock` class implements the `IDistributedLock` interface. +- The `EtcdLeaseDistributedLockProvider` class implements the `IDistributedLockProvider` interface. + +## Implementation notes + +The `EtcdLeaseDistributedLock` implementation leverages etcd's lease mechanism and distributed lock primitives. It uses etcd's built-in lease functionality to automatically manage lock expiration and renewal. + +The implementation creates a lease with a specified duration and uses that lease to acquire a distributed lock. The lease is automatically renewed in the background to ensure the lock remains held until explicitly released. This provides automatic cleanup in case the process holding the lock crashes or becomes unresponsive. + +Because of how etcd locks work, the acquire operation cannot truly block. If waiting to acquire a lock that is not available, the implementation will periodically sleep and retry until the lock can be taken or the acquire timeout elapses. Because of this, these classes are maximally efficient when using `TryAcquire` semantics with a timeout of zero. + +## Options + +In addition to specifying the lock name and etcd client, some additional tuning options are available through the `EtcdLeaseOptionsBuilder`: + +- `Duration` determines how long the lease will be initially claimed for (because of auto-renewal, locks can be held for longer). Must be between 15 and 60 seconds, or infinite. Defaults to 30s. +- `RenewalCadence` determines how frequently the hold on the lock will be renewed to the full `Duration`. Defaults to 1/3 of `Duration`. To disable auto-renewal, specify `Timeout.InfiniteTimeSpan`. +- `BusyWaitSleepTime` specifies a range of times that the implementation will sleep between attempts to acquire a lock that is currently held by someone else. A random number in the range will be chosen for each sleep. Lower values increase responsiveness but increase the number of calls made to etcd. The default is [250ms, 1s]. + +## Example with options + +```C# +using dotnet_etcd; +using Medallion.Threading.Etcd; + +var client = new EtcdClient("http://localhost:2379"); +var @lock = new EtcdLeaseDistributedLock(client, "MyLockName", options => +{ + options.Duration(TimeSpan.FromSeconds(45)) + .RenewalCadence(TimeSpan.FromSeconds(15)) + .BusyWaitSleepTime(TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(500)); +}); + +await using (await @lock.AcquireAsync()) +{ + // I have the lock with custom options +} +``` + +## Using the provider + +```C# +using dotnet_etcd; +using Medallion.Threading.Etcd; + +var client = new EtcdClient("http://localhost:2379"); +var provider = new EtcdLeaseDistributedLockProvider(client); + +var @lock = provider.CreateLock("MyLockName"); +await using (await @lock.AcquireAsync()) +{ + // I have the lock +} +``` \ No newline at end of file diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 0023a608..b35dbf82 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -22,10 +22,12 @@ + + \ No newline at end of file diff --git a/src/DistributedLock.Core/packages.lock.json b/src/DistributedLock.Core/packages.lock.json index 2f96fece..8d912171 100644 --- a/src/DistributedLock.Core/packages.lock.json +++ b/src/DistributedLock.Core/packages.lock.json @@ -11,12 +11,6 @@ "System.Threading.Tasks.Extensions": "4.5.4" } }, - "Microsoft.CodeAnalysis.PublicApiAnalyzers": { - "type": "Direct", - "requested": "[3.3.4, )", - "resolved": "3.3.4", - "contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA==" - }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", "requested": "[1.0.3, )", @@ -81,12 +75,6 @@ "System.Threading.Tasks.Extensions": "4.5.4" } }, - "Microsoft.CodeAnalysis.PublicApiAnalyzers": { - "type": "Direct", - "requested": "[3.3.4, )", - "resolved": "3.3.4", - "contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA==" - }, "Microsoft.SourceLink.GitHub": { "type": "Direct", "requested": "[8.0.0, )", @@ -136,12 +124,6 @@ } }, ".NETStandard,Version=v2.1": { - "Microsoft.CodeAnalysis.PublicApiAnalyzers": { - "type": "Direct", - "requested": "[3.3.4, )", - "resolved": "3.3.4", - "contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA==" - }, "Microsoft.SourceLink.GitHub": { "type": "Direct", "requested": "[8.0.0, )", @@ -164,17 +146,11 @@ } }, "net8.0": { - "Microsoft.CodeAnalysis.PublicApiAnalyzers": { - "type": "Direct", - "requested": "[3.3.4, )", - "resolved": "3.3.4", - "contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA==" - }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.4, )", - "resolved": "8.0.4", - "contentHash": "PZb5nfQ+U19nhnmnR9T1jw+LTmozhuG2eeuzuW5A7DqxD/UXW2ucjmNJqnqOuh8rdPzM3MQXoF8AfFCedJdCUw==" + "requested": "[8.0.19, )", + "resolved": "8.0.19", + "contentHash": "IhHf+zeZiaE5EXRyxILd4qM+Hj9cxV3sa8MpzZgeEhpvaG3a1VEGF6UCaPFLO44Kua3JkLKluE0SWVamS50PlA==" }, "Microsoft.SourceLink.GitHub": { "type": "Direct", diff --git a/src/DistributedLock.Etcd/DistributedLock.Etcd.csproj b/src/DistributedLock.Etcd/DistributedLock.Etcd.csproj new file mode 100644 index 00000000..655f7846 --- /dev/null +++ b/src/DistributedLock.Etcd/DistributedLock.Etcd.csproj @@ -0,0 +1,65 @@ + + + + netstandard2.1 + Medallion.Threading.FileSystem + True + 4 + Latest + enable + enable + + + + 1.0.3 + 1.0.0.0 + Michael Adelson + Provides a distributed lock implementation based on etcd + Copyright © 2020 Michael Adelson + MIT + distributed etcd lock + https://github.com/madelson/DistributedLock + https://github.com/madelson/DistributedLock + 1.0.0.0 + See https://github.com/madelson/DistributedLock#release-notes + true + ..\DistributedLock.snk + + + + True + True + True + + + embedded + + true + true + + + + False + 1591 + TRACE;DEBUG + + + + + + + + + + all + + + + + + + + + + + \ No newline at end of file diff --git a/src/DistributedLock.Etcd/EtcdClientWrapper.cs b/src/DistributedLock.Etcd/EtcdClientWrapper.cs new file mode 100644 index 00000000..0d243330 --- /dev/null +++ b/src/DistributedLock.Etcd/EtcdClientWrapper.cs @@ -0,0 +1,54 @@ +using dotnet_etcd.interfaces; +using Etcdserverpb; +using Grpc.Core; +using Medallion.Threading.Internal; +using V3Lockpb; + +namespace Medallion.Threading.Etcd; + +internal class EtcdClientWrapper +{ + private readonly IEtcdClient _etcdClient; + public EtcdClientWrapper(IEtcdClient etcdClient) + { + this._etcdClient = etcdClient ?? throw new ArgumentNullException(nameof(etcdClient)); + } + + public ValueTask LeaseGrantAsync(LeaseGrantRequest request, CancellationToken cancellationToken) + { + return SyncViaAsync.IsSynchronous + ? new ValueTask(this._etcdClient.LeaseGrant(request, + cancellationToken: cancellationToken)) + : new ValueTask( + this._etcdClient.LeaseGrantAsync(request, cancellationToken: cancellationToken)); + } + + public Task LeaseKeepAliveAsync(long leaseId, CancellationToken token) + => this._etcdClient.LeaseKeepAlive(leaseId, token); + + public async ValueTask LockAsync(LockRequest lockRequest, CancellationToken cancellationToken) + { + var response = SyncViaAsync.IsSynchronous + ? this._etcdClient.Lock(lockRequest, cancellationToken: cancellationToken) + : await this._etcdClient.LockAsync(lockRequest, cancellationToken: cancellationToken).ConfigureAwait(false); + + if (response == null) + { + throw new RpcException(new Status(StatusCode.Internal, "Lock failed")); + } + + return response; + } + + public async ValueTask LeaseRevokeAsync(LeaseRevokeRequest leaseRevokeRequest) + { + if (SyncViaAsync.IsSynchronous) + { + this._etcdClient.LeaseRevoke(leaseRevokeRequest); + } + else + { + await this._etcdClient.LeaseRevokeAsync(leaseRevokeRequest).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/DistributedLock.Etcd/EtcdLeaseDistributedLock.IDistributedLock.cs b/src/DistributedLock.Etcd/EtcdLeaseDistributedLock.IDistributedLock.cs new file mode 100644 index 00000000..008ecbb3 --- /dev/null +++ b/src/DistributedLock.Etcd/EtcdLeaseDistributedLock.IDistributedLock.cs @@ -0,0 +1,81 @@ +using Medallion.Threading.Internal; + +namespace Medallion.Threading.Etcd; + +public partial class EtcdLeaseDistributedLock +{ + // AUTO-GENERATED + + IDistributedSynchronizationHandle? IDistributedLock.TryAcquire(TimeSpan timeout, CancellationToken cancellationToken) => + this.TryAcquire(timeout, cancellationToken); + IDistributedSynchronizationHandle IDistributedLock.Acquire(TimeSpan? timeout, CancellationToken cancellationToken) => + this.Acquire(timeout, cancellationToken); + ValueTask IDistributedLock.TryAcquireAsync(TimeSpan timeout, CancellationToken cancellationToken) => + this.TryAcquireAsync(timeout, cancellationToken).Convert(To.ValueTask); + ValueTask IDistributedLock.AcquireAsync(TimeSpan? timeout, CancellationToken cancellationToken) => + this.AcquireAsync(timeout, cancellationToken).Convert(To.ValueTask); + + /// + /// Attempts to acquire the lock synchronously. Usage: + /// + /// using (var handle = myLock.TryAcquire(...)) + /// { + /// if (handle != null) { /* we have the lock! */ } + /// } + /// // dispose releases the lock if we took it + /// + /// + /// How long to wait before giving up on the acquisition attempt. Defaults to 0 + /// Specifies a token by which the wait can be canceled + /// An which can be used to release the lock or null on failure + public EtcdLeaseDistributedLockHandle? TryAcquire(TimeSpan timeout = default, CancellationToken cancellationToken = default) => + DistributedLockHelpers.TryAcquire(this, timeout, cancellationToken); + + /// + /// Acquires the lock synchronously, failing with if the attempt times out. Usage: + /// + /// using (myLock.Acquire(...)) + /// { + /// /* we have the lock! */ + /// } + /// // dispose releases the lock + /// + /// + /// How long to wait before giving up on the acquisition attempt. Defaults to + /// Specifies a token by which the wait can be canceled + /// An which can be used to release the lock + public EtcdLeaseDistributedLockHandle Acquire(TimeSpan? timeout = null, CancellationToken cancellationToken = default) => + DistributedLockHelpers.Acquire(this, timeout, cancellationToken); + + /// + /// Attempts to acquire the lock asynchronously. Usage: + /// + /// await using (var handle = await myLock.TryAcquireAsync(...)) + /// { + /// if (handle != null) { /* we have the lock! */ } + /// } + /// // dispose releases the lock if we took it + /// + /// + /// How long to wait before giving up on the acquisition attempt. Defaults to 0 + /// Specifies a token by which the wait can be canceled + /// An which can be used to release the lock or null on failure + public ValueTask TryAcquireAsync(TimeSpan timeout = default, CancellationToken cancellationToken = default) => + this.As>().InternalTryAcquireAsync(timeout, cancellationToken); + + /// + /// Acquires the lock asynchronously, failing with if the attempt times out. Usage: + /// + /// await using (await myLock.AcquireAsync(...)) + /// { + /// /* we have the lock! */ + /// } + /// // dispose releases the lock + /// + /// + /// How long to wait before giving up on the acquisition attempt. Defaults to + /// Specifies a token by which the wait can be canceled + /// An which can be used to release the lock + public ValueTask AcquireAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default) => + DistributedLockHelpers.AcquireAsync(this, timeout, cancellationToken); +} \ No newline at end of file diff --git a/src/DistributedLock.Etcd/EtcdLeaseDistributedLock.cs b/src/DistributedLock.Etcd/EtcdLeaseDistributedLock.cs new file mode 100644 index 00000000..78c849ba --- /dev/null +++ b/src/DistributedLock.Etcd/EtcdLeaseDistributedLock.cs @@ -0,0 +1,71 @@ +using dotnet_etcd.interfaces; +using Etcdserverpb; +using Medallion.Threading.Internal; +using V3Lockpb; + +namespace Medallion.Threading.Etcd; + +/// +/// A distributed lock based on holding an exclusive handle to a lock file. The file will be deleted when the lock is released. +/// +public sealed partial class EtcdLeaseDistributedLock : IInternalDistributedLock +{ + private readonly EtcdClientWrapper _etcdClient; + + private readonly (TimeoutValue duration, TimeoutValue renewalCadence, TimeoutValue minBusyWaitSleepTime, + TimeoutValue maxBusyWaitSleepTime) _options; + + public EtcdLeaseDistributedLock(IEtcdClient client, string lockName, + Action? options = null) + { + // TODO must support all non-null lock name via getsafename + this.Name = lockName ?? throw new ArgumentNullException(nameof(lockName)); + if (lockName.Length == 0) { throw new FormatException($"{nameof(lockName)}: may not have an empty file name"); } + + this._etcdClient = new EtcdClientWrapper(client); + this._options = EtcdLeaseOptionsBuilder.GetOptions(options); + } + + // todo revisit API + /// + /// Implements + /// + public string Name { get; } + + + ValueTask IInternalDistributedLock. + InternalTryAcquireAsync(TimeoutValue timeout, CancellationToken cancellationToken) => + BusyWaitHelper.WaitAsync( + state: this, + tryGetValue: (@this, token) => @this.TryAcquireAsync(token), + timeout: timeout, + minSleepTime: this._options.minBusyWaitSleepTime, + maxSleepTime: this._options.maxBusyWaitSleepTime, + cancellationToken + ); + + private async ValueTask TryAcquireAsync(CancellationToken cancellationToken) + { + // TODO implement LeaseHandle + cancellationToken.ThrowIfCancellationRequested(); + // TODO renewnal cadence should not be here + var leaseResponse = await this._etcdClient.LeaseGrantAsync( + new LeaseGrantRequest { TTL = this._options.renewalCadence.InSeconds }, + cancellationToken: cancellationToken).ConfigureAwait(false); + var leaseId = leaseResponse.ID; + var cancellationTokenSource = new CancellationTokenSource(); + _ = this._etcdClient.LeaseKeepAliveAsync(leaseId, cancellationTokenSource.Token).ConfigureAwait(false); + var response = + await this._etcdClient.LockAsync( + new LockRequest { Name = Google.Protobuf.ByteString.CopyFromUtf8(this.Name), Lease = leaseId, }, + cancellationToken: cancellationTokenSource.Token).ConfigureAwait(false); + var actualKey = response.Key; + return new EtcdLeaseDistributedLockHandle(actualKey.ToString(), this._etcdClient, leaseId); + } + + public static string GetSafeName(string name) + { + // TODO figure + return DistributedLockHelpers.ToSafeName(name, 1000, s => s); + } +} \ No newline at end of file diff --git a/src/DistributedLock.Etcd/EtcdLeaseDistributedLockHandle.cs b/src/DistributedLock.Etcd/EtcdLeaseDistributedLockHandle.cs new file mode 100644 index 00000000..0e27c1e3 --- /dev/null +++ b/src/DistributedLock.Etcd/EtcdLeaseDistributedLockHandle.cs @@ -0,0 +1,44 @@ +using Etcdserverpb; +using Medallion.Threading.Internal; + +namespace Medallion.Threading.Etcd; + +public sealed class EtcdLeaseDistributedLockHandle : IDistributedSynchronizationHandle +{ + private readonly string _key; + private readonly long _leaseKey; + private readonly EtcdClientWrapper _client; + + internal EtcdLeaseDistributedLockHandle(string key, EtcdClientWrapper client, long leaseKey) + { + this._key = key ?? throw new ArgumentNullException(nameof(key)); + this._client = client ?? throw new ArgumentNullException(nameof(client)); + this._leaseKey = leaseKey; + // Because this is a lease, managed finalization mostly won't be strictly necessary here. Where it comes in handy is: + // (1) Ensuring blob deletion if we own the blob + // (2) Helping release infinite-duration leases (rare case) + // (3) In testing, avoiding having to wait 15+ seconds for lease expiration + } + + + /// + /// Implements + /// TODO implement HandleLostToken + /// + public CancellationToken HandleLostToken => new(); + + + /// + /// Releases the lock + /// + public void Dispose() => this.DisposeSyncViaAsync(); + + /// + /// Releases the lock asynchronously + /// + public ValueTask DisposeAsync() + { + //TODO lease revoke will die here + return this._client.LeaseRevokeAsync(new LeaseRevokeRequest { ID = this._leaseKey }); + } +} \ No newline at end of file diff --git a/src/DistributedLock.Etcd/EtcdLeaseDistributedLockProvider.cs b/src/DistributedLock.Etcd/EtcdLeaseDistributedLockProvider.cs new file mode 100644 index 00000000..8f3567a3 --- /dev/null +++ b/src/DistributedLock.Etcd/EtcdLeaseDistributedLockProvider.cs @@ -0,0 +1,29 @@ +using dotnet_etcd.interfaces; + +namespace Medallion.Threading.Etcd; + +/// +/// Implements for +/// +public sealed class EtcdLeaseDistributedLockProvider : IDistributedLockProvider +{ + private readonly IEtcdClient _blobContainerClient; + private readonly Action? _options; + + /// + /// Constructs a provider that scopes blobs within the provided and uses the provided . + /// + public EtcdLeaseDistributedLockProvider(IEtcdClient blobContainerClient, + Action? options = null) + { + this._blobContainerClient = blobContainerClient ?? throw new ArgumentNullException(nameof(blobContainerClient)); + this._options = options; + } + + /// + /// Constructs an with the given . + /// + public EtcdLeaseDistributedLock CreateLock(string name) => new(this._blobContainerClient, name, this._options); + + IDistributedLock IDistributedLockProvider.CreateLock(string name) => this.CreateLock(name); +} \ No newline at end of file diff --git a/src/DistributedLock.Etcd/EtcdLeaseOptionsBuilder.cs b/src/DistributedLock.Etcd/EtcdLeaseOptionsBuilder.cs new file mode 100644 index 00000000..88038ce3 --- /dev/null +++ b/src/DistributedLock.Etcd/EtcdLeaseOptionsBuilder.cs @@ -0,0 +1,140 @@ +using Medallion.Threading.Internal; + +namespace Medallion.Threading.Etcd; + +/// +/// Options for EtcdLocks including renewal, MinLease,MaxLease TODO what to write here +/// +public class EtcdLeaseOptionsBuilder +{ + /// + /// From https://docs.microsoft.com/en-us/rest/api/storageservices/lease-blob: + /// "The lock duration can be 15 to 60 seconds, or can be infinite" + /// + private static readonly TimeoutValue MinLeaseDuration = TimeSpan.FromSeconds(15); + + /// + /// From https://docs.microsoft.com/en-us/rest/api/storageservices/lease-blob: + /// "The lock duration can be 15 to 60 seconds, or can be infinite" + /// + private static readonly TimeoutValue MaxNonInfiniteLeaseDuration = TimeSpan.FromSeconds(60); + + /// + /// From https://docs.microsoft.com/en-us/rest/api/storageservices/lease-blob: + /// "The lock duration can be 15 to 60 seconds, or can be infinite" + /// + private static readonly TimeoutValue DefaultLeaseDuration = TimeSpan.FromSeconds(30); + + private TimeoutValue? _duration, _renewalCadence, _minBusyWaitSleepTime, _maxBusyWaitSleepTime; + + internal EtcdLeaseOptionsBuilder() { } + + /// + /// Specifies how long the lease will last, absent auto-renewal. + /// + /// If auto-renewal is enabled (the default), then a shorter duration means more frequent auto-renewal requests, + /// while an infinite duration means no auto-renewal requests. Furthermore, if the lease-holding process were to + /// exit without explicitly releasing, then duration determines how long other processes would need to wait in + /// order to acquire the lease. + /// + /// If auto-renewal is disabled, then duration determines how long the lease will be held. + /// + /// Defaults to 30s. + /// + public EtcdLeaseOptionsBuilder Duration(TimeSpan duration) + { + var durationTimeoutValue = new TimeoutValue(duration, nameof(duration)); + if (durationTimeoutValue.CompareTo(MinLeaseDuration) < 0 + || (!durationTimeoutValue.IsInfinite && durationTimeoutValue.CompareTo(MaxNonInfiniteLeaseDuration) > 0)) + { + throw new ArgumentOutOfRangeException(nameof(duration), duration, + $"Must be infinite or in [{MinLeaseDuration}, {MaxNonInfiniteLeaseDuration}]"); + } + + this._duration = durationTimeoutValue; + return this; + } + + /// + /// Determines how frequently the lease will be renewed when held. More frequent renewal means more unnecessary requests + /// but also a lower chance of losing the lease due to the process hanging or otherwise failing to get its renewal request in + /// before the lease duration expires. + /// + /// To disable auto-renewal, specify + /// + /// Defaults to 1/3 of the specified lease duration (may be infinite). + /// + public EtcdLeaseOptionsBuilder RenewalCadence(TimeSpan renewalCadence) + { + this._renewalCadence = new TimeoutValue(renewalCadence, nameof(renewalCadence)); + return this; + } + + /// + /// Waiting to acquire a lease requires a busy wait that alternates acquire attempts and sleeps. + /// This determines how much time is spent sleeping between attempts. Lower values will raise the + /// volume of acquire requests under contention but will also raise the responsiveness (how long + /// it takes a waiter to notice that a contended the lease has become available). + /// + /// Specifying a range of values allows the implementation to select an actual value in the range + /// at random for each sleep. This helps avoid the case where two clients become "synchronized" + /// in such a way that results in one client monopolizing the lease. + /// + /// The default is [250ms, 1s] + /// + public EtcdLeaseOptionsBuilder BusyWaitSleepTime(TimeSpan min, TimeSpan max) + { + var minTimeoutValue = new TimeoutValue(min, nameof(min)); + var maxTimeoutValue = new TimeoutValue(max, nameof(max)); + + if (minTimeoutValue.IsInfinite) { throw new ArgumentOutOfRangeException(nameof(min), "may not be infinite"); } + + if (maxTimeoutValue.IsInfinite || maxTimeoutValue.CompareTo(min) < 0) + { + throw new ArgumentOutOfRangeException(nameof(max), max, + "must be non-infinite and greater than " + nameof(min)); + } + + this._minBusyWaitSleepTime = minTimeoutValue; + this._maxBusyWaitSleepTime = maxTimeoutValue; + return this; + } + + internal static (TimeoutValue duration, TimeoutValue renewalCadence, TimeoutValue minBusyWaitSleepTime, TimeoutValue + maxBusyWaitSleepTime) GetOptions(Action? optionsBuilder) + { + EtcdLeaseOptionsBuilder? options; + if (optionsBuilder != null) + { + options = new EtcdLeaseOptionsBuilder(); + optionsBuilder(options); + + if (options._renewalCadence is { } renewalCadence && !renewalCadence.IsInfinite) + { + var duration = options._duration ?? DefaultLeaseDuration; + if (renewalCadence.CompareTo(duration) >= 0) + { + throw new ArgumentOutOfRangeException( + nameof(renewalCadence), + renewalCadence.TimeSpan, + $"{nameof(renewalCadence)} must not be larger than {nameof(duration)} ({duration}). To disable auto-renewal, specify {nameof(Timeout)}.{nameof(Timeout.InfiniteTimeSpan)}" + ); + } + } + } + else + { + options = null; + } + + var durationToUse = options?._duration ?? DefaultLeaseDuration; + return ( + duration: durationToUse, + renewalCadence: options?._renewalCadence ?? (durationToUse.IsInfinite + ? Timeout.InfiniteTimeSpan + : TimeSpan.FromMilliseconds(durationToUse.InMilliseconds / 3.0)), + minBusyWaitSleepTime: options?._minBusyWaitSleepTime ?? TimeSpan.FromMilliseconds(250), + maxBusyWaitSleepTime: options?._maxBusyWaitSleepTime ?? TimeSpan.FromSeconds(1) + ); + } +} \ No newline at end of file diff --git a/src/DistributedLock.Etcd/PublicAPI.Shipped.txt b/src/DistributedLock.Etcd/PublicAPI.Shipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/src/DistributedLock.Etcd/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/DistributedLock.Etcd/PublicAPI.Unshipped.txt b/src/DistributedLock.Etcd/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..dab03716 --- /dev/null +++ b/src/DistributedLock.Etcd/PublicAPI.Unshipped.txt @@ -0,0 +1,5 @@ +Medallion.Threading.Etcd.EtcdLeaseDistributedLock +Medallion.Threading.Etcd.EtcdLeaseDistributedLock.EtcdLeaseDistributedLock(string! lockName, string! address) -> void +Medallion.Threading.Etcd.EtcdLeaseDistributedLockHandle +Medallion.Threading.Etcd.EtcdLeaseDistributedLockHandle.Dispose() -> void +Medallion.Threading.Etcd.EtcdLeaseDistributedLockHandle.DisposeAsync() -> System.Threading.Tasks.ValueTask \ No newline at end of file diff --git a/src/DistributedLock.Etcd/packages.lock.json b/src/DistributedLock.Etcd/packages.lock.json new file mode 100644 index 00000000..af1ca2cd --- /dev/null +++ b/src/DistributedLock.Etcd/packages.lock.json @@ -0,0 +1,149 @@ +{ + "version": 2, + "dependencies": { + ".NETStandard,Version=v2.1": { + "dotnet-etcd": { + "type": "Direct", + "requested": "[5.2.1, )", + "resolved": "5.2.1", + "contentHash": "fmnjf4/TG1C4KTbxIxQ1xJhkGFDrgWHvks3XSQnCBvRtkBP5/UCI82MQxD68/TvetVuVpSoVdlXMD5Nq5kZP5g==", + "dependencies": { + "DnsClient": "1.6.1", + "Google.Protobuf": "3.21.2", + "Grpc.Net.Client": "2.47.0" + } + }, + "Microsoft.CodeAnalysis.PublicApiAnalyzers": { + "type": "Direct", + "requested": "[3.3.4, )", + "resolved": "3.3.4", + "contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA==" + }, + "Microsoft.SourceLink.GitHub": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "dependencies": { + "Microsoft.Build.Tasks.Git": "8.0.0", + "Microsoft.SourceLink.Common": "8.0.0" + } + }, + "DnsClient": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "4H/f2uYJOZ+YObZjpY9ABrKZI+JNw3uizp6oMzTXwDw6F+2qIPhpRl/1t68O/6e98+vqNiYGu+lswmwdYUy3gg==", + "dependencies": { + "Microsoft.Win32.Registry": "5.0.0" + } + }, + "Google.Protobuf": { + "type": "Transitive", + "resolved": "3.21.2", + "contentHash": "91vdjMcbesfibUtE2n6bVWelT+7J7hExMb9WAEQXuM748+aoIrrUECkIBCmQG+RiM4KvEccVgutFdzl1l6G+Rw==", + "dependencies": { + "System.Memory": "4.5.3", + "System.Runtime.CompilerServices.Unsafe": "4.5.2" + } + }, + "Grpc.Core.Api": { + "type": "Transitive", + "resolved": "2.47.0", + "contentHash": "oZXapxH/2WHAVALghNauo+r/bp6zjgQ6r0v8FizLLQg0/j/FkK2u3WZ7cLOL9Y5H4oLg+wLclO8FSvNTQpNR5Q==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "Grpc.Net.Client": { + "type": "Transitive", + "resolved": "2.47.0", + "contentHash": "DLbUC3T8dMmhA2iqpaJo0X4g7fAi3FyVbDd0/jBXrlqc/bcyA1wPBzLx6mWOWoGUb5S89xL2svnsM8SfzzNa2Q==", + "dependencies": { + "Grpc.Net.Common": "2.47.0", + "Microsoft.Extensions.Logging.Abstractions": "3.0.3", + "System.Diagnostics.DiagnosticSource": "4.5.1" + } + }, + "Grpc.Net.Common": { + "type": "Transitive", + "resolved": "2.47.0", + "contentHash": "Cv76h7noEN2s9cwIdEspOeDjeqcU6Nm34OmjSbRhD/FDBXFmG7rmSfJTPCEB1LYjZWfmf7uUH+nYcHR1I7oQqw==", + "dependencies": { + "Grpc.Core.Api": "2.47.0" + } + }, + "Microsoft.Build.Tasks.Git": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "3.0.3", + "contentHash": "m2Jyi/MEn043WMI1I6J1ALuCThktZ93rd7eqzYeLmMcA0bdZC+TBVl0LuEbEWM01dWeeBjOoagjNwQTzOi2r6A==" + }, + "Microsoft.SourceLink.Common": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Memory": "4.5.4", + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "zCno/m44ymWhgLFh7tELDG9587q0l/EynPM0m4KgLaWQbz/TEKvNRX2YT5ip2qXW/uayifQ2ZqbnErsKJ4lYrQ==" + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Numerics.Vectors": "4.4.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ==" + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3TIsJhD1EiiT0w2CcDMN/iSSwnNnsrnbzeVHSKkaEgV85txMprmuO+Yq2AdSbeVGcg28pdNDTPK87tJhX7VFHw==" + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", + "dependencies": { + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, + "distributedlock.core": { + "type": "Project" + } + } + } +} \ No newline at end of file diff --git a/src/DistributedLock.Oracle/packages.lock.json b/src/DistributedLock.Oracle/packages.lock.json index b511da3b..fee82c94 100644 --- a/src/DistributedLock.Oracle/packages.lock.json +++ b/src/DistributedLock.Oracle/packages.lock.json @@ -8,6 +8,15 @@ "resolved": "3.3.4", "contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA==" }, + "Microsoft.NETFramework.ReferenceAssemblies": { + "type": "Direct", + "requested": "[1.0.3, )", + "resolved": "1.0.3", + "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", + "dependencies": { + "Microsoft.NETFramework.ReferenceAssemblies.net472": "1.0.3" + } + }, "Microsoft.SourceLink.GitHub": { "type": "Direct", "requested": "[8.0.0, )", @@ -43,6 +52,11 @@ "resolved": "8.0.0", "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" }, + "Microsoft.NETFramework.ReferenceAssemblies.net472": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "0E7evZXHXaDYYiLRfpyXvCh+yzM2rNTyuZDI+ZO7UUqSc6GfjePiXTdqJGtgIKUwdI81tzQKmaWprnUiPj9hAw==" + }, "Microsoft.SourceLink.Common": { "type": "Transitive", "resolved": "8.0.0", diff --git a/src/DistributedLock.Postgres/packages.lock.json b/src/DistributedLock.Postgres/packages.lock.json index 797320b6..26032eb5 100644 --- a/src/DistributedLock.Postgres/packages.lock.json +++ b/src/DistributedLock.Postgres/packages.lock.json @@ -518,9 +518,9 @@ }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.4, )", - "resolved": "8.0.4", - "contentHash": "PZb5nfQ+U19nhnmnR9T1jw+LTmozhuG2eeuzuW5A7DqxD/UXW2ucjmNJqnqOuh8rdPzM3MQXoF8AfFCedJdCUw==" + "requested": "[8.0.19, )", + "resolved": "8.0.19", + "contentHash": "IhHf+zeZiaE5EXRyxILd4qM+Hj9cxV3sa8MpzZgeEhpvaG3a1VEGF6UCaPFLO44Kua3JkLKluE0SWVamS50PlA==" }, "Microsoft.SourceLink.GitHub": { "type": "Direct", diff --git a/src/DistributedLock.Tests/DistributedLock.Tests.csproj b/src/DistributedLock.Tests/DistributedLock.Tests.csproj index be82219e..a9ccfb99 100644 --- a/src/DistributedLock.Tests/DistributedLock.Tests.csproj +++ b/src/DistributedLock.Tests/DistributedLock.Tests.csproj @@ -27,6 +27,7 @@ + diff --git a/src/DistributedLock.Tests/Infrastructure/Azure/AzureSetUpFixture.cs b/src/DistributedLock.Tests/Infrastructure/Azure/AzureSetUpFixture.cs index 9e410d46..50827089 100644 --- a/src/DistributedLock.Tests/Infrastructure/Azure/AzureSetUpFixture.cs +++ b/src/DistributedLock.Tests/Infrastructure/Azure/AzureSetUpFixture.cs @@ -1,5 +1,6 @@ using Azure.Storage.Blobs; -using Medallion.Shell; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; using NUnit.Framework; using System.Diagnostics; @@ -8,60 +9,35 @@ namespace Medallion.Threading.Tests.Azure; [SetUpFixture] public class AzureSetUpFixture { - // https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite?tabs=visual-studio%2Cblob-storage - private const string EmulatorProcessName = "azurite"; - - private bool _startedEmulator; + private const string DockerImageName = "mcr.microsoft.com/azure-storage/azurite"; + private const int BlobServicePort = 10000; + private IContainer? _container; [OneTimeSetUp] - public void OneTimeSetUp() + public async Task OneTimeSetUp() { - var existingProcesses = Process.GetProcessesByName(EmulatorProcessName); - if (existingProcesses.Any()) - { - Console.WriteLine($"Emulator already running (PID={existingProcesses[0].Id})"); - foreach (var process in existingProcesses) { process.Dispose(); } - } - else - { - var emulatorExePaths = Directory.GetFiles(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Microsoft Visual Studio"), $"{EmulatorProcessName}.exe", SearchOption.AllDirectories) - .OrderByDescending(File.GetLastWriteTimeUtc) - .ToArray(); - if (!emulatorExePaths.Any()) - { - throw new FileNotFoundException($"Could not locate {EmulatorProcessName}. This is required to run Azure tests. See https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite"); - } - - // Note: we used to hang on to this command to kill it later; we no longer do that because this process seems to naturally exit - // by the end of the test and instead the emulator is running in another process. Therefore, we do a name-based lookup in teardown instead. - var command = Command.Run( - emulatorExePaths[0], - // After updating to Azure.Storage.Blobs 12.19.1 I started getting an error saying that I had to update Azurite or pass this flag. - // AFAIK the only way to update Azurite is to update VS, which did not fix the issue. Therefore, I am passing this flag instead. - ["start", "--skipApiVersionCheck"], - o => o.StartInfo(i => i.RedirectStandardInput = false) - .WorkingDirectory(Path.GetDirectoryName(this.GetType().Assembly.Location)!)) - .RedirectTo(Console.Out) - .RedirectStandardErrorTo(Console.Error); - Console.WriteLine($"Launched {EmulatorProcessName}"); - this._startedEmulator = true; - } - - new BlobContainerClient(AzureCredentials.ConnectionString, AzureCredentials.DefaultBlobContainerName).CreateIfNotExists(); + // Create a new instance of a container. + this._container = new ContainerBuilder() + .WithImage(DockerImageName) + // Bind port 8080 of the container to a random port on the host. + .WithPortBinding(BlobServicePort, 10000) + // Wait until the HTTP endpoint of the container is available. + .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(10000)) + // Build the container configuration. + .Build(); + await this._container.StartAsync() + .ConfigureAwait(false); + // Wait for Azurite to be ready + + await new BlobContainerClient(AzureCredentials.ConnectionString, AzureCredentials.DefaultBlobContainerName) + .CreateIfNotExistsAsync(); } [OneTimeTearDown] - public void OneTimeTearDown() + public async Task OneTimeTearDown() { - new BlobContainerClient(AzureCredentials.ConnectionString, AzureCredentials.DefaultBlobContainerName).DeleteIfExists(); - - if (this._startedEmulator) - { - foreach (var process in Process.GetProcessesByName(EmulatorProcessName)) - { - process.Kill(); - process.WaitForExit(); - } - } + await new BlobContainerClient(AzureCredentials.ConnectionString, AzureCredentials.DefaultBlobContainerName) + .DeleteIfExistsAsync(); + await this._container.DisposeAsync(); } -} +} \ No newline at end of file diff --git a/src/DistributedLock.Tests/Infrastructure/Shared/EtcdCredentials.cs b/src/DistributedLock.Tests/Infrastructure/Shared/EtcdCredentials.cs new file mode 100644 index 00000000..ea9b1244 --- /dev/null +++ b/src/DistributedLock.Tests/Infrastructure/Shared/EtcdCredentials.cs @@ -0,0 +1,7 @@ + +namespace Medallion.Threading.Tests; + +public static class EtcdCredentials +{ + public const string EtcdHost = "http://localhost:2379?Secure=false?NoPassword=true"; +} \ No newline at end of file diff --git a/src/DistributedLock.Tests/Tests/CombinatorialTests.cs b/src/DistributedLock.Tests/Tests/CombinatorialTests.cs index cfa4c9ab..408da3c0 100644 --- a/src/DistributedLock.Tests/Tests/CombinatorialTests.cs +++ b/src/DistributedLock.Tests/Tests/CombinatorialTests.cs @@ -6,6 +6,11 @@ namespace Medallion.Threading.Tests.Azure public class Core_AzureBlobLease_AzureBlobLeaseSynchronizationStrategyTest : DistributedLockCoreTestCases { } } +namespace Medallion.Threading.Tests.Etcd +{ + public class Core_EtcdLease_EtcdLeaseSynchronizationStrategyTest : DistributedLockCoreTestCases { } +} + namespace Medallion.Threading.Tests.FileSystem { [Category("CI")] public class Core_File_FileSynchronizationStrategyTest : DistributedLockCoreTestCases { } diff --git a/src/DistributedLock.Tests/Tests/Etcd/EtcdClusterSetup.cs b/src/DistributedLock.Tests/Tests/Etcd/EtcdClusterSetup.cs new file mode 100644 index 00000000..a1d051ee --- /dev/null +++ b/src/DistributedLock.Tests/Tests/Etcd/EtcdClusterSetup.cs @@ -0,0 +1,96 @@ +using dotnet_etcd; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; +using DotNet.Testcontainers.Networks; +using NUnit.Framework; + +namespace Medallion.Threading.Tests.Etcd; + +internal class EtcdClusterSetup +{ + private readonly List _etcdContainers = []; + private INetwork _network; + private const string DockerImageName = "gcr.io/etcd-development/etcd:v3.6.1"; + private const int BaseClientPort = 2379; + private const int BasePeerPort = 2380; + private readonly List _connectionStrings = []; + + + public async Task ClusterSetup() + { + // setup 3 nodes etcd cluster for proper testing + var network = new NetworkBuilder() + .WithName("test-etcd-network") + .Build(); + this._network = network; + + var initialCluster = string.Join(",", + Enumerable.Range(0, 3).Select(j => $"infra{j}=http://infra{j}:{BasePeerPort + j * 2}")); + await network.CreateAsync().ConfigureAwait(false); + for (var i = 0; i < 3; i++) + { + var nodeName = $"infra{i}"; + var clientPort = BaseClientPort + i * 2; + var peerPort = BasePeerPort + i * 2; + var container = new ContainerBuilder() + .WithImage(DockerImageName) + .WithName(nodeName) + .WithNetwork(network) + .WithNetworkAliases(nodeName) + .WithPortBinding(clientPort, BaseClientPort) + .WithPortBinding(peerPort, BasePeerPort) + .WithEnvironment("ETCD_NAME", nodeName) + .WithEnvironment("ETCD_INITIAL_CLUSTER", initialCluster) + .WithEnvironment("ETCD_INITIAL_CLUSTER_STATE", "new") + .WithEnvironment("ETCD_INITIAL_CLUSTER_TOKEN", "etcd-cluster-1") + .WithEnvironment("ETCD_INITIAL_ADVERTISE_PEER_URLS", $"http://{nodeName}:{peerPort}") + .WithEnvironment("ETCD_LISTEN_PEER_URLS", $"http://0.0.0.0:{peerPort}") + .WithEnvironment("ETCD_LISTEN_CLIENT_URLS", $"http://0.0.0.0:{clientPort}") + .WithEnvironment("ETCD_ADVERTISE_CLIENT_URLS", $"http://localhost:{clientPort}") + .Build(); + this._etcdContainers.Add(container); + this._connectionStrings.Add($"http://localhost:{clientPort}"); + } + + + TestContext.WriteLine("This is a message to the test output."); + var tasks = this._etcdContainers.Select(container => container.StartAsync()); + TestContext.WriteLine("This is a message to the test output."); + await Task.WhenAll(tasks); + } + + public async Task TearDownCluster() + { + var valueTasks = this._etcdContainers.Select(container => container.DisposeAsync()); + foreach (var task in valueTasks) + { + await task; + } + + await this._network.DisposeAsync(); + } + + internal EtcdClient CreateClientToEtcdCluster() + { + return new EtcdClient(string.Join(",", this._connectionStrings)); + } +} + + +[SetUpFixture] +public class EtcdSetupFixture +{ + internal static readonly EtcdClusterSetup EtcdClusterSetup = new EtcdClusterSetup(); + + [OneTimeSetUp] + public async Task OneTimeSetUp() + { + await EtcdClusterSetup.ClusterSetup(); + } + + [OneTimeTearDown] + public async Task OneTimeTearDown() + { + await EtcdClusterSetup.TearDownCluster(); + } +} diff --git a/src/DistributedLock.Tests/Tests/Etcd/EtcdLockTest.cs b/src/DistributedLock.Tests/Tests/Etcd/EtcdLockTest.cs new file mode 100644 index 00000000..a31c50ff --- /dev/null +++ b/src/DistributedLock.Tests/Tests/Etcd/EtcdLockTest.cs @@ -0,0 +1,47 @@ +using Medallion.Threading.Etcd; +using NUnit.Framework; + +namespace Medallion.Threading.Tests.Etcd; + +public class EtcdLockTest +{ + private readonly EtcdClusterSetup _etcdClusterBuilder = EtcdSetupFixture.EtcdClusterSetup; + + + [Test] + public void EtcdBasicAcquireLockSync_HappyPath() + { + using var client = this._etcdClusterBuilder.CreateClientToEtcdCluster(); + var lock2 = new EtcdLeaseDistributedLock(client, "etcd"); + using var handle2 = lock2.TryAcquire(); + Assert.That(handle2, Is.Not.Null, "Failed to acquire lock"); + } + + [Test] + public async Task EtcdBasicAcquireLockAsync_HappyPath() + { + using var client = this._etcdClusterBuilder.CreateClientToEtcdCluster(); + var lock2 = new EtcdLeaseDistributedLock(client, "etcd"); + await using var handle2 = await lock2.TryAcquireAsync(); + Assert.That(handle2, Is.Not.Null, "Failed to acquire lock"); + } + + + [Test] + public async Task CreateLockAndAcquireInDifferentScope() + { + using var client = this._etcdClusterBuilder.CreateClientToEtcdCluster(); + + async Task GetHandle() + { + var lock1 = new EtcdLeaseDistributedLock(client, "lock"); + return await lock1.TryAcquireAsync(); + } + + var handle = await GetHandle(); + handle.Dispose(); + // take the same lock again should be possible + var lock2 = new EtcdLeaseDistributedLock(client, "lock"); + await using var handle2 = await lock2.TryAcquireAsync(); + } +} \ No newline at end of file diff --git a/src/DistributedLock.Tests/Tests/Etcd/EtcdSomeOtherTest.cs b/src/DistributedLock.Tests/Tests/Etcd/EtcdSomeOtherTest.cs new file mode 100644 index 00000000..e682e4f0 --- /dev/null +++ b/src/DistributedLock.Tests/Tests/Etcd/EtcdSomeOtherTest.cs @@ -0,0 +1,31 @@ +using Medallion.Threading.FileSystem; +using NUnit.Framework; + +namespace Medallion.Threading.Tests.Etcd; + +public class EtcdDistributedLockWindowsTest +{ + /// + /// Example of where always ignoring during file creation + /// would be problematic. + /// + [Test] + public void TestThrowsUnauthorizedAccessExceptionInCaseOfFilePermissionViolation() + { + var @lock = new FileDistributedLock(new DirectoryInfo(@"C:\Windows"), Guid.NewGuid().ToString()); + Assert.Throws(() => @lock.TryAcquire()?.Dispose()); + } + + /// + /// Example of where always ignoring during directory creation + /// would be problematic. + /// + [Test] + public void TestThrowsUnauthorizedAccessExceptionInCaseOfDirectoryPermissionViolation() + { + var @lock = new FileDistributedLock(new DirectoryInfo(@"C:\Windows\MedallionDistributedLock"), Guid.NewGuid().ToString()); + var exception = Assert.Throws(() => @lock.TryAcquire()?.Dispose())!; + Assert.That(exception.InnerException, Is.InstanceOf()); + Assert.That(Directory.Exists(Path.GetDirectoryName(@lock.Name)), Is.False); + } +} diff --git a/src/DistributedLock.Tests/Tests/Etcd/EtcdSynchronizationProviderTest.cs b/src/DistributedLock.Tests/Tests/Etcd/EtcdSynchronizationProviderTest.cs new file mode 100644 index 00000000..c631b0d5 --- /dev/null +++ b/src/DistributedLock.Tests/Tests/Etcd/EtcdSynchronizationProviderTest.cs @@ -0,0 +1,28 @@ +using Medallion.Threading.Etcd; +using Medallion.Threading.FileSystem; +using NUnit.Framework; + +namespace Medallion.Threading.Tests.Etcd; + +public class EtcdSynchronizationProviderTest +{ + + private readonly EtcdClusterSetup _etcdClusterBuilder = EtcdSetupFixture.EtcdClusterSetup; + + + [Test] + public void TestArgumentValidation() + { + Assert.Throws(() => new EtcdLeaseDistributedLockProvider(null!)); + } + + + [Test] + public async Task BasicTest() + { + var provider = new EtcdLeaseDistributedLockProvider(this._etcdClusterBuilder.CreateClientToEtcdCluster()); + var lock1 = provider.CreateLock("lockTest"); + await using var handle1 = await lock1.TryAcquireAsync(); + Assert.That(handle1, Is.Not.Null); + } +} diff --git a/src/DistributedLock.Tests/Tests/Etcd/TestEtcdStrategies.cs b/src/DistributedLock.Tests/Tests/Etcd/TestEtcdStrategies.cs new file mode 100644 index 00000000..45b4c811 --- /dev/null +++ b/src/DistributedLock.Tests/Tests/Etcd/TestEtcdStrategies.cs @@ -0,0 +1,59 @@ +using Medallion.Threading.Etcd; + +namespace Medallion.Threading.Tests.Etcd; + +public sealed class + TestingEtcdLeaseDistributedLockProvider : TestingLockProvider +{ + public override IDistributedLock CreateLockWithExactName(string name) + { + return new EtcdLeaseDistributedLock(EtcdSetupFixture.EtcdClusterSetup.CreateClientToEtcdCluster(), name, + this.Strategy.Options); + } + + public override string GetSafeName(string name) => + EtcdLeaseDistributedLock.GetSafeName(name); +} +public sealed class TestingEtcdLeaseSynchronizationStrategy : TestingSynchronizationStrategy +{ + private readonly DisposableCollection _disposables = new(); + + private static readonly Action DefaultTestingOptions = o => + // for test speed + o.BusyWaitSleepTime(TimeSpan.FromMilliseconds(10), TimeSpan.FromMilliseconds(25)); + + + public Action? Options { get; set; } = DefaultTestingOptions; + public bool CreateBlobBeforeLockIsCreated { get; set; } + + public override IDisposable? PrepareForHandleLost() + { + this.Options = o => + { + DefaultTestingOptions(o); + o.RenewalCadence(TimeSpan.FromMilliseconds(10)); + }; + + return new HandleLostScope(); + } + + public override void PrepareForHighContention(ref int maxConcurrentAcquires) + { + this.Options = null; // reduces # of requests under high contention + this.CreateBlobBeforeLockIsCreated = true; + } + + public override void Dispose() + { + try { this._disposables.Dispose(); } + finally { base.Dispose(); } + } + + private class HandleLostScope : IDisposable + { + public void Dispose() + { + // TODO: ??? + } + } +} \ No newline at end of file diff --git a/src/DistributedLock.Tests/packages.lock.json b/src/DistributedLock.Tests/packages.lock.json index a0d85a11..88894fb7 100644 --- a/src/DistributedLock.Tests/packages.lock.json +++ b/src/DistributedLock.Tests/packages.lock.json @@ -1,593 +1,6 @@ { "version": 2, "dependencies": { - ".NETFramework,Version=v4.7.2": { - "MedallionShell.StrongName": { - "type": "Direct", - "requested": "[1.6.2, )", - "resolved": "1.6.2", - "contentHash": "x7kIh8HiLHQrm5tcLEwNXhYfIHjQoK8ZS9MPx/LcCgNubtfFVJZm8Kk5/FSOalHjlXizcLAm6733L691l8cr/Q==" - }, - "Microsoft.NET.Test.Sdk": { - "type": "Direct", - "requested": "[17.9.0, )", - "resolved": "17.9.0", - "contentHash": "7GUNAUbJYn644jzwLm5BD3a2p9C1dmP8Hr6fDPDxgItQk9hBs1Svdxzz07KQ/UphMSmgza9AbijBJGmw5D658A==", - "dependencies": { - "Microsoft.CodeCoverage": "17.9.0" - } - }, - "Moq": { - "type": "Direct", - "requested": "[4.20.70, )", - "resolved": "4.20.70", - "contentHash": "4rNnAwdpXJBuxqrOCzCyICXHSImOTRktCgCWXWykuF1qwoIsVvEnR7PjbMk/eLOxWvhmj5Kwt+kDV3RGUYcNwg==", - "dependencies": { - "Castle.Core": "5.1.1", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "NUnit": { - "type": "Direct", - "requested": "[3.14.0, )", - "resolved": "3.14.0", - "contentHash": "R7iPwD7kbOaP3o2zldWJbWeMQAvDKD0uld27QvA3PAALl1unl7x0v2J7eGiJOYjimV/BuGT4VJmr45RjS7z4LA==" - }, - "NUnit.Analyzers": { - "type": "Direct", - "requested": "[4.1.0, )", - "resolved": "4.1.0", - "contentHash": "Odd1RusSMnfswIiCPbokAqmlcCCXjQ20poaXWrw+CWDnBY1vQ/x6ZGqgyJXpebPq5Uf8uEBe5iOAySsCdSrWdQ==" - }, - "NUnit3TestAdapter": { - "type": "Direct", - "requested": "[4.5.0, )", - "resolved": "4.5.0", - "contentHash": "s8JpqTe9bI2f49Pfr3dFRfoVSuFQyraTj68c3XXjIS/MRGvvkLnrg6RLqnTjdShX+AdFUCCU/4Xex58AdUfs6A==" - }, - "System.Data.SqlClient": { - "type": "Direct", - "requested": "[4.8.6, )", - "resolved": "4.8.6", - "contentHash": "2Ij/LCaTQRyAi5lAv7UUTV9R2FobC8xN9mE0fXBZohum/xLl8IZVmE98Rq5ugQHjCgTBRKqpXRb4ORulRdA6Ig==" - }, - "Azure.Core": { - "type": "Transitive", - "resolved": "1.38.0", - "contentHash": "IuEgCoVA0ef7E4pQtpC3+TkPbzaoQfa77HlfJDmfuaJUCVJmn7fT0izamZiryW5sYUFKizsftIxMkXKbgIcPMQ==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "1.1.1", - "System.ClientModel": "1.0.0", - "System.Diagnostics.DiagnosticSource": "6.0.1", - "System.Memory.Data": "1.0.2", - "System.Numerics.Vectors": "4.5.0", - "System.Text.Encodings.Web": "4.7.2", - "System.Text.Json": "4.7.2", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "Azure.Identity": { - "type": "Transitive", - "resolved": "1.11.4", - "contentHash": "Sf4BoE6Q3jTgFkgBkx7qztYOFELBCo+wQgpYDwal/qJ1unBH73ywPztIJKXBXORRzAeNijsuxhk94h0TIMvfYg==", - "dependencies": { - "Azure.Core": "1.38.0", - "Microsoft.Identity.Client": "4.61.3", - "Microsoft.Identity.Client.Extensions.Msal": "4.61.3", - "System.Memory": "4.5.4", - "System.Security.Cryptography.ProtectedData": "4.7.0", - "System.Text.Json": "4.7.2", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "Azure.Storage.Common": { - "type": "Transitive", - "resolved": "12.18.1", - "contentHash": "ohCslqP9yDKIn+DVjBEOBuieB1QwsUCz+BwHYNaJ3lcIsTSiI4Evnq81HcKe8CqM8qvdModbipVQKpnxpbdWqA==", - "dependencies": { - "Azure.Core": "1.36.0", - "System.IO.Hashing": "6.0.0" - } - }, - "Castle.Core": { - "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==" - }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==", - "dependencies": { - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "Microsoft.Bcl.HashCode": { - "type": "Transitive", - "resolved": "1.1.1", - "contentHash": "MalY0Y/uM/LjXtHfX/26l2VtN4LDNZ2OE3aumNOHDLsT4fNYy2hiHXI4CXCqKpNUNm7iJ2brrc4J89UdaL56FA==" - }, - "Microsoft.CodeCoverage": { - "type": "Transitive", - "resolved": "17.9.0", - "contentHash": "RGD37ZSrratfScYXm7M0HjvxMxZyWZL4jm+XgMZbkIY1UPgjUpbNA/t+WTGj/rC/0Hm9A3IrH3ywbKZkOCnoZA==" - }, - "Microsoft.Data.SqlClient.SNI": { - "type": "Transitive", - "resolved": "5.2.0", - "contentHash": "0p2KMVc8WSC5JWgO+OdhYJiRM41dp6w2Dsd9JfEiHLPc6nyxBQgSrx9TYlbC8fRT2RK+HyWzDlv9ofFtxMOwQg==" - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "8.0.0", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "System.Buffers": "4.5.1", - "System.Memory": "4.5.5" - } - }, - "Microsoft.Identity.Client": { - "type": "Transitive", - "resolved": "4.61.3", - "contentHash": "naJo/Qm35Caaoxp5utcw+R8eU8ZtLz2ALh8S+gkekOYQ1oazfCQMWVT4NJ/FnHzdIJlm8dMz0oMpMGCabx5odA==", - "dependencies": { - "Microsoft.IdentityModel.Abstractions": "6.35.0", - "System.Diagnostics.DiagnosticSource": "6.0.1" - } - }, - "Microsoft.Identity.Client.Extensions.Msal": { - "type": "Transitive", - "resolved": "4.61.3", - "contentHash": "PWnJcznrSGr25MN8ajlc2XIDW4zCFu0U6FkpaNLEWLgd1NgFCp5uDY3mqLDgM8zCN8hqj8yo5wHYfLB2HjcdGw==", - "dependencies": { - "Microsoft.Identity.Client": "4.61.3", - "System.IO.FileSystem.AccessControl": "5.0.0", - "System.Security.Cryptography.ProtectedData": "4.5.0" - } - }, - "Microsoft.IdentityModel.Abstractions": { - "type": "Transitive", - "resolved": "6.35.0", - "contentHash": "xuR8E4Rd96M41CnUSCiOJ2DBh+z+zQSmyrYHdYhD6K4fXBcQGVnRCFQ0efROUYpP+p0zC1BLKr0JRpVuujTZSg==" - }, - "Microsoft.IdentityModel.JsonWebTokens": { - "type": "Transitive", - "resolved": "6.35.0", - "contentHash": "9wxai3hKgZUb4/NjdRKfQd0QJvtXKDlvmGMYACbEC8DFaicMFCFhQFZq9ZET1kJLwZahf2lfY5Gtcpsx8zYzbg==", - "dependencies": { - "Microsoft.IdentityModel.Tokens": "6.35.0", - "System.Text.Encoding": "4.3.0", - "System.Text.Encodings.Web": "4.7.2", - "System.Text.Json": "4.7.2" - } - }, - "Microsoft.IdentityModel.Logging": { - "type": "Transitive", - "resolved": "6.35.0", - "contentHash": "jePrSfGAmqT81JDCNSY+fxVWoGuJKt9e6eJ+vT7+quVS55nWl//jGjUQn4eFtVKt4rt5dXaleZdHRB9J9AJZ7Q==", - "dependencies": { - "Microsoft.IdentityModel.Abstractions": "6.35.0" - } - }, - "Microsoft.IdentityModel.Protocols": { - "type": "Transitive", - "resolved": "6.35.0", - "contentHash": "BPQhlDzdFvv1PzaUxNSk+VEPwezlDEVADIKmyxubw7IiELK18uJ06RQ9QKKkds30XI+gDu9n8j24XQ8w7fjWcg==", - "dependencies": { - "Microsoft.IdentityModel.Logging": "6.35.0", - "Microsoft.IdentityModel.Tokens": "6.35.0" - } - }, - "Microsoft.IdentityModel.Protocols.OpenIdConnect": { - "type": "Transitive", - "resolved": "6.35.0", - "contentHash": "LMtVqnECCCdSmyFoCOxIE5tXQqkOLrvGrL7OxHg41DIm1bpWtaCdGyVcTAfOQpJXvzND9zUKIN/lhngPkYR8vg==", - "dependencies": { - "Microsoft.IdentityModel.Protocols": "6.35.0", - "System.IdentityModel.Tokens.Jwt": "6.35.0", - "System.Text.Encoding": "4.3.0", - "System.Text.Encodings.Web": "4.7.2", - "System.Text.Json": "4.7.2" - } - }, - "Microsoft.IdentityModel.Tokens": { - "type": "Transitive", - "resolved": "6.35.0", - "contentHash": "RN7lvp7s3Boucg1NaNAbqDbxtlLj5Qeb+4uSS1TeK5FSBVM40P4DKaTKChT43sHyKfh7V0zkrMph6DdHvyA4bg==", - "dependencies": { - "Microsoft.IdentityModel.Logging": "6.35.0", - "System.Text.Encoding": "4.3.0", - "System.Text.Encodings.Web": "4.7.2", - "System.Text.Json": "4.7.2" - } - }, - "Pipelines.Sockets.Unofficial": { - "type": "Transitive", - "resolved": "2.2.8", - "contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==", - "dependencies": { - "System.IO.Pipelines": "5.0.1" - } - }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.5.1", - "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" - }, - "System.ClientModel": { - "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "I3CVkvxeqFYjIVEP59DnjbeoGNfo/+SZrCLpRz2v/g0gpCHaEMPtWSY0s9k/7jR1rAsLNg2z2u1JRB76tPjnIw==", - "dependencies": { - "System.Memory.Data": "1.0.2", - "System.Text.Json": "4.7.2" - } - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==", - "dependencies": { - "System.Memory": "4.5.5", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "System.Configuration.ConfigurationManager": { - "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "jXw9MlUu/kRfEU0WyTptAVueupqIeE3/rl0EZDMlf8pcvJnitQ8HeVEp69rZdaStXwTV72boi/Bhw8lOeO+U2w==", - "dependencies": { - "System.Security.Permissions": "6.0.0" - } - }, - "System.Diagnostics.DiagnosticSource": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ==", - "dependencies": { - "System.Memory": "4.5.5", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "System.Formats.Asn1": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "XqKba7Mm/koKSjKMfW82olQdmfbI5yqeoLV/tidRp7fbh5rmHAQ5raDI/7SU0swTzv+jgqtUGkzmFxuUg0it1A==", - "dependencies": { - "System.Buffers": "4.5.1", - "System.Memory": "4.5.5", - "System.ValueTuple": "4.5.0" - } - }, - "System.IdentityModel.Tokens.Jwt": { - "type": "Transitive", - "resolved": "6.35.0", - "contentHash": "yxGIQd3BFK7F6S62/7RdZk3C/mfwyVxvh6ngd1VYMBmbJ1YZZA9+Ku6suylVtso0FjI0wbElpJ0d27CdsyLpBQ==", - "dependencies": { - "Microsoft.IdentityModel.JsonWebTokens": "6.35.0", - "Microsoft.IdentityModel.Tokens": "6.35.0" - } - }, - "System.IO.Compression": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==" - }, - "System.IO.FileSystem.AccessControl": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "SxHB3nuNrpptVk+vZ/F+7OHEpoHUIKKMl02bUmYHQr1r+glbZQxs7pRtsf4ENO29TVm2TH3AEeep2fJcy92oYw==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "System.IO.Hashing": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "Rfm2jYCaUeGysFEZjDe7j1R4x6Z6BzumS/vUT5a1AA/AWJuGX71PoGB0RmpyX3VmrGqVnAwtfMn39OHR8Y/5+g==", - "dependencies": { - "System.Buffers": "4.5.1", - "System.Memory": "4.5.4" - } - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "5.0.1", - "contentHash": "qEePWsaq9LoEEIqhbGe6D5J8c9IqQOUuTzzV6wn1POlfdLkJliZY3OlB0j0f17uMWlqZYjH7txj+2YbyrIA8Yg==", - "dependencies": { - "System.Buffers": "4.5.1", - "System.Memory": "4.5.4", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "System.Memory": { - "type": "Transitive", - "resolved": "4.5.5", - "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", - "dependencies": { - "System.Buffers": "4.5.1", - "System.Numerics.Vectors": "4.5.0", - "System.Runtime.CompilerServices.Unsafe": "4.5.3" - } - }, - "System.Memory.Data": { - "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "JGkzeqgBsiZwKJZ1IxPNsDFZDhUvuEdX8L8BDC8N3KOj+6zMcNU28CNN59TpZE/VJYy9cP+5M+sbxtWJx3/xtw==", - "dependencies": { - "System.Text.Encodings.Web": "4.7.2", - "System.Text.Json": "4.6.0" - } - }, - "System.Numerics.Vectors": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" - }, - "System.Runtime.InteropServices.RuntimeInformation": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==" - }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "AUADIc0LIEQe7MzC+I0cl0rAT8RrTAKFHl53yHjEUzNVIaUlhFY11vc2ebiVJzVBuOzun6F7FBA+8KAbGTTedQ==", - "dependencies": { - "System.Security.Principal.Windows": "5.0.0" - } - }, - "System.Security.Cryptography.ProtectedData": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "ehYW0m9ptxpGWvE4zgqongBVWpSDU/JCFD4K7krxkQwSz/sFQjEXCUqpvencjy6DYDbn7Ig09R8GFffu8TtneQ==" - }, - "System.Security.Permissions": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "T/uuc7AklkDoxmcJ7LGkyX1CcSviZuLCa4jg3PekfJ7SU0niF0IVTXwUiNVP9DSpzou2PpxJ+eNY2IfDM90ZCg==", - "dependencies": { - "System.Security.AccessControl": "6.0.0" - } - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - }, - "System.Text.Encoding": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==" - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==", - "dependencies": { - "System.Buffers": "4.5.1", - "System.Memory": "4.5.5", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "System.Text.Json": { - "type": "Transitive", - "resolved": "8.0.5", - "contentHash": "0f1B50Ss7rqxXiaBJyzUu9bWFOO2/zSlifZ/UNMdiIpDYe4cY4LQQicP4nirK1OS31I43rn062UIJ1Q9bpmHpg==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "8.0.0", - "System.Buffers": "4.5.1", - "System.Memory": "4.5.5", - "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Text.Encodings.Web": "8.0.0", - "System.Threading.Tasks.Extensions": "4.5.4", - "System.ValueTuple": "4.5.0" - } - }, - "System.Threading.Channels": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "CMaFr7v+57RW7uZfZkPExsPB6ljwzhjACWW1gfU35Y56rk72B/Wu+sTqxVmGSk4SFUlPc3cjeKND0zktziyjBA==", - "dependencies": { - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "System.Threading.Tasks.Extensions": { - "type": "Transitive", - "resolved": "4.5.4", - "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "4.5.3" - } - }, - "System.ValueTuple": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==" - }, - "distributedlock": { - "type": "Project", - "dependencies": { - "DistributedLock.Azure": "[1.0.2, )", - "DistributedLock.FileSystem": "[1.0.2, )", - "DistributedLock.MySql": "[1.0.2, )", - "DistributedLock.Oracle": "[1.0.4, )", - "DistributedLock.Postgres": "[1.2.0, )", - "DistributedLock.Redis": "[1.0.3, )", - "DistributedLock.SqlServer": "[1.0.6, )", - "DistributedLock.WaitHandles": "[1.0.1, )", - "DistributedLock.ZooKeeper": "[1.0.0, )" - } - }, - "distributedlock.azure": { - "type": "Project", - "dependencies": { - "Azure.Storage.Blobs": "[12.19.1, )", - "DistributedLock.Core": "[1.0.8, )" - } - }, - "distributedlock.core": { - "type": "Project", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "[8.0.0, )", - "System.ValueTuple": "[4.5.0, )" - } - }, - "distributedlock.filesystem": { - "type": "Project", - "dependencies": { - "DistributedLock.Core": "[1.0.8, )" - } - }, - "distributedlock.mysql": { - "type": "Project", - "dependencies": { - "DistributedLock.Core": "[1.0.8, )", - "MySqlConnector": "[2.3.5, )" - } - }, - "distributedlock.oracle": { - "type": "Project", - "dependencies": { - "DistributedLock.Core": "[1.0.8, )", - "Oracle.ManagedDataAccess": "[23.6.1, )" - } - }, - "distributedlock.postgres": { - "type": "Project", - "dependencies": { - "DistributedLock.Core": "[1.0.8, )", - "Npgsql": "[8.0.6, )" - } - }, - "distributedlock.redis": { - "type": "Project", - "dependencies": { - "DistributedLock.Core": "[1.0.8, )", - "StackExchange.Redis": "[2.7.27, )" - } - }, - "distributedlock.sqlserver": { - "type": "Project", - "dependencies": { - "DistributedLock.Core": "[1.0.8, )", - "Microsoft.Data.SqlClient": "[5.2.2, )" - } - }, - "distributedlock.waithandles": { - "type": "Project", - "dependencies": { - "DistributedLock.Core": "[1.0.8, )" - } - }, - "distributedlock.zookeeper": { - "type": "Project", - "dependencies": { - "DistributedLock.Core": "[1.0.8, )", - "ZooKeeperNetEx": "[3.4.12.4, )" - } - }, - "Azure.Storage.Blobs": { - "type": "CentralTransitive", - "requested": "[12.19.1, )", - "resolved": "12.19.1", - "contentHash": "x43hWFJ4sPQ23TD4piCwT+KlQpZT8pNDAzqj6yUCqh+WJ2qcQa17e1gh6ZOeT2QNFQTTDSuR56fm2bIV7i11/w==", - "dependencies": { - "Azure.Storage.Common": "12.18.1", - "System.Text.Json": "4.7.2" - } - }, - "Microsoft.Data.SqlClient": { - "type": "CentralTransitive", - "requested": "[5.2.2, )", - "resolved": "5.2.2", - "contentHash": "mtoeRMh7F/OA536c/Cnh8L4H0uLSKB5kSmoi54oN7Fp0hNJDy22IqyMhaMH4PkDCqI7xL//Fvg9ldtuPHG0h5g==", - "dependencies": { - "Azure.Identity": "1.11.4", - "Microsoft.Data.SqlClient.SNI": "5.2.0", - "Microsoft.Identity.Client": "4.61.3", - "Microsoft.IdentityModel.JsonWebTokens": "6.35.0", - "Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.35.0", - "System.Buffers": "4.5.1", - "System.Configuration.ConfigurationManager": "6.0.1", - "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", - "System.Text.Encodings.Web": "6.0.0" - } - }, - "MySqlConnector": { - "type": "CentralTransitive", - "requested": "[2.3.5, )", - "resolved": "2.3.5", - "contentHash": "AmEfUPkFl+Ev6jJ8Dhns3CYHBfD12RHzGYWuLt6DfG6/af6YvOMyPz74ZPPjBYQGRJkumD2Z48Kqm8s5DJuhLA==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "7.0.1", - "System.Diagnostics.DiagnosticSource": "7.0.2", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "Npgsql": { - "type": "CentralTransitive", - "requested": "[8.0.6, )", - "resolved": "8.0.6", - "contentHash": "KaS6CY5kY2Sd0P00MSeFcOI3t2DiQ4UWG8AuRpVOUeDWITOKfoEEG91DP3cmT6aerixPkjwKgXxnpDxIkDpO6g==", - "dependencies": { - "Microsoft.Bcl.HashCode": "1.1.1", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "System.Collections.Immutable": "8.0.0", - "System.Diagnostics.DiagnosticSource": "8.0.0", - "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Text.Json": "8.0.5", - "System.Threading.Channels": "8.0.0" - } - }, - "Oracle.ManagedDataAccess": { - "type": "CentralTransitive", - "requested": "[23.6.1, )", - "resolved": "23.6.1", - "contentHash": "EZi+mahzUwQFWs9Is8ed94eTzWOlfCLMd+DDWukf/h/brTz1wB9Qk3fsxBrjw9+fEXrxDgx4uXNiPHNPRS3BeQ==", - "dependencies": { - "System.Diagnostics.DiagnosticSource": "6.0.1", - "System.Formats.Asn1": "8.0.1", - "System.Text.Json": "8.0.5", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "StackExchange.Redis": { - "type": "CentralTransitive", - "requested": "[2.7.27, )", - "resolved": "2.7.27", - "contentHash": "Uqc2OQHglqj9/FfGQ6RkKFkZfHySfZlfmbCl+hc+u2I/IqunfelQ7QJi7ZhvAJxUtu80pildVX6NPLdDaUffOw==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "5.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Pipelines.Sockets.Unofficial": "2.2.8", - "System.IO.Compression": "4.3.0", - "System.Threading.Channels": "5.0.0" - } - }, - "ZooKeeperNetEx": { - "type": "CentralTransitive", - "requested": "[3.4.12.4, )", - "resolved": "3.4.12.4", - "contentHash": "YECtByVSH7TRjQKplwOWiKyanCqYE5eEkGk5YtHJgsnbZ6+p1o0Gvs5RIsZLotiAVa6Niez1BJyKY/RDY/L6zg==" - } - }, "net8.0": { "MedallionShell.StrongName": { "type": "Direct", @@ -646,6 +59,19 @@ "runtime.native.System.Data.SqlClient.sni": "4.7.0" } }, + "Testcontainers": { + "type": "Direct", + "requested": "[4.6.0, )", + "resolved": "4.6.0", + "contentHash": "JFGPwuEqVuY3qJiGuxR+2J+E+dGc+1TgbBooB1346zUFKJqH6zWeSCXju5Siwek3ExOVRES4AbECXoaYt9jkXA==", + "dependencies": { + "Docker.DotNet.Enhanced": "3.128.3", + "Docker.DotNet.Enhanced.X509": "3.128.3", + "Microsoft.Extensions.Logging.Abstractions": "8.0.3", + "SSH.NET": "2024.2.0", + "SharpZipLib": "1.4.2" + } + }, "Azure.Core": { "type": "Transitive", "resolved": "1.38.0", @@ -684,6 +110,11 @@ "System.IO.Hashing": "6.0.0" } }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==" + }, "Castle.Core": { "type": "Transitive", "resolved": "5.1.1", @@ -692,6 +123,61 @@ "System.Diagnostics.EventLog": "6.0.0" } }, + "DnsClient": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "4H/f2uYJOZ+YObZjpY9ABrKZI+JNw3uizp6oMzTXwDw6F+2qIPhpRl/1t68O/6e98+vqNiYGu+lswmwdYUy3gg==", + "dependencies": { + "Microsoft.Win32.Registry": "5.0.0" + } + }, + "Docker.DotNet.Enhanced": { + "type": "Transitive", + "resolved": "3.128.3", + "contentHash": "yOiBJfGltxjMTPBRVPt2Yeiqy2rLNQSf5x0U57FgipbsGnCXSm1a04UHi3h2QtABJSpTSHoBw+LPqA6yN9CGFg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.3", + "System.IO.Pipelines": "8.0.0" + } + }, + "Docker.DotNet.Enhanced.X509": { + "type": "Transitive", + "resolved": "3.128.3", + "contentHash": "U6G7H+vX0Htk86Fd1xS5i7iRGd0C33vx61sLH8gI9uzxqQJRfeYCbuzQOOp3J7m4zVZ2mP6x9gB9onspgP0QCg==", + "dependencies": { + "Docker.DotNet.Enhanced": "3.128.3" + } + }, + "Google.Protobuf": { + "type": "Transitive", + "resolved": "3.21.2", + "contentHash": "91vdjMcbesfibUtE2n6bVWelT+7J7hExMb9WAEQXuM748+aoIrrUECkIBCmQG+RiM4KvEccVgutFdzl1l6G+Rw==" + }, + "Grpc.Core.Api": { + "type": "Transitive", + "resolved": "2.47.0", + "contentHash": "oZXapxH/2WHAVALghNauo+r/bp6zjgQ6r0v8FizLLQg0/j/FkK2u3WZ7cLOL9Y5H4oLg+wLclO8FSvNTQpNR5Q==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "Grpc.Net.Client": { + "type": "Transitive", + "resolved": "2.47.0", + "contentHash": "DLbUC3T8dMmhA2iqpaJo0X4g7fAi3FyVbDd0/jBXrlqc/bcyA1wPBzLx6mWOWoGUb5S89xL2svnsM8SfzzNa2Q==", + "dependencies": { + "Grpc.Net.Common": "2.47.0", + "Microsoft.Extensions.Logging.Abstractions": "3.0.3" + } + }, + "Grpc.Net.Common": { + "type": "Transitive", + "resolved": "2.47.0", + "contentHash": "Cv76h7noEN2s9cwIdEspOeDjeqcU6Nm34OmjSbRhD/FDBXFmG7rmSfJTPCEB1LYjZWfmf7uUH+nYcHR1I7oQqw==", + "dependencies": { + "Grpc.Core.Api": "2.47.0" + } + }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "1.1.1", @@ -714,15 +200,15 @@ }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" + "resolved": "8.0.2", + "contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==" }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", + "resolved": "8.0.3", + "contentHash": "dL0QGToTxggRLMYY4ZYX5AMwBb+byQBd/5dMiZE07Nv73o6I5Are3C7eQTh7K2+A4ct0PVISSr7TZANbiNb2yQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2" } }, "Microsoft.Identity.Client": { @@ -797,8 +283,8 @@ }, "Microsoft.NETCore.Platforms": { "type": "Transitive", - "resolved": "3.1.0", - "contentHash": "z7aeg8oHln2CuNulfhiLYxCVMPEwBl3rzicjvIX+4sUuCwvXw5oXQEtbiU2c0z4qYL5L3Kmx0mMA/+t/SbY67w==" + "resolved": "5.0.0", + "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" }, "Microsoft.NETCore.Targets": { "type": "Transitive", @@ -829,11 +315,11 @@ }, "Microsoft.Win32.Registry": { "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "KSrRMb5vNi0CWSGG1++id2ZOs/1QhRqROt+qgbEAdQuGjGrFcl4AOl4/exGPUYz2wUnU42nvJqon1T3U0kPXLA==", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", "dependencies": { - "System.Security.AccessControl": "4.7.0", - "System.Security.Principal.Windows": "4.7.0" + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" } }, "NETStandard.Library": { @@ -893,6 +379,19 @@ "resolved": "4.4.0", "contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA==" }, + "SharpZipLib": { + "type": "Transitive", + "resolved": "1.4.2", + "contentHash": "yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A==" + }, + "SSH.NET": { + "type": "Transitive", + "resolved": "2024.2.0", + "contentHash": "9r+4UF2P51lTztpd+H7SJywk7WgmlWB//Cm2o96c6uGVZU5r58ys2/cD9pCgTk0zCdSkfflWL1WtqQ9I4IVO9Q==", + "dependencies": { + "BouncyCastle.Cryptography": "2.4.0" + } + }, "System.ClientModel": { "type": "Transitive", "resolved": "1.0.0", @@ -958,8 +457,8 @@ }, "System.IO.Pipelines": { "type": "Transitive", - "resolved": "5.0.1", - "contentHash": "qEePWsaq9LoEEIqhbGe6D5J8c9IqQOUuTzzV6wn1POlfdLkJliZY3OlB0j0f17uMWlqZYjH7txj+2YbyrIA8Yg==" + "resolved": "8.0.0", + "contentHash": "FHNOatmUq0sqJOkTx+UF/9YK1f180cnW5FVqnQMvYUN0elp6wFzbtPSiqbo1/ru8ICp43JM1i7kKkk6GsNGHlA==" }, "System.Memory": { "type": "Transitive", @@ -1009,11 +508,11 @@ }, "System.Security.AccessControl": { "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "JECvTt5aFF3WT3gHpfofL2MNNP6v84sxtXxpqhLBCcDRzqsPBmHhQ6shv4DwwN2tRlzsUxtb3G9M3763rbXKDg==", + "resolved": "5.0.0", + "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", "dependencies": { - "Microsoft.NETCore.Platforms": "3.1.0", - "System.Security.Principal.Windows": "4.7.0" + "Microsoft.NETCore.Platforms": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" } }, "System.Security.Cryptography.Cng": { @@ -1036,8 +535,8 @@ }, "System.Security.Principal.Windows": { "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "ojD0PX0XhneCsUbAZVKdb7h/70vyYMDYs85lwEI+LngEONe/17A0cFaRFqZU+sOEidcVswYWikYOQ9PPfjlbtQ==" + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" }, "System.Text.Encoding": { "type": "Transitive", @@ -1068,10 +567,11 @@ "type": "Project", "dependencies": { "DistributedLock.Azure": "[1.0.2, )", - "DistributedLock.FileSystem": "[1.0.2, )", + "DistributedLock.Etcd": "[1.0.3, )", + "DistributedLock.FileSystem": "[1.0.3, )", "DistributedLock.MySql": "[1.0.2, )", "DistributedLock.Oracle": "[1.0.4, )", - "DistributedLock.Postgres": "[1.2.0, )", + "DistributedLock.Postgres": "[1.3.0, )", "DistributedLock.Redis": "[1.0.3, )", "DistributedLock.SqlServer": "[1.0.6, )", "DistributedLock.WaitHandles": "[1.0.1, )", @@ -1088,6 +588,13 @@ "distributedlock.core": { "type": "Project" }, + "distributedlock.etcd": { + "type": "Project", + "dependencies": { + "DistributedLock.Core": "[1.0.8, )", + "dotnet-etcd": "[5.2.1, )" + } + }, "distributedlock.filesystem": { "type": "Project", "dependencies": { @@ -1153,6 +660,17 @@ "System.Text.Json": "4.7.2" } }, + "dotnet-etcd": { + "type": "CentralTransitive", + "requested": "[5.2.1, )", + "resolved": "5.2.1", + "contentHash": "fmnjf4/TG1C4KTbxIxQ1xJhkGFDrgWHvks3XSQnCBvRtkBP5/UCI82MQxD68/TvetVuVpSoVdlXMD5Nq5kZP5g==", + "dependencies": { + "DnsClient": "1.6.1", + "Google.Protobuf": "3.21.2", + "Grpc.Net.Client": "2.47.0" + } + }, "Microsoft.Data.SqlClient": { "type": "CentralTransitive", "requested": "[5.2.2, )", diff --git a/src/DistributedLock.sln b/src/DistributedLock.sln index 61d17d38..b4c9f60a 100644 --- a/src/DistributedLock.sln +++ b/src/DistributedLock.sln @@ -47,6 +47,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution package.readme.md = package.readme.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DistributedLock.Etcd", "DistributedLock.Etcd\DistributedLock.Etcd.csproj", "{E86AA401-6EE6-4B39-8E14-A44188E908AA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -109,6 +111,10 @@ Global {1CAB9A1D-0C02-459C-A90E-47819832BD58}.Debug|Any CPU.Build.0 = Debug|Any CPU {1CAB9A1D-0C02-459C-A90E-47819832BD58}.Release|Any CPU.ActiveCfg = Release|Any CPU {1CAB9A1D-0C02-459C-A90E-47819832BD58}.Release|Any CPU.Build.0 = Release|Any CPU + {E86AA401-6EE6-4B39-8E14-A44188E908AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E86AA401-6EE6-4B39-8E14-A44188E908AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E86AA401-6EE6-4B39-8E14-A44188E908AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E86AA401-6EE6-4B39-8E14-A44188E908AA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/DistributedLock/DistributedLock.csproj b/src/DistributedLock/DistributedLock.csproj index 22c5b77f..aee75cc0 100644 --- a/src/DistributedLock/DistributedLock.csproj +++ b/src/DistributedLock/DistributedLock.csproj @@ -54,6 +54,7 @@ + diff --git a/src/DistributedLock/packages.lock.json b/src/DistributedLock/packages.lock.json index d43eb309..4f1576f4 100644 --- a/src/DistributedLock/packages.lock.json +++ b/src/DistributedLock/packages.lock.json @@ -573,6 +573,15 @@ "resolved": "3.3.4", "contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA==" }, + "Microsoft.NETFramework.ReferenceAssemblies": { + "type": "Direct", + "requested": "[1.0.3, )", + "resolved": "1.0.3", + "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", + "dependencies": { + "Microsoft.NETFramework.ReferenceAssemblies.net472": "1.0.3" + } + }, "Microsoft.SourceLink.GitHub": { "type": "Direct", "requested": "[8.0.0, )", @@ -738,6 +747,11 @@ "System.Text.Json": "4.7.2" } }, + "Microsoft.NETFramework.ReferenceAssemblies.net472": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "0E7evZXHXaDYYiLRfpyXvCh+yzM2rNTyuZDI+ZO7UUqSc6GfjePiXTdqJGtgIKUwdI81tzQKmaWprnUiPj9hAw==" + }, "Microsoft.SourceLink.Common": { "type": "Transitive", "resolved": "8.0.0", @@ -1812,6 +1826,49 @@ "System.IO.Hashing": "6.0.0" } }, + "DnsClient": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "4H/f2uYJOZ+YObZjpY9ABrKZI+JNw3uizp6oMzTXwDw6F+2qIPhpRl/1t68O/6e98+vqNiYGu+lswmwdYUy3gg==", + "dependencies": { + "Microsoft.Win32.Registry": "5.0.0" + } + }, + "Google.Protobuf": { + "type": "Transitive", + "resolved": "3.21.2", + "contentHash": "91vdjMcbesfibUtE2n6bVWelT+7J7hExMb9WAEQXuM748+aoIrrUECkIBCmQG+RiM4KvEccVgutFdzl1l6G+Rw==", + "dependencies": { + "System.Memory": "4.5.3", + "System.Runtime.CompilerServices.Unsafe": "4.5.2" + } + }, + "Grpc.Core.Api": { + "type": "Transitive", + "resolved": "2.47.0", + "contentHash": "oZXapxH/2WHAVALghNauo+r/bp6zjgQ6r0v8FizLLQg0/j/FkK2u3WZ7cLOL9Y5H4oLg+wLclO8FSvNTQpNR5Q==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "Grpc.Net.Client": { + "type": "Transitive", + "resolved": "2.47.0", + "contentHash": "DLbUC3T8dMmhA2iqpaJo0X4g7fAi3FyVbDd0/jBXrlqc/bcyA1wPBzLx6mWOWoGUb5S89xL2svnsM8SfzzNa2Q==", + "dependencies": { + "Grpc.Net.Common": "2.47.0", + "Microsoft.Extensions.Logging.Abstractions": "3.0.3", + "System.Diagnostics.DiagnosticSource": "4.5.1" + } + }, + "Grpc.Net.Common": { + "type": "Transitive", + "resolved": "2.47.0", + "contentHash": "Cv76h7noEN2s9cwIdEspOeDjeqcU6Nm34OmjSbRhD/FDBXFmG7rmSfJTPCEB1LYjZWfmf7uUH+nYcHR1I7oQqw==", + "dependencies": { + "Grpc.Core.Api": "2.47.0" + } + }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "8.0.0", @@ -2273,6 +2330,13 @@ "distributedlock.core": { "type": "Project" }, + "distributedlock.etcd": { + "type": "Project", + "dependencies": { + "DistributedLock.Core": "[1.0.8, )", + "dotnet-etcd": "[5.2.1, )" + } + }, "distributedlock.filesystem": { "type": "Project", "dependencies": { @@ -2338,6 +2402,17 @@ "System.Text.Json": "4.7.2" } }, + "dotnet-etcd": { + "type": "CentralTransitive", + "requested": "[5.2.1, )", + "resolved": "5.2.1", + "contentHash": "fmnjf4/TG1C4KTbxIxQ1xJhkGFDrgWHvks3XSQnCBvRtkBP5/UCI82MQxD68/TvetVuVpSoVdlXMD5Nq5kZP5g==", + "dependencies": { + "DnsClient": "1.6.1", + "Google.Protobuf": "3.21.2", + "Grpc.Net.Client": "2.47.0" + } + }, "Microsoft.Data.SqlClient": { "type": "CentralTransitive", "requested": "[5.2.2, )", diff --git a/src/DistributedLockTaker/packages.lock.json b/src/DistributedLockTaker/packages.lock.json index 2f58c624..99fbf8fe 100644 --- a/src/DistributedLockTaker/packages.lock.json +++ b/src/DistributedLockTaker/packages.lock.json @@ -2,6 +2,15 @@ "version": 2, "dependencies": { ".NETFramework,Version=v4.7.2": { + "Microsoft.NETFramework.ReferenceAssemblies": { + "type": "Direct", + "requested": "[1.0.3, )", + "resolved": "1.0.3", + "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", + "dependencies": { + "Microsoft.NETFramework.ReferenceAssemblies.net472": "1.0.3" + } + }, "Azure.Core": { "type": "Transitive", "resolved": "1.38.0", @@ -152,6 +161,11 @@ "System.Text.Json": "4.7.2" } }, + "Microsoft.NETFramework.ReferenceAssemblies.net472": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "0E7evZXHXaDYYiLRfpyXvCh+yzM2rNTyuZDI+ZO7UUqSc6GfjePiXTdqJGtgIKUwdI81tzQKmaWprnUiPj9hAw==" + }, "Pipelines.Sockets.Unofficial": { "type": "Transitive", "resolved": "2.2.8", @@ -366,10 +380,10 @@ "type": "Project", "dependencies": { "DistributedLock.Azure": "[1.0.2, )", - "DistributedLock.FileSystem": "[1.0.2, )", + "DistributedLock.FileSystem": "[1.0.3, )", "DistributedLock.MySql": "[1.0.2, )", "DistributedLock.Oracle": "[1.0.4, )", - "DistributedLock.Postgres": "[1.2.0, )", + "DistributedLock.Postgres": "[1.3.0, )", "DistributedLock.Redis": "[1.0.3, )", "DistributedLock.SqlServer": "[1.0.6, )", "DistributedLock.WaitHandles": "[1.0.1, )", @@ -529,84 +543,12 @@ "contentHash": "YECtByVSH7TRjQKplwOWiKyanCqYE5eEkGk5YtHJgsnbZ6+p1o0Gvs5RIsZLotiAVa6Niez1BJyKY/RDY/L6zg==" } }, - ".NETFramework,Version=v4.7.2/win7-x86": { - "System.Configuration.ConfigurationManager": { - "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "jXw9MlUu/kRfEU0WyTptAVueupqIeE3/rl0EZDMlf8pcvJnitQ8HeVEp69rZdaStXwTV72boi/Bhw8lOeO+U2w==", - "dependencies": { - "System.Security.Permissions": "6.0.0" - } - }, - "System.IO.Compression": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==" - }, - "System.IO.FileSystem.AccessControl": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "SxHB3nuNrpptVk+vZ/F+7OHEpoHUIKKMl02bUmYHQr1r+glbZQxs7pRtsf4ENO29TVm2TH3AEeep2fJcy92oYw==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "System.Runtime.InteropServices.RuntimeInformation": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==" - }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "AUADIc0LIEQe7MzC+I0cl0rAT8RrTAKFHl53yHjEUzNVIaUlhFY11vc2ebiVJzVBuOzun6F7FBA+8KAbGTTedQ==", - "dependencies": { - "System.Security.Principal.Windows": "5.0.0" - } - }, - "System.Security.Cryptography.ProtectedData": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "ehYW0m9ptxpGWvE4zgqongBVWpSDU/JCFD4K7krxkQwSz/sFQjEXCUqpvencjy6DYDbn7Ig09R8GFffu8TtneQ==" - }, - "System.Security.Permissions": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "T/uuc7AklkDoxmcJ7LGkyX1CcSviZuLCa4jg3PekfJ7SU0niF0IVTXwUiNVP9DSpzou2PpxJ+eNY2IfDM90ZCg==", - "dependencies": { - "System.Security.AccessControl": "6.0.0" - } - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - }, - "Microsoft.Data.SqlClient": { - "type": "CentralTransitive", - "requested": "[5.2.2, )", - "resolved": "5.2.2", - "contentHash": "mtoeRMh7F/OA536c/Cnh8L4H0uLSKB5kSmoi54oN7Fp0hNJDy22IqyMhaMH4PkDCqI7xL//Fvg9ldtuPHG0h5g==", - "dependencies": { - "Azure.Identity": "1.11.4", - "Microsoft.Data.SqlClient.SNI": "5.2.0", - "Microsoft.Identity.Client": "4.61.3", - "Microsoft.IdentityModel.JsonWebTokens": "6.35.0", - "Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.35.0", - "System.Buffers": "4.5.1", - "System.Configuration.ConfigurationManager": "6.0.1", - "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", - "System.Text.Encodings.Web": "6.0.0" - } - } - }, "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.4, )", - "resolved": "8.0.4", - "contentHash": "PZb5nfQ+U19nhnmnR9T1jw+LTmozhuG2eeuzuW5A7DqxD/UXW2ucjmNJqnqOuh8rdPzM3MQXoF8AfFCedJdCUw==" + "requested": "[8.0.19, )", + "resolved": "8.0.19", + "contentHash": "IhHf+zeZiaE5EXRyxILd4qM+Hj9cxV3sa8MpzZgeEhpvaG3a1VEGF6UCaPFLO44Kua3JkLKluE0SWVamS50PlA==" }, "Azure.Core": { "type": "Transitive", @@ -646,6 +588,44 @@ "System.IO.Hashing": "6.0.0" } }, + "DnsClient": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "4H/f2uYJOZ+YObZjpY9ABrKZI+JNw3uizp6oMzTXwDw6F+2qIPhpRl/1t68O/6e98+vqNiYGu+lswmwdYUy3gg==", + "dependencies": { + "Microsoft.Win32.Registry": "5.0.0" + } + }, + "Google.Protobuf": { + "type": "Transitive", + "resolved": "3.21.2", + "contentHash": "91vdjMcbesfibUtE2n6bVWelT+7J7hExMb9WAEQXuM748+aoIrrUECkIBCmQG+RiM4KvEccVgutFdzl1l6G+Rw==" + }, + "Grpc.Core.Api": { + "type": "Transitive", + "resolved": "2.47.0", + "contentHash": "oZXapxH/2WHAVALghNauo+r/bp6zjgQ6r0v8FizLLQg0/j/FkK2u3WZ7cLOL9Y5H4oLg+wLclO8FSvNTQpNR5Q==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "Grpc.Net.Client": { + "type": "Transitive", + "resolved": "2.47.0", + "contentHash": "DLbUC3T8dMmhA2iqpaJo0X4g7fAi3FyVbDd0/jBXrlqc/bcyA1wPBzLx6mWOWoGUb5S89xL2svnsM8SfzzNa2Q==", + "dependencies": { + "Grpc.Net.Common": "2.47.0", + "Microsoft.Extensions.Logging.Abstractions": "3.0.3" + } + }, + "Grpc.Net.Common": { + "type": "Transitive", + "resolved": "2.47.0", + "contentHash": "Cv76h7noEN2s9cwIdEspOeDjeqcU6Nm34OmjSbRhD/FDBXFmG7rmSfJTPCEB1LYjZWfmf7uUH+nYcHR1I7oQqw==", + "dependencies": { + "Grpc.Core.Api": "2.47.0" + } + }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "1.1.1", @@ -746,8 +726,8 @@ }, "Microsoft.NETCore.Platforms": { "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + "resolved": "5.0.0", + "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" }, "Microsoft.NETCore.Targets": { "type": "Transitive", @@ -759,6 +739,15 @@ "resolved": "1.0.0", "contentHash": "N4KeF3cpcm1PUHym1RmakkzfkEv3GRMyofVv40uXsQhCQeglr2OHNcUk2WOG51AKpGO8ynGpo9M/kFXSzghwug==" }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, "Oracle.ManagedDataAccess.Core": { "type": "Transitive", "resolved": "23.6.1", @@ -887,6 +876,15 @@ "resolved": "6.0.0", "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, "System.Security.Cryptography.Cng": { "type": "Transitive", "resolved": "4.5.0", @@ -905,6 +903,11 @@ "resolved": "8.0.0", "contentHash": "+TUFINV2q2ifyXauQXRwy4CiBhqvDEDZeVJU7qfxya4aRYOKzVBpN+4acx25VcPB9ywUN6C0n8drWl110PhZEg==" }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, "System.Text.Encoding": { "type": "Transitive", "resolved": "4.3.0", @@ -934,10 +937,11 @@ "type": "Project", "dependencies": { "DistributedLock.Azure": "[1.0.2, )", - "DistributedLock.FileSystem": "[1.0.2, )", + "DistributedLock.Etcd": "[1.0.3, )", + "DistributedLock.FileSystem": "[1.0.3, )", "DistributedLock.MySql": "[1.0.2, )", "DistributedLock.Oracle": "[1.0.4, )", - "DistributedLock.Postgres": "[1.2.0, )", + "DistributedLock.Postgres": "[1.3.0, )", "DistributedLock.Redis": "[1.0.3, )", "DistributedLock.SqlServer": "[1.0.6, )", "DistributedLock.WaitHandles": "[1.0.1, )", @@ -954,6 +958,13 @@ "distributedlock.core": { "type": "Project" }, + "distributedlock.etcd": { + "type": "Project", + "dependencies": { + "DistributedLock.Core": "[1.0.8, )", + "dotnet-etcd": "[5.2.1, )" + } + }, "distributedlock.filesystem": { "type": "Project", "dependencies": { @@ -1019,6 +1030,17 @@ "System.Text.Json": "4.7.2" } }, + "dotnet-etcd": { + "type": "CentralTransitive", + "requested": "[5.2.1, )", + "resolved": "5.2.1", + "contentHash": "fmnjf4/TG1C4KTbxIxQ1xJhkGFDrgWHvks3XSQnCBvRtkBP5/UCI82MQxD68/TvetVuVpSoVdlXMD5Nq5kZP5g==", + "dependencies": { + "DnsClient": "1.6.1", + "Google.Protobuf": "3.21.2", + "Grpc.Net.Client": "2.47.0" + } + }, "Microsoft.Data.SqlClient": { "type": "CentralTransitive", "requested": "[5.2.2, )", @@ -1075,134 +1097,6 @@ "resolved": "3.4.12.4", "contentHash": "YECtByVSH7TRjQKplwOWiKyanCqYE5eEkGk5YtHJgsnbZ6+p1o0Gvs5RIsZLotiAVa6Niez1BJyKY/RDY/L6zg==" } - }, - "net8.0/win7-x86": { - "Microsoft.Data.SqlClient.SNI.runtime": { - "type": "Transitive", - "resolved": "5.2.0", - "contentHash": "po1jhvFd+8pbfvJR/puh+fkHi0GRanAdvayh/0e47yaM6CXWZ6opUjCMFuYlAnD2LcbyvQE7fPJKvogmaUcN+w==" - }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "1.1.1", - "contentHash": "TMBuzAHpTenGbGgk0SMTwyEkyijY/Eae4ZGsFNYJvAr/LDn1ku3Etp3FPxChmDp5HHF3kzJuoaa08N0xjqAJfQ==" - }, - "Microsoft.NETCore.Targets": { - "type": "Transitive", - "resolved": "1.1.3", - "contentHash": "3Wrmi0kJDzClwAC+iBdUBpEKmEle8FQNsCs77fkiOIw/9oYA07bL1EZNX0kQ2OMN3xpwvl0vAtOCYY3ndDNlhQ==" - }, - "runtime.any.System.Runtime": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "fRS7zJgaG9NkifaAxGGclDDoRn9HC7hXACl52Or06a/fxdzDajWb5wov3c6a+gVSlekRoexfjwQSK9sh5um5LQ==", - "dependencies": { - "System.Private.Uri": "4.3.0" - } - }, - "runtime.any.System.Text.Encoding": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "+ihI5VaXFCMVPJNstG4O4eo1CfbrByLxRrQQTqOTp1ttK0kUKDqOdBSTaCB2IBk/QtjDrs6+x4xuezyMXdm0HQ==" - }, - "runtime.win7.System.Private.Uri": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "Q+IBgaPYicSQs2tBlmXqbS25c/JLIthWrgrpMwxKSOobW/OqIMVFruUGfuaz4QABVzV8iKdCAbN7APY7Tclbnw==" - }, - "System.Diagnostics.EventLog": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" - }, - "System.Diagnostics.PerformanceCounter": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "lX6DXxtJqVGWw7N/QmVoiCyVQ+Q/Xp+jVXPr3gLK1jJExSn1qmAjJQeb8gnOYeeBTG3E3PmG1nu92eYj/TEjpg==", - "dependencies": { - "System.Configuration.ConfigurationManager": "8.0.0" - } - }, - "System.DirectoryServices.Protocols": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "puwJxURHDrYLGTQdsHyeMS72ClTqYa4lDYz6LHSbkZEk5hq8H8JfsO4MyYhB5BMMxg93jsQzLUwrnCumj11UIg==" - }, - "System.Runtime": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "runtime.any.System.Runtime": "4.3.0" - } - }, - "System.Runtime.Caching": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "4TmlmvGp4kzZomm7J2HJn6IIx0UUrQyhBDyb5O1XiunZlQImXW+B8b7W/sTPcXhSf9rp5NR5aDtQllwbB5elOQ==", - "dependencies": { - "System.Configuration.ConfigurationManager": "8.0.0" - } - }, - "System.Security.Cryptography.Cng": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "WG3r7EyjUe9CMPFSs6bty5doUqT+q9pbI80hlNzo2SkPkZ4VTuZkGWjpp77JB8+uaL4DFPRdBsAY+DX3dBK92A==" - }, - "System.Security.Cryptography.Pkcs": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "ULmp3xoOwNYjOYp4JZ2NK/6NdTgiN1GQXzVVN1njQ7LOZ0d0B9vyMnhyqbIi9Qw4JXj1JgCsitkTShboHRx7Eg==", - "dependencies": { - "System.Formats.Asn1": "8.0.0" - } - }, - "System.Text.Encoding": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "runtime.any.System.Text.Encoding": "4.3.0" - } - }, - "Microsoft.Data.SqlClient": { - "type": "CentralTransitive", - "requested": "[5.2.2, )", - "resolved": "5.2.2", - "contentHash": "mtoeRMh7F/OA536c/Cnh8L4H0uLSKB5kSmoi54oN7Fp0hNJDy22IqyMhaMH4PkDCqI7xL//Fvg9ldtuPHG0h5g==", - "dependencies": { - "Azure.Identity": "1.11.4", - "Microsoft.Data.SqlClient.SNI.runtime": "5.2.0", - "Microsoft.Identity.Client": "4.61.3", - "Microsoft.IdentityModel.JsonWebTokens": "6.35.0", - "Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.35.0", - "Microsoft.SqlServer.Server": "1.0.0", - "System.Configuration.ConfigurationManager": "8.0.0", - "System.Runtime.Caching": "8.0.0" - } - }, - "System.Private.Uri": { - "type": "CentralTransitive", - "requested": "[4.3.2, )", - "resolved": "4.3.2", - "contentHash": "o1+7RJnu3Ik3PazR7Z7tJhjPdE000Eq2KGLLWhqJJKXj04wrS8lwb1OFtDF9jzXXADhUuZNJZlPc98uwwqmpFA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.1", - "Microsoft.NETCore.Targets": "1.1.3", - "runtime.win7.System.Private.Uri": "4.3.0" - } - }, - "System.Threading.AccessControl": { - "type": "CentralTransitive", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "cIed5+HuYz+eV9yu9TH95zPkqmm1J9Qps9wxjB335sU8tsqc2kGdlTEH9FZzZeCS8a7mNSEsN8ZkyhQp1gfdEw==" - } } } } \ No newline at end of file