From c9a75ea02558b1c53ca0050b35e47f124cbbfbc7 Mon Sep 17 00:00:00 2001 From: Pavel Gross Date: Mon, 21 Oct 2024 10:48:00 +0300 Subject: [PATCH 1/3] [#24] Client: Implement pool part1 first iteration - base classes and methods Signed-off-by: Pavel Gross --- src/FrostFS.SDK.ClientV2/Cache.cs | 23 - src/FrostFS.SDK.ClientV2/Caches.cs | 22 + src/FrostFS.SDK.ClientV2/CllientKey.cs | 13 +- .../Exceptions/SessionExpiredException.cs | 18 + .../Exceptions/SessionNotFoundException.cs | 18 + src/FrostFS.SDK.ClientV2/FrostFSClient.cs | 91 ++- .../GlobalSuppressions.cs | 8 + .../Interfaces/IFrostFSClient.cs | 16 +- .../Mappers/ContainerId.cs | 4 +- src/FrostFS.SDK.ClientV2/Mappers/OwnerId.cs | 8 +- .../Models/Chain/ChainTarget.cs | 93 ++- .../Models/Chain/FrostFsChain.cs | 61 +- .../Models/Chain/FrostFsTargetType.cs | 17 +- .../Models/Session/FrostFsSessionToken.cs | 5 +- .../Parameters/{Context.cs => CallContext.cs} | 2 +- .../Parameters/IContext.cs | 2 +- .../Parameters/PrmBalance.cs | 5 + .../Parameters/PrmBase.cs | 2 +- .../Poll/ClientStatusMonitor.cs | 165 +++++ .../Poll/ClientWrapper.cs | 137 ++++ src/FrostFS.SDK.ClientV2/Poll/DialOptions.cs | 16 + .../Poll/HealthyStatus.cs | 18 + .../Poll/IClientStatus.cs | 28 + .../Poll/InitParameters.cs | 34 + src/FrostFS.SDK.ClientV2/Poll/InnerPool.cs | 47 ++ src/FrostFS.SDK.ClientV2/Poll/MethodIndex.cs | 24 + src/FrostFS.SDK.ClientV2/Poll/MethodStatus.cs | 19 + src/FrostFS.SDK.ClientV2/Poll/NodeParam.cs | 12 + .../Poll/NodeStatistic.cs | 12 + src/FrostFS.SDK.ClientV2/Poll/NodesParam.cs | 12 + src/FrostFS.SDK.ClientV2/Poll/Pool.cs | 651 ++++++++++++++++++ .../Poll/RebalanceParameters.cs | 16 + src/FrostFS.SDK.ClientV2/Poll/RequestInfo.cs | 14 + src/FrostFS.SDK.ClientV2/Poll/Sampler.cs | 85 +++ src/FrostFS.SDK.ClientV2/Poll/SessionCache.cs | 29 + src/FrostFS.SDK.ClientV2/Poll/Statistic.cs | 12 + .../Poll/StatusSnapshot.cs | 8 + src/FrostFS.SDK.ClientV2/Poll/WorkList.cs | 26 + src/FrostFS.SDK.ClientV2/Poll/WrapperPrm.cs | 34 + .../Services/AccountingServiceProvider.cs | 40 ++ .../{Shared => }/ApeManagerServiceProvider.cs | 10 +- .../Services/ContainerServiceProvider.cs | 14 +- .../Services/NetmapServiceProvider.cs | 30 +- .../Services/ObjectServiceProvider.cs | 30 +- .../Services/SessionServiceProvider.cs | 6 +- .../Services/Shared/ContextAccessor.cs | 4 +- .../Services/Shared/SessionProvider.cs | 9 +- ...ntEnvironment.cs => EnvironmentContext.cs} | 2 +- .../Tools/NetworkSettings.cs | 4 + src/FrostFS.SDK.ClientV2/Tools/ObjectTools.cs | 8 +- .../Tools/RequestSigner.cs | 5 + .../accounting/Extension.Message.cs | 62 ++ src/FrostFS.SDK.Tests/ContainerTest.cs | 41 +- src/FrostFS.SDK.Tests/ContainerTestsBase.cs | 40 ++ src/FrostFS.SDK.Tests/GlobalSuppressions.cs | 6 + src/FrostFS.SDK.Tests/MetricsInterceptor.cs | 6 +- .../Mocks/AsyncStreamReaderMock.cs | 7 +- .../Mocks/ClientStreamWriter.cs | 10 +- .../ContainerServiceBase.cs | 7 +- .../ContainerServiceMocks/GetContainerMock.cs | 8 +- src/FrostFS.SDK.Tests/Mocks/NetworkMocker.cs | 13 +- src/FrostFS.SDK.Tests/Mocks/ObjectMock.cs | 11 +- src/FrostFS.SDK.Tests/Mocks/SessionMock.cs | 5 +- src/FrostFS.SDK.Tests/NetworkTest.cs | 84 +-- src/FrostFS.SDK.Tests/NetworkTestsBase.cs | 48 ++ src/FrostFS.SDK.Tests/ObjectTest.cs | 84 +-- src/FrostFS.SDK.Tests/ObjectTestsBase.cs | 59 ++ src/FrostFS.SDK.Tests/PoolSmokeTests.cs | 603 ++++++++++++++++ src/FrostFS.SDK.Tests/SessionTests.cs | 52 +- src/FrostFS.SDK.Tests/SessionTestsBase.cs | 48 ++ .../{SmokeTests.cs => SmokeClientTests.cs} | 73 +- src/FrostFS.SDK.Tests/SmokeTestsBase.cs | 18 +- 72 files changed, 2786 insertions(+), 468 deletions(-) delete mode 100644 src/FrostFS.SDK.ClientV2/Cache.cs create mode 100644 src/FrostFS.SDK.ClientV2/Caches.cs create mode 100644 src/FrostFS.SDK.ClientV2/Exceptions/SessionExpiredException.cs create mode 100644 src/FrostFS.SDK.ClientV2/Exceptions/SessionNotFoundException.cs create mode 100644 src/FrostFS.SDK.ClientV2/GlobalSuppressions.cs rename src/FrostFS.SDK.ClientV2/Parameters/{Context.cs => CallContext.cs} (97%) create mode 100644 src/FrostFS.SDK.ClientV2/Parameters/PrmBalance.cs create mode 100644 src/FrostFS.SDK.ClientV2/Poll/ClientStatusMonitor.cs create mode 100644 src/FrostFS.SDK.ClientV2/Poll/ClientWrapper.cs create mode 100644 src/FrostFS.SDK.ClientV2/Poll/DialOptions.cs create mode 100644 src/FrostFS.SDK.ClientV2/Poll/HealthyStatus.cs create mode 100644 src/FrostFS.SDK.ClientV2/Poll/IClientStatus.cs create mode 100644 src/FrostFS.SDK.ClientV2/Poll/InitParameters.cs create mode 100644 src/FrostFS.SDK.ClientV2/Poll/InnerPool.cs create mode 100644 src/FrostFS.SDK.ClientV2/Poll/MethodIndex.cs create mode 100644 src/FrostFS.SDK.ClientV2/Poll/MethodStatus.cs create mode 100644 src/FrostFS.SDK.ClientV2/Poll/NodeParam.cs create mode 100644 src/FrostFS.SDK.ClientV2/Poll/NodeStatistic.cs create mode 100644 src/FrostFS.SDK.ClientV2/Poll/NodesParam.cs create mode 100644 src/FrostFS.SDK.ClientV2/Poll/Pool.cs create mode 100644 src/FrostFS.SDK.ClientV2/Poll/RebalanceParameters.cs create mode 100644 src/FrostFS.SDK.ClientV2/Poll/RequestInfo.cs create mode 100644 src/FrostFS.SDK.ClientV2/Poll/Sampler.cs create mode 100644 src/FrostFS.SDK.ClientV2/Poll/SessionCache.cs create mode 100644 src/FrostFS.SDK.ClientV2/Poll/Statistic.cs create mode 100644 src/FrostFS.SDK.ClientV2/Poll/StatusSnapshot.cs create mode 100644 src/FrostFS.SDK.ClientV2/Poll/WorkList.cs create mode 100644 src/FrostFS.SDK.ClientV2/Poll/WrapperPrm.cs create mode 100644 src/FrostFS.SDK.ClientV2/Services/AccountingServiceProvider.cs rename src/FrostFS.SDK.ClientV2/Services/{Shared => }/ApeManagerServiceProvider.cs (90%) rename src/FrostFS.SDK.ClientV2/Tools/{ClientEnvironment.cs => EnvironmentContext.cs} (87%) create mode 100644 src/FrostFS.SDK.ProtosV2/accounting/Extension.Message.cs create mode 100644 src/FrostFS.SDK.Tests/ContainerTestsBase.cs create mode 100644 src/FrostFS.SDK.Tests/GlobalSuppressions.cs create mode 100644 src/FrostFS.SDK.Tests/NetworkTestsBase.cs create mode 100644 src/FrostFS.SDK.Tests/ObjectTestsBase.cs create mode 100644 src/FrostFS.SDK.Tests/PoolSmokeTests.cs create mode 100644 src/FrostFS.SDK.Tests/SessionTestsBase.cs rename src/FrostFS.SDK.Tests/{SmokeTests.cs => SmokeClientTests.cs} (88%) diff --git a/src/FrostFS.SDK.ClientV2/Cache.cs b/src/FrostFS.SDK.ClientV2/Cache.cs deleted file mode 100644 index 19ec83b..0000000 --- a/src/FrostFS.SDK.ClientV2/Cache.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.Extensions.Caching.Memory; - -namespace FrostFS.SDK.ClientV2 -{ - internal static class Cache - { - private static readonly IMemoryCache _ownersCache = new MemoryCache(new MemoryCacheOptions - { - // TODO: get from options? - SizeLimit = 256 - }); - - private static readonly IMemoryCache _containersCache = new MemoryCache(new MemoryCacheOptions - { - // TODO: get from options? - SizeLimit = 1024 - }); - - internal static IMemoryCache Owners => _ownersCache; - - internal static IMemoryCache Containers => _containersCache; - } -} diff --git a/src/FrostFS.SDK.ClientV2/Caches.cs b/src/FrostFS.SDK.ClientV2/Caches.cs new file mode 100644 index 0000000..ae6a290 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Caches.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Caching.Memory; + +namespace FrostFS.SDK.ClientV2; + +internal static class Caches +{ + private static readonly IMemoryCache _ownersCache = new MemoryCache(new MemoryCacheOptions + { + // TODO: get from options? + SizeLimit = 256 + }); + + private static readonly IMemoryCache _containersCache = new MemoryCache(new MemoryCacheOptions + { + // TODO: get from options? + SizeLimit = 1024 + }); + + internal static IMemoryCache Owners => _ownersCache; + + internal static IMemoryCache Containers => _containersCache; +} diff --git a/src/FrostFS.SDK.ClientV2/CllientKey.cs b/src/FrostFS.SDK.ClientV2/CllientKey.cs index 5b10485..a7542cb 100644 --- a/src/FrostFS.SDK.ClientV2/CllientKey.cs +++ b/src/FrostFS.SDK.ClientV2/CllientKey.cs @@ -4,12 +4,11 @@ using FrostFS.SDK.Cryptography; using Google.Protobuf; -namespace FrostFS.SDK.ClientV2 -{ - public class ClientKey(ECDsa key) - { - internal ECDsa ECDsaKey { get; } = key; +namespace FrostFS.SDK.ClientV2; - internal ByteString PublicKeyProto { get; } = ByteString.CopyFrom(key.PublicKey()); - } +public class ClientKey(ECDsa key) +{ + internal ECDsa ECDsaKey { get; } = key; + + internal ByteString PublicKeyProto { get; } = ByteString.CopyFrom(key.PublicKey()); } diff --git a/src/FrostFS.SDK.ClientV2/Exceptions/SessionExpiredException.cs b/src/FrostFS.SDK.ClientV2/Exceptions/SessionExpiredException.cs new file mode 100644 index 0000000..44404dd --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Exceptions/SessionExpiredException.cs @@ -0,0 +1,18 @@ +using System; + +namespace FrostFS.SDK.ClientV2; + +public class SessionExpiredException : FrostFsException +{ + public SessionExpiredException() + { + } + + public SessionExpiredException(string message) : base(message) + { + } + + public SessionExpiredException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Exceptions/SessionNotFoundException.cs b/src/FrostFS.SDK.ClientV2/Exceptions/SessionNotFoundException.cs new file mode 100644 index 0000000..5ec0a09 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Exceptions/SessionNotFoundException.cs @@ -0,0 +1,18 @@ +using System; + +namespace FrostFS.SDK.ClientV2; + +public class SessionNotFoundException : FrostFsException +{ + public SessionNotFoundException() + { + } + + public SessionNotFoundException(string message) : base(message) + { + } + + public SessionNotFoundException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/FrostFSClient.cs b/src/FrostFS.SDK.ClientV2/FrostFSClient.cs index 4a680de..2ca9e96 100644 --- a/src/FrostFS.SDK.ClientV2/FrostFSClient.cs +++ b/src/FrostFS.SDK.ClientV2/FrostFSClient.cs @@ -6,10 +6,12 @@ using System.Threading.Tasks; using Frostfs.V2.Ape; using Frostfs.V2.Apemanager; +using FrostFS.Accounting; using FrostFS.Container; using FrostFS.Netmap; using FrostFS.Object; using FrostFS.SDK.ClientV2.Interfaces; +using FrostFS.SDK.ClientV2.Services; using FrostFS.SDK.Cryptography; using FrostFS.Session; @@ -35,7 +37,9 @@ public class FrostFSClient : IFrostFSClient internal ObjectService.ObjectServiceClient? ObjectServiceClient { get; set; } - internal ClientEnvironment ClientCtx { get; set; } + internal AccountingService.AccountingServiceClient? AccountingServiceClient { get; set; } + + internal EnvironmentContext ClientCtx { get; set; } public static IFrostFSClient GetInstance(IOptions clientOptions, GrpcChannelOptions? channelOptions = null) { @@ -89,7 +93,7 @@ public class FrostFSClient : IFrostFSClient var ecdsaKey = settings.Value.Key.LoadWif(); FrostFsOwner.FromKey(ecdsaKey); - ClientCtx = new ClientEnvironment( + ClientCtx = new EnvironmentContext( client: this, key: ecdsaKey, owner: FrostFsOwner.FromKey(ecdsaKey), @@ -110,7 +114,7 @@ public class FrostFSClient : IFrostFSClient var channel = InitGrpcChannel(clientSettings.Host, channelOptions); - ClientCtx = new ClientEnvironment( + ClientCtx = new EnvironmentContext( this, key: null, owner: null, @@ -131,7 +135,7 @@ public class FrostFSClient : IFrostFSClient var channel = InitGrpcChannel(clientSettings.Host, channelOptions); - ClientCtx = new ClientEnvironment( + ClientCtx = new EnvironmentContext( this, key: ecdsaKey, owner: FrostFsOwner.FromKey(ecdsaKey), @@ -142,6 +146,16 @@ public class FrostFSClient : IFrostFSClient // CheckFrostFsVersionSupport(new Context { Timeout = TimeSpan.FromSeconds(20) }); } + internal FrostFSClient(WrapperPrm prm) + { + ClientCtx = new EnvironmentContext( + client: this, + key: prm.Key, + owner: FrostFsOwner.FromKey(prm.Key!), + channel: InitGrpcChannel(prm.Address, null), //prm.GrpcChannelOptions), + version: new FrostFsVersion(2, 13)); + } + public void Dispose() { Dispose(true); @@ -305,16 +319,16 @@ public class FrostFSClient : IFrostFSClient } #endregion - #region SessionImplementation + #region Session Implementation public async Task CreateSessionAsync(PrmSessionCreate args) { if (args is null) throw new ArgumentNullException(nameof(args)); var session = await CreateSessionInternalAsync(args).ConfigureAwait(false); - var token = session.Serialize(); - return new FrostFsSessionToken(token); + var token = session.Serialize(); + return new FrostFsSessionToken(token, session.Body.Id.ToUuid()); } internal Task CreateSessionInternalAsync(PrmSessionCreate args) @@ -327,8 +341,18 @@ public class FrostFSClient : IFrostFSClient } #endregion + #region Accounting Implementation + public async Task GetBalanceAsync(PrmBalance? args) + { + args ??= new PrmBalance(); + + var service = GetAccouningService(args); + return await service.GetBallance(args).ConfigureAwait(false); + } + #endregion + #region ToolsImplementation - public FrostFsObjectId CalculateObjectId(FrostFsObjectHeader header, Context ctx) + public FrostFsObjectId CalculateObjectId(FrostFsObjectHeader header, CallContext ctx) { if (header == null) throw new ArgumentNullException(nameof(header)); @@ -337,7 +361,7 @@ public class FrostFSClient : IFrostFSClient } #endregion - private async void CheckFrostFsVersionSupport(Context? ctx = default) + private async void CheckFrostFsVersionSupport(CallContext? ctx = default) { var args = new PrmNodeInfo { Context = ctx }; @@ -359,7 +383,7 @@ public class FrostFSClient : IFrostFSClient if (isDisposed) throw new InvalidObjectException("Client is disposed."); - ctx.Context ??= new Context(); + ctx.Context ??= new CallContext(); if (ctx.Context.Key == null) { @@ -441,6 +465,16 @@ public class FrostFSClient : IFrostFSClient return new ApeManagerServiceProvider(client, ClientCtx); } + private AccountingServiceProvider GetAccouningService(IContext ctx) + { + var callInvoker = SetupEnvironment(ctx); + var client = AccountingServiceClient ?? (callInvoker != null + ? new AccountingService.AccountingServiceClient(callInvoker) + : new AccountingService.AccountingServiceClient(ClientCtx.Channel)); + + return new AccountingServiceProvider(client, ClientCtx); + } + private ContainerServiceProvider GetContainerService(IContext ctx) { var callInvoker = SetupEnvironment(ctx); @@ -461,6 +495,16 @@ public class FrostFSClient : IFrostFSClient return new ObjectServiceProvider(client, ClientCtx); } + private AccountingServiceProvider GetAccountService(IContext ctx) + { + var callInvoker = SetupEnvironment(ctx); + var client = AccountingServiceClient ?? (callInvoker != null + ? new AccountingService.AccountingServiceClient(callInvoker) + : new AccountingService.AccountingServiceClient(ClientCtx.Channel)); + + return new AccountingServiceProvider(client, ClientCtx); + } + private static GrpcChannel InitGrpcChannel(string host, GrpcChannelOptions? channelOptions) { try @@ -480,4 +524,31 @@ public class FrostFSClient : IFrostFSClient throw new ArgumentException($"Host '{host}' has invalid format. Error: {e.Message}"); } } + + public async Task Dial(CallContext ctx) + { + try + { + var prm = new PrmBalance { Context = ctx }; + + var service = GetAccouningService(prm); + var balance = await service.GetBallance(prm).ConfigureAwait(false); + + return null; + } + catch (FrostFsException ex) + { + return ex.Message; + } + } + + public bool RestartIfUnhealthy(CallContext ctx) + { + throw new NotImplementedException(); + } + + public void Close() + { + Dispose(); + } } diff --git a/src/FrostFS.SDK.ClientV2/GlobalSuppressions.cs b/src/FrostFS.SDK.ClientV2/GlobalSuppressions.cs new file mode 100644 index 0000000..c87cda6 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "", Scope = "member", Target = "~M:FrostFS.SDK.ClientV2.Sampler.Next~System.Int32")] diff --git a/src/FrostFS.SDK.ClientV2/Interfaces/IFrostFSClient.cs b/src/FrostFS.SDK.ClientV2/Interfaces/IFrostFSClient.cs index 3af0c0b..193d128 100644 --- a/src/FrostFS.SDK.ClientV2/Interfaces/IFrostFSClient.cs +++ b/src/FrostFS.SDK.ClientV2/Interfaces/IFrostFSClient.cs @@ -19,7 +19,7 @@ public interface IFrostFSClient : IDisposable Task CreateSessionAsync(PrmSessionCreate args); #endregion - #region ApeMAnager + #region ApeManager Task AddChainAsync(PrmApeChainAdd args); Task RemoveChainAsync(PrmApeChainRemove args); @@ -51,7 +51,17 @@ public interface IFrostFSClient : IDisposable IAsyncEnumerable SearchObjectsAsync(PrmObjectSearch args); #endregion - #region Tools - FrostFsObjectId CalculateObjectId(FrostFsObjectHeader header, Context ctx); + #region Account + Task GetBalanceAsync(PrmBalance? args = null); #endregion + + #region Tools + FrostFsObjectId CalculateObjectId(FrostFsObjectHeader header, CallContext ctx); + #endregion + + public Task Dial(CallContext ctx); + + public bool RestartIfUnhealthy(CallContext ctx); + + public void Close(); } diff --git a/src/FrostFS.SDK.ClientV2/Mappers/ContainerId.cs b/src/FrostFS.SDK.ClientV2/Mappers/ContainerId.cs index 86449e2..7a5359d 100644 --- a/src/FrostFS.SDK.ClientV2/Mappers/ContainerId.cs +++ b/src/FrostFS.SDK.ClientV2/Mappers/ContainerId.cs @@ -24,14 +24,14 @@ public static class ContainerIdMapper var containerId = model.GetValue() ?? throw new ArgumentNullException(nameof(model)); - if (!Cache.Containers.TryGetValue(containerId, out ContainerID? message)) + if (!Caches.Containers.TryGetValue(containerId, out ContainerID? message)) { message = new ContainerID { Value = ByteString.CopyFrom(Base58.Decode(containerId)) }; - Cache.Containers.Set(containerId, message, _oneHourExpiration); + Caches.Containers.Set(containerId, message, _oneHourExpiration); } return message!; diff --git a/src/FrostFS.SDK.ClientV2/Mappers/OwnerId.cs b/src/FrostFS.SDK.ClientV2/Mappers/OwnerId.cs index 6f3276d..c2e61e6 100644 --- a/src/FrostFS.SDK.ClientV2/Mappers/OwnerId.cs +++ b/src/FrostFS.SDK.ClientV2/Mappers/OwnerId.cs @@ -22,14 +22,14 @@ public static class OwnerIdMapper throw new ArgumentNullException(nameof(model)); } - if (!Cache.Owners.TryGetValue(model, out OwnerID? message)) + if (!Caches.Owners.TryGetValue(model, out OwnerID? message)) { message = new OwnerID { Value = ByteString.CopyFrom(model.ToHash()) }; - Cache.Owners.Set(model, message, _oneHourExpiration); + Caches.Owners.Set(model, message, _oneHourExpiration); } return message!; @@ -42,11 +42,11 @@ public static class OwnerIdMapper throw new ArgumentNullException(nameof(message)); } - if (!Cache.Owners.TryGetValue(message, out FrostFsOwner? model)) + if (!Caches.Owners.TryGetValue(message, out FrostFsOwner? model)) { model = new FrostFsOwner(Base58.Encode(message.Value.ToByteArray())); - Cache.Owners.Set(message, model, _oneHourExpiration); + Caches.Owners.Set(message, model, _oneHourExpiration); } return model!; diff --git a/src/FrostFS.SDK.ClientV2/Models/Chain/ChainTarget.cs b/src/FrostFS.SDK.ClientV2/Models/Chain/ChainTarget.cs index e9ce527..a009df2 100644 --- a/src/FrostFS.SDK.ClientV2/Models/Chain/ChainTarget.cs +++ b/src/FrostFS.SDK.ClientV2/Models/Chain/ChainTarget.cs @@ -2,62 +2,61 @@ using Frostfs.V2.Ape; -namespace FrostFS.SDK.ClientV2 +namespace FrostFS.SDK.ClientV2; + +public struct FrostFsChainTarget(FrostFsTargetType type, string name) : IEquatable { - public struct FrostFsChainTarget(FrostFsTargetType type, string name) : IEquatable + private ChainTarget? chainTarget; + + public FrostFsTargetType Type { get; } = type; + + public string Name { get; } = name; + + internal ChainTarget GetChainTarget() { - private ChainTarget? chainTarget; - - public FrostFsTargetType Type { get; } = type; - - public string Name { get; } = name; - - internal ChainTarget GetChainTarget() + return chainTarget ??= new ChainTarget { - return chainTarget ??= new ChainTarget - { - Type = GetTargetType(Type), - Name = Name - }; - } + Type = GetTargetType(Type), + Name = Name + }; + } - private static TargetType GetTargetType(FrostFsTargetType type) + private static TargetType GetTargetType(FrostFsTargetType type) + { + return type switch { - return type switch - { - FrostFsTargetType.Undefined => TargetType.Undefined, - FrostFsTargetType.Namespace => TargetType.Namespace, - FrostFsTargetType.Container => TargetType.Container, - FrostFsTargetType.User => TargetType.User, - FrostFsTargetType.Group => TargetType.Group, - _ => throw new ArgumentException("Unexpected value for TargetType", nameof(type)), - }; - } + FrostFsTargetType.Undefined => TargetType.Undefined, + FrostFsTargetType.Namespace => TargetType.Namespace, + FrostFsTargetType.Container => TargetType.Container, + FrostFsTargetType.User => TargetType.User, + FrostFsTargetType.Group => TargetType.Group, + _ => throw new ArgumentException("Unexpected value for TargetType", nameof(type)), + }; + } - public override readonly bool Equals(object obj) - { - var target = (FrostFsChainTarget)obj; - return Equals(target); - } + public override readonly bool Equals(object obj) + { + var target = (FrostFsChainTarget)obj; + return Equals(target); + } - public override readonly int GetHashCode() - { - return $"{Name}{Type}".GetHashCode(); - } + public override readonly int GetHashCode() + { + return $"{Name}{Type}".GetHashCode(); + } - public static bool operator ==(FrostFsChainTarget left, FrostFsChainTarget right) - { - return left.Equals(right); - } + public static bool operator ==(FrostFsChainTarget left, FrostFsChainTarget right) + { + return left.Equals(right); + } - public static bool operator !=(FrostFsChainTarget left, FrostFsChainTarget right) - { - return !(left == right); - } + public static bool operator !=(FrostFsChainTarget left, FrostFsChainTarget right) + { + return !(left == right); + } - public readonly bool Equals(FrostFsChainTarget other) - { - return Type == other.Type && Name.Equals(other.Name, StringComparison.Ordinal); - } + public readonly bool Equals(FrostFsChainTarget other) + { + return Type == other.Type && Name.Equals(other.Name, StringComparison.Ordinal); } } diff --git a/src/FrostFS.SDK.ClientV2/Models/Chain/FrostFsChain.cs b/src/FrostFS.SDK.ClientV2/Models/Chain/FrostFsChain.cs index d6b4b1d..1f4d251 100644 --- a/src/FrostFS.SDK.ClientV2/Models/Chain/FrostFsChain.cs +++ b/src/FrostFS.SDK.ClientV2/Models/Chain/FrostFsChain.cs @@ -1,42 +1,41 @@ using Google.Protobuf; -namespace FrostFS.SDK.ClientV2 +namespace FrostFS.SDK.ClientV2; + +public struct FrostFsChain(byte[] raw) : System.IEquatable { - public struct FrostFsChain(byte[] raw) : System.IEquatable + private ByteString? grpcRaw; + + public byte[] Raw { get; } = raw; + + internal ByteString GetRaw() { - private ByteString? grpcRaw; + return grpcRaw ??= ByteString.CopyFrom(Raw); + } - public byte[] Raw { get; } = raw; + public override readonly bool Equals(object obj) + { + var chain = (FrostFsChain)obj; + return Equals(chain); + } - internal ByteString GetRaw() - { - return grpcRaw ??= ByteString.CopyFrom(Raw); - } + public override readonly int GetHashCode() + { + return Raw.GetHashCode(); + } - public override readonly bool Equals(object obj) - { - var chain = (FrostFsChain)obj; - return Equals(chain); - } + public static bool operator ==(FrostFsChain left, FrostFsChain right) + { + return left.Equals(right); + } - public override readonly int GetHashCode() - { - return Raw.GetHashCode(); - } + public static bool operator !=(FrostFsChain left, FrostFsChain right) + { + return !(left == right); + } - public static bool operator ==(FrostFsChain left, FrostFsChain right) - { - return left.Equals(right); - } - - public static bool operator !=(FrostFsChain left, FrostFsChain right) - { - return !(left == right); - } - - public readonly bool Equals(FrostFsChain other) - { - return Raw == other.Raw; - } + public readonly bool Equals(FrostFsChain other) + { + return Raw == other.Raw; } } diff --git a/src/FrostFS.SDK.ClientV2/Models/Chain/FrostFsTargetType.cs b/src/FrostFS.SDK.ClientV2/Models/Chain/FrostFsTargetType.cs index 03b1521..ed5fa27 100644 --- a/src/FrostFS.SDK.ClientV2/Models/Chain/FrostFsTargetType.cs +++ b/src/FrostFS.SDK.ClientV2/Models/Chain/FrostFsTargetType.cs @@ -1,11 +1,10 @@ -namespace FrostFS.SDK.ClientV2 +namespace FrostFS.SDK.ClientV2; + +public enum FrostFsTargetType { - public enum FrostFsTargetType - { - Undefined = 0, - Namespace, - Container, - User, - Group - } + Undefined = 0, + Namespace, + Container, + User, + Group } diff --git a/src/FrostFS.SDK.ClientV2/Models/Session/FrostFsSessionToken.cs b/src/FrostFS.SDK.ClientV2/Models/Session/FrostFsSessionToken.cs index 8954780..672754d 100644 --- a/src/FrostFS.SDK.ClientV2/Models/Session/FrostFsSessionToken.cs +++ b/src/FrostFS.SDK.ClientV2/Models/Session/FrostFsSessionToken.cs @@ -1,6 +1,9 @@ +using System; + namespace FrostFS.SDK; -public class FrostFsSessionToken(byte[] token) +public class FrostFsSessionToken(byte[] token, Guid id) { + public Guid Id { get; private set; } = id; public byte[] Token { get; private set; } = token; } diff --git a/src/FrostFS.SDK.ClientV2/Parameters/Context.cs b/src/FrostFS.SDK.ClientV2/Parameters/CallContext.cs similarity index 97% rename from src/FrostFS.SDK.ClientV2/Parameters/Context.cs rename to src/FrostFS.SDK.ClientV2/Parameters/CallContext.cs index 0e96929..ff4bc27 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/Context.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/CallContext.cs @@ -11,7 +11,7 @@ using Grpc.Core.Interceptors; namespace FrostFS.SDK.ClientV2; -public class Context() +public class CallContext() { private ReadOnlyCollection? interceptors; diff --git a/src/FrostFS.SDK.ClientV2/Parameters/IContext.cs b/src/FrostFS.SDK.ClientV2/Parameters/IContext.cs index bd333f7..8c6f1df 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/IContext.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/IContext.cs @@ -7,5 +7,5 @@ public interface IContext /// callbacks, interceptors. /// /// Additional parameters for calling the method - Context? Context { get; set; } + CallContext? Context { get; set; } } diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmBalance.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmBalance.cs new file mode 100644 index 0000000..3a07fde --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmBalance.cs @@ -0,0 +1,5 @@ +namespace FrostFS.SDK.ClientV2; + +public sealed class PrmBalance() : PrmBase +{ +} diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmBase.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmBase.cs index 2a6e542..79e5e7e 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/PrmBase.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmBase.cs @@ -10,5 +10,5 @@ public class PrmBase(NameValueCollection? xheaders = null) : IContext public NameValueCollection XHeaders { get; } = xheaders ?? []; /// - public Context? Context { get; set; } + public CallContext? Context { get; set; } } diff --git a/src/FrostFS.SDK.ClientV2/Poll/ClientStatusMonitor.cs b/src/FrostFS.SDK.ClientV2/Poll/ClientStatusMonitor.cs new file mode 100644 index 0000000..e864148 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Poll/ClientStatusMonitor.cs @@ -0,0 +1,165 @@ +using System; +using System.Threading; + +using Microsoft.Extensions.Logging; + +namespace FrostFS.SDK.ClientV2; + +// clientStatusMonitor count error rate and other statistics for connection. +public class ClientStatusMonitor : IClientStatus +{ + private static readonly MethodIndex[] MethodIndexes = + [ + MethodIndex.methodBalanceGet, + MethodIndex.methodContainerPut, + MethodIndex.methodContainerGet, + MethodIndex.methodContainerList, + MethodIndex.methodContainerDelete, + MethodIndex.methodEndpointInfo, + MethodIndex.methodNetworkInfo, + MethodIndex.methodNetMapSnapshot, + MethodIndex.methodObjectPut, + MethodIndex.methodObjectDelete, + MethodIndex.methodObjectGet, + MethodIndex.methodObjectHead, + MethodIndex.methodObjectRange, + MethodIndex.methodObjectPatch, + MethodIndex.methodSessionCreate, + MethodIndex.methodAPEManagerAddChain, + MethodIndex.methodAPEManagerRemoveChain, + MethodIndex.methodAPEManagerListChains, + MethodIndex.methodLast + ]; + + public static string GetMethodName(MethodIndex index) + { + return index switch + { + MethodIndex.methodBalanceGet => "BalanceGet", + MethodIndex.methodContainerPut => "ContainerPut", + MethodIndex.methodContainerGet => "ContainerGet", + MethodIndex.methodContainerList => "ContainerList", + MethodIndex.methodContainerDelete => "ContainerDelete", + MethodIndex.methodEndpointInfo => "EndpointInfo", + MethodIndex.methodNetworkInfo => "NetworkInfo", + MethodIndex.methodNetMapSnapshot => "NetMapSnapshot", + MethodIndex.methodObjectPut => "ObjectPut", + MethodIndex.methodObjectDelete => "ObjectDelete", + MethodIndex.methodObjectGet => "ObjectGet", + MethodIndex.methodObjectHead => "ObjectHead", + MethodIndex.methodObjectRange => "ObjectRange", + MethodIndex.methodObjectPatch => "ObjectPatch", + MethodIndex.methodSessionCreate => "SessionCreate", + MethodIndex.methodAPEManagerAddChain => "APEManagerAddChain", + MethodIndex.methodAPEManagerRemoveChain => "APEManagerRemoveChain", + MethodIndex.methodAPEManagerListChains => "APEManagerListChains", + _ => throw new NotImplementedException(), + }; + } + + private readonly object _lock = new(); + + private readonly ILogger? logger; + private int healthy; + + public ClientStatusMonitor(ILogger? logger, string address, uint errorThreshold) + { + this.logger = logger; + healthy = (int)HealthyStatus.Healthy; + Address = address; + ErrorThreshold = errorThreshold; + + Methods = new MethodStatus[MethodIndexes.Length]; + + for (int i = 0; i < MethodIndexes.Length; i++) + { + Methods[i] = new MethodStatus(GetMethodName(MethodIndexes[i])); + } + } + + public string Address { get; } + + internal uint ErrorThreshold { get; } + + public uint CurrentErrorCount { get; set; } + + public ulong OverallErrorCount { get; set; } + + public MethodStatus[] Methods { get; private set; } + + public bool IsHealthy() + { + return Interlocked.CompareExchange(ref healthy, -1, -1) == (int)HealthyStatus.Healthy; + } + + public bool IsDialed() + { + return Interlocked.CompareExchange(ref healthy, -1, -1) != (int)HealthyStatus.UnhealthyOnDial; + } + + public void SetHealthy() + { + Interlocked.Exchange(ref healthy, (int)HealthyStatus.Healthy); + } + public void SetUnhealthy() + { + Interlocked.Exchange(ref healthy, (int)HealthyStatus.UnhealthyOnRequest); + } + + public void SetUnhealthyOnDial() + { + Interlocked.Exchange(ref healthy, (int)HealthyStatus.UnhealthyOnDial); + } + + public void IncErrorRate() + { + bool thresholdReached; + lock (_lock) + { + CurrentErrorCount++; + OverallErrorCount++; + + thresholdReached = CurrentErrorCount >= ErrorThreshold; + + if (thresholdReached) + { + SetUnhealthy(); + + CurrentErrorCount = 0; + } + } + + if (thresholdReached) + { + logger?.Log(LogLevel.Warning, "Error threshold reached. Address {Address}, threshold {Threshold}", Address, ErrorThreshold); + } + } + + public uint GetCurrentErrorRate() + { + lock (_lock) + { + return CurrentErrorCount; + } + } + + public ulong GetOverallErrorRate() + { + lock (_lock) + { + return OverallErrorCount; + } + } + + public StatusSnapshot[] MethodsStatus() + { + var result = new StatusSnapshot[Methods.Length]; + + for (int i = 0; i < result.Length; i++) + { + result[i] = Methods[i].Snapshot!; + } + + return result; + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Poll/ClientWrapper.cs b/src/FrostFS.SDK.ClientV2/Poll/ClientWrapper.cs new file mode 100644 index 0000000..3891250 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Poll/ClientWrapper.cs @@ -0,0 +1,137 @@ +using System.Threading.Tasks; + +namespace FrostFS.SDK.ClientV2; + +// clientWrapper is used by default, alternative implementations are intended for testing purposes only. +public class ClientWrapper +{ + private readonly object _lock = new(); + + public ClientWrapper(WrapperPrm wrapperPrm) + { + WrapperPrm = wrapperPrm; + StatusMonitor = new ClientStatusMonitor(wrapperPrm.Logger, wrapperPrm.Address, wrapperPrm.ErrorThreshold); + + try + { + Client = new FrostFSClient(WrapperPrm); + StatusMonitor.SetHealthy(); + } + catch (FrostFsException) + { + } + } + + internal FrostFSClient? Client { get; private set; } + + internal WrapperPrm WrapperPrm { get; } + + internal ClientStatusMonitor StatusMonitor { get; } + + internal FrostFSClient? GetClient() + { + lock (_lock) + { + if (StatusMonitor.IsHealthy()) + { + return Client; + } + + return null; + } + } + + // dial establishes a connection to the server from the FrostFS network. + // Returns an error describing failure reason. If failed, the client + // SHOULD NOT be used. + internal async Task Dial(CallContext ctx) + { + var client = GetClient(); + + if (client == null) + return "pool client unhealthy"; + + var result = await client.Dial(ctx).ConfigureAwait(false); + if (!string.IsNullOrEmpty(result)) + { + StatusMonitor.SetUnhealthyOnDial(); + return result; + } + + return null; + } + + private async Task ScheduleGracefulClose() + { + if (Client == null) + return; + + await Task.Delay((int)WrapperPrm.GracefulCloseOnSwitchTimeout).ConfigureAwait(false); + + Client.Close(); + } + + // restartIfUnhealthy checks healthy status of client and recreate it if status is unhealthy. + // Indicating if status was changed by this function call and returns error that caused unhealthy status. + internal async Task RestartIfUnhealthy(CallContext ctx) + { + bool wasHealthy; + + try + { + var prmNodeInfo = new PrmNodeInfo { Context = ctx }; + var response = await Client!.GetNodeInfoAsync(prmNodeInfo).ConfigureAwait(false); + return false; + } + catch (FrostFsException) + { + wasHealthy = true; + } + + // if connection is dialed before, to avoid routine/connection leak, + // pool has to close it and then initialize once again. + if (StatusMonitor.IsDialed()) + { + await ScheduleGracefulClose().ConfigureAwait(false); + } + +#pragma warning disable CA2000 // Dispose objects before losing scope: will be disposed manually + FrostFSClient client = new(WrapperPrm); +#pragma warning restore CA2000 + + //TODO: set additioanl params + var error = await client.Dial(ctx).ConfigureAwait(false); + if (!string.IsNullOrEmpty(error)) + { + StatusMonitor.SetUnhealthyOnDial(); + return wasHealthy; + } + + lock (_lock) + { + Client = client; + } + + try + { + var prmNodeInfo = new PrmNodeInfo { Context = ctx }; + var res = await client.GetNodeInfoAsync(prmNodeInfo).ConfigureAwait(false); + } + catch (FrostFsException) + { + StatusMonitor.SetUnhealthy(); + return wasHealthy; + } + + StatusMonitor.SetHealthy(); + return !wasHealthy; + } + + internal void IncRequests(ulong elapsed, MethodIndex method) + { + var methodStat = StatusMonitor.Methods[(int)method]; + + methodStat.IncRequests(elapsed); + } +} + diff --git a/src/FrostFS.SDK.ClientV2/Poll/DialOptions.cs b/src/FrostFS.SDK.ClientV2/Poll/DialOptions.cs new file mode 100644 index 0000000..813d373 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Poll/DialOptions.cs @@ -0,0 +1,16 @@ +namespace FrostFS.SDK.ClientV2; + +public class DialOptions +{ + public bool Block { get; set; } + + public bool ReturnLastError { get; set; } + + public ulong Timeout { get; set; } + + public string? Authority { get; set; } + + public bool DisableRetry { get; set; } + + public bool DisableHealthCheck { get; set; } +} diff --git a/src/FrostFS.SDK.ClientV2/Poll/HealthyStatus.cs b/src/FrostFS.SDK.ClientV2/Poll/HealthyStatus.cs new file mode 100644 index 0000000..f87c1dd --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Poll/HealthyStatus.cs @@ -0,0 +1,18 @@ +namespace FrostFS.SDK.ClientV2; + +// values for healthy status of clientStatusMonitor. +public enum HealthyStatus +{ + // statusUnhealthyOnDial is set when dialing to the endpoint is failed, + // so there is no connection to the endpoint, and pool should not close it + // before re-establishing connection once again. + UnhealthyOnDial, + + // statusUnhealthyOnRequest is set when communication after dialing to the + // endpoint is failed due to immediate or accumulated errors, connection is + // available and pool should close it before re-establishing connection once again. + UnhealthyOnRequest, + + // statusHealthy is set when connection is ready to be used by the pool. + Healthy +} diff --git a/src/FrostFS.SDK.ClientV2/Poll/IClientStatus.cs b/src/FrostFS.SDK.ClientV2/Poll/IClientStatus.cs new file mode 100644 index 0000000..cbf597e --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Poll/IClientStatus.cs @@ -0,0 +1,28 @@ +namespace FrostFS.SDK.ClientV2; + +public interface IClientStatus +{ + // isHealthy checks if the connection can handle requests. + bool IsHealthy(); + + // isDialed checks if the connection was created. + bool IsDialed(); + + // setUnhealthy marks client as unhealthy. + void SetUnhealthy(); + + // address return address of endpoint. + string Address { get; } + + // currentErrorRate returns current errors rate. + // After specific threshold connection is considered as unhealthy. + // Pool.startRebalance routine can make this connection healthy again. + uint GetCurrentErrorRate(); + + // overallErrorRate returns the number of all happened errors. + ulong GetOverallErrorRate(); + + // methodsStatus returns statistic for all used methods. + StatusSnapshot[] MethodsStatus(); +} + diff --git a/src/FrostFS.SDK.ClientV2/Poll/InitParameters.cs b/src/FrostFS.SDK.ClientV2/Poll/InitParameters.cs new file mode 100644 index 0000000..e05c31a --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Poll/InitParameters.cs @@ -0,0 +1,34 @@ +using System; +using System.Security.Cryptography; + +using Microsoft.Extensions.Logging; + +namespace FrostFS.SDK.ClientV2; + +// InitParameters contains values used to initialize connection Pool. +public class InitParameters +{ + public ECDsa? Key { get; set; } + + public ulong NodeDialTimeout { get; set; } + + public ulong NodeStreamTimeout { get; set; } + + public ulong HealthcheckTimeout { get; set; } + + public ulong ClientRebalanceInterval { get; set; } + + public ulong SessionExpirationDuration { get; set; } + + public uint ErrorThreshold { get; set; } + + public NodeParam[]? NodeParams { get; set; } + + public DialOptions[]? DialOptions { get; set; } + + public Func? ClientBuilder { get; set; } + + public ulong GracefulCloseOnSwitchTimeout { get; set; } + + public ILogger? Logger { get; set; } +} diff --git a/src/FrostFS.SDK.ClientV2/Poll/InnerPool.cs b/src/FrostFS.SDK.ClientV2/Poll/InnerPool.cs new file mode 100644 index 0000000..a535260 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Poll/InnerPool.cs @@ -0,0 +1,47 @@ +using FrostFS.SDK.ClientV2; + +internal sealed class InnerPool +{ + private readonly object _lock = new(); + + internal InnerPool(Sampler sampler, ClientWrapper[] clients) + { + Clients = clients; + Sampler = sampler; + } + + internal Sampler Sampler { get; set; } + + internal ClientWrapper[] Clients { get; } + + internal ClientWrapper? Connection() + { + lock (_lock) + { + if (Clients.Length == 1) + { + var client = Clients[0]; + if (client.StatusMonitor.IsHealthy()) + { + return client; + } + } + else + { + var attempts = 3 * Clients.Length; + + for (int i = 0; i < attempts; i++) + { + int index = Sampler.Next(); + + if (Clients[index].StatusMonitor.IsHealthy()) + { + return Clients[index]; + } + } + } + + return null; + } + } +} diff --git a/src/FrostFS.SDK.ClientV2/Poll/MethodIndex.cs b/src/FrostFS.SDK.ClientV2/Poll/MethodIndex.cs new file mode 100644 index 0000000..8a7008b --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Poll/MethodIndex.cs @@ -0,0 +1,24 @@ +namespace FrostFS.SDK.ClientV2; + +public enum MethodIndex +{ + methodBalanceGet, + methodContainerPut, + methodContainerGet, + methodContainerList, + methodContainerDelete, + methodEndpointInfo, + methodNetworkInfo, + methodNetMapSnapshot, + methodObjectPut, + methodObjectDelete, + methodObjectGet, + methodObjectHead, + methodObjectRange, + methodObjectPatch, + methodSessionCreate, + methodAPEManagerAddChain, + methodAPEManagerRemoveChain, + methodAPEManagerListChains, + methodLast +} diff --git a/src/FrostFS.SDK.ClientV2/Poll/MethodStatus.cs b/src/FrostFS.SDK.ClientV2/Poll/MethodStatus.cs new file mode 100644 index 0000000..33cad49 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Poll/MethodStatus.cs @@ -0,0 +1,19 @@ +namespace FrostFS.SDK.ClientV2; + +public class MethodStatus(string name) +{ + private readonly object _lock = new(); + + public string Name { get; } = name; + + public StatusSnapshot Snapshot { get; set; } = new StatusSnapshot(); + + internal void IncRequests(ulong elapsed) + { + lock (_lock) + { + Snapshot.AllTime += elapsed; + Snapshot.AllRequests++; + } + } +} diff --git a/src/FrostFS.SDK.ClientV2/Poll/NodeParam.cs b/src/FrostFS.SDK.ClientV2/Poll/NodeParam.cs new file mode 100644 index 0000000..f95772b --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Poll/NodeParam.cs @@ -0,0 +1,12 @@ +namespace FrostFS.SDK.ClientV2; + +// NodeParam groups parameters of remote node. +[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "")] +public readonly struct NodeParam(int priority, string address, float weight) +{ + public int Priority { get; } = priority; + + public string Address { get; } = address; + + public float Weight { get; } = weight; +} diff --git a/src/FrostFS.SDK.ClientV2/Poll/NodeStatistic.cs b/src/FrostFS.SDK.ClientV2/Poll/NodeStatistic.cs new file mode 100644 index 0000000..d51aa83 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Poll/NodeStatistic.cs @@ -0,0 +1,12 @@ +namespace FrostFS.SDK.ClientV2; + +public class NodeStatistic +{ + public string? Address { get; internal set; } + + public StatusSnapshot[]? Methods { get; internal set; } + + public ulong OverallErrors { get; internal set; } + + public uint CurrentErrors { get; internal set; } +} diff --git a/src/FrostFS.SDK.ClientV2/Poll/NodesParam.cs b/src/FrostFS.SDK.ClientV2/Poll/NodesParam.cs new file mode 100644 index 0000000..eaeef7c --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Poll/NodesParam.cs @@ -0,0 +1,12 @@ +using System.Collections.ObjectModel; + +namespace FrostFS.SDK.ClientV2; + +public class NodesParam(int priority) +{ + public int Priority { get; } = priority; + + public Collection Addresses { get; } = []; + + public Collection Weights { get; } = []; +} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Poll/Pool.cs b/src/FrostFS.SDK.ClientV2/Poll/Pool.cs new file mode 100644 index 0000000..a304e93 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Poll/Pool.cs @@ -0,0 +1,651 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Frostfs.V2.Ape; + +using FrostFS.Refs; +using FrostFS.SDK.ClientV2.Interfaces; +using FrostFS.SDK.ClientV2.Mappers.GRPC; +using FrostFS.SDK.Cryptography; + +using Microsoft.Extensions.Logging; + + +namespace FrostFS.SDK.ClientV2; + +public partial class Pool : IFrostFSClient +{ + const int defaultSessionTokenExpirationDuration = 100; // in epochs + + const int defaultErrorThreshold = 100; + + const int defaultGracefulCloseOnSwitchTimeout = 10; //Seconds; + const int defaultRebalanceInterval = 15; //Seconds; + const int defaultHealthcheckTimeout = 4; //Seconds; + const int defaultDialTimeout = 5; //Seconds; + const int defaultStreamTimeout = 10; //Seconds; + + private readonly object _lock = new(); + + private InnerPool[]? InnerPools { get; set; } + + private ECDsa Key { get; set; } + + private byte[] PublicKey { get; } + + private OwnerID? _ownerId; + private FrostFsOwner? _owner; + + private FrostFsOwner Owner + { + get + { + _owner ??= new FrostFsOwner(Key.PublicKey().PublicKeyToAddress()); + return _owner; + } + } + + private OwnerID OwnerId + { + get + { + if (_ownerId == null) + { + _owner = new FrostFsOwner(Key.PublicKey().PublicKeyToAddress()); + _ownerId = _owner.ToMessage(); + } + return _ownerId; + } + } + + internal CancellationTokenSource CancellationTokenSource { get; } = new CancellationTokenSource(); + + private SessionCache Cache { get; set; } + + private ulong SessionTokenDuration { get; set; } + + private RebalanceParameters RebalanceParams { get; set; } + + private Func ClientBuilder; + + private bool disposedValue; + + private ILogger? Logger { get; set; } + + private ulong MaxObjectSize { get; set; } + + public IClientStatus? ClientStatus { get; } + + // NewPool creates connection pool using parameters. + public Pool(InitParameters options) + { + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (options.Key == null) + { + throw new FrostFsException($"Missed required parameter {nameof(options.Key)}"); + } + + var nodesParams = AdjustNodeParams(options.NodeParams); + + var cache = new SessionCache(options.SessionExpirationDuration); + + FillDefaultInitParams(options, cache); + + Key = options.Key; + PublicKey = Key.PublicKey(); + + Cache = cache; + Logger = options.Logger; + SessionTokenDuration = options.SessionExpirationDuration; + + RebalanceParams = new RebalanceParameters( + nodesParams.ToArray(), + options.HealthcheckTimeout, + options.ClientRebalanceInterval, + options.SessionExpirationDuration); + + ClientBuilder = options.ClientBuilder!; + } + + private void SetupContext(CallContext ctx) + { + if (ctx == null) + { + throw new ArgumentNullException(nameof(ctx)); + } + + ctx.Key ??= Key; + } + + // Dial establishes a connection to the servers from the FrostFS network. + // It also starts a routine that checks the health of the nodes and + // updates the weights of the nodes for balancing. + // Returns an error describing failure reason. + // + // If failed, the Pool SHOULD NOT be used. + // + // See also InitParameters.SetClientRebalanceInterval. + public async Task Dial(CallContext ctx) + { + SetupContext(ctx); + + var inner = new InnerPool[RebalanceParams.NodesParams.Length]; + + bool atLeastOneHealthy = false; + int i = 0; + foreach (var nodeParams in RebalanceParams.NodesParams) + { + var clients = new ClientWrapper[nodeParams.Weights.Count]; + + for (int j = 0; j < nodeParams.Addresses.Count; j++) + { + var client = ClientBuilder(nodeParams.Addresses[j]); + clients[j] = client; + + var error = await client.Dial(ctx).ConfigureAwait(false); + if (!string.IsNullOrEmpty(error)) + { + Logger?.LogWarning("Failed to build client. Address {Address}, {Error})", client.WrapperPrm.Address, error); + continue; + } + + try + { + var token = await InitSessionForDuration(ctx, client, RebalanceParams.SessionExpirationDuration, Key, false) + .ConfigureAwait(false); + + var key = FormCacheKey(nodeParams.Addresses[j], Key, false); + _ = Cache.Cache[key] = token; + } + catch (FrostFsException ex) + { + client.StatusMonitor.SetUnhealthy(); + Logger?.LogWarning("Failed to create frostfs session token for client. Address {Address}, {Error})", + client.WrapperPrm.Address, ex.Message); + + continue; + } + + atLeastOneHealthy = true; + } + + var sampler = new Sampler(nodeParams.Weights.ToArray()); + + inner[i] = new InnerPool(sampler, clients); + } + + if (!atLeastOneHealthy) + return "at least one node must be healthy"; + + InnerPools = inner; + + var res = await GetNetworkSettingsAsync(new PrmNetworkSettings { Context = ctx }).ConfigureAwait(false); + + MaxObjectSize = res.MaxObjectSize; + + StartRebalance(ctx); + + return null; + } + + private static IEnumerable AdjustNodeParams(NodeParam[]? nodeParams) + { + if (nodeParams == null || nodeParams.Length == 0) + { + throw new ArgumentException("No FrostFS peers configured"); + } + + Dictionary nodesParamsDict = new(nodeParams.Length); + foreach (var nodeParam in nodeParams) + { + if (!nodesParamsDict.TryGetValue(nodeParam.Priority, out var nodes)) + { + nodes = new NodesParam(nodeParam.Priority); + nodesParamsDict[nodeParam.Priority] = nodes; + } + + nodes.Addresses.Add(nodeParam.Address); + nodes.Weights.Add(nodeParam.Weight); + } + + var nodesParams = new List(nodesParamsDict.Count); + + foreach (var key in nodesParamsDict.Keys) + { + var nodes = nodesParamsDict[key]; + var newWeights = AdjustWeights([.. nodes.Weights]); + nodes.Weights.Clear(); + foreach (var weight in newWeights) + { + nodes.Weights.Add(weight); + } + + nodesParams.Add(nodes); + } + + return nodesParams.OrderBy(n => n.Priority); + } + + private static double[] AdjustWeights(double[] weights) + { + var adjusted = new double[weights.Length]; + + var sum = weights.Sum(); + + if (sum > 0) + { + for (int i = 0; i < weights.Length; i++) + { + adjusted[i] = weights[i] / sum; + } + } + + return adjusted; + } + + private static void FillDefaultInitParams(InitParameters parameters, SessionCache cache) + { + if (parameters.SessionExpirationDuration == 0) + parameters.SessionExpirationDuration = defaultSessionTokenExpirationDuration; + + if (parameters.ErrorThreshold == 0) + parameters.ErrorThreshold = defaultErrorThreshold; + + if (parameters.ClientRebalanceInterval <= 0) + parameters.ClientRebalanceInterval = defaultRebalanceInterval; + + if (parameters.GracefulCloseOnSwitchTimeout <= 0) + parameters.GracefulCloseOnSwitchTimeout = defaultGracefulCloseOnSwitchTimeout; + + if (parameters.HealthcheckTimeout <= 0) + parameters.HealthcheckTimeout = defaultHealthcheckTimeout; + + if (parameters.NodeDialTimeout <= 0) + parameters.NodeDialTimeout = defaultDialTimeout; + + if (parameters.NodeStreamTimeout <= 0) + parameters.NodeStreamTimeout = defaultStreamTimeout; + + if (cache.TokenDuration == 0) + cache.TokenDuration = defaultSessionTokenExpirationDuration; + + parameters.ClientBuilder ??= new Func((address) => + { + var wrapperPrm = new WrapperPrm + { + Address = address, + Key = parameters.Key, + Logger = parameters.Logger, + DialTimeout = parameters.NodeDialTimeout, + StreamTimeout = parameters.NodeStreamTimeout, + ErrorThreshold = parameters.ErrorThreshold, + GracefulCloseOnSwitchTimeout = parameters.GracefulCloseOnSwitchTimeout + }; + + return new ClientWrapper(wrapperPrm); + } + ); + } + + private FrostFSClient? Сonnection() + { + foreach (var pool in InnerPools!) + { + var client = pool.Connection(); + if (client != null) + { + return client.Client; + } + } + + return null; + } + + private static async Task InitSessionForDuration(CallContext ctx, ClientWrapper cw, ulong duration, ECDsa key, bool clientCut) + { + var client = cw.Client; + var networkInfo = await client!.GetNetworkSettingsAsync(new PrmNetworkSettings { Context = ctx }).ConfigureAwait(false); + + var epoch = networkInfo.Epoch; + + ulong exp = ulong.MaxValue - epoch < duration + ? ulong.MaxValue + : epoch + duration; + + var prmSessionCreate = new PrmSessionCreate(exp) { Context = ctx }; + + return await client.CreateSessionAsync(prmSessionCreate).ConfigureAwait(false); + } + + private static string FormCacheKey(string address, ECDsa key, bool clientCut) + { + var k = key.PrivateKey; + var stype = clientCut ? "client" : "server"; + + return $"{address}{stype}{k}"; + } + + public void Close() + { + CancellationTokenSource.Cancel(); + + if (InnerPools != null) + { + // close all clients + foreach (var innerPool in InnerPools) + foreach (var client in innerPool.Clients) + if (client.StatusMonitor.IsDialed()) + client.Client?.Close(); + } + } + + // startRebalance runs loop to monitor connection healthy status. + internal void StartRebalance(CallContext ctx) + { + var buffers = new double[RebalanceParams.NodesParams.Length][]; + + for (int i = 0; i < RebalanceParams.NodesParams.Length; i++) + { + var parameters = this.RebalanceParams.NodesParams[i]; + buffers[i] = new double[parameters.Weights.Count]; + + Task.Run(async () => + { + await Task.Delay((int)RebalanceParams.ClientRebalanceInterval).ConfigureAwait(false); + UpdateNodesHealth(ctx, buffers); + }); + } + } + + private void UpdateNodesHealth(CallContext ctx, double[][] buffers) + { + var tasks = new Task[InnerPools!.Length]; + + for (int i = 0; i < InnerPools.Length; i++) + { + var bufferWeights = buffers[i]; + + tasks[i] = Task.Run(() => UpdateInnerNodesHealth(ctx, i, bufferWeights)); + } + + Task.WaitAll(tasks); + } + + private async ValueTask UpdateInnerNodesHealth(CallContext ctx, int poolIndex, double[] bufferWeights) + { + if (poolIndex > InnerPools!.Length - 1) + { + return; + } + + var pool = InnerPools[poolIndex]; + + var options = RebalanceParams; + + int healthyChanged = 0; + + var tasks = new Task[pool.Clients.Length]; + + for (int j = 0; j < pool.Clients.Length; j++) + { + var client = pool.Clients[j]; + var healthy = false; + string? error = null; + var changed = false; + + try + { + // check timeout settings + changed = await client.RestartIfUnhealthy(ctx).ConfigureAwait(false); + healthy = true; + bufferWeights[j] = options.NodesParams[poolIndex].Weights[j]; + } + catch (FrostFsException e) + { + error = e.Message; + bufferWeights[j] = 0; + + Cache.DeleteByPrefix(client.StatusMonitor.Address); + } + + if (changed) + { + StringBuilder fields = new($"address {client.StatusMonitor.Address}, healthy {healthy}"); + if (string.IsNullOrEmpty(error)) + { + fields.Append($", reason {error}"); + Logger?.Log(LogLevel.Warning, "Health has changed: {Fields}", fields.ToString()); + + Interlocked.Exchange(ref healthyChanged, 1); + } + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + + if (Interlocked.CompareExchange(ref healthyChanged, -1, -1) == 1) + { + var probabilities = AdjustWeights(bufferWeights); + + lock (_lock) + { + pool.Sampler = new Sampler(probabilities); + } + } + } + } + + private bool CheckSessionTokenErr(Exception error, string address) + { + if (error == null) + { + return false; + } + + if (error is SessionNotFoundException || error is SessionExpiredException) + { + this.Cache.DeleteByPrefix(address); + return true; + } + + return false; + } + + public Statistic Statistic() + { + if (InnerPools == null) + { + throw new InvalidObjectException(nameof(Pool)); + } + + var statistics = new Statistic(); + + foreach (var inner in InnerPools) + { + int nodeIndex = 0; + int valueIndex = 0; + var nodes = new string[inner.Clients.Length]; + + lock (_lock) + { + foreach (var client in inner.Clients) + { + if (client.StatusMonitor.IsHealthy()) + { + nodes[valueIndex++] = client.StatusMonitor.Address; + } + + var node = new NodeStatistic + { + Address = client.StatusMonitor.Address, + Methods = client.StatusMonitor.MethodsStatus(), + OverallErrors = client.StatusMonitor.GetOverallErrorRate(), + CurrentErrors = client.StatusMonitor.GetCurrentErrorRate() + }; + + statistics.Nodes[nodeIndex++] = node; + + statistics.OverallErrors += node.OverallErrors; + } + + if (statistics.CurrentNodes == null || statistics.CurrentNodes.Length == 0) + { + statistics.CurrentNodes = nodes; + } + } + } + + return statistics; + } + + public async Task GetNetmapSnapshotAsync(PrmNetmapSnapshot? args = null) + { + var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); + return await client.GetNetmapSnapshotAsync(args).ConfigureAwait(false); + } + + public async Task GetNodeInfoAsync(PrmNodeInfo? args = null) + { + var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); + return await client.GetNodeInfoAsync(args).ConfigureAwait(false); + } + + public async Task GetNetworkSettingsAsync(PrmNetworkSettings? args = null) + { + var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); + return await client.GetNetworkSettingsAsync(args).ConfigureAwait(false); + } + + public async Task CreateSessionAsync(PrmSessionCreate args) + { + var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); + return await client.CreateSessionAsync(args).ConfigureAwait(false); + } + + public async Task AddChainAsync(PrmApeChainAdd args) + { + var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); + return await client.AddChainAsync(args).ConfigureAwait(false); + } + + public async Task RemoveChainAsync(PrmApeChainRemove args) + { + var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); + await client.RemoveChainAsync(args).ConfigureAwait(false); + } + + public async Task ListChainAsync(PrmApeChainList args) + { + var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); + return await client.ListChainAsync(args).ConfigureAwait(false); + } + + public async Task GetContainerAsync(PrmContainerGet args) + { + var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); + return await client.GetContainerAsync(args).ConfigureAwait(false); + } + + public IAsyncEnumerable ListContainersAsync(PrmContainerGetAll? args = null) + { + var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); + return client.ListContainersAsync(args); + } + + public async Task CreateContainerAsync(PrmContainerCreate args) + { + var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); + return await client.CreateContainerAsync(args).ConfigureAwait(false); + } + + public async Task DeleteContainerAsync(PrmContainerDelete args) + { + var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); + await client.DeleteContainerAsync(args).ConfigureAwait(false); + } + + public async Task GetObjectHeadAsync(PrmObjectHeadGet args) + { + var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); + return await client.GetObjectHeadAsync(args).ConfigureAwait(false); + } + + public async Task GetObjectAsync(PrmObjectGet args) + { + var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); + return await client.GetObjectAsync(args).ConfigureAwait(false); + } + + public async Task PutObjectAsync(PrmObjectPut args) + { + var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); + return await client.PutObjectAsync(args).ConfigureAwait(false); + } + + public async Task PutSingleObjectAsync(PrmSingleObjectPut args) + { + var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); + return await client.PutSingleObjectAsync(args).ConfigureAwait(false); + } + + public async Task DeleteObjectAsync(PrmObjectDelete args) + { + var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); + await client.DeleteObjectAsync(args).ConfigureAwait(false); + } + + public IAsyncEnumerable SearchObjectsAsync(PrmObjectSearch args) + { + var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); + return client.SearchObjectsAsync(args); + } + + public async Task GetBalanceAsync(PrmBalance? args = null) + { + var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); + return await client.GetBalanceAsync(args).ConfigureAwait(false); + } + + public bool RestartIfUnhealthy(CallContext ctx) + { + throw new NotImplementedException(); + } + + public bool IsHealthy() + { + throw new NotImplementedException(); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + Close(); + } + + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public FrostFsObjectId CalculateObjectId(FrostFsObjectHeader header, CallContext ctx) + { + throw new NotImplementedException(); + } +} diff --git a/src/FrostFS.SDK.ClientV2/Poll/RebalanceParameters.cs b/src/FrostFS.SDK.ClientV2/Poll/RebalanceParameters.cs new file mode 100644 index 0000000..b1bc9b9 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Poll/RebalanceParameters.cs @@ -0,0 +1,16 @@ +namespace FrostFS.SDK.ClientV2; + +public class RebalanceParameters( + NodesParam[] nodesParams, + ulong nodeRequestTimeout, + ulong clientRebalanceInterval, + ulong sessionExpirationDuration) +{ + public NodesParam[] NodesParams { get; set; } = nodesParams; + + public ulong NodeRequestTimeout { get; set; } = nodeRequestTimeout; + + public ulong ClientRebalanceInterval { get; set; } = clientRebalanceInterval; + + public ulong SessionExpirationDuration { get; set; } = sessionExpirationDuration; +} diff --git a/src/FrostFS.SDK.ClientV2/Poll/RequestInfo.cs b/src/FrostFS.SDK.ClientV2/Poll/RequestInfo.cs new file mode 100644 index 0000000..9eb931f --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Poll/RequestInfo.cs @@ -0,0 +1,14 @@ +using System; + +namespace FrostFS.SDK.ClientV2; + +// RequestInfo groups info about pool request. +struct RequestInfo +{ + public string Address { get; set; } + + public MethodIndex MethodIndex { get; set; } + + public TimeSpan Elapsed { get; set; } +} + diff --git a/src/FrostFS.SDK.ClientV2/Poll/Sampler.cs b/src/FrostFS.SDK.ClientV2/Poll/Sampler.cs new file mode 100644 index 0000000..0d4a1e0 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Poll/Sampler.cs @@ -0,0 +1,85 @@ +using System; + +namespace FrostFS.SDK.ClientV2; + +internal sealed class Sampler +{ + private readonly object _lock = new(); + + private Random random = new(); + + internal double[] Probabilities { get; set; } + internal int[] Alias { get; set; } + + internal Sampler(double[] probabilities) + { + var small = new WorkList(); + var large = new WorkList(); + + var n = probabilities.Length; + + // sampler.randomGenerator = rand.New(source) + Probabilities = new double[n]; + Alias = new int[n]; + + // Compute scaled probabilities. + var p = new double[n]; + + for (int i = 0; i < n; i++) + { + p[i] = probabilities[i] * n; + if (p[i] < 1) + small.Add(i); + else + large.Add(i); + } + + while (small.Length > 0 && large.Length > 0) + { + var l = small.Remove(); + var g = large.Remove(); + + Probabilities[l] = p[l]; + Alias[l] = g; + + p[g] = p[g] + p[l] - 1; + + if (p[g] < 1) + small.Add(g); + else + large.Add(g); + } + + while (large.Length > 0) + { + var g = large.Remove(); + Probabilities[g] = 1; + } + + while (small.Length > 0) + { + var l = small.Remove(); + probabilities[l] = 1; + } + } + + internal int Next() + { + var n = Alias.Length; + + int i; + double f; + lock (_lock) + { + i = random.Next(0, n - 1); + f = random.NextDouble(); + } + + if (f < Probabilities[i]) + { + return i; + } + + return Alias[i]; + } +} diff --git a/src/FrostFS.SDK.ClientV2/Poll/SessionCache.cs b/src/FrostFS.SDK.ClientV2/Poll/SessionCache.cs new file mode 100644 index 0000000..eccb0a5 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Poll/SessionCache.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections; + +namespace FrostFS.SDK.ClientV2; + +internal struct SessionCache +{ + public SessionCache(ulong sessionExpirationDuration) + { + TokenDuration = sessionExpirationDuration; + } + + internal Hashtable Cache { get; } = []; + + internal ulong CurrentEpoch { get; set; } + + internal ulong TokenDuration { get; set; } + + internal void DeleteByPrefix(string prefix) + { + foreach (var key in Cache.Keys) + { + if (((string)key).StartsWith(prefix, StringComparison.Ordinal)) + { + Cache.Remove(key); + } + } + } +} diff --git a/src/FrostFS.SDK.ClientV2/Poll/Statistic.cs b/src/FrostFS.SDK.ClientV2/Poll/Statistic.cs new file mode 100644 index 0000000..58fa72d --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Poll/Statistic.cs @@ -0,0 +1,12 @@ +using System.Collections.ObjectModel; + +namespace FrostFS.SDK.ClientV2; + +public sealed class Statistic +{ + public ulong OverallErrors { get; internal set; } + + public Collection Nodes { get; } = []; + + public string[]? CurrentNodes { get; internal set; } +} diff --git a/src/FrostFS.SDK.ClientV2/Poll/StatusSnapshot.cs b/src/FrostFS.SDK.ClientV2/Poll/StatusSnapshot.cs new file mode 100644 index 0000000..9434cb9 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Poll/StatusSnapshot.cs @@ -0,0 +1,8 @@ +namespace FrostFS.SDK.ClientV2; + +public class StatusSnapshot() +{ + public ulong AllTime { get; internal set; } + + public ulong AllRequests { get; internal set; } +} diff --git a/src/FrostFS.SDK.ClientV2/Poll/WorkList.cs b/src/FrostFS.SDK.ClientV2/Poll/WorkList.cs new file mode 100644 index 0000000..39551f4 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Poll/WorkList.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Linq; + +namespace FrostFS.SDK.ClientV2; + +internal sealed class WorkList +{ + private readonly List elements = []; + + internal int Length + { + get { return elements.Count; } + } + + internal void Add(int element) + { + elements.Add(element); + } + + internal int Remove() + { + int last = elements.LastOrDefault(); + elements.RemoveAt(elements.Count - 1); + return last; + } +} diff --git a/src/FrostFS.SDK.ClientV2/Poll/WrapperPrm.cs b/src/FrostFS.SDK.ClientV2/Poll/WrapperPrm.cs new file mode 100644 index 0000000..b389f68 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Poll/WrapperPrm.cs @@ -0,0 +1,34 @@ +using System; +using System.Security.Cryptography; + +using Grpc.Net.Client; + +using Microsoft.Extensions.Logging; + +namespace FrostFS.SDK.ClientV2; + +// wrapperPrm is params to create clientWrapper. +[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "")] +public struct WrapperPrm +{ + internal ILogger? Logger { get; set; } + + internal string Address { get; set; } + + internal ECDsa? Key { get; set; } + + internal ulong DialTimeout { get; set; } + + internal ulong StreamTimeout { get; set; } + + internal uint ErrorThreshold { get; set; } + + internal Action ResponseInfoCallback { get; set; } + + internal Action PoolRequestInfoCallback { get; set; } + + internal GrpcChannelOptions GrpcChannelOptions { get; set; } + + internal ulong GracefulCloseOnSwitchTimeout { get; set; } +} + diff --git a/src/FrostFS.SDK.ClientV2/Services/AccountingServiceProvider.cs b/src/FrostFS.SDK.ClientV2/Services/AccountingServiceProvider.cs new file mode 100644 index 0000000..94dda54 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Services/AccountingServiceProvider.cs @@ -0,0 +1,40 @@ +using System.Threading.Tasks; + +using FrostFS.Accounting; + +namespace FrostFS.SDK.ClientV2; + +internal sealed class AccountingServiceProvider : ContextAccessor +{ + private readonly AccountingService.AccountingServiceClient? _accountingServiceClient; + + internal AccountingServiceProvider( + AccountingService.AccountingServiceClient? accountingServiceClient, + EnvironmentContext context) + : base(context) + { + _accountingServiceClient = accountingServiceClient; + } + + internal async Task GetBallance(PrmBalance args) + { + var ctx = args.Context!; + + BalanceRequest request = new() + { + Body = new() + { + OwnerId = ctx.OwnerId!.OwnerID + } + }; + + request.AddMetaHeader(args.XHeaders); + request.Sign(ctx.Key!); + + var response = await _accountingServiceClient!.BalanceAsync(request, null, ctx.Deadline, ctx.CancellationToken); + + Verifier.CheckResponse(response); + + return response.Body.Balance; + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Services/Shared/ApeManagerServiceProvider.cs b/src/FrostFS.SDK.ClientV2/Services/ApeManagerServiceProvider.cs similarity index 90% rename from src/FrostFS.SDK.ClientV2/Services/Shared/ApeManagerServiceProvider.cs rename to src/FrostFS.SDK.ClientV2/Services/ApeManagerServiceProvider.cs index ea92329..1c5ab8c 100644 --- a/src/FrostFS.SDK.ClientV2/Services/Shared/ApeManagerServiceProvider.cs +++ b/src/FrostFS.SDK.ClientV2/Services/ApeManagerServiceProvider.cs @@ -3,13 +3,13 @@ using System.Threading.Tasks; using Frostfs.V2.Ape; using Frostfs.V2.Apemanager; -namespace FrostFS.SDK.ClientV2; +namespace FrostFS.SDK.ClientV2.Services; internal sealed class ApeManagerServiceProvider : ContextAccessor { private readonly APEManagerService.APEManagerServiceClient? _apeManagerServiceClient; - internal ApeManagerServiceProvider(APEManagerService.APEManagerServiceClient? apeManagerServiceClient, ClientEnvironment context) + internal ApeManagerServiceProvider(APEManagerService.APEManagerServiceClient? apeManagerServiceClient, EnvironmentContext context) : base(context) { _apeManagerServiceClient = apeManagerServiceClient; @@ -18,7 +18,7 @@ internal sealed class ApeManagerServiceProvider : ContextAccessor internal async Task AddChainAsync(PrmApeChainAdd args) { var ctx = args.Context!; - ctx.Key ??= Context.Key?.ECDsaKey; + ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; if (ctx.Key == null) throw new InvalidObjectException(nameof(ctx.Key)); @@ -45,7 +45,7 @@ internal sealed class ApeManagerServiceProvider : ContextAccessor internal async Task RemoveChainAsync(PrmApeChainRemove args) { var ctx = args.Context!; - ctx.Key ??= Context.Key?.ECDsaKey; + ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; if (ctx.Key == null) throw new InvalidObjectException(nameof(ctx.Key)); @@ -70,7 +70,7 @@ internal sealed class ApeManagerServiceProvider : ContextAccessor internal async Task ListChainAsync(PrmApeChainList args) { var ctx = args.Context!; - ctx.Key ??= Context.Key?.ECDsaKey; + ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; if (ctx.Key == null) throw new InvalidObjectException(nameof(ctx.Key)); diff --git a/src/FrostFS.SDK.ClientV2/Services/ContainerServiceProvider.cs b/src/FrostFS.SDK.ClientV2/Services/ContainerServiceProvider.cs index c42b2ba..14a3e2d 100644 --- a/src/FrostFS.SDK.ClientV2/Services/ContainerServiceProvider.cs +++ b/src/FrostFS.SDK.ClientV2/Services/ContainerServiceProvider.cs @@ -12,11 +12,11 @@ using FrostFS.Session; namespace FrostFS.SDK.ClientV2; -internal sealed class ContainerServiceProvider(ContainerService.ContainerServiceClient service, ClientEnvironment context) : ContextAccessor(context), ISessionProvider +internal sealed class ContainerServiceProvider(ContainerService.ContainerServiceClient service, EnvironmentContext envCtx) : ContextAccessor(envCtx), ISessionProvider { - readonly SessionProvider sessions = new(context); + readonly SessionProvider sessions = new(envCtx); - public async ValueTask GetOrCreateSession(ISessionToken args, Context ctx) + public async ValueTask GetOrCreateSession(ISessionToken args, CallContext ctx) { return await sessions.GetOrCreateSession(args, ctx).ConfigureAwait(false); } @@ -35,8 +35,8 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService internal async IAsyncEnumerable ListContainersAsync(PrmContainerGetAll args) { var ctx = args.Context!; - ctx.OwnerId ??= Context.Owner; - ctx.Key ??= Context.Key?.ECDsaKey; + ctx.OwnerId ??= EnvironmentContext.Owner; + ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; if (ctx.Key == null) throw new InvalidObjectException(nameof(ctx.Key)); @@ -147,7 +147,7 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService Verifier.CheckResponse(response); } - private static GetRequest GetContainerRequest(ContainerID id, NameValueCollection? xHeaders, Context ctx) + private static GetRequest GetContainerRequest(ContainerID id, NameValueCollection? xHeaders, CallContext ctx) { if (ctx.Key == null) throw new InvalidObjectException(nameof(ctx.Key)); @@ -172,7 +172,7 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService Removed } - private async Task WaitForContainer(WaitExpects expect, ContainerID id, PrmWait? waitParams, Context ctx) + private async Task WaitForContainer(WaitExpects expect, ContainerID id, PrmWait? waitParams, CallContext ctx) { var request = GetContainerRequest(id, null, ctx); diff --git a/src/FrostFS.SDK.ClientV2/Services/NetmapServiceProvider.cs b/src/FrostFS.SDK.ClientV2/Services/NetmapServiceProvider.cs index 91cca73..91645fb 100644 --- a/src/FrostFS.SDK.ClientV2/Services/NetmapServiceProvider.cs +++ b/src/FrostFS.SDK.ClientV2/Services/NetmapServiceProvider.cs @@ -12,27 +12,33 @@ internal sealed class NetmapServiceProvider : ContextAccessor { private readonly NetmapService.NetmapServiceClient netmapServiceClient; - internal NetmapServiceProvider(NetmapService.NetmapServiceClient netmapServiceClient, ClientEnvironment context) + internal NetmapServiceProvider(NetmapService.NetmapServiceClient netmapServiceClient, EnvironmentContext context) : base(context) { this.netmapServiceClient = netmapServiceClient; } - internal async Task GetNetworkSettingsAsync(Context ctx) + internal async Task GetNetworkSettingsAsync(CallContext ctx) { - if (Context.NetworkSettings != null) - return Context.NetworkSettings; + if (EnvironmentContext.NetworkSettings != null) + return EnvironmentContext.NetworkSettings; - var info = await GetNetworkInfoAsync(ctx).ConfigureAwait(false); + var response = await GetNetworkInfoAsync(ctx).ConfigureAwait(false); var settings = new NetworkSettings(); - foreach (var param in info.Body.NetworkInfo.NetworkConfig.Parameters) + var info = response.Body.NetworkInfo; + + settings.Epoch = info.CurrentEpoch; + settings.MagicNumber = info.MagicNumber; + settings.MsPerBlock = info.MsPerBlock; + + foreach (var param in info.NetworkConfig.Parameters) { SetNetworksParam(param, settings); } - Context.NetworkSettings = settings; + EnvironmentContext.NetworkSettings = settings; return settings; } @@ -40,7 +46,7 @@ internal sealed class NetmapServiceProvider : ContextAccessor internal async Task GetLocalNodeInfoAsync(PrmNodeInfo args) { var ctx = args.Context!; - ctx.Key ??= Context.Key?.ECDsaKey; + ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; if (ctx.Key == null) throw new InvalidObjectException(nameof(ctx.Key)); @@ -53,6 +59,8 @@ internal sealed class NetmapServiceProvider : ContextAccessor request.AddMetaHeader(args.XHeaders); request.Sign(ctx.Key); + + var response = await netmapServiceClient.LocalNodeInfoAsync(request, null, ctx.Deadline, ctx.CancellationToken); Verifier.CheckResponse(response); @@ -60,9 +68,9 @@ internal sealed class NetmapServiceProvider : ContextAccessor return response.Body.ToModel(); } - internal async Task GetNetworkInfoAsync(Context ctx) + internal async Task GetNetworkInfoAsync(CallContext ctx) { - ctx.Key ??= Context.Key?.ECDsaKey; + ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; if (ctx.Key == null) throw new InvalidObjectException(nameof(ctx.Key)); @@ -83,7 +91,7 @@ internal sealed class NetmapServiceProvider : ContextAccessor internal async Task GetNetmapSnapshotAsync(PrmNetmapSnapshot args) { var ctx = args.Context!; - ctx.Key ??= Context.Key?.ECDsaKey; + ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; if (ctx.Key == null) throw new InvalidObjectException(nameof(ctx.Key)); diff --git a/src/FrostFS.SDK.ClientV2/Services/ObjectServiceProvider.cs b/src/FrostFS.SDK.ClientV2/Services/ObjectServiceProvider.cs index dce9787..6ab3362 100644 --- a/src/FrostFS.SDK.ClientV2/Services/ObjectServiceProvider.cs +++ b/src/FrostFS.SDK.ClientV2/Services/ObjectServiceProvider.cs @@ -20,14 +20,14 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider private readonly SessionProvider sessions; private ObjectService.ObjectServiceClient client; - internal ObjectServiceProvider(ObjectService.ObjectServiceClient client, ClientEnvironment env) + internal ObjectServiceProvider(ObjectService.ObjectServiceClient client, EnvironmentContext env) : base(env) { - this.sessions = new(Context); + this.sessions = new(EnvironmentContext); this.client = client; } - public async ValueTask GetOrCreateSession(ISessionToken args, Context ctx) + public async ValueTask GetOrCreateSession(ISessionToken args, CallContext ctx) { return await sessions.GetOrCreateSession(args, ctx).ConfigureAwait(false); } @@ -35,7 +35,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider internal async Task GetObjectHeadAsync(PrmObjectHeadGet args) { var ctx = args.Context!; - ctx.Key ??= Context.Key?.ECDsaKey; + ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; if (ctx.Key == null) throw new InvalidObjectException(nameof(ctx.Key)); @@ -74,7 +74,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider { var ctx = args.Context!; - ctx.Key ??= Context.Key?.ECDsaKey; + ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; if (ctx.Key == null) throw new InvalidObjectException(nameof(ctx.Key)); @@ -108,7 +108,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider internal async Task DeleteObjectAsync(PrmObjectDelete args) { var ctx = args.Context!; - ctx.Key ??= Context.Key?.ECDsaKey; + ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; if (ctx.Key == null) throw new InvalidObjectException(nameof(ctx.Key)); @@ -238,7 +238,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider var ctx = args.Context!; var tokenRaw = await GetOrCreateSession(args, ctx).ConfigureAwait(false); - var token = new FrostFsSessionToken(tokenRaw.Serialize()); + var token = new FrostFsSessionToken(tokenRaw.Serialize(), tokenRaw.Body.Id.ToUuid()); args.SessionToken = token; @@ -254,7 +254,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider if (args.MaxObjectSizeCache == 0) { - var networkSettings = await Context.Client.GetNetworkSettingsAsync(new PrmNetworkSettings() { Context = ctx }) + var networkSettings = await EnvironmentContext.Client.GetNetworkSettingsAsync(new PrmNetworkSettings() { Context = ctx }) .ConfigureAwait(false); args.MaxObjectSizeCache = (int)networkSettings.MaxObjectSize; @@ -352,7 +352,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider } else { - chunkBuffer = Context.GetArrayPool(Constants.ObjectChunkSize).Rent(chunkSize); + chunkBuffer = EnvironmentContext.GetArrayPool(Constants.ObjectChunkSize).Rent(chunkSize); isRentBuffer = true; } @@ -404,7 +404,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider } } - private async Task GetUploadStream(PrmObjectPut args, Context ctx) + private async Task GetUploadStream(PrmObjectPut args, CallContext ctx) { var header = args.Header!; @@ -449,7 +449,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider return await PutObjectInit(initRequest, ctx).ConfigureAwait(false); } - private async Task GetObject(GetRequest request, Context ctx) + private async Task GetObject(GetRequest request, CallContext ctx) { var reader = GetObjectInit(request, ctx); @@ -461,7 +461,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider return modelObject; } - private ObjectReader GetObjectInit(GetRequest initRequest, Context ctx) + private ObjectReader GetObjectInit(GetRequest initRequest, CallContext ctx) { if (initRequest is null) throw new ArgumentNullException(nameof(initRequest)); @@ -471,7 +471,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider return new ObjectReader(call); } - private async Task PutObjectInit(PutRequest initRequest, Context ctx) + private async Task PutObjectInit(PutRequest initRequest, CallContext ctx) { if (initRequest is null) { @@ -485,7 +485,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider return new ObjectStreamer(call); } - private async IAsyncEnumerable SearchObjects(SearchRequest request, Context ctx) + private async IAsyncEnumerable SearchObjects(SearchRequest request, CallContext ctx) { using var stream = GetSearchReader(request, ctx); @@ -503,7 +503,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider } } - private SearchReader GetSearchReader(SearchRequest initRequest, Context ctx) + private SearchReader GetSearchReader(SearchRequest initRequest, CallContext ctx) { if (initRequest is null) { diff --git a/src/FrostFS.SDK.ClientV2/Services/SessionServiceProvider.cs b/src/FrostFS.SDK.ClientV2/Services/SessionServiceProvider.cs index de22165..1dfe53f 100644 --- a/src/FrostFS.SDK.ClientV2/Services/SessionServiceProvider.cs +++ b/src/FrostFS.SDK.ClientV2/Services/SessionServiceProvider.cs @@ -10,7 +10,7 @@ internal sealed class SessionServiceProvider : ContextAccessor { private readonly SessionService.SessionServiceClient? _sessionServiceClient; - internal SessionServiceProvider(SessionService.SessionServiceClient? sessionServiceClient, ClientEnvironment context) + internal SessionServiceProvider(SessionService.SessionServiceClient? sessionServiceClient, EnvironmentContext context) : base(context) { _sessionServiceClient = sessionServiceClient; @@ -20,7 +20,7 @@ internal sealed class SessionServiceProvider : ContextAccessor { var ctx = args.Context!; - ctx.OwnerId ??= Context.Owner; + ctx.OwnerId ??= EnvironmentContext.Owner; var request = new CreateRequest { @@ -37,7 +37,7 @@ internal sealed class SessionServiceProvider : ContextAccessor return await CreateSession(request, args.Context!).ConfigureAwait(false); } - internal async Task CreateSession(CreateRequest request, Context ctx) + internal async Task CreateSession(CreateRequest request, CallContext ctx) { var response = await _sessionServiceClient!.CreateAsync(request, null, ctx.Deadline, ctx.CancellationToken); diff --git a/src/FrostFS.SDK.ClientV2/Services/Shared/ContextAccessor.cs b/src/FrostFS.SDK.ClientV2/Services/Shared/ContextAccessor.cs index 05797de..3fb7674 100644 --- a/src/FrostFS.SDK.ClientV2/Services/Shared/ContextAccessor.cs +++ b/src/FrostFS.SDK.ClientV2/Services/Shared/ContextAccessor.cs @@ -1,6 +1,6 @@ namespace FrostFS.SDK.ClientV2; -internal class ContextAccessor(ClientEnvironment context) +internal class ContextAccessor(EnvironmentContext context) { - protected ClientEnvironment Context { get; set; } = context; + protected EnvironmentContext EnvironmentContext { get; set; } = context; } diff --git a/src/FrostFS.SDK.ClientV2/Services/Shared/SessionProvider.cs b/src/FrostFS.SDK.ClientV2/Services/Shared/SessionProvider.cs index 8d0d1f5..51fe337 100644 --- a/src/FrostFS.SDK.ClientV2/Services/Shared/SessionProvider.cs +++ b/src/FrostFS.SDK.ClientV2/Services/Shared/SessionProvider.cs @@ -4,16 +4,17 @@ namespace FrostFS.SDK.ClientV2; internal interface ISessionProvider { - ValueTask GetOrCreateSession(ISessionToken args, Context ctx); + ValueTask GetOrCreateSession(ISessionToken args, CallContext ctx); } -internal sealed class SessionProvider(ClientEnvironment env) +internal sealed class SessionProvider(EnvironmentContext envCtx) { - public async ValueTask GetOrCreateSession(ISessionToken args, Context ctx) + // TODO: implement cache for session in the next iteration + public async ValueTask GetOrCreateSession(ISessionToken args, CallContext ctx) { if (args.SessionToken is null) { - return await env.Client.CreateSessionInternalAsync(new PrmSessionCreate(uint.MaxValue) { Context = ctx }) + return await envCtx.Client.CreateSessionInternalAsync(new PrmSessionCreate(uint.MaxValue) { Context = ctx }) .ConfigureAwait(false); } diff --git a/src/FrostFS.SDK.ClientV2/Tools/ClientEnvironment.cs b/src/FrostFS.SDK.ClientV2/Tools/EnvironmentContext.cs similarity index 87% rename from src/FrostFS.SDK.ClientV2/Tools/ClientEnvironment.cs rename to src/FrostFS.SDK.ClientV2/Tools/EnvironmentContext.cs index 11023e7..d0596c9 100644 --- a/src/FrostFS.SDK.ClientV2/Tools/ClientEnvironment.cs +++ b/src/FrostFS.SDK.ClientV2/Tools/EnvironmentContext.cs @@ -6,7 +6,7 @@ using Grpc.Net.Client; namespace FrostFS.SDK.ClientV2; -public class ClientEnvironment(FrostFSClient client, ECDsa? key, FrostFsOwner? owner, GrpcChannel channel, FrostFsVersion version) : IDisposable +public class EnvironmentContext(FrostFSClient client, ECDsa? key, FrostFsOwner? owner, GrpcChannel channel, FrostFsVersion version) : IDisposable { private ArrayPool? _arrayPool; diff --git a/src/FrostFS.SDK.ClientV2/Tools/NetworkSettings.cs b/src/FrostFS.SDK.ClientV2/Tools/NetworkSettings.cs index d06ce03..524f358 100644 --- a/src/FrostFS.SDK.ClientV2/Tools/NetworkSettings.cs +++ b/src/FrostFS.SDK.ClientV2/Tools/NetworkSettings.cs @@ -4,6 +4,10 @@ namespace FrostFS.SDK.ClientV2; public class NetworkSettings { + public ulong Epoch { get; internal set; } + public ulong MagicNumber { get; internal set; } + public long MsPerBlock { get; internal set; } + public ulong AuditFee { get; internal set; } public ulong BasicIncomeRate { get; internal set; } public ulong ContainerFee { get; internal set; } diff --git a/src/FrostFS.SDK.ClientV2/Tools/ObjectTools.cs b/src/FrostFS.SDK.ClientV2/Tools/ObjectTools.cs index 0303626..f4d659c 100644 --- a/src/FrostFS.SDK.ClientV2/Tools/ObjectTools.cs +++ b/src/FrostFS.SDK.ClientV2/Tools/ObjectTools.cs @@ -11,7 +11,7 @@ namespace FrostFS.SDK.ClientV2; internal static class ObjectTools { - internal static FrostFsObjectId CalculateObjectId(FrostFsObjectHeader header, Context ctx) + internal static FrostFsObjectId CalculateObjectId(FrostFsObjectHeader header, CallContext ctx) { var grpcHeader = CreateHeader(header, [], ctx); @@ -21,7 +21,7 @@ internal static class ObjectTools return new ObjectID { Value = grpcHeader.Sha256() }.ToModel(); } - internal static Object.Object CreateObject(FrostFsObject @object, Context ctx) + internal static Object.Object CreateObject(FrostFsObject @object, CallContext ctx) { @object.Header.OwnerId ??= ctx.OwnerId; @object.Header.Version ??= ctx.Version; @@ -53,7 +53,7 @@ internal static class ObjectTools return obj; } - internal static void SetSplitValues(Header grpcHeader, FrostFsSplit split, Context ctx) + internal static void SetSplitValues(Header grpcHeader, FrostFsSplit split, CallContext ctx) { if (split == null) return; @@ -85,7 +85,7 @@ internal static class ObjectTools grpcHeader.Split.Previous = split.Previous?.ToMessage(); } - internal static Header CreateHeader(FrostFsObjectHeader header, byte[]? payload, Context ctx) + internal static Header CreateHeader(FrostFsObjectHeader header, byte[]? payload, CallContext ctx) { header.OwnerId ??= ctx.OwnerId; header.Version ??= ctx.Version; diff --git a/src/FrostFS.SDK.ClientV2/Tools/RequestSigner.cs b/src/FrostFS.SDK.ClientV2/Tools/RequestSigner.cs index 13d87b0..f1006a4 100644 --- a/src/FrostFS.SDK.ClientV2/Tools/RequestSigner.cs +++ b/src/FrostFS.SDK.ClientV2/Tools/RequestSigner.cs @@ -76,6 +76,11 @@ public static class RequestSigner public static byte[] SignData(this ECDsa key, byte[] data) { + if (key is null) + { + throw new ArgumentNullException(nameof(key)); + } + var hash = new byte[65]; hash[0] = 0x04; diff --git a/src/FrostFS.SDK.ProtosV2/accounting/Extension.Message.cs b/src/FrostFS.SDK.ProtosV2/accounting/Extension.Message.cs new file mode 100644 index 0000000..af77424 --- /dev/null +++ b/src/FrostFS.SDK.ProtosV2/accounting/Extension.Message.cs @@ -0,0 +1,62 @@ +using FrostFS.SDK.ProtosV2.Interfaces; +using FrostFS.Session; + +using Google.Protobuf; + +namespace FrostFS.Accounting; + +public partial class BalanceRequest : IRequest +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} + +public partial class BalanceResponse : IResponse +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/ContainerTest.cs b/src/FrostFS.SDK.Tests/ContainerTest.cs index e58e18c..8f4e868 100644 --- a/src/FrostFS.SDK.Tests/ContainerTest.cs +++ b/src/FrostFS.SDK.Tests/ContainerTest.cs @@ -1,49 +1,14 @@ +using System.Diagnostics.CodeAnalysis; + using FrostFS.SDK.ClientV2; -using FrostFS.SDK.ClientV2.Interfaces; using FrostFS.SDK.ClientV2.Mappers.GRPC; using FrostFS.SDK.Cryptography; using Google.Protobuf; -using Microsoft.Extensions.Options; - namespace FrostFS.SDK.Tests; -public abstract class ContainerTestsBase -{ - protected readonly string key = "KwHDAJ66o8FoLBjVbjP2sWBmgBMGjt7Vv4boA7xQrBoAYBE397Aq"; - - protected IOptions Settings { get; set; } - protected ContainerMocker Mocker { get; set; } - - protected ContainerTestsBase() - { - Settings = Options.Create(new SingleOwnerClientSettings - { - Key = key, - Host = "http://localhost:8080" - }); - - Mocker = new ContainerMocker(this.key) - { - PlacementPolicy = new FrostFsPlacementPolicy(true, new FrostFsReplica(1)), - Version = new FrostFsVersion(2, 13), - ContainerGuid = Guid.NewGuid() - }; - } - - protected IFrostFSClient GetClient() - { - return ClientV2.FrostFSClient.GetTestInstance( - Settings, - null, - new NetworkMocker(this.key).GetMock().Object, - new SessionMocker(this.key).GetMock().Object, - Mocker.GetMock().Object, - new ObjectMocker(this.key).GetMock().Object); - } -} - +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")] public class ContainerTest : ContainerTestsBase { [Fact] diff --git a/src/FrostFS.SDK.Tests/ContainerTestsBase.cs b/src/FrostFS.SDK.Tests/ContainerTestsBase.cs new file mode 100644 index 0000000..f9bfc9a --- /dev/null +++ b/src/FrostFS.SDK.Tests/ContainerTestsBase.cs @@ -0,0 +1,40 @@ +using FrostFS.SDK.ClientV2.Interfaces; + +using Microsoft.Extensions.Options; + +namespace FrostFS.SDK.Tests; + +public abstract class ContainerTestsBase +{ + internal readonly string key = "KwHDAJ66o8FoLBjVbjP2sWBmgBMGjt7Vv4boA7xQrBoAYBE397Aq"; + + protected IOptions Settings { get; set; } + protected ContainerMocker Mocker { get; set; } + + protected ContainerTestsBase() + { + Settings = Options.Create(new SingleOwnerClientSettings + { + Key = key, + Host = "http://localhost:8080" + }); + + Mocker = new ContainerMocker(this.key) + { + PlacementPolicy = new FrostFsPlacementPolicy(true, new FrostFsReplica(1)), + Version = new FrostFsVersion(2, 13), + ContainerGuid = Guid.NewGuid() + }; + } + + protected IFrostFSClient GetClient() + { + return ClientV2.FrostFSClient.GetTestInstance( + Settings, + null, + new NetworkMocker(this.key).GetMock().Object, + new SessionMocker(this.key).GetMock().Object, + Mocker.GetMock().Object, + new ObjectMocker(this.key).GetMock().Object); + } +} diff --git a/src/FrostFS.SDK.Tests/GlobalSuppressions.cs b/src/FrostFS.SDK.Tests/GlobalSuppressions.cs new file mode 100644 index 0000000..d1eb84b --- /dev/null +++ b/src/FrostFS.SDK.Tests/GlobalSuppressions.cs @@ -0,0 +1,6 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + + diff --git a/src/FrostFS.SDK.Tests/MetricsInterceptor.cs b/src/FrostFS.SDK.Tests/MetricsInterceptor.cs index 958a73e..9f5d824 100644 --- a/src/FrostFS.SDK.Tests/MetricsInterceptor.cs +++ b/src/FrostFS.SDK.Tests/MetricsInterceptor.cs @@ -12,7 +12,9 @@ public class MetricsInterceptor() : Interceptor ClientInterceptorContext context, AsyncUnaryCallContinuation continuation) { - var call = continuation(request, context); + ArgumentNullException.ThrowIfNull(continuation); + + using var call = continuation(request, context); return new AsyncUnaryCall( HandleUnaryResponse(call), @@ -27,7 +29,7 @@ public class MetricsInterceptor() : Interceptor var watch = new Stopwatch(); watch.Start(); - var response = await call.ResponseAsync; + var response = await call.ResponseAsync.ConfigureAwait(false); watch.Stop(); diff --git a/src/FrostFS.SDK.Tests/Mocks/AsyncStreamReaderMock.cs b/src/FrostFS.SDK.Tests/Mocks/AsyncStreamReaderMock.cs index 22172f2..63e3d64 100644 --- a/src/FrostFS.SDK.Tests/Mocks/AsyncStreamReaderMock.cs +++ b/src/FrostFS.SDK.Tests/Mocks/AsyncStreamReaderMock.cs @@ -26,8 +26,11 @@ public class AsyncStreamReaderMock(string key, FrostFsObjectHeader objectHeader) OwnerId = objectHeader.OwnerId!.ToMessage() }; - foreach (var attr in objectHeader.Attributes) - header.Attributes.Add(attr.ToMessage()); + if (objectHeader.Attributes != null) + { + foreach (var attr in objectHeader.Attributes) + header.Attributes.Add(attr.ToMessage()); + } var response = new GetResponse { diff --git a/src/FrostFS.SDK.Tests/Mocks/ClientStreamWriter.cs b/src/FrostFS.SDK.Tests/Mocks/ClientStreamWriter.cs index 9836d37..f386468 100644 --- a/src/FrostFS.SDK.Tests/Mocks/ClientStreamWriter.cs +++ b/src/FrostFS.SDK.Tests/Mocks/ClientStreamWriter.cs @@ -1,3 +1,5 @@ +using System.Collections.ObjectModel; + using FrostFS.SDK.ProtosV2.Interfaces; using Grpc.Core; @@ -6,13 +8,15 @@ namespace FrostFS.SDK.Tests; public class ClientStreamWriter : IClientStreamWriter { - public List Messages { get; set; } = []; + private WriteOptions? _options; + + public Collection Messages { get; } = []; public bool CompletedTask { get; private set; } public WriteOptions? WriteOptions { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); + get => _options; + set => _options = value; } public Task CompleteAsync() diff --git a/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/ContainerServiceBase.cs b/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/ContainerServiceBase.cs index ba34cde..e727cfb 100644 --- a/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/ContainerServiceBase.cs +++ b/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/ContainerServiceBase.cs @@ -25,7 +25,10 @@ public abstract class ServiceBase(string key) public static FrostFsVersion DefaultVersion { get; } = new(2, 13); public static FrostFsPlacementPolicy DefaultPlacementPolicy { get; } = new FrostFsPlacementPolicy(true, new FrostFsReplica(1)); - public Metadata Metadata { get; protected set; } +#pragma warning disable CA2227 // this is specific object, should be treated as is + public Metadata? Metadata { get; set; } +#pragma warning restore CA2227 + public DateTime? DateTime { get; protected set; } public CancellationToken CancellationToken { get; protected set; } @@ -35,6 +38,8 @@ public abstract class ServiceBase(string key) protected ResponseVerificationHeader GetResponseVerificationHeader(IResponse response) { + ArgumentNullException.ThrowIfNull(response); + var verifyHeader = new ResponseVerificationHeader { MetaSignature = new Refs.Signature diff --git a/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/GetContainerMock.cs b/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/GetContainerMock.cs index d77322a..aa4d048 100644 --- a/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/GetContainerMock.cs +++ b/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/GetContainerMock.cs @@ -1,3 +1,5 @@ +using System.Collections.ObjectModel; + using FrostFS.Container; using FrostFS.Refs; using FrostFS.SDK.ClientV2; @@ -41,7 +43,7 @@ public class ContainerMocker(string key) : ContainerServiceBase(key) putResponse.VerifyHeader = GetResponseVerificationHeader(putResponse); var metadata = new Metadata(); - var putContainerResponse = new AsyncUnaryCall( + using var putContainerResponse = new AsyncUnaryCall( Task.FromResult(putResponse), Task.FromResult(metadata), () => new Grpc.Core.Status(StatusCode.OK, string.Empty), @@ -180,7 +182,7 @@ public class ContainerMocker(string key) : ContainerServiceBase(key) public bool ReturnContainerRemoved { get; set; } - public List ContainerIds { get; set; } = []; + public Collection ContainerIds { get; } = []; - public List> Requests { get; set; } = []; + public Collection> Requests { get; } = []; } diff --git a/src/FrostFS.SDK.Tests/Mocks/NetworkMocker.cs b/src/FrostFS.SDK.Tests/Mocks/NetworkMocker.cs index 72b3aab..003d7ac 100644 --- a/src/FrostFS.SDK.Tests/Mocks/NetworkMocker.cs +++ b/src/FrostFS.SDK.Tests/Mocks/NetworkMocker.cs @@ -25,18 +25,17 @@ public class NetworkMocker(string key) : ServiceBase(key) "MaintenanceModeAllowed" ]; - public Dictionary? Parameters { get; set; } + public Dictionary Parameters { get; } = []; - public LocalNodeInfoResponse NodeInfoResponse { get; set; } + public LocalNodeInfoResponse? NodeInfoResponse { get; set; } - public LocalNodeInfoRequest LocalNodeInfoRequest { get; set; } + public LocalNodeInfoRequest? LocalNodeInfoRequest { get; set; } - public NetworkInfoRequest NetworkInfoRequest { get; set; } + public NetworkInfoRequest? NetworkInfoRequest { get; set; } - public NetmapSnapshotResponse NetmapSnapshotResponse { get; set; } - - public NetmapSnapshotRequest NetmapSnapshotRequest { get; set; } + public NetmapSnapshotResponse? NetmapSnapshotResponse { get; set; } + public NetmapSnapshotRequest? NetmapSnapshotRequest { get; set; } public Mock GetMock() { diff --git a/src/FrostFS.SDK.Tests/Mocks/ObjectMock.cs b/src/FrostFS.SDK.Tests/Mocks/ObjectMock.cs index 1dc9152..d0b900d 100644 --- a/src/FrostFS.SDK.Tests/Mocks/ObjectMock.cs +++ b/src/FrostFS.SDK.Tests/Mocks/ObjectMock.cs @@ -1,3 +1,4 @@ +using System.Collections.ObjectModel; using System.Security.Cryptography; using FrostFS.Object; @@ -92,7 +93,7 @@ public class ObjectMocker(string key) : ObjectServiceBase(key) } - if (ResultObjectIds != null) + if (ResultObjectIds != null && ResultObjectIds.Count > 0) { PutResponse putResponse = new() { @@ -197,14 +198,14 @@ public class ObjectMocker(string key) : ObjectServiceBase(key) public Header? HeadResponse { get; set; } - public List? ResultObjectIds { get; set; } + public Collection? ResultObjectIds { get; } = []; public ClientStreamWriter? ClientStreamWriter { get; private set; } = new(); - public List PutSingleRequests { get; private set; } = []; + public Collection PutSingleRequests { get; private set; } = []; - public List DeleteRequests { get; private set; } = []; + public Collection DeleteRequests { get; private set; } = []; - public List HeadRequests { get; private set; } = []; + public Collection HeadRequests { get; private set; } = []; } diff --git a/src/FrostFS.SDK.Tests/Mocks/SessionMock.cs b/src/FrostFS.SDK.Tests/Mocks/SessionMock.cs index 359f10b..88ed60d 100644 --- a/src/FrostFS.SDK.Tests/Mocks/SessionMock.cs +++ b/src/FrostFS.SDK.Tests/Mocks/SessionMock.cs @@ -8,13 +8,14 @@ using Moq; namespace FrostFS.SDK.Tests; +[System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "No secure purpose")] public class SessionMocker(string key) : ServiceBase(key) { public byte[]? SessionId { get; set; } public byte[]? SessionKey { get; set; } - public CreateRequest CreateSessionRequest { get; private set; } + public CreateRequest? CreateSessionRequest { get; private set; } public Mock GetMock() { @@ -24,7 +25,7 @@ public class SessionMocker(string key) : ServiceBase(key) if (SessionId == null) { - SessionId = new byte[32]; + SessionId = new byte[16]; rand.NextBytes(SessionId); } diff --git a/src/FrostFS.SDK.Tests/NetworkTest.cs b/src/FrostFS.SDK.Tests/NetworkTest.cs index 0079d82..f09cafb 100644 --- a/src/FrostFS.SDK.Tests/NetworkTest.cs +++ b/src/FrostFS.SDK.Tests/NetworkTest.cs @@ -1,55 +1,13 @@ -using System.Security.Cryptography; +using System.Diagnostics.CodeAnalysis; using FrostFS.Netmap; using FrostFS.SDK.ClientV2; -using FrostFS.SDK.ClientV2.Interfaces; -using FrostFS.SDK.ClientV2; -using FrostFS.SDK.Cryptography; using Google.Protobuf; -using Microsoft.Extensions.Options; - namespace FrostFS.SDK.Tests; -public abstract class NetworkTestsBase -{ - protected readonly string key = "KwHDAJ66o8FoLBjVbjP2sWBmgBMGjt7Vv4boA7xQrBoAYBE397Aq"; - - protected IOptions Settings { get; set; } - - protected FrostFsVersion Version { get; set; } = new FrostFsVersion(2, 13); - - protected ECDsa ECDsaKey { get; set; } - protected FrostFsOwner OwnerId { get; set; } - protected NetworkMocker Mocker { get; set; } - - protected NetworkTestsBase() - { - Settings = Options.Create(new SingleOwnerClientSettings - { - Key = key, - Host = "http://localhost:8080" - }); - - ECDsaKey = key.LoadWif(); - OwnerId = FrostFsOwner.FromKey(ECDsaKey); - - Mocker = new NetworkMocker(this.key); - } - - protected IFrostFSClient GetClient() - { - return ClientV2.FrostFSClient.GetTestInstance( - Settings, - null, - Mocker.GetMock().Object, - new SessionMocker(this.key).GetMock().Object, - new ContainerMocker(this.key).GetMock().Object, - new ObjectMocker(this.key).GetMock().Object); - } -} - +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")] public class NetworkTest : NetworkTestsBase { [Theory] @@ -57,27 +15,24 @@ public class NetworkTest : NetworkTestsBase [InlineData(true)] public async void NetworkSettingsTest(bool useContext) { - Mocker.Parameters = new Dictionary - { - { "AuditFee", [1] }, - { "BasicIncomeRate", [2] }, - { "ContainerFee", [3] }, - { "ContainerAliasFee", [4] }, - { "EpochDuration", [5] }, - { "InnerRingCandidateFee", [6] }, - { "MaxECDataCount", [7] }, - { "MaxECParityCount", [8] }, - { "MaxObjectSize", [9] }, - { "WithdrawFee", [10] }, - { "HomomorphicHashingDisabled", [1] }, - { "MaintenanceModeAllowed", [1] }, - }; + Mocker.Parameters.Add("AuditFee", [1]); + Mocker.Parameters.Add("BasicIncomeRate", [2]); + Mocker.Parameters.Add("ContainerFee", [3]); + Mocker.Parameters.Add("ContainerAliasFee", [4]); + Mocker.Parameters.Add("EpochDuration", [5]); + Mocker.Parameters.Add("InnerRingCandidateFee", [6]); + Mocker.Parameters.Add("MaxECDataCount", [7]); + Mocker.Parameters.Add("MaxECParityCount", [8]); + Mocker.Parameters.Add("MaxObjectSize", [9]); + Mocker.Parameters.Add("WithdrawFee", [10]); + Mocker.Parameters.Add("HomomorphicHashingDisabled", [1]); + Mocker.Parameters.Add("MaintenanceModeAllowed", [1]); var param = new PrmNetworkSettings(); if (useContext) { - param.Context = new Context + param.Context = new CallContext { CancellationToken = Mocker.CancellationTokenSource.Token, Timeout = TimeSpan.FromSeconds(20), @@ -119,6 +74,7 @@ public class NetworkTest : NetworkTestsBase } else { + Assert.NotNull(Mocker.NetworkInfoRequest); Assert.Empty(Mocker.NetworkInfoRequest.MetaHeader.XHeaders); Assert.Null(Mocker.DateTime); } @@ -127,6 +83,7 @@ public class NetworkTest : NetworkTestsBase [Theory] [InlineData(false)] [InlineData(true)] + public async void NetmapSnapshotTest(bool useContext) { var body = new NetmapSnapshotResponse.Types.Body @@ -164,7 +121,7 @@ public class NetworkTest : NetworkTestsBase if (useContext) { param.XHeaders.Add("headerKey1", "headerValue1"); - param.Context = new Context + param.Context = new CallContext { CancellationToken = Mocker.CancellationTokenSource.Token, Timeout = TimeSpan.FromSeconds(20), @@ -210,6 +167,7 @@ public class NetworkTest : NetworkTestsBase if (useContext) { + Assert.NotNull(Mocker.NetmapSnapshotRequest); Assert.Single(Mocker.NetmapSnapshotRequest.MetaHeader.XHeaders); Assert.Equal(param.XHeaders.Keys[0], Mocker.NetmapSnapshotRequest.MetaHeader.XHeaders.First().Key); Assert.Equal(param.XHeaders[param.XHeaders.Keys[0]], Mocker.NetmapSnapshotRequest.MetaHeader.XHeaders.First().Value); @@ -222,6 +180,7 @@ public class NetworkTest : NetworkTestsBase } else { + Assert.NotNull(Mocker.NetmapSnapshotRequest); Assert.Empty(Mocker.NetmapSnapshotRequest.MetaHeader.XHeaders); Assert.Null(Mocker.DateTime); } @@ -254,7 +213,7 @@ public class NetworkTest : NetworkTestsBase if (useContext) { param.XHeaders.Add("headerKey1", "headerValue1"); - param.Context = new Context + param.Context = new CallContext { CancellationToken = Mocker.CancellationTokenSource.Token, Timeout = TimeSpan.FromSeconds(20), @@ -282,6 +241,7 @@ public class NetworkTest : NetworkTestsBase Assert.Equal("value1", result.Attributes["key1"]); Assert.Equal("value2", result.Attributes["key2"]); + Assert.NotNull(Mocker.LocalNodeInfoRequest); if (useContext) { Assert.Single(Mocker.LocalNodeInfoRequest.MetaHeader.XHeaders); diff --git a/src/FrostFS.SDK.Tests/NetworkTestsBase.cs b/src/FrostFS.SDK.Tests/NetworkTestsBase.cs new file mode 100644 index 0000000..071b425 --- /dev/null +++ b/src/FrostFS.SDK.Tests/NetworkTestsBase.cs @@ -0,0 +1,48 @@ +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; + +using FrostFS.SDK.ClientV2.Interfaces; +using FrostFS.SDK.Cryptography; + +using Microsoft.Extensions.Options; + +namespace FrostFS.SDK.Tests; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")] +public abstract class NetworkTestsBase +{ + internal readonly string key = "KwHDAJ66o8FoLBjVbjP2sWBmgBMGjt7Vv4boA7xQrBoAYBE397Aq"; + + protected IOptions Settings { get; set; } + + protected FrostFsVersion Version { get; set; } = new FrostFsVersion(2, 13); + + protected ECDsa ECDsaKey { get; set; } + protected FrostFsOwner OwnerId { get; set; } + protected NetworkMocker Mocker { get; set; } + + protected NetworkTestsBase() + { + Settings = Options.Create(new SingleOwnerClientSettings + { + Key = key, + Host = "http://localhost:8080" + }); + + ECDsaKey = key.LoadWif(); + OwnerId = FrostFsOwner.FromKey(ECDsaKey); + + Mocker = new NetworkMocker(this.key); + } + + protected IFrostFSClient GetClient() + { + return ClientV2.FrostFSClient.GetTestInstance( + Settings, + null, + Mocker.GetMock().Object, + new SessionMocker(this.key).GetMock().Object, + new ContainerMocker(this.key).GetMock().Object, + new ObjectMocker(this.key).GetMock().Object); + } +} diff --git a/src/FrostFS.SDK.Tests/ObjectTest.cs b/src/FrostFS.SDK.Tests/ObjectTest.cs index af58346..07fa8e9 100644 --- a/src/FrostFS.SDK.Tests/ObjectTest.cs +++ b/src/FrostFS.SDK.Tests/ObjectTest.cs @@ -1,71 +1,19 @@ +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Security.Cryptography; using System.Text; using FrostFS.Refs; using FrostFS.SDK.ClientV2; -using FrostFS.SDK.ClientV2.Interfaces; using FrostFS.SDK.ClientV2.Mappers.GRPC; -using FrostFS.SDK.ClientV2; using FrostFS.SDK.Cryptography; using Google.Protobuf; -using Microsoft.Extensions.Options; - namespace FrostFS.SDK.Tests; -public abstract class ObjectTestsBase -{ - protected static readonly string key = "KwHDAJ66o8FoLBjVbjP2sWBmgBMGjt7Vv4boA7xQrBoAYBE397Aq"; - - protected IOptions Settings { get; set; } - protected FrostFsContainerId ContainerId { get; set; } - - protected NetworkMocker NetworkMocker { get; set; } = new NetworkMocker(key); - protected SessionMocker SessionMocker { get; set; } = new SessionMocker(key); - protected ContainerMocker ContainerMocker { get; set; } = new ContainerMocker(key); - protected ObjectMocker Mocker { get; set; } - - protected ObjectTestsBase() - { - var ecdsaKey = key.LoadWif(); - - Settings = Options.Create(new SingleOwnerClientSettings - { - Key = key, - Host = "http://localhost:8080" - }); - - Mocker = new ObjectMocker(key) - { - PlacementPolicy = new FrostFsPlacementPolicy(true, new FrostFsReplica(1)), - Version = new FrostFsVersion(2, 13), - ContainerGuid = Guid.NewGuid() - }; - - ContainerId = new FrostFsContainerId(Base58.Encode(Mocker.ContainerGuid.ToBytes())); - - Mocker.ObjectHeader = new( - ContainerId, - FrostFsObjectType.Regular, - [new FrostFsAttributePair("k", "v")], - null, - FrostFsOwner.FromKey(ecdsaKey), - new FrostFsVersion(2, 13)); - } - - protected IFrostFSClient GetClient() - { - return FrostFSClient.GetTestInstance( - Settings, - null, - NetworkMocker.GetMock().Object, - SessionMocker.GetMock().Object, - ContainerMocker.GetMock().Object, - Mocker.GetMock().Object); - } -} - +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")] +[SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "No secure purpose")] public class ObjectTest : ObjectTestsBase { [Fact] @@ -75,7 +23,7 @@ public class ObjectTest : ObjectTestsBase var ecdsaKey = key.LoadWif(); - var ctx = new Context + var ctx = new CallContext { Key = ecdsaKey, OwnerId = FrostFsOwner.FromKey(ecdsaKey), @@ -88,18 +36,21 @@ public class ObjectTest : ObjectTestsBase Assert.NotNull(result); - Assert.Equal(Mocker.ObjectHeader!.ContainerId.GetValue(), result.Header.ContainerId.GetValue()); - Assert.Equal(Mocker.ObjectHeader!.OwnerId!.Value, result.Header.OwnerId!.Value); + Assert.NotNull(Mocker.ObjectHeader); + + Assert.Equal(Mocker.ObjectHeader.ContainerId.GetValue(), result.Header.ContainerId.GetValue()); + Assert.Equal(Mocker.ObjectHeader.OwnerId!.Value, result.Header.OwnerId!.Value); Assert.Equal(Mocker.ObjectHeader.PayloadLength, result.Header.PayloadLength); + Assert.NotNull(result.Header.Attributes); Assert.Single(result.Header.Attributes); - Assert.Equal(Mocker.ObjectHeader.Attributes[0].Key, result.Header.Attributes[0].Key); - Assert.Equal(Mocker.ObjectHeader.Attributes[0].Value, result.Header.Attributes[0].Value); + Assert.Equal(Mocker.ObjectHeader.Attributes![0].Key, result.Header.Attributes[0].Key); + Assert.Equal(Mocker.ObjectHeader.Attributes![0].Value, result.Header.Attributes[0].Value); } [Fact] public async void PutObjectTest() { - Mocker.ResultObjectIds = new([SHA256.HashData([])]); + Mocker.ResultObjectIds.Add(SHA256.HashData([])); Random rnd = new(); var bytes = new byte[1024]; @@ -134,7 +85,7 @@ public class ObjectTest : ObjectTestsBase [Fact] public async void ClientCutTest() { - NetworkMocker.Parameters = new Dictionary() { { "MaxObjectSize", [0x0, 0xa] } }; + NetworkMocker.Parameters.Add("MaxObjectSize", [0x0, 0xa]); var blockSize = 2560; byte[] bytes = File.ReadAllBytes(@".\..\..\..\TestData\cat.jpg"); @@ -150,17 +101,19 @@ public class ObjectTest : ObjectTestsBase Random rnd = new(); - List objIds = new([new byte[32], new byte[32], new byte[32]]); + Collection objIds = new([new byte[32], new byte[32], new byte[32]]); rnd.NextBytes(objIds.ElementAt(0)); rnd.NextBytes(objIds.ElementAt(1)); rnd.NextBytes(objIds.ElementAt(2)); - Mocker.ResultObjectIds = objIds; + foreach (var objId in objIds) + Mocker.ResultObjectIds.Add(objId); var result = await GetClient().PutObjectAsync(param); var singleObjects = Mocker.PutSingleRequests.ToArray(); + Assert.NotNull(Mocker.ClientStreamWriter?.Messages); var streamObjects = Mocker.ClientStreamWriter.Messages.ToArray(); Assert.Single(singleObjects); @@ -262,6 +215,7 @@ public class ObjectTest : ObjectTestsBase Assert.Equal(FrostFsObjectType.Regular, response.ObjectType); + Assert.NotNull(response.Attributes); Assert.Single(response.Attributes); Assert.Equal(Mocker.HeadResponse.Attributes[0].Key, response.Attributes.First().Key); diff --git a/src/FrostFS.SDK.Tests/ObjectTestsBase.cs b/src/FrostFS.SDK.Tests/ObjectTestsBase.cs new file mode 100644 index 0000000..543bda6 --- /dev/null +++ b/src/FrostFS.SDK.Tests/ObjectTestsBase.cs @@ -0,0 +1,59 @@ +using FrostFS.SDK.ClientV2; +using FrostFS.SDK.ClientV2.Interfaces; +using FrostFS.SDK.Cryptography; + +using Microsoft.Extensions.Options; + +namespace FrostFS.SDK.Tests; + +public abstract class ObjectTestsBase +{ + protected static readonly string key = "KwHDAJ66o8FoLBjVbjP2sWBmgBMGjt7Vv4boA7xQrBoAYBE397Aq"; + + protected IOptions Settings { get; set; } + protected FrostFsContainerId ContainerId { get; set; } + + protected NetworkMocker NetworkMocker { get; set; } = new NetworkMocker(key); + protected SessionMocker SessionMocker { get; set; } = new SessionMocker(key); + protected ContainerMocker ContainerMocker { get; set; } = new ContainerMocker(key); + protected ObjectMocker Mocker { get; set; } + + protected ObjectTestsBase() + { + var ecdsaKey = key.LoadWif(); + + Settings = Options.Create(new SingleOwnerClientSettings + { + Key = key, + Host = "http://localhost:8080" + }); + + Mocker = new ObjectMocker(key) + { + PlacementPolicy = new FrostFsPlacementPolicy(true, new FrostFsReplica(1)), + Version = new FrostFsVersion(2, 13), + ContainerGuid = Guid.NewGuid() + }; + + ContainerId = new FrostFsContainerId(Base58.Encode(Mocker.ContainerGuid.ToBytes())); + + Mocker.ObjectHeader = new( + ContainerId, + FrostFsObjectType.Regular, + [new FrostFsAttributePair("k", "v")], + null, + FrostFsOwner.FromKey(ecdsaKey), + new FrostFsVersion(2, 13)); + } + + protected IFrostFSClient GetClient() + { + return FrostFSClient.GetTestInstance( + Settings, + null, + NetworkMocker.GetMock().Object, + SessionMocker.GetMock().Object, + ContainerMocker.GetMock().Object, + Mocker.GetMock().Object); + } +} diff --git a/src/FrostFS.SDK.Tests/PoolSmokeTests.cs b/src/FrostFS.SDK.Tests/PoolSmokeTests.cs new file mode 100644 index 0000000..87bf106 --- /dev/null +++ b/src/FrostFS.SDK.Tests/PoolSmokeTests.cs @@ -0,0 +1,603 @@ +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; + +using FrostFS.SDK.ClientV2; +using FrostFS.SDK.Cryptography; + +using Microsoft.Extensions.Options; + +using static FrostFS.Session.SessionToken.Types.Body; + +namespace FrostFS.SDK.SmokeTests; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")] +[SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "No secure purpose")] +public class PoolSmokeTests : SmokeTestsBase +{ + private static readonly PrmWait lightWait = new(100, 1); + + private InitParameters GetDefaultParams() + { + return new InitParameters + { + Key = keyString.LoadWif(), + + NodeParams = [new(1, this.url, 100.0f)], + DialOptions = [new() + { + Authority = "", + Block = false, + DisableHealthCheck = false, + DisableRetry = false, + ReturnLastError = true, + Timeout = 30_000_000 + } + ], + ClientBuilder = null, + GracefulCloseOnSwitchTimeout = 30_000_000, + Logger = null + }; + } + + [Fact] + public async void NetworkMapTest() + { + var options = GetDefaultParams(); + + using var pool = new Pool(options); + + var error = await pool.Dial(new CallContext()); + + Assert.Null(error); + + var result = await pool.GetNetmapSnapshotAsync(default); + + Assert.True(result.Epoch > 0); + Assert.Single(result.NodeInfoCollection); + + var item = result.NodeInfoCollection[0]; + Assert.Equal(2, item.Version.Major); + Assert.Equal(13, item.Version.Minor); + Assert.Equal(NodeState.Online, item.State); + Assert.True(item.PublicKey.Length > 0); + Assert.Single(item.Addresses); + Assert.Equal(9, item.Attributes.Count); + } + + [Fact] + public async void NodeInfoTest() + { + var options = GetDefaultParams(); + + using var pool = new Pool(options); + + var error = await pool.Dial(new CallContext()); + + Assert.Null(error); + + var result = await pool.GetNodeInfoAsync(); + + Assert.Equal(2, result.Version.Major); + Assert.Equal(13, result.Version.Minor); + Assert.Equal(NodeState.Online, result.State); + Assert.Equal(33, result.PublicKey.Length); + Assert.Single(result.Addresses); + Assert.Equal(9, result.Attributes.Count); + } + + [Fact] + public async void NodeInfoStatisticsTest() + { + var options = GetDefaultParams(); + + using var pool = new Pool(options); + + var callbackText = string.Empty; + + var ctx = new CallContext + { + Callback = (cs) => callbackText = $"{cs.MethodName} took {cs.ElapsedMicroSeconds} microseconds" + }; + + var error = await pool.Dial(ctx).ConfigureAwait(true); + + Assert.Null(error); + + using var client = FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.keyString, this.url)); + + var result = await client.GetNodeInfoAsync(); + + Assert.False(string.IsNullOrEmpty(callbackText)); + Assert.Contains(" took ", callbackText, StringComparison.Ordinal); + } + + [Fact] + public async void GetSessionTest() + { + var options = GetDefaultParams(); + + using var pool = new Pool(options); + + var error = await pool.Dial(new CallContext()).ConfigureAwait(true); + + Assert.Null(error); + + var prm = new PrmSessionCreate(100); + + var token = await pool.CreateSessionAsync(prm).ConfigureAwait(true); + + var session = new Session.SessionToken().Deserialize(token.Token); + + var ownerHash = Base58.Decode(OwnerId!.Value); + + Assert.NotNull(session); + Assert.Null(session.Body.Container); + Assert.Null(session.Body.Object); + Assert.Equal(16, session.Body.Id.Length); + Assert.Equal(100ul, session.Body.Lifetime.Exp); + Assert.Equal(ownerHash, session.Body.OwnerId.Value); + Assert.Equal(33, session.Body.SessionKey.Length); + Assert.Equal(ContextOneofCase.None, session.Body.ContextCase); + } + + [Fact] + public async void CreateObjectWithSessionToken() + { + var options = GetDefaultParams(); + + using var pool = new Pool(options); + + var error = await pool.Dial(new CallContext()).ConfigureAwait(true); + + Assert.Null(error); + + await Cleanup(pool); + + var token = await pool.CreateSessionAsync(new PrmSessionCreate(int.MaxValue)); + + var createContainerParam = new PrmContainerCreate( + new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1)))); + + createContainerParam.XHeaders.Add("key1", "value1"); + + var containerId = await pool.CreateContainerAsync(createContainerParam); + + var bytes = GetRandomBytes(1024); + + var param = new PrmObjectPut + { + Header = new FrostFsObjectHeader( + containerId: containerId, + type: FrostFsObjectType.Regular, + [new FrostFsAttributePair("fileName", "test")]), + Payload = new MemoryStream(bytes), + ClientCut = false, + SessionToken = token + }; + + var objectId = await pool.PutObjectAsync(param).ConfigureAwait(true); + + var @object = await pool.GetObjectAsync(new PrmObjectGet(containerId, objectId)); + + var downloadedBytes = new byte[@object.Header.PayloadLength]; + MemoryStream ms = new(downloadedBytes); + + ReadOnlyMemory? chunk = null; + while ((chunk = await @object.ObjectReader!.ReadChunk()) != null) + { + ms.Write(chunk.Value.Span); + } + + Assert.Equal(SHA256.HashData(bytes), SHA256.HashData(downloadedBytes)); + + await Cleanup(pool); + } + + [Fact] + public async void FilterTest() + { + var options = GetDefaultParams(); + + using var pool = new Pool(options); + + var error = await pool.Dial(new CallContext()).ConfigureAwait(true); + + Assert.Null(error); + + await Cleanup(pool); + + var createContainerParam = new PrmContainerCreate( + new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1)))) + { + WaitParams = lightWait + }; + + var containerId = await pool.CreateContainerAsync(createContainerParam); + + var bytes = new byte[] { 1, 2, 3 }; + + var ParentHeader = new FrostFsObjectHeader( + containerId: containerId, + type: FrostFsObjectType.Regular) + { + PayloadLength = 3 + }; + + var param = new PrmObjectPut + { + Header = new FrostFsObjectHeader( + containerId: containerId, + type: FrostFsObjectType.Regular, + [new FrostFsAttributePair("fileName", "test")], + new FrostFsSplit()), + Payload = new MemoryStream(bytes), + ClientCut = false + }; + + var objectId = await pool.PutObjectAsync(param); + + var head = await pool.GetObjectHeadAsync(new PrmObjectHeadGet(containerId, objectId)); + + var ecdsaKey = this.keyString.LoadWif(); + + var networkInfo = await pool.GetNetmapSnapshotAsync(); + + await CheckFilter(pool, containerId, new FilterByContainerId(FrostFsMatchType.Equals, containerId)); + + await CheckFilter(pool, containerId, new FilterByOwnerId(FrostFsMatchType.Equals, FrostFsOwner.FromKey(ecdsaKey))); + + await CheckFilter(pool, containerId, new FilterBySplitId(FrostFsMatchType.Equals, param.Header.Split!.SplitId)); + + await CheckFilter(pool, containerId, new FilterByAttributePair(FrostFsMatchType.Equals, "fileName", "test")); + + await CheckFilter(pool, containerId, new FilterByObjectId(FrostFsMatchType.Equals, objectId)); + + await CheckFilter(pool, containerId, new FilterByVersion(FrostFsMatchType.Equals, networkInfo.NodeInfoCollection[0].Version)); + + await CheckFilter(pool, containerId, new FilterByEpoch(FrostFsMatchType.Equals, networkInfo.Epoch)); + + await CheckFilter(pool, containerId, new FilterByPayloadLength(FrostFsMatchType.Equals, 3)); + + var checkSum = CheckSum.CreateCheckSum(bytes); + + await CheckFilter(pool, containerId, new FilterByPayloadHash(FrostFsMatchType.Equals, checkSum)); + + await CheckFilter(pool, containerId, new FilterByPhysicallyStored()); + } + + private static async Task CheckFilter(Pool pool, FrostFsContainerId containerId, IObjectFilter filter) + { + var resultObjectsCount = 0; + + PrmObjectSearch searchParam = new(containerId) { Filters = [filter] }; + + await foreach (var objId in pool.SearchObjectsAsync(searchParam)) + { + resultObjectsCount++; + var objHeader = await pool.GetObjectHeadAsync(new PrmObjectHeadGet(containerId, objId)); + } + + Assert.True(0 < resultObjectsCount, $"Filter for {filter.Key} doesn't work"); + } + + [Theory] + [InlineData(1)] + [InlineData(3 * 1024 * 1024)] // exactly one chunk size - 3MB + [InlineData(6 * 1024 * 1024 + 100)] + public async void SimpleScenarioTest(int objectSize) + { + var options = GetDefaultParams(); + + using var pool = new Pool(options); + + var error = await pool.Dial(new CallContext()).ConfigureAwait(true); + + Assert.Null(error); + + await Cleanup(pool); + + bool callbackInvoked = false; + var ctx = new CallContext + { + // Timeout = TimeSpan.FromSeconds(20), + Callback = new((CallStatistics cs) => + { + callbackInvoked = true; + Assert.True(cs.ElapsedMicroSeconds > 0); + }) + }; + + var createContainerParam = new PrmContainerCreate( + new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1)), [new("testKey", "testValue")])) + { + Context = ctx + }; + + var createdContainer = await pool.CreateContainerAsync(createContainerParam); + + var container = await pool.GetContainerAsync(new PrmContainerGet(createdContainer) { Context = ctx }); + Assert.NotNull(container); + Assert.True(callbackInvoked); + + var bytes = GetRandomBytes(objectSize); + + var param = new PrmObjectPut + { + Header = new FrostFsObjectHeader( + containerId: createdContainer, + type: FrostFsObjectType.Regular, + [new FrostFsAttributePair("fileName", "test")]), + Payload = new MemoryStream(bytes), + ClientCut = false, + Context = new CallContext + { + Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0)) + } + }; + + var objectId = await pool.PutObjectAsync(param); + + var filter = new FilterByAttributePair(FrostFsMatchType.Equals, "fileName", "test"); + + bool hasObject = false; + await foreach (var objId in pool.SearchObjectsAsync(new PrmObjectSearch(createdContainer) { Filters = [filter] })) + { + hasObject = true; + + var objHeader = await pool.GetObjectHeadAsync(new PrmObjectHeadGet(createdContainer, objectId)); + Assert.Equal((ulong)bytes.Length, objHeader.PayloadLength); + Assert.NotNull(objHeader.Attributes); + Assert.Single(objHeader.Attributes!); + Assert.Equal("fileName", objHeader.Attributes!.First().Key); + Assert.Equal("test", objHeader.Attributes!.First().Value); + } + + Assert.True(hasObject); + + var @object = await pool.GetObjectAsync(new PrmObjectGet(createdContainer, objectId)); + + var downloadedBytes = new byte[@object.Header.PayloadLength]; + MemoryStream ms = new(downloadedBytes); + + ReadOnlyMemory? chunk = null; + while ((chunk = await @object.ObjectReader!.ReadChunk()) != null) + { + ms.Write(chunk.Value.Span); + } + + Assert.Equal(SHA256.HashData(bytes), SHA256.HashData(downloadedBytes)); + + await Cleanup(pool); + + await foreach (var _ in pool.ListContainersAsync()) + { + Assert.Fail("Containers exist"); + } + } + + [Theory] + [InlineData(1)] + [InlineData(3 * 1024 * 1024)] // exactly one chunk size - 3MB + [InlineData(6 * 1024 * 1024 + 100)] + public async void SimpleScenarioWithSessionTest(int objectSize) + { + var options = GetDefaultParams(); + + using var pool = new Pool(options); + + var error = await pool.Dial(new CallContext()).ConfigureAwait(true); + + Assert.Null(error); + + var token = await pool.CreateSessionAsync(new PrmSessionCreate(int.MaxValue)); + + await Cleanup(pool); + + var ctx = new CallContext + { + Timeout = TimeSpan.FromSeconds(20), + Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0)) + }; + + var createContainerParam = new PrmContainerCreate( + new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1)))) + { + Context = ctx + }; + + var container = await pool.CreateContainerAsync(createContainerParam); + + var containerInfo = await pool.GetContainerAsync(new PrmContainerGet(container) { Context = ctx }); + Assert.NotNull(containerInfo); + + var bytes = GetRandomBytes(objectSize); + + var param = new PrmObjectPut + { + Header = new FrostFsObjectHeader( + containerId: container, + type: FrostFsObjectType.Regular, + [new FrostFsAttributePair("fileName", "test")]), + Payload = new MemoryStream(bytes), + ClientCut = false, + Context = new CallContext + { + Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0)) + }, + SessionToken = token + }; + + var objectId = await pool.PutObjectAsync(param); + + var filter = new FilterByAttributePair(FrostFsMatchType.Equals, "fileName", "test"); + + bool hasObject = false; + await foreach (var objId in pool.SearchObjectsAsync(new PrmObjectSearch(container) { Filters = [filter], SessionToken = token })) + { + hasObject = true; + + var objHeader = await pool.GetObjectHeadAsync(new PrmObjectHeadGet(container, objectId) { SessionToken = token }); + Assert.Equal((ulong)bytes.Length, objHeader.PayloadLength); + Assert.NotNull(objHeader.Attributes); + Assert.Single(objHeader.Attributes); + Assert.Equal("fileName", objHeader.Attributes.First().Key); + Assert.Equal("test", objHeader.Attributes.First().Value); + } + + Assert.True(hasObject); + + var @object = await pool.GetObjectAsync(new PrmObjectGet(container, objectId) { SessionToken = token }); + + var downloadedBytes = new byte[@object.Header.PayloadLength]; + MemoryStream ms = new(downloadedBytes); + + ReadOnlyMemory? chunk = null; + while ((chunk = await @object.ObjectReader!.ReadChunk()) != null) + { + ms.Write(chunk.Value.Span); + } + + Assert.Equal(SHA256.HashData(bytes), SHA256.HashData(downloadedBytes)); + + await Cleanup(pool); + + await foreach (var _ in pool.ListContainersAsync()) + { + Assert.Fail("Containers exist"); + } + } + + [Theory] + [InlineData(1)] + [InlineData(64 * 1024 * 1024)] // exactly 1 block size - 64MB + [InlineData(64 * 1024 * 1024 - 1)] + [InlineData(64 * 1024 * 1024 + 1)] + [InlineData(2 * 64 * 1024 * 1024 + 256)] + [InlineData(200)] + public async void ClientCutScenarioTest(int objectSize) + { + var options = GetDefaultParams(); + + using var pool = new Pool(options); + + var error = await pool.Dial(new CallContext()).ConfigureAwait(true); + + Assert.Null(error); + + await Cleanup(pool); + + var createContainerParam = new PrmContainerCreate(new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1)))) + { + WaitParams = lightWait + }; + + var containerId = await pool.CreateContainerAsync(createContainerParam); + + var ctx = new CallContext + { + Timeout = TimeSpan.FromSeconds(10), + Interceptors = new([new MetricsInterceptor()]) + }; + + var container = await pool.GetContainerAsync(new PrmContainerGet(containerId) { Context = ctx }); + + Assert.NotNull(container); + + byte[] bytes = GetRandomBytes(objectSize); + + var param = new PrmObjectPut + { + Header = new FrostFsObjectHeader( + containerId: containerId, + type: FrostFsObjectType.Regular, + [new FrostFsAttributePair("fileName", "test")]), + Payload = new MemoryStream(bytes), + ClientCut = true + }; + + var objectId = await pool.PutObjectAsync(param); + + var filter = new FilterByAttributePair(FrostFsMatchType.Equals, "fileName", "test"); + + bool hasObject = false; + await foreach (var objId in pool.SearchObjectsAsync(new PrmObjectSearch(containerId, filter))) + { + hasObject = true; + + var objHeader = await pool.GetObjectHeadAsync(new PrmObjectHeadGet(containerId, objectId)); + Assert.Equal((ulong)bytes.Length, objHeader.PayloadLength); + Assert.NotNull(objHeader.Attributes); + Assert.Single(objHeader.Attributes); + Assert.Equal("fileName", objHeader.Attributes[0].Key); + Assert.Equal("test", objHeader.Attributes[0].Value); + } + + Assert.True(hasObject); + + var @object = await pool.GetObjectAsync(new PrmObjectGet(containerId, objectId)); + + var downloadedBytes = new byte[@object.Header.PayloadLength]; + MemoryStream ms = new(downloadedBytes); + + ReadOnlyMemory? chunk = null; + while ((chunk = await @object.ObjectReader!.ReadChunk()) != null) + { + ms.Write(chunk.Value.Span); + } + + Assert.Equal(SHA256.HashData(bytes), SHA256.HashData(downloadedBytes)); + + await CheckFilter(pool, containerId, new FilterByRootObject()); + + await Cleanup(pool); + + var deadline = DateTime.UtcNow.Add(TimeSpan.FromSeconds(5)); + + IAsyncEnumerator? enumerator = null; + do + { + if (deadline <= DateTime.UtcNow) + { + Assert.Fail("Containers exist"); + break; + } + + enumerator = pool.ListContainersAsync().GetAsyncEnumerator(); + await Task.Delay(500); + } + while (await enumerator!.MoveNextAsync()); + } + + private static byte[] GetRandomBytes(int size) + { + Random rnd = new(); + var bytes = new byte[size]; + rnd.NextBytes(bytes); + return bytes; + } + + private static IOptions GetSingleOwnerOptions(string key, string url) + { + return Options.Create(new SingleOwnerClientSettings + { + Key = key, + Host = url + }); + } + + private static IOptions GetOptions(string url) + { + return Options.Create(new ClientSettings + { + Host = url + }); + } + + static async Task Cleanup(Pool pool) + { + await foreach (var cid in pool.ListContainersAsync()) + { + await pool.DeleteContainerAsync(new PrmContainerDelete(cid) { WaitParams = lightWait }).ConfigureAwait(true); + } + } +} diff --git a/src/FrostFS.SDK.Tests/SessionTests.cs b/src/FrostFS.SDK.Tests/SessionTests.cs index 2e17816..e343c82 100644 --- a/src/FrostFS.SDK.Tests/SessionTests.cs +++ b/src/FrostFS.SDK.Tests/SessionTests.cs @@ -1,56 +1,11 @@ -using System.Security.Cryptography; +using System.Diagnostics.CodeAnalysis; using FrostFS.SDK.ClientV2; -using FrostFS.SDK.ClientV2.Interfaces; using FrostFS.SDK.ClientV2.Mappers.GRPC; -using FrostFS.SDK.ClientV2; -using FrostFS.SDK.Cryptography; - -using Microsoft.Extensions.Options; namespace FrostFS.SDK.Tests; -public abstract class SessionTestsBase -{ - protected readonly string key = "KwHDAJ66o8FoLBjVbjP2sWBmgBMGjt7Vv4boA7xQrBoAYBE397Aq"; - - protected IOptions Settings { get; set; } - - - protected ECDsa ECDsaKey { get; set; } - protected FrostFsOwner OwnerId { get; set; } - protected SessionMocker Mocker { get; set; } - - protected SessionTestsBase() - { - Settings = Options.Create(new SingleOwnerClientSettings - { - Key = key, - Host = "http://localhost:8080" - }); - - ECDsaKey = key.LoadWif(); - OwnerId = FrostFsOwner.FromKey(ECDsaKey); - - Mocker = new SessionMocker(this.key) - { - PlacementPolicy = new FrostFsPlacementPolicy(true, new FrostFsReplica(1)), - Version = new FrostFsVersion(2, 13) - }; - } - - protected IFrostFSClient GetClient() - { - return ClientV2.FrostFSClient.GetTestInstance( - Settings, - null, - new NetworkMocker(this.key).GetMock().Object, - Mocker.GetMock().Object, - new ContainerMocker(this.key).GetMock().Object, - new ObjectMocker(this.key).GetMock().Object); - } -} - +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")] public class SessionTest : SessionTestsBase { [Theory] @@ -64,7 +19,7 @@ public class SessionTest : SessionTestsBase if (useContext) { param.XHeaders.Add("headerKey1", "headerValue1"); - param.Context = new Context + param.Context = new CallContext { CancellationToken = Mocker.CancellationTokenSource.Token, Timeout = TimeSpan.FromSeconds(20), @@ -101,7 +56,6 @@ public class SessionTest : SessionTestsBase Assert.NotNull(Mocker.CreateSessionRequest.MetaHeader); Assert.Equal(Mocker.Version.ToMessage(), Mocker.CreateSessionRequest.MetaHeader.Version); - Assert.Null(Mocker.Metadata); if (useContext) diff --git a/src/FrostFS.SDK.Tests/SessionTestsBase.cs b/src/FrostFS.SDK.Tests/SessionTestsBase.cs new file mode 100644 index 0000000..a8fbdaf --- /dev/null +++ b/src/FrostFS.SDK.Tests/SessionTestsBase.cs @@ -0,0 +1,48 @@ +using System.Security.Cryptography; + +using FrostFS.SDK.ClientV2.Interfaces; +using FrostFS.SDK.Cryptography; + +using Microsoft.Extensions.Options; + +namespace FrostFS.SDK.Tests; + +public abstract class SessionTestsBase +{ + internal readonly string key = "KwHDAJ66o8FoLBjVbjP2sWBmgBMGjt7Vv4boA7xQrBoAYBE397Aq"; + + protected IOptions Settings { get; set; } + + protected ECDsa ECDsaKey { get; set; } + protected FrostFsOwner OwnerId { get; set; } + protected SessionMocker Mocker { get; set; } + + protected SessionTestsBase() + { + Settings = Options.Create(new SingleOwnerClientSettings + { + Key = key, + Host = "http://localhost:8080" + }); + + ECDsaKey = key.LoadWif(); + OwnerId = FrostFsOwner.FromKey(ECDsaKey); + + Mocker = new SessionMocker(this.key) + { + PlacementPolicy = new FrostFsPlacementPolicy(true, new FrostFsReplica(1)), + Version = new FrostFsVersion(2, 13) + }; + } + + protected IFrostFSClient GetClient() + { + return ClientV2.FrostFSClient.GetTestInstance( + Settings, + null, + new NetworkMocker(this.key).GetMock().Object, + Mocker.GetMock().Object, + new ContainerMocker(this.key).GetMock().Object, + new ObjectMocker(this.key).GetMock().Object); + } +} diff --git a/src/FrostFS.SDK.Tests/SmokeTests.cs b/src/FrostFS.SDK.Tests/SmokeClientTests.cs similarity index 88% rename from src/FrostFS.SDK.Tests/SmokeTests.cs rename to src/FrostFS.SDK.Tests/SmokeClientTests.cs index 9c86ff9..54868d6 100644 --- a/src/FrostFS.SDK.Tests/SmokeTests.cs +++ b/src/FrostFS.SDK.Tests/SmokeClientTests.cs @@ -1,24 +1,42 @@ +using System.Diagnostics.CodeAnalysis; using System.Security.Cryptography; + using FrostFS.SDK.ClientV2; using FrostFS.SDK.ClientV2.Interfaces; using FrostFS.SDK.Cryptography; + using Microsoft.Extensions.Options; using static FrostFS.Session.SessionToken.Types.Body; -using FrostFS.SDK.ClientV2; namespace FrostFS.SDK.SmokeTests; -public class SmokeTests : SmokeTestsBase +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")] +[SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "No secure purpose")] +public class SmokeClientTests : SmokeTestsBase { private static readonly PrmWait lightWait = new(100, 1); + [Theory] + [InlineData(false)] + [InlineData(true)] + public async void AccountTest(bool isSingleOnwerClient) + { + using var client = isSingleOnwerClient + ? FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.keyString, this.url)) + : FrostFSClient.GetInstance(GetOptions(this.url)); + + PrmBalance? prm = isSingleOnwerClient ? default : new() { Context = Ctx }; + + var result = await client.GetBalanceAsync(prm); + } + [Theory] [InlineData(false)] [InlineData(true)] public async void NetworkMapTest(bool isSingleOnwerClient) { - using var client = isSingleOnwerClient ? FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.key, this.url)) : FrostFSClient.GetInstance(GetOptions(this.url)); + using var client = isSingleOnwerClient ? FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.keyString, this.url)) : FrostFSClient.GetInstance(GetOptions(this.url)); PrmNetmapSnapshot? prm = isSingleOnwerClient ? default : new() { Context = Ctx }; var result = await client.GetNetmapSnapshotAsync(prm); @@ -41,7 +59,7 @@ public class SmokeTests : SmokeTestsBase [InlineData(true)] public async void NodeInfoTest(bool isSingleOnwerClient) { - using var client = isSingleOnwerClient ? FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.key, this.url)) : FrostFSClient.GetInstance(GetOptions(this.url)); + using var client = isSingleOnwerClient ? FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.keyString, this.url)) : FrostFSClient.GetInstance(GetOptions(this.url)); PrmNodeInfo? prm = isSingleOnwerClient ? default : new() { Context = Ctx }; @@ -56,14 +74,14 @@ public class SmokeTests : SmokeTestsBase } [Fact] - public async void NodeInfo_Statistics_Test() + public async void NodeInfoStatisticsTest() { - var ctx = new Context + var ctx = new CallContext { Callback = (cs) => Console.WriteLine($"{cs.MethodName} took {cs.ElapsedMicroSeconds} microseconds") }; - using var client = FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.key, this.url)); + using var client = FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.keyString, this.url)); var result = await client.GetNodeInfoAsync(); } @@ -73,7 +91,7 @@ public class SmokeTests : SmokeTestsBase [InlineData(true)] public async void GetSessionTest(bool isSingleOnwerClient) { - using var client = isSingleOnwerClient ? FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.key, this.url)) : FrostFSClient.GetInstance(GetOptions(this.url)); + using var client = isSingleOnwerClient ? FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.keyString, this.url)) : FrostFSClient.GetInstance(GetOptions(this.url)); PrmSessionCreate? prm = isSingleOnwerClient ? new PrmSessionCreate(100) : new PrmSessionCreate(100) { Context = Ctx }; @@ -81,7 +99,7 @@ public class SmokeTests : SmokeTestsBase var session = new Session.SessionToken().Deserialize(token.Token); - var ownerHash = Base58.Decode(OwnerId.Value); + var ownerHash = Base58.Decode(OwnerId!.Value); Assert.NotNull(session); Assert.Null(session.Body.Container); @@ -96,7 +114,7 @@ public class SmokeTests : SmokeTestsBase [Fact] public async void CreateObjectWithSessionToken() { - using var client = FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.key, this.url)); + using var client = FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.keyString, this.url)); await Cleanup(client); @@ -144,11 +162,7 @@ public class SmokeTests : SmokeTestsBase [Fact] public async void FilterTest() { - using var client = FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.key, this.url)); - - //var prm = new PrmApeChainList(new FrostFsChainTarget(FrostFsTargetType.Namespace, "root")); - - //var chains = await client.ListChainAsync(prm); + using var client = FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.keyString, this.url)); await Cleanup(client); @@ -184,7 +198,7 @@ public class SmokeTests : SmokeTestsBase var head = await client.GetObjectHeadAsync(new PrmObjectHeadGet(containerId, objectId)); - var ecdsaKey = this.key.LoadWif(); + var ecdsaKey = this.keyString.LoadWif(); var networkInfo = await client.GetNetmapSnapshotAsync(); @@ -192,7 +206,7 @@ public class SmokeTests : SmokeTestsBase await CheckFilter(client, containerId, new FilterByOwnerId(FrostFsMatchType.Equals, FrostFsOwner.FromKey(ecdsaKey))); - await CheckFilter(client, containerId, new FilterBySplitId(FrostFsMatchType.Equals, param.Header.Split.SplitId)); + await CheckFilter(client, containerId, new FilterBySplitId(FrostFsMatchType.Equals, param.Header.Split!.SplitId)); await CheckFilter(client, containerId, new FilterByAttributePair(FrostFsMatchType.Equals, "fileName", "test")); @@ -232,12 +246,12 @@ public class SmokeTests : SmokeTestsBase [InlineData(6 * 1024 * 1024 + 100)] public async void SimpleScenarioTest(int objectSize) { - using var client = FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.key, this.url)); + using var client = FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.keyString, this.url)); await Cleanup(client); bool callbackInvoked = false; - var ctx = new Context + var ctx = new CallContext { // Timeout = TimeSpan.FromSeconds(20), Callback = new((CallStatistics cs) => @@ -269,7 +283,7 @@ public class SmokeTests : SmokeTestsBase [new FrostFsAttributePair("fileName", "test")]), Payload = new MemoryStream(bytes), ClientCut = false, - Context = new Context + Context = new CallContext { Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0)) } @@ -286,6 +300,7 @@ public class SmokeTests : SmokeTestsBase var objHeader = await client.GetObjectHeadAsync(new PrmObjectHeadGet(createdContainer, objectId)); Assert.Equal((ulong)bytes.Length, objHeader.PayloadLength); + Assert.NotNull(objHeader.Attributes); Assert.Single(objHeader.Attributes); Assert.Equal("fileName", objHeader.Attributes.First().Key); Assert.Equal("test", objHeader.Attributes.First().Value); @@ -304,7 +319,7 @@ public class SmokeTests : SmokeTestsBase ms.Write(chunk.Value.Span); } - Assert.Equal(MD5.HashData(bytes), MD5.HashData(downloadedBytes)); + Assert.Equal(SHA256.HashData(bytes), SHA256.HashData(downloadedBytes)); await Cleanup(client); @@ -320,13 +335,13 @@ public class SmokeTests : SmokeTestsBase [InlineData(6 * 1024 * 1024 + 100)] public async void SimpleScenarioWithSessionTest(int objectSize) { - using var client = FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.key, this.url)); + using var client = FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.keyString, this.url)); var token = await client.CreateSessionAsync(new PrmSessionCreate(int.MaxValue)); await Cleanup(client); - var ctx = new Context + var ctx = new CallContext { Timeout = TimeSpan.FromSeconds(20), Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0)) @@ -353,7 +368,7 @@ public class SmokeTests : SmokeTestsBase [new FrostFsAttributePair("fileName", "test")]), Payload = new MemoryStream(bytes), ClientCut = false, - Context = new Context + Context = new CallContext { Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0)) }, @@ -371,6 +386,7 @@ public class SmokeTests : SmokeTestsBase var objHeader = await client.GetObjectHeadAsync(new PrmObjectHeadGet(container, objectId) { SessionToken = token }); Assert.Equal((ulong)bytes.Length, objHeader.PayloadLength); + Assert.NotNull(objHeader.Attributes); Assert.Single(objHeader.Attributes); Assert.Equal("fileName", objHeader.Attributes.First().Key); Assert.Equal("test", objHeader.Attributes.First().Value); @@ -389,7 +405,7 @@ public class SmokeTests : SmokeTestsBase ms.Write(chunk.Value.Span); } - Assert.Equal(MD5.HashData(bytes), MD5.HashData(downloadedBytes)); + Assert.Equal(SHA256.HashData(bytes), SHA256.HashData(downloadedBytes)); await Cleanup(client); @@ -408,7 +424,7 @@ public class SmokeTests : SmokeTestsBase [InlineData(200)] public async void ClientCutScenarioTest(int objectSize) { - using var client = FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.key, this.url)); + using var client = FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.keyString, this.url)); await Cleanup(client); @@ -419,7 +435,7 @@ public class SmokeTests : SmokeTestsBase var containerId = await client.CreateContainerAsync(createContainerParam); - var ctx = new Context + var ctx = new CallContext { Timeout = TimeSpan.FromSeconds(10), Interceptors = new([new MetricsInterceptor()]) @@ -452,6 +468,7 @@ public class SmokeTests : SmokeTestsBase var objHeader = await client.GetObjectHeadAsync(new PrmObjectHeadGet(containerId, objectId)); Assert.Equal((ulong)bytes.Length, objHeader.PayloadLength); + Assert.NotNull(objHeader.Attributes); Assert.Single(objHeader.Attributes); Assert.Equal("fileName", objHeader.Attributes[0].Key); Assert.Equal("test", objHeader.Attributes[0].Value); @@ -470,7 +487,7 @@ public class SmokeTests : SmokeTestsBase ms.Write(chunk.Value.Span); } - Assert.Equal(MD5.HashData(bytes), MD5.HashData(downloadedBytes)); + Assert.Equal(SHA256.HashData(bytes), SHA256.HashData(downloadedBytes)); await CheckFilter(client, containerId, new FilterByRootObject()); diff --git a/src/FrostFS.SDK.Tests/SmokeTestsBase.cs b/src/FrostFS.SDK.Tests/SmokeTestsBase.cs index 5bf7733..6b42144 100644 --- a/src/FrostFS.SDK.Tests/SmokeTestsBase.cs +++ b/src/FrostFS.SDK.Tests/SmokeTestsBase.cs @@ -7,24 +7,24 @@ namespace FrostFS.SDK.SmokeTests; public abstract class SmokeTestsBase { - protected readonly string key = "KzPXA6669m2pf18XmUdoR8MnP1pi1PMmefiFujStVFnv7WR5SRmK"; - - protected readonly string url = "http://172.23.32.4:8080"; + internal readonly string keyString = "KzPXA6669m2pf18XmUdoR8MnP1pi1PMmefiFujStVFnv7WR5SRmK"; - protected ECDsa Key { get; } + internal readonly string url = "http://172.23.32.4:8080"; - protected FrostFsOwner OwnerId { get; } + protected ECDsa? Key { get; } - protected FrostFsVersion Version { get; } + protected FrostFsOwner? OwnerId { get; } - protected Context Ctx { get; } + protected FrostFsVersion? Version { get; } + + protected CallContext? Ctx { get; } protected SmokeTestsBase() { - Key = key.LoadWif(); + Key = keyString.LoadWif(); OwnerId = FrostFsOwner.FromKey(Key); Version = new FrostFsVersion(2, 13); - Ctx = new Context { Key = Key, OwnerId = OwnerId, Version = Version }; + Ctx = new CallContext { Key = Key, OwnerId = OwnerId, Version = Version }; } } -- 2.45.2 From ee2079837906b274ccaf075a42cf5b55c5d1942b Mon Sep 17 00:00:00 2001 From: Pavel Gross Date: Fri, 1 Nov 2024 10:30:28 +0300 Subject: [PATCH 2/3] [#24] Client: Implement pool part2 Signed-off-by: Pavel Gross --- .../FrostFsInvalidObjectException.cs | 18 + .../Exceptions/FrostFsResponseException.cs | 25 ++ .../Exceptions/FrostFsStreamException.cs | 18 + .../Exceptions/InvalidObjectException.cs | 18 - .../Exceptions/ResponseException.cs | 25 -- .../FrostFS.SDK.ClientV2.csproj | 4 +- src/FrostFS.SDK.ClientV2/FrostFSClient.cs | 81 ++--- .../Interceptors/ErrorInterceptor.cs | 68 ++++ .../Interceptors/MetricsInterceptor.cs | 14 +- .../Interfaces/IFrostFSClient.cs | 2 - .../Logging/FrostFsMessages.cs | 24 ++ .../Models/Client/ClientSettings.cs | 6 +- .../Models/Containers/FrostFsContainerId.cs | 4 +- .../Models/Containers/FrostFsContainerInfo.cs | 2 +- .../Models/Object/FrostFsObject.cs | 4 +- .../Parameters/CallContext.cs | 10 +- .../Parameters/IContext.cs | 2 +- .../Parameters/PrmApeChainList.cs | 2 +- .../Parameters/PrmApeChainRemove.cs | 2 +- .../Parameters/PrmApeRemoveAdd.cs | 2 +- .../Parameters/PrmBalance.cs | 2 +- .../Parameters/PrmBase.cs | 4 +- .../Parameters/PrmContainerCreate.cs | 2 +- .../Parameters/PrmContainerDelete.cs | 2 +- .../Parameters/PrmContainerGet.cs | 2 +- .../Parameters/PrmContainerGetAll.cs | 2 +- .../Parameters/PrmNetmapSnapshot.cs | 2 +- .../Parameters/PrmNetworkSettings.cs | 2 +- .../Parameters/PrmNodeInfo.cs | 2 +- .../Parameters/PrmObjectDelete.cs | 2 +- .../Parameters/PrmObjectGet.cs | 2 +- .../Parameters/PrmObjectHeadGet.cs | 2 +- .../Parameters/PrmObjectPut.cs | 2 +- .../Parameters/PrmObjectSearch.cs | 2 +- .../Parameters/PrmSessionCreate.cs | 2 +- .../Parameters/PrmSingleObjectPut.cs | 2 +- .../Poll/ClientStatusMonitor.cs | 20 +- .../Poll/ClientWrapper.cs | 72 ++-- src/FrostFS.SDK.ClientV2/Poll/DialOptions.cs | 16 - .../Poll/InitParameters.cs | 4 +- src/FrostFS.SDK.ClientV2/Poll/InnerPool.cs | 4 +- src/FrostFS.SDK.ClientV2/Poll/Pool.cs | 338 ++++++++++++------ .../Poll/RebalanceParameters.cs | 2 +- src/FrostFS.SDK.ClientV2/Poll/SessionCache.cs | 9 +- .../Services/AccountingServiceProvider.cs | 2 +- .../Services/ApeManagerServiceProvider.cs | 15 +- .../Services/ContainerServiceProvider.cs | 36 +- .../Services/NetmapServiceProvider.cs | 25 +- .../Services/ObjectServiceProvider.cs | 52 +-- .../Services/SessionServiceProvider.cs | 4 +- .../Services/Shared/ContextAccessor.cs | 4 +- .../Services/Shared/SessionProvider.cs | 5 +- ...EnvironmentContext.cs => ClientContext.cs} | 22 +- .../Tools/ObjectReader.cs | 6 +- src/FrostFS.SDK.ClientV2/Tools/ObjectTools.cs | 2 +- src/FrostFS.SDK.ClientV2/Tools/Verifier.cs | 4 +- src/FrostFS.SDK.Tests/CallbackInterceptor.cs | 33 ++ src/FrostFS.SDK.Tests/MetricsInterceptor.cs | 41 --- src/FrostFS.SDK.Tests/NetworkTest.cs | 37 +- src/FrostFS.SDK.Tests/ObjectTest.cs | 6 +- src/FrostFS.SDK.Tests/PoolSmokeTests.cs | 103 +++--- src/FrostFS.SDK.Tests/SessionTests.cs | 13 +- src/FrostFS.SDK.Tests/SmokeClientTests.cs | 87 +++-- 63 files changed, 801 insertions(+), 526 deletions(-) create mode 100644 src/FrostFS.SDK.ClientV2/Exceptions/FrostFsInvalidObjectException.cs create mode 100644 src/FrostFS.SDK.ClientV2/Exceptions/FrostFsResponseException.cs create mode 100644 src/FrostFS.SDK.ClientV2/Exceptions/FrostFsStreamException.cs delete mode 100644 src/FrostFS.SDK.ClientV2/Exceptions/InvalidObjectException.cs delete mode 100644 src/FrostFS.SDK.ClientV2/Exceptions/ResponseException.cs create mode 100644 src/FrostFS.SDK.ClientV2/Interceptors/ErrorInterceptor.cs create mode 100644 src/FrostFS.SDK.ClientV2/Logging/FrostFsMessages.cs delete mode 100644 src/FrostFS.SDK.ClientV2/Poll/DialOptions.cs rename src/FrostFS.SDK.ClientV2/Tools/{EnvironmentContext.cs => ClientContext.cs} (62%) create mode 100644 src/FrostFS.SDK.Tests/CallbackInterceptor.cs delete mode 100644 src/FrostFS.SDK.Tests/MetricsInterceptor.cs diff --git a/src/FrostFS.SDK.ClientV2/Exceptions/FrostFsInvalidObjectException.cs b/src/FrostFS.SDK.ClientV2/Exceptions/FrostFsInvalidObjectException.cs new file mode 100644 index 0000000..87e20e7 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Exceptions/FrostFsInvalidObjectException.cs @@ -0,0 +1,18 @@ +using System; + +namespace FrostFS.SDK.ClientV2; + +public class FrostFsInvalidObjectException : FrostFsException +{ + public FrostFsInvalidObjectException() + { + } + + public FrostFsInvalidObjectException(string message) : base(message) + { + } + + public FrostFsInvalidObjectException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Exceptions/FrostFsResponseException.cs b/src/FrostFS.SDK.ClientV2/Exceptions/FrostFsResponseException.cs new file mode 100644 index 0000000..6ef30aa --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Exceptions/FrostFsResponseException.cs @@ -0,0 +1,25 @@ +using System; + +namespace FrostFS.SDK.ClientV2; + +public class FrostFsResponseException : FrostFsException +{ + public FrostFsResponseStatus? Status { get; private set; } + + public FrostFsResponseException() + { + } + + public FrostFsResponseException(FrostFsResponseStatus status) + { + Status = status; + } + + public FrostFsResponseException(string message) : base(message) + { + } + + public FrostFsResponseException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Exceptions/FrostFsStreamException.cs b/src/FrostFS.SDK.ClientV2/Exceptions/FrostFsStreamException.cs new file mode 100644 index 0000000..45472e4 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Exceptions/FrostFsStreamException.cs @@ -0,0 +1,18 @@ +using System; + +namespace FrostFS.SDK.ClientV2; + +public class FrostFsStreamException : FrostFsException +{ + public FrostFsStreamException() + { + } + + public FrostFsStreamException(string message) : base(message) + { + } + + public FrostFsStreamException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Exceptions/InvalidObjectException.cs b/src/FrostFS.SDK.ClientV2/Exceptions/InvalidObjectException.cs deleted file mode 100644 index c15e591..0000000 --- a/src/FrostFS.SDK.ClientV2/Exceptions/InvalidObjectException.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace FrostFS.SDK.ClientV2; - -public class InvalidObjectException : Exception -{ - public InvalidObjectException() - { - } - - public InvalidObjectException(string message) : base(message) - { - } - - public InvalidObjectException(string message, Exception innerException) : base(message, innerException) - { - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Exceptions/ResponseException.cs b/src/FrostFS.SDK.ClientV2/Exceptions/ResponseException.cs deleted file mode 100644 index ce7f19a..0000000 --- a/src/FrostFS.SDK.ClientV2/Exceptions/ResponseException.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; - -namespace FrostFS.SDK.ClientV2; - -public class ResponseException : Exception -{ - public FrostFsResponseStatus? Status { get; private set; } - - public ResponseException() - { - } - - public ResponseException(FrostFsResponseStatus status) - { - Status = status; - } - - public ResponseException(string message) : base(message) - { - } - - public ResponseException(string message, Exception innerException) : base(message, innerException) - { - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/FrostFS.SDK.ClientV2.csproj b/src/FrostFS.SDK.ClientV2/FrostFS.SDK.ClientV2.csproj index b16e7e7..fefaaf2 100644 --- a/src/FrostFS.SDK.ClientV2/FrostFS.SDK.ClientV2.csproj +++ b/src/FrostFS.SDK.ClientV2/FrostFS.SDK.ClientV2.csproj @@ -22,10 +22,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + - + diff --git a/src/FrostFS.SDK.ClientV2/FrostFSClient.cs b/src/FrostFS.SDK.ClientV2/FrostFSClient.cs index 2ca9e96..e13b6b6 100644 --- a/src/FrostFS.SDK.ClientV2/FrostFSClient.cs +++ b/src/FrostFS.SDK.ClientV2/FrostFSClient.cs @@ -39,7 +39,7 @@ public class FrostFSClient : IFrostFSClient internal AccountingService.AccountingServiceClient? AccountingServiceClient { get; set; } - internal EnvironmentContext ClientCtx { get; set; } + internal ClientContext ClientCtx { get; set; } public static IFrostFSClient GetInstance(IOptions clientOptions, GrpcChannelOptions? channelOptions = null) { @@ -93,7 +93,7 @@ public class FrostFSClient : IFrostFSClient var ecdsaKey = settings.Value.Key.LoadWif(); FrostFsOwner.FromKey(ecdsaKey); - ClientCtx = new EnvironmentContext( + ClientCtx = new ClientContext( client: this, key: ecdsaKey, owner: FrostFsOwner.FromKey(ecdsaKey), @@ -108,13 +108,13 @@ public class FrostFSClient : IFrostFSClient private FrostFSClient(IOptions options, GrpcChannelOptions? channelOptions) { - var clientSettings = (options?.Value) ?? throw new ArgumentException("Options must be initialized"); + var clientSettings = (options?.Value) ?? throw new ArgumentNullException(nameof(options), "Options value must be initialized"); clientSettings.Validate(); var channel = InitGrpcChannel(clientSettings.Host, channelOptions); - ClientCtx = new EnvironmentContext( + ClientCtx = new ClientContext( this, key: null, owner: null, @@ -127,7 +127,7 @@ public class FrostFSClient : IFrostFSClient private FrostFSClient(IOptions options, GrpcChannelOptions? channelOptions) { - var clientSettings = (options?.Value) ?? throw new ArgumentException("Options must be initialized"); + var clientSettings = (options?.Value) ?? throw new ArgumentNullException(nameof(options), "Options value must be initialized"); clientSettings.Validate(); @@ -135,7 +135,7 @@ public class FrostFSClient : IFrostFSClient var channel = InitGrpcChannel(clientSettings.Host, channelOptions); - ClientCtx = new EnvironmentContext( + ClientCtx = new ClientContext( this, key: ecdsaKey, owner: FrostFsOwner.FromKey(ecdsaKey), @@ -146,14 +146,17 @@ public class FrostFSClient : IFrostFSClient // CheckFrostFsVersionSupport(new Context { Timeout = TimeSpan.FromSeconds(20) }); } - internal FrostFSClient(WrapperPrm prm) + internal FrostFSClient(WrapperPrm prm, SessionCache cache) { - ClientCtx = new EnvironmentContext( + ClientCtx = new ClientContext( client: this, key: prm.Key, owner: FrostFsOwner.FromKey(prm.Key!), channel: InitGrpcChannel(prm.Address, null), //prm.GrpcChannelOptions), - version: new FrostFsVersion(2, 13)); + version: new FrostFsVersion(2, 13)) + { + SessionCache = cache + }; } public void Dispose() @@ -363,10 +366,10 @@ public class FrostFSClient : IFrostFSClient private async void CheckFrostFsVersionSupport(CallContext? ctx = default) { - var args = new PrmNodeInfo { Context = ctx }; + var args = new PrmNodeInfo(ctx); if (ctx?.Version == null) - throw new InvalidObjectException(nameof(ctx.Version)); + throw new ArgumentNullException(nameof(ctx), "Version must be initialized"); var service = GetNetmapService(args); var localNodeInfo = await service.GetLocalNodeInfoAsync(args).ConfigureAwait(false); @@ -378,18 +381,16 @@ public class FrostFSClient : IFrostFSClient } } - private CallInvoker? SetupEnvironment(IContext ctx) + private CallInvoker? SetupClientContext(IContext ctx) { if (isDisposed) - throw new InvalidObjectException("Client is disposed."); + throw new FrostFsInvalidObjectException("Client is disposed."); - ctx.Context ??= new CallContext(); - - if (ctx.Context.Key == null) + if (ctx.Context!.Key == null) { if (ClientCtx.Key == null) { - throw new InvalidObjectException("Key is not initialized."); + throw new ArgumentNullException(nameof(ctx), "Key is not initialized."); } ctx.Context.Key = ClientCtx.Key.ECDsaKey; @@ -404,24 +405,23 @@ public class FrostFSClient : IFrostFSClient { if (ClientCtx.Version == null) { - throw new InvalidObjectException("Version is not initialized."); + throw new ArgumentNullException(nameof(ctx), "Version is not initialized."); } ctx.Context.Version = ClientCtx.Version; } CallInvoker? callInvoker = null; - if (ctx.Context.Interceptors != null && ctx.Context.Interceptors.Count > 0) - { - foreach (var interceptor in ctx.Context.Interceptors) - { - callInvoker = AddInvoker(callInvoker, interceptor); - } - } + + foreach (var interceptor in ctx.Context.Interceptors) + callInvoker = AddInvoker(callInvoker, interceptor); if (ctx.Context.Callback != null) callInvoker = AddInvoker(callInvoker, new MetricsInterceptor(ctx.Context.Callback)); + if (ctx.Context.PoolErrorHandler != null) + callInvoker = AddInvoker(callInvoker, new ErrorInterceptor(ctx.Context.PoolErrorHandler)); + return callInvoker; CallInvoker AddInvoker(CallInvoker? callInvoker, Interceptor interceptor) @@ -429,7 +429,7 @@ public class FrostFSClient : IFrostFSClient if (callInvoker == null) callInvoker = ClientCtx.Channel.Intercept(interceptor); else - callInvoker.Intercept(interceptor); + callInvoker = callInvoker.Intercept(interceptor); return callInvoker; } @@ -437,7 +437,7 @@ public class FrostFSClient : IFrostFSClient private NetmapServiceProvider GetNetmapService(IContext ctx) { - var callInvoker = SetupEnvironment(ctx); + var callInvoker = SetupClientContext(ctx); var client = NetmapServiceClient ?? (callInvoker != null ? new NetmapService.NetmapServiceClient(callInvoker) : new NetmapService.NetmapServiceClient(ClientCtx.Channel)); @@ -447,7 +447,7 @@ public class FrostFSClient : IFrostFSClient private SessionServiceProvider GetSessionService(IContext ctx) { - var callInvoker = SetupEnvironment(ctx); + var callInvoker = SetupClientContext(ctx); var client = SessionServiceClient ?? (callInvoker != null ? new SessionService.SessionServiceClient(callInvoker) : new SessionService.SessionServiceClient(ClientCtx.Channel)); @@ -457,7 +457,7 @@ public class FrostFSClient : IFrostFSClient private ApeManagerServiceProvider GetApeManagerService(IContext ctx) { - var callInvoker = SetupEnvironment(ctx); + var callInvoker = SetupClientContext(ctx); var client = ApeManagerServiceClient ?? (callInvoker != null ? new APEManagerService.APEManagerServiceClient(callInvoker) : new APEManagerService.APEManagerServiceClient(ClientCtx.Channel)); @@ -467,7 +467,7 @@ public class FrostFSClient : IFrostFSClient private AccountingServiceProvider GetAccouningService(IContext ctx) { - var callInvoker = SetupEnvironment(ctx); + var callInvoker = SetupClientContext(ctx); var client = AccountingServiceClient ?? (callInvoker != null ? new AccountingService.AccountingServiceClient(callInvoker) : new AccountingService.AccountingServiceClient(ClientCtx.Channel)); @@ -477,7 +477,7 @@ public class FrostFSClient : IFrostFSClient private ContainerServiceProvider GetContainerService(IContext ctx) { - var callInvoker = SetupEnvironment(ctx); + var callInvoker = SetupClientContext(ctx); var client = ContainerServiceClient ?? (callInvoker != null ? new ContainerService.ContainerServiceClient(callInvoker) : new ContainerService.ContainerServiceClient(ClientCtx.Channel)); @@ -487,7 +487,7 @@ public class FrostFSClient : IFrostFSClient private ObjectServiceProvider GetObjectService(IContext ctx) { - var callInvoker = SetupEnvironment(ctx); + var callInvoker = SetupClientContext(ctx); var client = ObjectServiceClient ?? (callInvoker != null ? new ObjectService.ObjectServiceClient(callInvoker) : new ObjectService.ObjectServiceClient(ClientCtx.Channel)); @@ -497,7 +497,7 @@ public class FrostFSClient : IFrostFSClient private AccountingServiceProvider GetAccountService(IContext ctx) { - var callInvoker = SetupEnvironment(ctx); + var callInvoker = SetupClientContext(ctx); var client = AccountingServiceClient ?? (callInvoker != null ? new AccountingService.AccountingServiceClient(callInvoker) : new AccountingService.AccountingServiceClient(ClientCtx.Channel)); @@ -527,19 +527,12 @@ public class FrostFSClient : IFrostFSClient public async Task Dial(CallContext ctx) { - try - { - var prm = new PrmBalance { Context = ctx }; + var prm = new PrmBalance(ctx); - var service = GetAccouningService(prm); - var balance = await service.GetBallance(prm).ConfigureAwait(false); + var service = GetAccouningService(prm); + _ = await service.GetBallance(prm).ConfigureAwait(false); - return null; - } - catch (FrostFsException ex) - { - return ex.Message; - } + return null; } public bool RestartIfUnhealthy(CallContext ctx) diff --git a/src/FrostFS.SDK.ClientV2/Interceptors/ErrorInterceptor.cs b/src/FrostFS.SDK.ClientV2/Interceptors/ErrorInterceptor.cs new file mode 100644 index 0000000..d76477f --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Interceptors/ErrorInterceptor.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading.Tasks; + +using Grpc.Core; +using Grpc.Core.Interceptors; + +namespace FrostFS.SDK.ClientV2; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", + Justification = "parameters are provided by GRPC infrastructure")] +public class ErrorInterceptor(Action handler) : Interceptor +{ + public override AsyncUnaryCall AsyncUnaryCall( + TRequest request, + ClientInterceptorContext context, + AsyncUnaryCallContinuation continuation) + { + var call = continuation(request, context); + + return new AsyncUnaryCall( + HandleUnaryResponse(call), + call.ResponseHeadersAsync, + call.GetStatus, + call.GetTrailers, + call.Dispose); + } + + public override AsyncClientStreamingCall AsyncClientStreamingCall( + ClientInterceptorContext context, + AsyncClientStreamingCallContinuation continuation) + { + var call = continuation(context); + + return new AsyncClientStreamingCall( + call.RequestStream, + HandleStreamResponse(call), + call.ResponseHeadersAsync, + call.GetStatus, + call.GetTrailers, + call.Dispose); + } + + private async Task HandleUnaryResponse(AsyncUnaryCall call) + { + try + { + return await call; + } + catch (Exception ex) + { + handler(ex); + throw; + } + } + + private async Task HandleStreamResponse(AsyncClientStreamingCall call) + { + try + { + return await call; + } + catch (Exception ex) + { + handler(ex); + throw; + } + } +} diff --git a/src/FrostFS.SDK.ClientV2/Interceptors/MetricsInterceptor.cs b/src/FrostFS.SDK.ClientV2/Interceptors/MetricsInterceptor.cs index 9b3199c..f4d93c0 100644 --- a/src/FrostFS.SDK.ClientV2/Interceptors/MetricsInterceptor.cs +++ b/src/FrostFS.SDK.ClientV2/Interceptors/MetricsInterceptor.cs @@ -7,6 +7,8 @@ using Grpc.Core.Interceptors; namespace FrostFS.SDK.ClientV2; +[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", + Justification = "parameters are provided by GRPC infrastructure")] public class MetricsInterceptor(Action callback) : Interceptor { public override AsyncUnaryCall AsyncUnaryCall( @@ -14,11 +16,6 @@ public class MetricsInterceptor(Action callback) : Interceptor ClientInterceptorContext context, AsyncUnaryCallContinuation continuation) { - if (continuation is null) - { - throw new ArgumentNullException(nameof(continuation)); - } - var call = continuation(request, context); return new AsyncUnaryCall( @@ -33,9 +30,6 @@ public class MetricsInterceptor(Action callback) : Interceptor ClientInterceptorContext context, AsyncClientStreamingCallContinuation continuation) { - if (continuation is null) - throw new ArgumentNullException(nameof(continuation)); - var call = continuation(context); return new AsyncClientStreamingCall( @@ -52,7 +46,7 @@ public class MetricsInterceptor(Action callback) : Interceptor var watch = new Stopwatch(); watch.Start(); - var response = await call.ResponseAsync.ConfigureAwait(false); + var response = await call; watch.Stop(); @@ -68,7 +62,7 @@ public class MetricsInterceptor(Action callback) : Interceptor var watch = new Stopwatch(); watch.Start(); - var response = await call.ResponseAsync.ConfigureAwait(false); + var response = await call; watch.Stop(); diff --git a/src/FrostFS.SDK.ClientV2/Interfaces/IFrostFSClient.cs b/src/FrostFS.SDK.ClientV2/Interfaces/IFrostFSClient.cs index 193d128..e81343b 100644 --- a/src/FrostFS.SDK.ClientV2/Interfaces/IFrostFSClient.cs +++ b/src/FrostFS.SDK.ClientV2/Interfaces/IFrostFSClient.cs @@ -61,7 +61,5 @@ public interface IFrostFSClient : IDisposable public Task Dial(CallContext ctx); - public bool RestartIfUnhealthy(CallContext ctx); - public void Close(); } diff --git a/src/FrostFS.SDK.ClientV2/Logging/FrostFsMessages.cs b/src/FrostFS.SDK.ClientV2/Logging/FrostFsMessages.cs new file mode 100644 index 0000000..2fd1fe1 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Logging/FrostFsMessages.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Logging; + +namespace FrostFS.SDK.ClientV2; + +internal static partial class FrostFsMessages +{ + [LoggerMessage(100, + LogLevel.Warning, + "Failed to create frostfs session token for client. Address {address}, {error}", + EventName = nameof(SessionCreationError))] + internal static partial void SessionCreationError(ILogger logger, string address, string error); + + [LoggerMessage(101, + LogLevel.Warning, + "Error threshold reached. Address {address}, threshold {threshold}", + EventName = nameof(ErrorЕhresholdReached))] + internal static partial void ErrorЕhresholdReached(ILogger logger, string address, uint threshold); + + [LoggerMessage(102, + LogLevel.Warning, + "Health has changed: {address} healthy {healthy}, reason {error}", + EventName = nameof(HealthChanged))] + internal static partial void HealthChanged(ILogger logger, string address, bool healthy, string error); +} diff --git a/src/FrostFS.SDK.ClientV2/Models/Client/ClientSettings.cs b/src/FrostFS.SDK.ClientV2/Models/Client/ClientSettings.cs index b1faf79..6ebec1f 100644 --- a/src/FrostFS.SDK.ClientV2/Models/Client/ClientSettings.cs +++ b/src/FrostFS.SDK.ClientV2/Models/Client/ClientSettings.cs @@ -15,7 +15,7 @@ public class ClientSettings { var errors = CheckFields(); if (errors != null) - ThrowException(errors); + ThrowSettingsException(errors); } protected Collection? CheckFields() @@ -29,7 +29,7 @@ public class ClientSettings return null; } - protected static void ThrowException(Collection errors) + protected static void ThrowSettingsException(Collection errors) { if (errors is null) { @@ -55,7 +55,7 @@ public class SingleOwnerClientSettings : ClientSettings { var errors = CheckFields(); if (errors != null) - ThrowException(errors); + ThrowSettingsException(errors); } protected new Collection? CheckFields() diff --git a/src/FrostFS.SDK.ClientV2/Models/Containers/FrostFsContainerId.cs b/src/FrostFS.SDK.ClientV2/Models/Containers/FrostFsContainerId.cs index 1ebbe04..06b87ca 100644 --- a/src/FrostFS.SDK.ClientV2/Models/Containers/FrostFsContainerId.cs +++ b/src/FrostFS.SDK.ClientV2/Models/Containers/FrostFsContainerId.cs @@ -31,7 +31,7 @@ public class FrostFsContainerId return this.modelId; } - throw new InvalidObjectException(); + throw new FrostFsInvalidObjectException(); } internal ContainerID ContainerID @@ -47,7 +47,7 @@ public class FrostFsContainerId return this.containerID; } - throw new InvalidObjectException(); + throw new FrostFsInvalidObjectException(); } } diff --git a/src/FrostFS.SDK.ClientV2/Models/Containers/FrostFsContainerInfo.cs b/src/FrostFS.SDK.ClientV2/Models/Containers/FrostFsContainerInfo.cs index e7336b2..affdbee 100644 --- a/src/FrostFS.SDK.ClientV2/Models/Containers/FrostFsContainerInfo.cs +++ b/src/FrostFS.SDK.ClientV2/Models/Containers/FrostFsContainerInfo.cs @@ -88,7 +88,7 @@ public class FrostFsContainerInfo { if (PlacementPolicy == null) { - throw new InvalidObjectException("PlacementPolicy is null"); + throw new ArgumentNullException("PlacementPolicy is null"); } this.container = new Container.Container() diff --git a/src/FrostFS.SDK.ClientV2/Models/Object/FrostFsObject.cs b/src/FrostFS.SDK.ClientV2/Models/Object/FrostFsObject.cs index a6c82db..f366b9e 100644 --- a/src/FrostFS.SDK.ClientV2/Models/Object/FrostFsObject.cs +++ b/src/FrostFS.SDK.ClientV2/Models/Object/FrostFsObject.cs @@ -1,4 +1,4 @@ -using FrostFS.SDK.ClientV2; +using System; namespace FrostFS.SDK; @@ -67,7 +67,7 @@ public class FrostFsObject public void SetParent(FrostFsObjectHeader largeObjectHeader) { if (Header?.Split == null) - throw new InvalidObjectException("The object is not initialized properly"); + throw new ArgumentNullException(nameof(largeObjectHeader), "Split value must not be null"); Header.Split.ParentHeader = largeObjectHeader; } diff --git a/src/FrostFS.SDK.ClientV2/Parameters/CallContext.cs b/src/FrostFS.SDK.ClientV2/Parameters/CallContext.cs index ff4bc27..24f076e 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/CallContext.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/CallContext.cs @@ -13,10 +13,10 @@ namespace FrostFS.SDK.ClientV2; public class CallContext() { - private ReadOnlyCollection? interceptors; - private ByteString? publicKeyCache; + internal Action? PoolErrorHandler { get; set; } + public ECDsa? Key { get; set; } public FrostFsOwner? OwnerId { get; set; } @@ -31,11 +31,7 @@ public class CallContext() public Action? Callback { get; set; } - public ReadOnlyCollection? Interceptors - { - get { return this.interceptors; } - set { this.interceptors = value; } - } + public Collection Interceptors { get; } = []; public ByteString? GetPublicKeyCache() { diff --git a/src/FrostFS.SDK.ClientV2/Parameters/IContext.cs b/src/FrostFS.SDK.ClientV2/Parameters/IContext.cs index 8c6f1df..128c6a0 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/IContext.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/IContext.cs @@ -7,5 +7,5 @@ public interface IContext /// callbacks, interceptors. /// /// Additional parameters for calling the method - CallContext? Context { get; set; } + CallContext? Context { get; } } diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmApeChainList.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmApeChainList.cs index a68e788..4202685 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/PrmApeChainList.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmApeChainList.cs @@ -1,6 +1,6 @@ namespace FrostFS.SDK.ClientV2; -public sealed class PrmApeChainList(FrostFsChainTarget target) : PrmBase +public sealed class PrmApeChainList(FrostFsChainTarget target, CallContext? ctx = null) : PrmBase(ctx) { public FrostFsChainTarget Target { get; } = target; } diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmApeChainRemove.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmApeChainRemove.cs index 9371b43..a7a9f5a 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/PrmApeChainRemove.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmApeChainRemove.cs @@ -1,6 +1,6 @@ namespace FrostFS.SDK.ClientV2; -public sealed class PrmApeChainRemove(FrostFsChainTarget target, FrostFsChain chain) : PrmBase +public sealed class PrmApeChainRemove(FrostFsChainTarget target, FrostFsChain chain, CallContext? ctx = null) : PrmBase(ctx) { public FrostFsChainTarget Target { get; } = target; diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmApeRemoveAdd.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmApeRemoveAdd.cs index 330f601..75ac0e7 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/PrmApeRemoveAdd.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmApeRemoveAdd.cs @@ -1,6 +1,6 @@ namespace FrostFS.SDK.ClientV2; -public sealed class PrmApeChainAdd(FrostFsChainTarget target, FrostFsChain chain) : PrmBase +public sealed class PrmApeChainAdd(FrostFsChainTarget target, FrostFsChain chain, CallContext? ctx = null) : PrmBase(ctx) { public FrostFsChainTarget Target { get; } = target; diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmBalance.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmBalance.cs index 3a07fde..df36980 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/PrmBalance.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmBalance.cs @@ -1,5 +1,5 @@ namespace FrostFS.SDK.ClientV2; -public sealed class PrmBalance() : PrmBase +public sealed class PrmBalance(CallContext? ctx = null) : PrmBase(ctx) { } diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmBase.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmBase.cs index 79e5e7e..e6fb030 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/PrmBase.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmBase.cs @@ -2,7 +2,7 @@ namespace FrostFS.SDK.ClientV2; -public class PrmBase(NameValueCollection? xheaders = null) : IContext +public class PrmBase(CallContext? ctx, NameValueCollection? xheaders = null) : IContext { /// /// FrostFS request X-Headers @@ -10,5 +10,5 @@ public class PrmBase(NameValueCollection? xheaders = null) : IContext public NameValueCollection XHeaders { get; } = xheaders ?? []; /// - public CallContext? Context { get; set; } + public CallContext Context { get; } = ctx ?? new CallContext(); } diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmContainerCreate.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmContainerCreate.cs index da4c50b..68eadc9 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/PrmContainerCreate.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmContainerCreate.cs @@ -1,6 +1,6 @@ namespace FrostFS.SDK.ClientV2; -public sealed class PrmContainerCreate(FrostFsContainerInfo container) : PrmBase, ISessionToken +public sealed class PrmContainerCreate(FrostFsContainerInfo container, CallContext? ctx = null) : PrmBase(ctx), ISessionToken { public FrostFsContainerInfo Container { get; set; } = container; diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmContainerDelete.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmContainerDelete.cs index c97e80f..9de85f8 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/PrmContainerDelete.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmContainerDelete.cs @@ -1,6 +1,6 @@ namespace FrostFS.SDK.ClientV2; -public sealed class PrmContainerDelete(FrostFsContainerId containerId) : PrmBase, ISessionToken +public sealed class PrmContainerDelete(FrostFsContainerId containerId, CallContext? ctx = null) : PrmBase(ctx), ISessionToken { public FrostFsContainerId ContainerId { get; set; } = containerId; diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmContainerGet.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmContainerGet.cs index f0c734b..85f0821 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/PrmContainerGet.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmContainerGet.cs @@ -1,6 +1,6 @@ namespace FrostFS.SDK.ClientV2; -public sealed class PrmContainerGet(FrostFsContainerId container) : PrmBase +public sealed class PrmContainerGet(FrostFsContainerId container, CallContext? ctx = null) : PrmBase(ctx) { public FrostFsContainerId Container { get; set; } = container; } diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmContainerGetAll.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmContainerGetAll.cs index b7d3980..ceec0c1 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/PrmContainerGetAll.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmContainerGetAll.cs @@ -1,5 +1,5 @@ namespace FrostFS.SDK.ClientV2; -public sealed class PrmContainerGetAll() : PrmBase() +public sealed class PrmContainerGetAll(CallContext? ctx = null) : PrmBase(ctx) { } diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmNetmapSnapshot.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmNetmapSnapshot.cs index 6031ca6..0938f86 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/PrmNetmapSnapshot.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmNetmapSnapshot.cs @@ -1,5 +1,5 @@ namespace FrostFS.SDK.ClientV2; -public sealed class PrmNetmapSnapshot() : PrmBase +public sealed class PrmNetmapSnapshot(CallContext? ctx = null) : PrmBase(ctx) { } diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmNetworkSettings.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmNetworkSettings.cs index f014f14..de2c13d 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/PrmNetworkSettings.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmNetworkSettings.cs @@ -1,5 +1,5 @@ namespace FrostFS.SDK.ClientV2; -public sealed class PrmNetworkSettings() : PrmBase +public sealed class PrmNetworkSettings(CallContext? ctx = null) : PrmBase(ctx) { } diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmNodeInfo.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmNodeInfo.cs index 05b541d..dbbcdd8 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/PrmNodeInfo.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmNodeInfo.cs @@ -1,5 +1,5 @@ namespace FrostFS.SDK.ClientV2; -public sealed class PrmNodeInfo() : PrmBase +public sealed class PrmNodeInfo(CallContext? ctx = null) : PrmBase(ctx) { } diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmObjectDelete.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmObjectDelete.cs index f1dad65..e168b31 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/PrmObjectDelete.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmObjectDelete.cs @@ -1,6 +1,6 @@ namespace FrostFS.SDK.ClientV2; -public sealed class PrmObjectDelete(FrostFsContainerId containerId, FrostFsObjectId objectId) : PrmBase, ISessionToken +public sealed class PrmObjectDelete(FrostFsContainerId containerId, FrostFsObjectId objectId, CallContext? ctx = null) : PrmBase(ctx), ISessionToken { public FrostFsContainerId ContainerId { get; set; } = containerId; diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmObjectGet.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmObjectGet.cs index 81a1e2f..11e64d7 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/PrmObjectGet.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmObjectGet.cs @@ -1,6 +1,6 @@ namespace FrostFS.SDK.ClientV2; -public sealed class PrmObjectGet(FrostFsContainerId containerId, FrostFsObjectId objectId) : PrmBase, ISessionToken +public sealed class PrmObjectGet(FrostFsContainerId containerId, FrostFsObjectId objectId, CallContext? ctx = null) : PrmBase(ctx), ISessionToken { public FrostFsContainerId ContainerId { get; set; } = containerId; diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmObjectHeadGet.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmObjectHeadGet.cs index 40410f3..0b948b7 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/PrmObjectHeadGet.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmObjectHeadGet.cs @@ -1,6 +1,6 @@ namespace FrostFS.SDK.ClientV2; -public sealed class PrmObjectHeadGet(FrostFsContainerId containerId, FrostFsObjectId objectId) : PrmBase, ISessionToken +public sealed class PrmObjectHeadGet(FrostFsContainerId containerId, FrostFsObjectId objectId, CallContext? ctx = null) : PrmBase(ctx), ISessionToken { public FrostFsContainerId ContainerId { get; set; } = containerId; diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmObjectPut.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmObjectPut.cs index 3d216d5..47a1e1e 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/PrmObjectPut.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmObjectPut.cs @@ -2,7 +2,7 @@ using System.IO; namespace FrostFS.SDK.ClientV2; -public sealed class PrmObjectPut : PrmBase, ISessionToken +public sealed class PrmObjectPut(CallContext? ctx = null) : PrmBase(ctx), ISessionToken { /// /// Need to provide values like ContainerId and ObjectType to create and object. diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmObjectSearch.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmObjectSearch.cs index a9310dd..723c1e7 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/PrmObjectSearch.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmObjectSearch.cs @@ -2,7 +2,7 @@ namespace FrostFS.SDK.ClientV2; -public sealed class PrmObjectSearch(FrostFsContainerId containerId, params IObjectFilter[] filters) : PrmBase, ISessionToken +public sealed class PrmObjectSearch(FrostFsContainerId containerId, CallContext? ctx = null, params IObjectFilter[] filters) : PrmBase(ctx), ISessionToken { /// /// Defines container for the search diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmSessionCreate.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmSessionCreate.cs index d7bcdb6..ab02f69 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/PrmSessionCreate.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmSessionCreate.cs @@ -1,6 +1,6 @@ namespace FrostFS.SDK.ClientV2; -public sealed class PrmSessionCreate(ulong expiration) : PrmBase +public sealed class PrmSessionCreate(ulong expiration, CallContext? ctx = null) : PrmBase(ctx) { public ulong Expiration { get; set; } = expiration; } diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmSingleObjectPut.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmSingleObjectPut.cs index 3a8fa5d..8cb8c19 100644 --- a/src/FrostFS.SDK.ClientV2/Parameters/PrmSingleObjectPut.cs +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmSingleObjectPut.cs @@ -1,6 +1,6 @@ namespace FrostFS.SDK.ClientV2; -public sealed class PrmSingleObjectPut(FrostFsObject frostFsObject) : PrmBase, ISessionToken +public sealed class PrmSingleObjectPut(FrostFsObject frostFsObject, CallContext? ctx = null) : PrmBase(ctx), ISessionToken { public FrostFsObject FrostFsObject { get; set; } = frostFsObject; diff --git a/src/FrostFS.SDK.ClientV2/Poll/ClientStatusMonitor.cs b/src/FrostFS.SDK.ClientV2/Poll/ClientStatusMonitor.cs index e864148..7200b37 100644 --- a/src/FrostFS.SDK.ClientV2/Poll/ClientStatusMonitor.cs +++ b/src/FrostFS.SDK.ClientV2/Poll/ClientStatusMonitor.cs @@ -27,8 +27,7 @@ public class ClientStatusMonitor : IClientStatus MethodIndex.methodSessionCreate, MethodIndex.methodAPEManagerAddChain, MethodIndex.methodAPEManagerRemoveChain, - MethodIndex.methodAPEManagerListChains, - MethodIndex.methodLast + MethodIndex.methodAPEManagerListChains ]; public static string GetMethodName(MethodIndex index) @@ -53,7 +52,7 @@ public class ClientStatusMonitor : IClientStatus MethodIndex.methodAPEManagerAddChain => "APEManagerAddChain", MethodIndex.methodAPEManagerRemoveChain => "APEManagerRemoveChain", MethodIndex.methodAPEManagerListChains => "APEManagerListChains", - _ => throw new NotImplementedException(), + _ => throw new ArgumentException("Unknown method", nameof(index)), }; } @@ -62,13 +61,12 @@ public class ClientStatusMonitor : IClientStatus private readonly ILogger? logger; private int healthy; - public ClientStatusMonitor(ILogger? logger, string address, uint errorThreshold) + public ClientStatusMonitor(ILogger? logger, string address) { this.logger = logger; healthy = (int)HealthyStatus.Healthy; - Address = address; - ErrorThreshold = errorThreshold; + Address = address; Methods = new MethodStatus[MethodIndexes.Length]; for (int i = 0; i < MethodIndexes.Length; i++) @@ -79,7 +77,7 @@ public class ClientStatusMonitor : IClientStatus public string Address { get; } - internal uint ErrorThreshold { get; } + internal uint ErrorThreshold { get; set; } public uint CurrentErrorCount { get; set; } @@ -89,7 +87,8 @@ public class ClientStatusMonitor : IClientStatus public bool IsHealthy() { - return Interlocked.CompareExchange(ref healthy, -1, -1) == (int)HealthyStatus.Healthy; + var res = Interlocked.CompareExchange(ref healthy, -1, -1) == (int)HealthyStatus.Healthy; + return res; } public bool IsDialed() @@ -124,14 +123,13 @@ public class ClientStatusMonitor : IClientStatus if (thresholdReached) { SetUnhealthy(); - CurrentErrorCount = 0; } } - if (thresholdReached) + if (thresholdReached && logger != null) { - logger?.Log(LogLevel.Warning, "Error threshold reached. Address {Address}, threshold {Threshold}", Address, ErrorThreshold); + FrostFsMessages.ErrorЕhresholdReached(logger, Address, ErrorThreshold); } } diff --git a/src/FrostFS.SDK.ClientV2/Poll/ClientWrapper.cs b/src/FrostFS.SDK.ClientV2/Poll/ClientWrapper.cs index 3891250..ddde002 100644 --- a/src/FrostFS.SDK.ClientV2/Poll/ClientWrapper.cs +++ b/src/FrostFS.SDK.ClientV2/Poll/ClientWrapper.cs @@ -1,38 +1,36 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; + +using Grpc.Core; namespace FrostFS.SDK.ClientV2; // clientWrapper is used by default, alternative implementations are intended for testing purposes only. -public class ClientWrapper +public class ClientWrapper : ClientStatusMonitor { private readonly object _lock = new(); - public ClientWrapper(WrapperPrm wrapperPrm) + private SessionCache sessionCache; + + + internal ClientWrapper(WrapperPrm wrapperPrm, Pool pool) : base(wrapperPrm.Logger, wrapperPrm.Address) { WrapperPrm = wrapperPrm; - StatusMonitor = new ClientStatusMonitor(wrapperPrm.Logger, wrapperPrm.Address, wrapperPrm.ErrorThreshold); + ErrorThreshold = wrapperPrm.ErrorThreshold; - try - { - Client = new FrostFSClient(WrapperPrm); - StatusMonitor.SetHealthy(); - } - catch (FrostFsException) - { - } + sessionCache = pool.SessionCache; + Client = new FrostFSClient(WrapperPrm, sessionCache); } internal FrostFSClient? Client { get; private set; } internal WrapperPrm WrapperPrm { get; } - internal ClientStatusMonitor StatusMonitor { get; } - internal FrostFSClient? GetClient() { lock (_lock) { - if (StatusMonitor.IsHealthy()) + if (IsHealthy()) { return Client; } @@ -44,21 +42,29 @@ public class ClientWrapper // dial establishes a connection to the server from the FrostFS network. // Returns an error describing failure reason. If failed, the client // SHOULD NOT be used. - internal async Task Dial(CallContext ctx) + internal async Task Dial(CallContext ctx) { - var client = GetClient(); + var client = GetClient() ?? throw new FrostFsInvalidObjectException("pool client unhealthy"); - if (client == null) - return "pool client unhealthy"; + await client.Dial(ctx).ConfigureAwait(false); + } - var result = await client.Dial(ctx).ConfigureAwait(false); - if (!string.IsNullOrEmpty(result)) + internal void HandleError(Exception ex) + { + if (ex is FrostFsResponseException responseException && responseException.Status != null) { - StatusMonitor.SetUnhealthyOnDial(); - return result; + switch (responseException.Status.Code) + { + case FrostFsStatusCode.Internal: + case FrostFsStatusCode.WrongMagicNumber: + case FrostFsStatusCode.SignatureVerificationFailure: + case FrostFsStatusCode.NodeUnderMaintenance: + IncErrorRate(); + return; + } } - return null; + IncErrorRate(); } private async Task ScheduleGracefulClose() @@ -79,31 +85,31 @@ public class ClientWrapper try { - var prmNodeInfo = new PrmNodeInfo { Context = ctx }; + var prmNodeInfo = new PrmNodeInfo(ctx); var response = await Client!.GetNodeInfoAsync(prmNodeInfo).ConfigureAwait(false); return false; } - catch (FrostFsException) + catch (RpcException) { wasHealthy = true; } // if connection is dialed before, to avoid routine/connection leak, // pool has to close it and then initialize once again. - if (StatusMonitor.IsDialed()) + if (IsDialed()) { await ScheduleGracefulClose().ConfigureAwait(false); } #pragma warning disable CA2000 // Dispose objects before losing scope: will be disposed manually - FrostFSClient client = new(WrapperPrm); + FrostFSClient client = new(WrapperPrm, sessionCache); #pragma warning restore CA2000 //TODO: set additioanl params var error = await client.Dial(ctx).ConfigureAwait(false); if (!string.IsNullOrEmpty(error)) { - StatusMonitor.SetUnhealthyOnDial(); + SetUnhealthyOnDial(); return wasHealthy; } @@ -114,22 +120,22 @@ public class ClientWrapper try { - var prmNodeInfo = new PrmNodeInfo { Context = ctx }; + var prmNodeInfo = new PrmNodeInfo(ctx); var res = await client.GetNodeInfoAsync(prmNodeInfo).ConfigureAwait(false); } catch (FrostFsException) { - StatusMonitor.SetUnhealthy(); + SetUnhealthy(); return wasHealthy; } - StatusMonitor.SetHealthy(); + SetHealthy(); return !wasHealthy; } internal void IncRequests(ulong elapsed, MethodIndex method) { - var methodStat = StatusMonitor.Methods[(int)method]; + var methodStat = Methods[(int)method]; methodStat.IncRequests(elapsed); } diff --git a/src/FrostFS.SDK.ClientV2/Poll/DialOptions.cs b/src/FrostFS.SDK.ClientV2/Poll/DialOptions.cs deleted file mode 100644 index 813d373..0000000 --- a/src/FrostFS.SDK.ClientV2/Poll/DialOptions.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace FrostFS.SDK.ClientV2; - -public class DialOptions -{ - public bool Block { get; set; } - - public bool ReturnLastError { get; set; } - - public ulong Timeout { get; set; } - - public string? Authority { get; set; } - - public bool DisableRetry { get; set; } - - public bool DisableHealthCheck { get; set; } -} diff --git a/src/FrostFS.SDK.ClientV2/Poll/InitParameters.cs b/src/FrostFS.SDK.ClientV2/Poll/InitParameters.cs index e05c31a..f02bd9e 100644 --- a/src/FrostFS.SDK.ClientV2/Poll/InitParameters.cs +++ b/src/FrostFS.SDK.ClientV2/Poll/InitParameters.cs @@ -1,6 +1,8 @@ using System; using System.Security.Cryptography; +using Grpc.Net.Client; + using Microsoft.Extensions.Logging; namespace FrostFS.SDK.ClientV2; @@ -24,7 +26,7 @@ public class InitParameters public NodeParam[]? NodeParams { get; set; } - public DialOptions[]? DialOptions { get; set; } + public GrpcChannelOptions[]? DialOptions { get; set; } public Func? ClientBuilder { get; set; } diff --git a/src/FrostFS.SDK.ClientV2/Poll/InnerPool.cs b/src/FrostFS.SDK.ClientV2/Poll/InnerPool.cs index a535260..104f1f1 100644 --- a/src/FrostFS.SDK.ClientV2/Poll/InnerPool.cs +++ b/src/FrostFS.SDK.ClientV2/Poll/InnerPool.cs @@ -21,7 +21,7 @@ internal sealed class InnerPool if (Clients.Length == 1) { var client = Clients[0]; - if (client.StatusMonitor.IsHealthy()) + if (client.IsHealthy()) { return client; } @@ -34,7 +34,7 @@ internal sealed class InnerPool { int index = Sampler.Next(); - if (Clients[index].StatusMonitor.IsHealthy()) + if (Clients[index].IsHealthy()) { return Clients[index]; } diff --git a/src/FrostFS.SDK.ClientV2/Poll/Pool.cs b/src/FrostFS.SDK.ClientV2/Poll/Pool.cs index a304e93..2602746 100644 --- a/src/FrostFS.SDK.ClientV2/Poll/Pool.cs +++ b/src/FrostFS.SDK.ClientV2/Poll/Pool.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -13,6 +12,8 @@ using FrostFS.SDK.ClientV2.Interfaces; using FrostFS.SDK.ClientV2.Mappers.GRPC; using FrostFS.SDK.Cryptography; +using Grpc.Core; + using Microsoft.Extensions.Logging; @@ -36,7 +37,7 @@ public partial class Pool : IFrostFSClient private ECDsa Key { get; set; } - private byte[] PublicKey { get; } + private string PublicKey { get; } private OwnerID? _ownerId; private FrostFsOwner? _owner; @@ -65,7 +66,7 @@ public partial class Pool : IFrostFSClient internal CancellationTokenSource CancellationTokenSource { get; } = new CancellationTokenSource(); - private SessionCache Cache { get; set; } + internal SessionCache SessionCache { get; set; } private ulong SessionTokenDuration { get; set; } @@ -75,7 +76,7 @@ public partial class Pool : IFrostFSClient private bool disposedValue; - private ILogger? Logger { get; set; } + private ILogger? logger { get; set; } private ulong MaxObjectSize { get; set; } @@ -91,20 +92,20 @@ public partial class Pool : IFrostFSClient if (options.Key == null) { - throw new FrostFsException($"Missed required parameter {nameof(options.Key)}"); + throw new ArgumentException($"Missed required parameter {nameof(options.Key)}"); } var nodesParams = AdjustNodeParams(options.NodeParams); var cache = new SessionCache(options.SessionExpirationDuration); - FillDefaultInitParams(options, cache); + FillDefaultInitParams(options, this); Key = options.Key; - PublicKey = Key.PublicKey(); + PublicKey = $"{Key.PublicKey()}"; - Cache = cache; - Logger = options.Logger; + SessionCache = cache; + logger = options.Logger; SessionTokenDuration = options.SessionExpirationDuration; RebalanceParams = new RebalanceParameters( @@ -148,47 +149,54 @@ public partial class Pool : IFrostFSClient for (int j = 0; j < nodeParams.Addresses.Count; j++) { - var client = ClientBuilder(nodeParams.Addresses[j]); - clients[j] = client; - - var error = await client.Dial(ctx).ConfigureAwait(false); - if (!string.IsNullOrEmpty(error)) - { - Logger?.LogWarning("Failed to build client. Address {Address}, {Error})", client.WrapperPrm.Address, error); - continue; - } - + ClientWrapper? client = null; + bool dialed = false; try { + client = clients[j] = ClientBuilder(nodeParams.Addresses[j]); + + await client.Dial(ctx).ConfigureAwait(false); + dialed = true; + var token = await InitSessionForDuration(ctx, client, RebalanceParams.SessionExpirationDuration, Key, false) .ConfigureAwait(false); - var key = FormCacheKey(nodeParams.Addresses[j], Key, false); - _ = Cache.Cache[key] = token; + var key = FormCacheKey(nodeParams.Addresses[j], Key.PrivateKey().ToString()); + _ = SessionCache.Cache[key] = token; + + atLeastOneHealthy = true; } - catch (FrostFsException ex) + catch (RpcException ex) { - client.StatusMonitor.SetUnhealthy(); - Logger?.LogWarning("Failed to create frostfs session token for client. Address {Address}, {Error})", - client.WrapperPrm.Address, ex.Message); + if (!dialed) + client!.SetUnhealthyOnDial(); + else + client!.SetUnhealthy(); - continue; + if (logger != null) + { + FrostFsMessages.SessionCreationError(logger, client!.WrapperPrm.Address, ex.Message); + } + } + catch (FrostFsInvalidObjectException) + { + break; } - - atLeastOneHealthy = true; } var sampler = new Sampler(nodeParams.Weights.ToArray()); inner[i] = new InnerPool(sampler, clients); + + i++; } if (!atLeastOneHealthy) - return "at least one node must be healthy"; + return "At least one node must be healthy"; InnerPools = inner; - var res = await GetNetworkSettingsAsync(new PrmNetworkSettings { Context = ctx }).ConfigureAwait(false); + var res = await GetNetworkSettingsAsync(new PrmNetworkSettings(ctx)).ConfigureAwait(false); MaxObjectSize = res.MaxObjectSize; @@ -252,7 +260,7 @@ public partial class Pool : IFrostFSClient return adjusted; } - private static void FillDefaultInitParams(InitParameters parameters, SessionCache cache) + private static void FillDefaultInitParams(InitParameters parameters, Pool pool) { if (parameters.SessionExpirationDuration == 0) parameters.SessionExpirationDuration = defaultSessionTokenExpirationDuration; @@ -275,8 +283,8 @@ public partial class Pool : IFrostFSClient if (parameters.NodeStreamTimeout <= 0) parameters.NodeStreamTimeout = defaultStreamTimeout; - if (cache.TokenDuration == 0) - cache.TokenDuration = defaultSessionTokenExpirationDuration; + if (parameters.SessionExpirationDuration == 0) + parameters.SessionExpirationDuration = defaultSessionTokenExpirationDuration; parameters.ClientBuilder ??= new Func((address) => { @@ -291,29 +299,29 @@ public partial class Pool : IFrostFSClient GracefulCloseOnSwitchTimeout = parameters.GracefulCloseOnSwitchTimeout }; - return new ClientWrapper(wrapperPrm); + return new ClientWrapper(wrapperPrm, pool); } ); } - private FrostFSClient? Сonnection() + private ClientWrapper Сonnection() { foreach (var pool in InnerPools!) { var client = pool.Connection(); if (client != null) { - return client.Client; + return client; } } - return null; + throw new FrostFsException("Cannot find alive client"); } private static async Task InitSessionForDuration(CallContext ctx, ClientWrapper cw, ulong duration, ECDsa key, bool clientCut) { var client = cw.Client; - var networkInfo = await client!.GetNetworkSettingsAsync(new PrmNetworkSettings { Context = ctx }).ConfigureAwait(false); + var networkInfo = await client!.GetNetworkSettingsAsync(new PrmNetworkSettings(ctx)).ConfigureAwait(false); var epoch = networkInfo.Epoch; @@ -321,17 +329,14 @@ public partial class Pool : IFrostFSClient ? ulong.MaxValue : epoch + duration; - var prmSessionCreate = new PrmSessionCreate(exp) { Context = ctx }; + var prmSessionCreate = new PrmSessionCreate(exp, ctx); return await client.CreateSessionAsync(prmSessionCreate).ConfigureAwait(false); } - private static string FormCacheKey(string address, ECDsa key, bool clientCut) + internal static string FormCacheKey(string address, string key) { - var k = key.PrivateKey; - var stype = clientCut ? "client" : "server"; - - return $"{address}{stype}{k}"; + return $"{address}{key}"; } public void Close() @@ -343,7 +348,7 @@ public partial class Pool : IFrostFSClient // close all clients foreach (var innerPool in InnerPools) foreach (var client in innerPool.Clients) - if (client.StatusMonitor.IsDialed()) + if (client.IsDialed()) client.Client?.Close(); } } @@ -355,7 +360,7 @@ public partial class Pool : IFrostFSClient for (int i = 0; i < RebalanceParams.NodesParams.Length; i++) { - var parameters = this.RebalanceParams.NodesParams[i]; + var parameters = RebalanceParams.NodesParams[i]; buffers[i] = new double[parameters.Weights.Count]; Task.Run(async () => @@ -405,25 +410,27 @@ public partial class Pool : IFrostFSClient try { // check timeout settings - changed = await client.RestartIfUnhealthy(ctx).ConfigureAwait(false); + changed = await client!.RestartIfUnhealthy(ctx).ConfigureAwait(false); healthy = true; bufferWeights[j] = options.NodesParams[poolIndex].Weights[j]; } + // TODO: specify catch (FrostFsException e) { error = e.Message; bufferWeights[j] = 0; - Cache.DeleteByPrefix(client.StatusMonitor.Address); + SessionCache.DeleteByPrefix(client.Address); } if (changed) { - StringBuilder fields = new($"address {client.StatusMonitor.Address}, healthy {healthy}"); - if (string.IsNullOrEmpty(error)) + if (!string.IsNullOrEmpty(error)) { - fields.Append($", reason {error}"); - Logger?.Log(LogLevel.Warning, "Health has changed: {Fields}", fields.ToString()); + if (logger != null) + { + FrostFsMessages.HealthChanged(logger, client.Address, healthy, error!); + } Interlocked.Exchange(ref healthyChanged, 1); } @@ -443,6 +450,8 @@ public partial class Pool : IFrostFSClient } } + + // TODO: remove private bool CheckSessionTokenErr(Exception error, string address) { if (error == null) @@ -452,7 +461,7 @@ public partial class Pool : IFrostFSClient if (error is SessionNotFoundException || error is SessionExpiredException) { - this.Cache.DeleteByPrefix(address); + this.SessionCache.DeleteByPrefix(address); return true; } @@ -463,14 +472,13 @@ public partial class Pool : IFrostFSClient { if (InnerPools == null) { - throw new InvalidObjectException(nameof(Pool)); + throw new FrostFsInvalidObjectException(nameof(Pool)); } var statistics = new Statistic(); foreach (var inner in InnerPools) { - int nodeIndex = 0; int valueIndex = 0; var nodes = new string[inner.Clients.Length]; @@ -478,20 +486,22 @@ public partial class Pool : IFrostFSClient { foreach (var client in inner.Clients) { - if (client.StatusMonitor.IsHealthy()) + if (client.IsHealthy()) { - nodes[valueIndex++] = client.StatusMonitor.Address; + nodes[valueIndex] = client.Address; } var node = new NodeStatistic { - Address = client.StatusMonitor.Address, - Methods = client.StatusMonitor.MethodsStatus(), - OverallErrors = client.StatusMonitor.GetOverallErrorRate(), - CurrentErrors = client.StatusMonitor.GetCurrentErrorRate() + Address = client.Address, + Methods = client.MethodsStatus(), + OverallErrors = client.GetOverallErrorRate(), + CurrentErrors = client.GetCurrentErrorRate() }; - statistics.Nodes[nodeIndex++] = node; + statistics.Nodes.Add(node); + + valueIndex++; statistics.OverallErrors += node.OverallErrors; } @@ -508,120 +518,234 @@ public partial class Pool : IFrostFSClient public async Task GetNetmapSnapshotAsync(PrmNetmapSnapshot? args = null) { - var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); - return await client.GetNetmapSnapshotAsync(args).ConfigureAwait(false); + var client = Сonnection(); + + args ??= new(); + args.Context.PoolErrorHandler = client.HandleError; + + return await client.Client!.GetNetmapSnapshotAsync(args).ConfigureAwait(false); } public async Task GetNodeInfoAsync(PrmNodeInfo? args = null) { - var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); - return await client.GetNodeInfoAsync(args).ConfigureAwait(false); + var client = Сonnection(); + + args ??= new(); + args.Context.PoolErrorHandler = client.HandleError; + + return await client.Client!.GetNodeInfoAsync(args).ConfigureAwait(false); } public async Task GetNetworkSettingsAsync(PrmNetworkSettings? args = null) { - var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); - return await client.GetNetworkSettingsAsync(args).ConfigureAwait(false); + var client = Сonnection(); + + args ??= new(); + args.Context.PoolErrorHandler = client.HandleError; + + return await client.Client!.GetNetworkSettingsAsync(args).ConfigureAwait(false); } public async Task CreateSessionAsync(PrmSessionCreate args) { - var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); - return await client.CreateSessionAsync(args).ConfigureAwait(false); + if (args is null) + { + throw new ArgumentNullException(nameof(args)); + } + + var client = Сonnection(); + + args.Context.PoolErrorHandler = client.HandleError; + + return await client.Client!.CreateSessionAsync(args).ConfigureAwait(false); } public async Task AddChainAsync(PrmApeChainAdd args) { - var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); - return await client.AddChainAsync(args).ConfigureAwait(false); + if (args is null) + { + throw new ArgumentNullException(nameof(args)); + } + + var client = Сonnection(); + + args.Context.PoolErrorHandler = client.HandleError; + + return await client.Client!.AddChainAsync(args).ConfigureAwait(false); } public async Task RemoveChainAsync(PrmApeChainRemove args) { - var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); - await client.RemoveChainAsync(args).ConfigureAwait(false); + if (args is null) + { + throw new ArgumentNullException(nameof(args)); + } + + var client = Сonnection(); + + args.Context.PoolErrorHandler = client.HandleError; + + await client.Client!.RemoveChainAsync(args).ConfigureAwait(false); } public async Task ListChainAsync(PrmApeChainList args) { - var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); - return await client.ListChainAsync(args).ConfigureAwait(false); + if (args is null) + { + throw new ArgumentNullException(nameof(args)); + } + + var client = Сonnection(); + + args.Context.PoolErrorHandler = client.HandleError; + + return await client.Client!.ListChainAsync(args).ConfigureAwait(false); } public async Task GetContainerAsync(PrmContainerGet args) { - var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); - return await client.GetContainerAsync(args).ConfigureAwait(false); + if (args is null) + { + throw new ArgumentNullException(nameof(args)); + } + + var client = Сonnection(); + + args.Context.PoolErrorHandler = client.HandleError; + + return await client.Client!.GetContainerAsync(args).ConfigureAwait(false); } public IAsyncEnumerable ListContainersAsync(PrmContainerGetAll? args = null) { - var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); - return client.ListContainersAsync(args); + var client = Сonnection(); + + args ??= new(); + args.Context.PoolErrorHandler = client.HandleError; + + return client.Client!.ListContainersAsync(args); } public async Task CreateContainerAsync(PrmContainerCreate args) { - var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); - return await client.CreateContainerAsync(args).ConfigureAwait(false); + if (args is null) + { + throw new ArgumentNullException(nameof(args)); + } + + var client = Сonnection(); + + args.Context.PoolErrorHandler = client.HandleError; + + return await client.Client!.CreateContainerAsync(args).ConfigureAwait(false); } public async Task DeleteContainerAsync(PrmContainerDelete args) { - var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); - await client.DeleteContainerAsync(args).ConfigureAwait(false); + if (args is null) + { + throw new ArgumentNullException(nameof(args)); + } + + var client = Сonnection(); + + args.Context.PoolErrorHandler = client.HandleError; + + await client.Client!.DeleteContainerAsync(args).ConfigureAwait(false); } public async Task GetObjectHeadAsync(PrmObjectHeadGet args) { - var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); - return await client.GetObjectHeadAsync(args).ConfigureAwait(false); + if (args is null) + { + throw new ArgumentNullException(nameof(args)); + } + + var client = Сonnection(); + + args.Context.PoolErrorHandler = client.HandleError; + + return await client.Client!.GetObjectHeadAsync(args).ConfigureAwait(false); } public async Task GetObjectAsync(PrmObjectGet args) { - var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); - return await client.GetObjectAsync(args).ConfigureAwait(false); + if (args is null) + { + throw new ArgumentNullException(nameof(args)); + } + + var client = Сonnection(); + + args.Context.PoolErrorHandler = client.HandleError; + + return await client.Client!.GetObjectAsync(args).ConfigureAwait(false); } public async Task PutObjectAsync(PrmObjectPut args) { - var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); - return await client.PutObjectAsync(args).ConfigureAwait(false); + if (args is null) + { + throw new ArgumentNullException(nameof(args)); + } + + var client = Сonnection(); + + args.Context.PoolErrorHandler = client.HandleError; + + return await client.Client!.PutObjectAsync(args).ConfigureAwait(false); } public async Task PutSingleObjectAsync(PrmSingleObjectPut args) { - var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); - return await client.PutSingleObjectAsync(args).ConfigureAwait(false); + if (args is null) + { + throw new ArgumentNullException(nameof(args)); + } + + var client = Сonnection(); + + args.Context.PoolErrorHandler = client.HandleError; + + return await client.Client!.PutSingleObjectAsync(args).ConfigureAwait(false); } public async Task DeleteObjectAsync(PrmObjectDelete args) { - var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); - await client.DeleteObjectAsync(args).ConfigureAwait(false); + if (args is null) + { + throw new ArgumentNullException(nameof(args)); + } + + var client = Сonnection(); + + args.Context.PoolErrorHandler = client.HandleError; + + await client.Client!.DeleteObjectAsync(args).ConfigureAwait(false); } public IAsyncEnumerable SearchObjectsAsync(PrmObjectSearch args) { - var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); - return client.SearchObjectsAsync(args); + if (args is null) + { + throw new ArgumentNullException(nameof(args)); + } + + var client = Сonnection(); + + args.Context.PoolErrorHandler = client.HandleError; + + return client.Client!.SearchObjectsAsync(args); } - public async Task GetBalanceAsync(PrmBalance? args = null) + public async Task GetBalanceAsync(PrmBalance? args) { - var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); - return await client.GetBalanceAsync(args).ConfigureAwait(false); - } + var client = Сonnection(); - public bool RestartIfUnhealthy(CallContext ctx) - { - throw new NotImplementedException(); - } + args ??= new(); + args.Context.PoolErrorHandler = client.HandleError; - public bool IsHealthy() - { - throw new NotImplementedException(); + return await client.Client!.GetBalanceAsync(args).ConfigureAwait(false); } protected virtual void Dispose(bool disposing) diff --git a/src/FrostFS.SDK.ClientV2/Poll/RebalanceParameters.cs b/src/FrostFS.SDK.ClientV2/Poll/RebalanceParameters.cs index b1bc9b9..a443b2d 100644 --- a/src/FrostFS.SDK.ClientV2/Poll/RebalanceParameters.cs +++ b/src/FrostFS.SDK.ClientV2/Poll/RebalanceParameters.cs @@ -3,7 +3,7 @@ public class RebalanceParameters( NodesParam[] nodesParams, ulong nodeRequestTimeout, - ulong clientRebalanceInterval, + ulong clientRebalanceInterval, ulong sessionExpirationDuration) { public NodesParam[] NodesParams { get; set; } = nodesParams; diff --git a/src/FrostFS.SDK.ClientV2/Poll/SessionCache.cs b/src/FrostFS.SDK.ClientV2/Poll/SessionCache.cs index eccb0a5..f65046f 100644 --- a/src/FrostFS.SDK.ClientV2/Poll/SessionCache.cs +++ b/src/FrostFS.SDK.ClientV2/Poll/SessionCache.cs @@ -3,18 +3,13 @@ using System.Collections; namespace FrostFS.SDK.ClientV2; -internal struct SessionCache +internal struct SessionCache(ulong sessionExpirationDuration) { - public SessionCache(ulong sessionExpirationDuration) - { - TokenDuration = sessionExpirationDuration; - } - internal Hashtable Cache { get; } = []; internal ulong CurrentEpoch { get; set; } - internal ulong TokenDuration { get; set; } + internal ulong TokenDuration { get; set; } = sessionExpirationDuration; internal void DeleteByPrefix(string prefix) { diff --git a/src/FrostFS.SDK.ClientV2/Services/AccountingServiceProvider.cs b/src/FrostFS.SDK.ClientV2/Services/AccountingServiceProvider.cs index 94dda54..b5fa42b 100644 --- a/src/FrostFS.SDK.ClientV2/Services/AccountingServiceProvider.cs +++ b/src/FrostFS.SDK.ClientV2/Services/AccountingServiceProvider.cs @@ -10,7 +10,7 @@ internal sealed class AccountingServiceProvider : ContextAccessor internal AccountingServiceProvider( AccountingService.AccountingServiceClient? accountingServiceClient, - EnvironmentContext context) + ClientContext context) : base(context) { _accountingServiceClient = accountingServiceClient; diff --git a/src/FrostFS.SDK.ClientV2/Services/ApeManagerServiceProvider.cs b/src/FrostFS.SDK.ClientV2/Services/ApeManagerServiceProvider.cs index 1c5ab8c..d05e636 100644 --- a/src/FrostFS.SDK.ClientV2/Services/ApeManagerServiceProvider.cs +++ b/src/FrostFS.SDK.ClientV2/Services/ApeManagerServiceProvider.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using Frostfs.V2.Ape; @@ -9,7 +10,7 @@ internal sealed class ApeManagerServiceProvider : ContextAccessor { private readonly APEManagerService.APEManagerServiceClient? _apeManagerServiceClient; - internal ApeManagerServiceProvider(APEManagerService.APEManagerServiceClient? apeManagerServiceClient, EnvironmentContext context) + internal ApeManagerServiceProvider(APEManagerService.APEManagerServiceClient? apeManagerServiceClient, ClientContext context) : base(context) { _apeManagerServiceClient = apeManagerServiceClient; @@ -18,10 +19,10 @@ internal sealed class ApeManagerServiceProvider : ContextAccessor internal async Task AddChainAsync(PrmApeChainAdd args) { var ctx = args.Context!; - ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; + ctx.Key ??= ClientContext.Key?.ECDsaKey; if (ctx.Key == null) - throw new InvalidObjectException(nameof(ctx.Key)); + throw new ArgumentNullException(nameof(args), "Key is null"); AddChainRequest request = new() { @@ -45,10 +46,10 @@ internal sealed class ApeManagerServiceProvider : ContextAccessor internal async Task RemoveChainAsync(PrmApeChainRemove args) { var ctx = args.Context!; - ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; + ctx.Key ??= ClientContext.Key?.ECDsaKey; if (ctx.Key == null) - throw new InvalidObjectException(nameof(ctx.Key)); + throw new ArgumentNullException(nameof(args), "Key is null"); RemoveChainRequest request = new() { @@ -70,10 +71,10 @@ internal sealed class ApeManagerServiceProvider : ContextAccessor internal async Task ListChainAsync(PrmApeChainList args) { var ctx = args.Context!; - ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; + ctx.Key ??= ClientContext.Key?.ECDsaKey; if (ctx.Key == null) - throw new InvalidObjectException(nameof(ctx.Key)); + throw new ArgumentNullException(nameof(args), "Key is null"); ListChainsRequest request = new() { diff --git a/src/FrostFS.SDK.ClientV2/Services/ContainerServiceProvider.cs b/src/FrostFS.SDK.ClientV2/Services/ContainerServiceProvider.cs index 14a3e2d..4e00a7d 100644 --- a/src/FrostFS.SDK.ClientV2/Services/ContainerServiceProvider.cs +++ b/src/FrostFS.SDK.ClientV2/Services/ContainerServiceProvider.cs @@ -12,20 +12,28 @@ using FrostFS.Session; namespace FrostFS.SDK.ClientV2; -internal sealed class ContainerServiceProvider(ContainerService.ContainerServiceClient service, EnvironmentContext envCtx) : ContextAccessor(envCtx), ISessionProvider +internal sealed class ContainerServiceProvider(ContainerService.ContainerServiceClient service, ClientContext clientCtx) : ContextAccessor(clientCtx), ISessionProvider { - readonly SessionProvider sessions = new(envCtx); + private SessionProvider? sessions; public async ValueTask GetOrCreateSession(ISessionToken args, CallContext ctx) { + sessions ??= new(ClientContext); + + if (ClientContext.SessionCache.Cache != null && + ClientContext.SessionCache.Cache.ContainsKey(ClientContext.SessionCacheKey)) + { + return (SessionToken)ClientContext.SessionCache.Cache[ClientContext.SessionCacheKey]; + } + return await sessions.GetOrCreateSession(args, ctx).ConfigureAwait(false); } internal async Task GetContainerAsync(PrmContainerGet args) { - GetRequest request = GetContainerRequest(args.Container.ContainerID, args.XHeaders, args.Context!); + GetRequest request = GetContainerRequest(args.Container.ContainerID, args.XHeaders, args.Context); - var response = await service.GetAsync(request, null, args.Context!.Deadline, args.Context.CancellationToken); + var response = await service.GetAsync(request, null, args.Context.Deadline, args.Context.CancellationToken); Verifier.CheckResponse(response); @@ -35,13 +43,13 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService internal async IAsyncEnumerable ListContainersAsync(PrmContainerGetAll args) { var ctx = args.Context!; - ctx.OwnerId ??= EnvironmentContext.Owner; - ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; + ctx.OwnerId ??= ClientContext.Owner; + ctx.Key ??= ClientContext.Key?.ECDsaKey; if (ctx.Key == null) - throw new InvalidObjectException(nameof(ctx.Key)); + throw new ArgumentNullException(nameof(args), "Key is null"); if (ctx.OwnerId == null) - throw new InvalidObjectException(nameof(ctx.OwnerId)); + throw new ArgumentException(nameof(ctx.OwnerId)); var request = new ListRequest { @@ -74,11 +82,11 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService grpcContainer.Version ??= ctx.Version?.ToMessage(); if (ctx.Key == null) - throw new InvalidObjectException(nameof(ctx.Key)); + throw new ArgumentNullException(nameof(args), "Key is null"); if (grpcContainer.OwnerId == null) - throw new InvalidObjectException(nameof(grpcContainer.OwnerId)); + throw new ArgumentException(nameof(grpcContainer.OwnerId)); if (grpcContainer.Version == null) - throw new InvalidObjectException(nameof(grpcContainer.Version)); + throw new ArgumentException(nameof(grpcContainer.Version)); var request = new PutRequest { @@ -114,7 +122,7 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService { var ctx = args.Context!; if (ctx.Key == null) - throw new InvalidObjectException(nameof(ctx.Key)); + throw new ArgumentNullException(nameof(args), "Key is null"); var request = new DeleteRequest { @@ -150,7 +158,7 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService private static GetRequest GetContainerRequest(ContainerID id, NameValueCollection? xHeaders, CallContext ctx) { if (ctx.Key == null) - throw new InvalidObjectException(nameof(ctx.Key)); + throw new ArgumentNullException(nameof(ctx), "Key is null"); var request = new GetRequest { @@ -207,7 +215,7 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService await Task.Delay(waitParams.PollInterval).ConfigureAwait(false); } - catch (ResponseException ex) + catch (FrostFsResponseException ex) { if (DateTime.UtcNow >= deadLine) throw new TimeoutException(); diff --git a/src/FrostFS.SDK.ClientV2/Services/NetmapServiceProvider.cs b/src/FrostFS.SDK.ClientV2/Services/NetmapServiceProvider.cs index 91645fb..0241e31 100644 --- a/src/FrostFS.SDK.ClientV2/Services/NetmapServiceProvider.cs +++ b/src/FrostFS.SDK.ClientV2/Services/NetmapServiceProvider.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Text; using System.Threading.Tasks; @@ -12,7 +13,7 @@ internal sealed class NetmapServiceProvider : ContextAccessor { private readonly NetmapService.NetmapServiceClient netmapServiceClient; - internal NetmapServiceProvider(NetmapService.NetmapServiceClient netmapServiceClient, EnvironmentContext context) + internal NetmapServiceProvider(NetmapService.NetmapServiceClient netmapServiceClient, ClientContext context) : base(context) { this.netmapServiceClient = netmapServiceClient; @@ -20,8 +21,8 @@ internal sealed class NetmapServiceProvider : ContextAccessor internal async Task GetNetworkSettingsAsync(CallContext ctx) { - if (EnvironmentContext.NetworkSettings != null) - return EnvironmentContext.NetworkSettings; + if (ClientContext.NetworkSettings != null) + return ClientContext.NetworkSettings; var response = await GetNetworkInfoAsync(ctx).ConfigureAwait(false); @@ -38,7 +39,7 @@ internal sealed class NetmapServiceProvider : ContextAccessor SetNetworksParam(param, settings); } - EnvironmentContext.NetworkSettings = settings; + ClientContext.NetworkSettings = settings; return settings; } @@ -46,10 +47,10 @@ internal sealed class NetmapServiceProvider : ContextAccessor internal async Task GetLocalNodeInfoAsync(PrmNodeInfo args) { var ctx = args.Context!; - ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; + ctx.Key ??= ClientContext.Key?.ECDsaKey; if (ctx.Key == null) - throw new InvalidObjectException(nameof(ctx.Key)); + throw new ArgumentNullException(nameof(args), "Key is null"); var request = new LocalNodeInfoRequest { @@ -59,8 +60,6 @@ internal sealed class NetmapServiceProvider : ContextAccessor request.AddMetaHeader(args.XHeaders); request.Sign(ctx.Key); - - var response = await netmapServiceClient.LocalNodeInfoAsync(request, null, ctx.Deadline, ctx.CancellationToken); Verifier.CheckResponse(response); @@ -70,10 +69,10 @@ internal sealed class NetmapServiceProvider : ContextAccessor internal async Task GetNetworkInfoAsync(CallContext ctx) { - ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; + ctx.Key ??= ClientContext.Key?.ECDsaKey; if (ctx.Key == null) - throw new InvalidObjectException(nameof(ctx.Key)); + throw new ArgumentNullException(nameof(ctx), "Key is null"); var request = new NetworkInfoRequest(); @@ -91,10 +90,10 @@ internal sealed class NetmapServiceProvider : ContextAccessor internal async Task GetNetmapSnapshotAsync(PrmNetmapSnapshot args) { var ctx = args.Context!; - ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; + ctx.Key ??= ClientContext.Key?.ECDsaKey; if (ctx.Key == null) - throw new InvalidObjectException(nameof(ctx.Key)); + throw new ArgumentNullException(nameof(args), "Key is null"); var request = new NetmapSnapshotRequest(); diff --git a/src/FrostFS.SDK.ClientV2/Services/ObjectServiceProvider.cs b/src/FrostFS.SDK.ClientV2/Services/ObjectServiceProvider.cs index 6ab3362..bcea7c7 100644 --- a/src/FrostFS.SDK.ClientV2/Services/ObjectServiceProvider.cs +++ b/src/FrostFS.SDK.ClientV2/Services/ObjectServiceProvider.cs @@ -15,30 +15,32 @@ using Google.Protobuf; namespace FrostFS.SDK.ClientV2; -internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider +internal sealed class ObjectServiceProvider(ObjectService.ObjectServiceClient client, ClientContext clientCtx) + : ContextAccessor(clientCtx), ISessionProvider { - private readonly SessionProvider sessions; - private ObjectService.ObjectServiceClient client; - - internal ObjectServiceProvider(ObjectService.ObjectServiceClient client, EnvironmentContext env) - : base(env) - { - this.sessions = new(EnvironmentContext); - this.client = client; - } + private SessionProvider? sessions; + private readonly ObjectService.ObjectServiceClient client = client; public async ValueTask GetOrCreateSession(ISessionToken args, CallContext ctx) { + sessions ??= new(ClientContext); + + if (ClientContext.SessionCache.Cache != null && + ClientContext.SessionCache.Cache.ContainsKey(ClientContext.SessionCacheKey)) + { + return (SessionToken)ClientContext.SessionCache.Cache[ClientContext.SessionCacheKey]; + } + return await sessions.GetOrCreateSession(args, ctx).ConfigureAwait(false); } internal async Task GetObjectHeadAsync(PrmObjectHeadGet args) { var ctx = args.Context!; - ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; + ctx.Key ??= ClientContext.Key?.ECDsaKey; if (ctx.Key == null) - throw new InvalidObjectException(nameof(ctx.Key)); + throw new ArgumentNullException(nameof(args), "Key is null"); var request = new HeadRequest { @@ -74,10 +76,10 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider { var ctx = args.Context!; - ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; + ctx.Key ??= ClientContext.Key?.ECDsaKey; if (ctx.Key == null) - throw new InvalidObjectException(nameof(ctx.Key)); + throw new ArgumentNullException(nameof(args), "Key is null"); var request = new GetRequest { @@ -108,10 +110,10 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider internal async Task DeleteObjectAsync(PrmObjectDelete args) { var ctx = args.Context!; - ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; + ctx.Key ??= ClientContext.Key?.ECDsaKey; if (ctx.Key == null) - throw new InvalidObjectException(nameof(ctx.Key)); + throw new ArgumentNullException(nameof(args), "Key is null"); var request = new DeleteRequest { @@ -145,7 +147,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider var ctx = args.Context!; if (ctx.Key == null) - throw new InvalidObjectException(nameof(ctx.Key)); + throw new ArgumentNullException(nameof(args), "Key is null"); var request = new SearchRequest { @@ -183,10 +185,10 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider throw new ArgumentNullException(nameof(args)); if (args.Header == null) - throw new ArgumentException(nameof(args.Header)); + throw new ArgumentNullException(nameof(args), "Header is null"); if (args.Payload == null) - throw new ArgumentException(nameof(args.Payload)); + throw new ArgumentNullException(nameof(args), "Payload is null"); if (args.ClientCut) return await PutClientCutObject(args).ConfigureAwait(false); @@ -206,7 +208,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider var ctx = args.Context!; if (ctx.Key == null) - throw new InvalidObjectException(nameof(ctx.Key)); + throw new ArgumentNullException(nameof(args), "Key is null"); var grpcObject = ObjectTools.CreateObject(args.FrostFsObject, ctx); @@ -254,7 +256,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider if (args.MaxObjectSizeCache == 0) { - var networkSettings = await EnvironmentContext.Client.GetNetworkSettingsAsync(new PrmNetworkSettings() { Context = ctx }) + var networkSettings = await ClientContext.Client.GetNetworkSettingsAsync(new PrmNetworkSettings(ctx)) .ConfigureAwait(false); args.MaxObjectSizeCache = (int)networkSettings.MaxObjectSize; @@ -306,7 +308,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider var linkObject = new FrostFsLinkObject(header.ContainerId, split!.SplitId, largeObjectHeader, sentObjectIds); - _ = await PutSingleObjectAsync(new PrmSingleObjectPut(linkObject) { Context = args.Context }).ConfigureAwait(false); + _ = await PutSingleObjectAsync(new PrmSingleObjectPut(linkObject, args.Context)).ConfigureAwait(false); var parentHeader = args.Header.GetHeader(); @@ -331,7 +333,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider { var ctx = args.Context!; if (ctx.Key == null) - throw new InvalidObjectException(nameof(ctx.Key)); + throw new ArgumentNullException(nameof(args), "Key is null"); var payload = args.Payload!; @@ -352,7 +354,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider } else { - chunkBuffer = EnvironmentContext.GetArrayPool(Constants.ObjectChunkSize).Rent(chunkSize); + chunkBuffer = ClientContext.GetArrayPool(Constants.ObjectChunkSize).Rent(chunkSize); isRentBuffer = true; } @@ -409,7 +411,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider var header = args.Header!; if (ctx.Key == null) - throw new InvalidObjectException(nameof(ctx.Key)); + throw new ArgumentNullException(nameof(args), "Key is null"); header.OwnerId ??= ctx.OwnerId; header.Version ??= ctx.Version; diff --git a/src/FrostFS.SDK.ClientV2/Services/SessionServiceProvider.cs b/src/FrostFS.SDK.ClientV2/Services/SessionServiceProvider.cs index 1dfe53f..73418d0 100644 --- a/src/FrostFS.SDK.ClientV2/Services/SessionServiceProvider.cs +++ b/src/FrostFS.SDK.ClientV2/Services/SessionServiceProvider.cs @@ -10,7 +10,7 @@ internal sealed class SessionServiceProvider : ContextAccessor { private readonly SessionService.SessionServiceClient? _sessionServiceClient; - internal SessionServiceProvider(SessionService.SessionServiceClient? sessionServiceClient, EnvironmentContext context) + internal SessionServiceProvider(SessionService.SessionServiceClient? sessionServiceClient, ClientContext context) : base(context) { _sessionServiceClient = sessionServiceClient; @@ -20,7 +20,7 @@ internal sealed class SessionServiceProvider : ContextAccessor { var ctx = args.Context!; - ctx.OwnerId ??= EnvironmentContext.Owner; + ctx.OwnerId ??= ClientContext.Owner; var request = new CreateRequest { diff --git a/src/FrostFS.SDK.ClientV2/Services/Shared/ContextAccessor.cs b/src/FrostFS.SDK.ClientV2/Services/Shared/ContextAccessor.cs index 3fb7674..70fc9c9 100644 --- a/src/FrostFS.SDK.ClientV2/Services/Shared/ContextAccessor.cs +++ b/src/FrostFS.SDK.ClientV2/Services/Shared/ContextAccessor.cs @@ -1,6 +1,6 @@ namespace FrostFS.SDK.ClientV2; -internal class ContextAccessor(EnvironmentContext context) +internal class ContextAccessor(ClientContext context) { - protected EnvironmentContext EnvironmentContext { get; set; } = context; + protected ClientContext ClientContext { get; set; } = context; } diff --git a/src/FrostFS.SDK.ClientV2/Services/Shared/SessionProvider.cs b/src/FrostFS.SDK.ClientV2/Services/Shared/SessionProvider.cs index 51fe337..e18de09 100644 --- a/src/FrostFS.SDK.ClientV2/Services/Shared/SessionProvider.cs +++ b/src/FrostFS.SDK.ClientV2/Services/Shared/SessionProvider.cs @@ -7,14 +7,13 @@ internal interface ISessionProvider ValueTask GetOrCreateSession(ISessionToken args, CallContext ctx); } -internal sealed class SessionProvider(EnvironmentContext envCtx) +internal sealed class SessionProvider(ClientContext envCtx) { - // TODO: implement cache for session in the next iteration public async ValueTask GetOrCreateSession(ISessionToken args, CallContext ctx) { if (args.SessionToken is null) { - return await envCtx.Client.CreateSessionInternalAsync(new PrmSessionCreate(uint.MaxValue) { Context = ctx }) + return await envCtx.Client.CreateSessionInternalAsync(new PrmSessionCreate(uint.MaxValue, ctx)) .ConfigureAwait(false); } diff --git a/src/FrostFS.SDK.ClientV2/Tools/EnvironmentContext.cs b/src/FrostFS.SDK.ClientV2/Tools/ClientContext.cs similarity index 62% rename from src/FrostFS.SDK.ClientV2/Tools/EnvironmentContext.cs rename to src/FrostFS.SDK.ClientV2/Tools/ClientContext.cs index d0596c9..c0a1db3 100644 --- a/src/FrostFS.SDK.ClientV2/Tools/EnvironmentContext.cs +++ b/src/FrostFS.SDK.ClientV2/Tools/ClientContext.cs @@ -2,16 +2,21 @@ using System; using System.Buffers; using System.Security.Cryptography; +using FrostFS.SDK.Cryptography; + using Grpc.Net.Client; namespace FrostFS.SDK.ClientV2; -public class EnvironmentContext(FrostFSClient client, ECDsa? key, FrostFsOwner? owner, GrpcChannel channel, FrostFsVersion version) : IDisposable +public class ClientContext(FrostFSClient client, ECDsa? key, FrostFsOwner? owner, GrpcChannel channel, FrostFsVersion version) : IDisposable { private ArrayPool? _arrayPool; + private string? sessionKey; internal FrostFsOwner? Owner { get; } = owner; + internal string? Address { get; } = channel.Target; + internal GrpcChannel Channel { get; private set; } = channel; internal FrostFsVersion Version { get; } = version; @@ -22,6 +27,21 @@ public class EnvironmentContext(FrostFSClient client, ECDsa? key, FrostFsOwner? internal ClientKey? Key { get; } = key != null ? new ClientKey(key) : null; + internal SessionCache SessionCache { get; set; } + + internal string? SessionCacheKey + { + get + { + if (sessionKey == null && Key != null && Address != null) + { + sessionKey = Pool.FormCacheKey(Address, Key.ECDsaKey.PrivateKey().ToString()); + } + + return sessionKey; + } + } + /// /// Custom pool is used for predefined sizes of buffers like grpc chunk /// diff --git a/src/FrostFS.SDK.ClientV2/Tools/ObjectReader.cs b/src/FrostFS.SDK.ClientV2/Tools/ObjectReader.cs index b1ad4f7..7df2343 100644 --- a/src/FrostFS.SDK.ClientV2/Tools/ObjectReader.cs +++ b/src/FrostFS.SDK.ClientV2/Tools/ObjectReader.cs @@ -17,13 +17,13 @@ public sealed class ObjectReader(AsyncServerStreamingCall call) : I internal async Task ReadHeader() { if (!await Call.ResponseStream.MoveNext().ConfigureAwait(false)) - throw new InvalidOperationException("unexpected end of stream"); + throw new FrostFsStreamException("unexpected end of stream"); var response = Call.ResponseStream.Current; Verifier.CheckResponse(response); if (response.Body.ObjectPartCase != GetResponse.Types.Body.ObjectPartOneofCase.Init) - throw new InvalidOperationException("unexpected message type"); + throw new FrostFsStreamException("unexpected message type"); return new Object.Object { @@ -41,7 +41,7 @@ public sealed class ObjectReader(AsyncServerStreamingCall call) : I Verifier.CheckResponse(response); if (response.Body.ObjectPartCase != GetResponse.Types.Body.ObjectPartOneofCase.Chunk) - throw new InvalidOperationException("unexpected message type"); + throw new FrostFsStreamException("unexpected message type"); return response.Body.Chunk.Memory; } diff --git a/src/FrostFS.SDK.ClientV2/Tools/ObjectTools.cs b/src/FrostFS.SDK.ClientV2/Tools/ObjectTools.cs index f4d659c..91df2ad 100644 --- a/src/FrostFS.SDK.ClientV2/Tools/ObjectTools.cs +++ b/src/FrostFS.SDK.ClientV2/Tools/ObjectTools.cs @@ -59,7 +59,7 @@ internal static class ObjectTools return; if (ctx.Key == null) - throw new InvalidObjectException(nameof(ctx.Key)); + throw new FrostFsInvalidObjectException(nameof(ctx.Key)); grpcHeader.Split = new Header.Types.Split { diff --git a/src/FrostFS.SDK.ClientV2/Tools/Verifier.cs b/src/FrostFS.SDK.ClientV2/Tools/Verifier.cs index 2896209..c87ef0a 100644 --- a/src/FrostFS.SDK.ClientV2/Tools/Verifier.cs +++ b/src/FrostFS.SDK.ClientV2/Tools/Verifier.cs @@ -122,7 +122,7 @@ public static class Verifier var status = resp.MetaHeader.Status.ToModel(); if (status != null && !status.IsSuccess) - throw new ResponseException(status); + throw new FrostFsResponseException(status); } /// @@ -137,6 +137,6 @@ public static class Verifier } if (!request.Verify()) - throw new FormatException($"invalid response, type={request.GetType()}"); + throw new FrostFsResponseException($"invalid response, type={request.GetType()}"); } } \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/CallbackInterceptor.cs b/src/FrostFS.SDK.Tests/CallbackInterceptor.cs new file mode 100644 index 0000000..c4eb726 --- /dev/null +++ b/src/FrostFS.SDK.Tests/CallbackInterceptor.cs @@ -0,0 +1,33 @@ +using Grpc.Core; +using Grpc.Core.Interceptors; + +namespace FrostFS.SDK.SmokeTests; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", + Justification = "parameters are provided by GRPC infrastructure")] +public class CallbackInterceptor(Action? callback = null) : Interceptor +{ + public override AsyncUnaryCall AsyncUnaryCall( + TRequest request, + ClientInterceptorContext context, + AsyncUnaryCallContinuation continuation) + { + var call = continuation(request, context); + + return new AsyncUnaryCall( + HandleUnaryResponse(call), + call.ResponseHeadersAsync, + call.GetStatus, + call.GetTrailers, + call.Dispose); + } + + private async Task HandleUnaryResponse(AsyncUnaryCall call) + { + var response = await call; + + callback?.Invoke($"elapsed"); + + return response; + } +} diff --git a/src/FrostFS.SDK.Tests/MetricsInterceptor.cs b/src/FrostFS.SDK.Tests/MetricsInterceptor.cs deleted file mode 100644 index 9f5d824..0000000 --- a/src/FrostFS.SDK.Tests/MetricsInterceptor.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Diagnostics; - -using Grpc.Core; -using Grpc.Core.Interceptors; - -namespace FrostFS.SDK.SmokeTests; - -public class MetricsInterceptor() : Interceptor -{ - public override AsyncUnaryCall AsyncUnaryCall( - TRequest request, - ClientInterceptorContext context, - AsyncUnaryCallContinuation continuation) - { - ArgumentNullException.ThrowIfNull(continuation); - - using var call = continuation(request, context); - - return new AsyncUnaryCall( - HandleUnaryResponse(call), - call.ResponseHeadersAsync, - call.GetStatus, - call.GetTrailers, - call.Dispose); - } - - private static async Task HandleUnaryResponse(AsyncUnaryCall call) - { - var watch = new Stopwatch(); - watch.Start(); - - var response = await call.ResponseAsync.ConfigureAwait(false); - - watch.Stop(); - - // Do something with call info - // var elapsed = watch.ElapsedTicks * 1_000_000/Stopwatch.Frequency; - - return response; - } -} diff --git a/src/FrostFS.SDK.Tests/NetworkTest.cs b/src/FrostFS.SDK.Tests/NetworkTest.cs index f09cafb..99fa2b2 100644 --- a/src/FrostFS.SDK.Tests/NetworkTest.cs +++ b/src/FrostFS.SDK.Tests/NetworkTest.cs @@ -28,19 +28,16 @@ public class NetworkTest : NetworkTestsBase Mocker.Parameters.Add("HomomorphicHashingDisabled", [1]); Mocker.Parameters.Add("MaintenanceModeAllowed", [1]); - var param = new PrmNetworkSettings(); - - if (useContext) - { - param.Context = new CallContext + var param = useContext ? + new PrmNetworkSettings(new CallContext { CancellationToken = Mocker.CancellationTokenSource.Token, Timeout = TimeSpan.FromSeconds(20), OwnerId = OwnerId, Key = ECDsaKey, Version = Version - }; - } + }) + : new PrmNetworkSettings(); var validTimeoutFrom = DateTime.UtcNow.AddSeconds(20); @@ -116,12 +113,11 @@ public class NetworkTest : NetworkTestsBase Mocker.NetmapSnapshotResponse = new NetmapSnapshotResponse { Body = body }; - var param = new PrmNetmapSnapshot(); + PrmNetmapSnapshot param; if (useContext) { - param.XHeaders.Add("headerKey1", "headerValue1"); - param.Context = new CallContext + var ctx = new CallContext { CancellationToken = Mocker.CancellationTokenSource.Token, Timeout = TimeSpan.FromSeconds(20), @@ -129,6 +125,14 @@ public class NetworkTest : NetworkTestsBase Key = ECDsaKey, Version = Version }; + + param = new(ctx); + param.XHeaders.Add("headerKey1", "headerValue1"); + + } + else + { + param = new(); } var validTimeoutFrom = DateTime.UtcNow.AddSeconds(20); @@ -208,12 +212,11 @@ public class NetworkTest : NetworkTestsBase Mocker.NodeInfoResponse = new LocalNodeInfoResponse { Body = body }; - var param = new PrmNodeInfo(); + PrmNodeInfo param; if (useContext) { - param.XHeaders.Add("headerKey1", "headerValue1"); - param.Context = new CallContext + var ctx = new CallContext { CancellationToken = Mocker.CancellationTokenSource.Token, Timeout = TimeSpan.FromSeconds(20), @@ -221,6 +224,14 @@ public class NetworkTest : NetworkTestsBase Key = ECDsaKey, Version = Version }; + + param = new(ctx); + param.XHeaders.Add("headerKey1", "headerValue1"); + + } + else + { + param = new(); } var validTimeoutFrom = DateTime.UtcNow.AddSeconds(20); diff --git a/src/FrostFS.SDK.Tests/ObjectTest.cs b/src/FrostFS.SDK.Tests/ObjectTest.cs index 07fa8e9..85b42ca 100644 --- a/src/FrostFS.SDK.Tests/ObjectTest.cs +++ b/src/FrostFS.SDK.Tests/ObjectTest.cs @@ -32,7 +32,7 @@ public class ObjectTest : ObjectTestsBase var objectId = client.CalculateObjectId(Mocker.ObjectHeader!, ctx); - var result = await client.GetObjectAsync(new PrmObjectGet(ContainerId, objectId) { Context = ctx }); + var result = await client.GetObjectAsync(new PrmObjectGet(ContainerId, objectId, ctx)); Assert.NotNull(result); @@ -50,7 +50,7 @@ public class ObjectTest : ObjectTestsBase [Fact] public async void PutObjectTest() { - Mocker.ResultObjectIds.Add(SHA256.HashData([])); + Mocker.ResultObjectIds!.Add(SHA256.HashData([])); Random rnd = new(); var bytes = new byte[1024]; @@ -107,7 +107,7 @@ public class ObjectTest : ObjectTestsBase rnd.NextBytes(objIds.ElementAt(2)); foreach (var objId in objIds) - Mocker.ResultObjectIds.Add(objId); + Mocker.ResultObjectIds!.Add(objId); var result = await GetClient().PutObjectAsync(param); diff --git a/src/FrostFS.SDK.Tests/PoolSmokeTests.cs b/src/FrostFS.SDK.Tests/PoolSmokeTests.cs index 87bf106..01320b5 100644 --- a/src/FrostFS.SDK.Tests/PoolSmokeTests.cs +++ b/src/FrostFS.SDK.Tests/PoolSmokeTests.cs @@ -23,16 +23,6 @@ public class PoolSmokeTests : SmokeTestsBase Key = keyString.LoadWif(), NodeParams = [new(1, this.url, 100.0f)], - DialOptions = [new() - { - Authority = "", - Block = false, - DisableHealthCheck = false, - DisableRetry = false, - ReturnLastError = true, - Timeout = 30_000_000 - } - ], ClientBuilder = null, GracefulCloseOnSwitchTimeout = 30_000_000, Logger = null @@ -85,6 +75,44 @@ public class PoolSmokeTests : SmokeTestsBase Assert.Equal(9, result.Attributes.Count); } + [Fact] + public async void NodeInfoStatisticsTwoNodesTest() + { + var options = new InitParameters + { + Key = keyString.LoadWif(), + NodeParams = [ + new(1, this.url, 100.0f), + new(2, this.url.Replace('0', '1'), 100.0f) + ], + ClientBuilder = null, + GracefulCloseOnSwitchTimeout = 30_000_000, + Logger = null + }; + + using var pool = new Pool(options); + + var callbackText = string.Empty; + + var ctx = new CallContext + { + Callback = (cs) => callbackText = $"{cs.MethodName} took {cs.ElapsedMicroSeconds} microseconds" + }; + + var error = await pool.Dial(ctx).ConfigureAwait(true); + + Assert.Null(error); + + using var client = FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.keyString, this.url)); + + var result = await client.GetNodeInfoAsync(); + + var statistics = pool.Statistic(); + + Assert.False(string.IsNullOrEmpty(callbackText)); + Assert.Contains(" took ", callbackText, StringComparison.Ordinal); + } + [Fact] public async void NodeInfoStatisticsTest() { @@ -308,31 +336,29 @@ public class PoolSmokeTests : SmokeTestsBase }; var createContainerParam = new PrmContainerCreate( - new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1)), [new("testKey", "testValue")])) - { - Context = ctx - }; + new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1)), [new("testKey", "testValue")]), ctx); var createdContainer = await pool.CreateContainerAsync(createContainerParam); - var container = await pool.GetContainerAsync(new PrmContainerGet(createdContainer) { Context = ctx }); + var container = await pool.GetContainerAsync(new PrmContainerGet(createdContainer)); Assert.NotNull(container); Assert.True(callbackInvoked); var bytes = GetRandomBytes(objectSize); - var param = new PrmObjectPut + var ctx1 = new CallContext + { + Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0)) + }; + + var param = new PrmObjectPut(ctx1) { Header = new FrostFsObjectHeader( containerId: createdContainer, type: FrostFsObjectType.Regular, [new FrostFsAttributePair("fileName", "test")]), Payload = new MemoryStream(bytes), - ClientCut = false, - Context = new CallContext - { - Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0)) - } + ClientCut = false }; var objectId = await pool.PutObjectAsync(param); @@ -400,19 +426,16 @@ public class PoolSmokeTests : SmokeTestsBase }; var createContainerParam = new PrmContainerCreate( - new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1)))) - { - Context = ctx - }; + new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1))), ctx); var container = await pool.CreateContainerAsync(createContainerParam); - var containerInfo = await pool.GetContainerAsync(new PrmContainerGet(container) { Context = ctx }); + var containerInfo = await pool.GetContainerAsync(new PrmContainerGet(container, ctx)); Assert.NotNull(containerInfo); var bytes = GetRandomBytes(objectSize); - var param = new PrmObjectPut + var param = new PrmObjectPut(new CallContext { Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0)) }) { Header = new FrostFsObjectHeader( containerId: container, @@ -420,10 +443,6 @@ public class PoolSmokeTests : SmokeTestsBase [new FrostFsAttributePair("fileName", "test")]), Payload = new MemoryStream(bytes), ClientCut = false, - Context = new CallContext - { - Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0)) - }, SessionToken = token }; @@ -496,10 +515,11 @@ public class PoolSmokeTests : SmokeTestsBase var ctx = new CallContext { Timeout = TimeSpan.FromSeconds(10), - Interceptors = new([new MetricsInterceptor()]) }; - var container = await pool.GetContainerAsync(new PrmContainerGet(containerId) { Context = ctx }); + ctx.Interceptors.Add(new CallbackInterceptor()); + + var container = await pool.GetContainerAsync(new PrmContainerGet(containerId, ctx)); Assert.NotNull(container); @@ -520,7 +540,7 @@ public class PoolSmokeTests : SmokeTestsBase var filter = new FilterByAttributePair(FrostFsMatchType.Equals, "fileName", "test"); bool hasObject = false; - await foreach (var objId in pool.SearchObjectsAsync(new PrmObjectSearch(containerId, filter))) + await foreach (var objId in pool.SearchObjectsAsync(new PrmObjectSearch(containerId, null, filter))) { hasObject = true; @@ -551,21 +571,10 @@ public class PoolSmokeTests : SmokeTestsBase await Cleanup(pool); - var deadline = DateTime.UtcNow.Add(TimeSpan.FromSeconds(5)); - - IAsyncEnumerator? enumerator = null; - do + await foreach (var cid in pool.ListContainersAsync()) { - if (deadline <= DateTime.UtcNow) - { - Assert.Fail("Containers exist"); - break; - } - - enumerator = pool.ListContainersAsync().GetAsyncEnumerator(); - await Task.Delay(500); + Assert.Fail($"Container {cid.GetValue()} exist"); } - while (await enumerator!.MoveNextAsync()); } private static byte[] GetRandomBytes(int size) diff --git a/src/FrostFS.SDK.Tests/SessionTests.cs b/src/FrostFS.SDK.Tests/SessionTests.cs index e343c82..8009f5c 100644 --- a/src/FrostFS.SDK.Tests/SessionTests.cs +++ b/src/FrostFS.SDK.Tests/SessionTests.cs @@ -14,12 +14,11 @@ public class SessionTest : SessionTestsBase public async void CreateSessionTest(bool useContext) { var exp = 100u; - var param = new PrmSessionCreate(exp); + PrmSessionCreate param; if (useContext) { - param.XHeaders.Add("headerKey1", "headerValue1"); - param.Context = new CallContext + var ctx = new CallContext { CancellationToken = Mocker.CancellationTokenSource.Token, Timeout = TimeSpan.FromSeconds(20), @@ -27,6 +26,14 @@ public class SessionTest : SessionTestsBase Key = ECDsaKey, Version = Mocker.Version }; + + param = new PrmSessionCreate(exp, ctx); + + param.XHeaders.Add("headerKey1", "headerValue1"); + } + else + { + param = new PrmSessionCreate(exp); } var validTimeoutFrom = DateTime.UtcNow.AddSeconds(20); diff --git a/src/FrostFS.SDK.Tests/SmokeClientTests.cs b/src/FrostFS.SDK.Tests/SmokeClientTests.cs index 54868d6..426aa9b 100644 --- a/src/FrostFS.SDK.Tests/SmokeClientTests.cs +++ b/src/FrostFS.SDK.Tests/SmokeClientTests.cs @@ -26,7 +26,7 @@ public class SmokeClientTests : SmokeTestsBase ? FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.keyString, this.url)) : FrostFSClient.GetInstance(GetOptions(this.url)); - PrmBalance? prm = isSingleOnwerClient ? default : new() { Context = Ctx }; + PrmBalance? prm = isSingleOnwerClient ? default : new(Ctx); var result = await client.GetBalanceAsync(prm); } @@ -38,7 +38,7 @@ public class SmokeClientTests : SmokeTestsBase { using var client = isSingleOnwerClient ? FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.keyString, this.url)) : FrostFSClient.GetInstance(GetOptions(this.url)); - PrmNetmapSnapshot? prm = isSingleOnwerClient ? default : new() { Context = Ctx }; + PrmNetmapSnapshot? prm = isSingleOnwerClient ? default : new(Ctx); var result = await client.GetNetmapSnapshotAsync(prm); Assert.True(result.Epoch > 0); @@ -59,9 +59,11 @@ public class SmokeClientTests : SmokeTestsBase [InlineData(true)] public async void NodeInfoTest(bool isSingleOnwerClient) { - using var client = isSingleOnwerClient ? FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.keyString, this.url)) : FrostFSClient.GetInstance(GetOptions(this.url)); + using var client = isSingleOnwerClient + ? FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.keyString, this.url)) + : FrostFSClient.GetInstance(GetOptions(this.url)); - PrmNodeInfo? prm = isSingleOnwerClient ? default : new() { Context = Ctx }; + PrmNodeInfo? prm = isSingleOnwerClient ? default : new(Ctx); var result = await client.GetNodeInfoAsync(prm); @@ -93,7 +95,7 @@ public class SmokeClientTests : SmokeTestsBase { using var client = isSingleOnwerClient ? FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.keyString, this.url)) : FrostFSClient.GetInstance(GetOptions(this.url)); - PrmSessionCreate? prm = isSingleOnwerClient ? new PrmSessionCreate(100) : new PrmSessionCreate(100) { Context = Ctx }; + PrmSessionCreate? prm = isSingleOnwerClient ? new PrmSessionCreate(100) : new PrmSessionCreate(100, Ctx); var token = await client.CreateSessionAsync(prm); @@ -262,31 +264,27 @@ public class SmokeClientTests : SmokeTestsBase }; var createContainerParam = new PrmContainerCreate( - new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1)), [new("testKey", "testValue")])) - { - Context = ctx - }; + new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1)), [new("testKey", "testValue")]), ctx); var createdContainer = await client.CreateContainerAsync(createContainerParam); - var container = await client.GetContainerAsync(new PrmContainerGet(createdContainer) { Context = ctx }); + var container = await client.GetContainerAsync(new PrmContainerGet(createdContainer, ctx)); Assert.NotNull(container); Assert.True(callbackInvoked); var bytes = GetRandomBytes(objectSize); - var param = new PrmObjectPut + var param = new PrmObjectPut(new CallContext + { + Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0)) + }) { Header = new FrostFsObjectHeader( containerId: createdContainer, type: FrostFsObjectType.Regular, [new FrostFsAttributePair("fileName", "test")]), Payload = new MemoryStream(bytes), - ClientCut = false, - Context = new CallContext - { - Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0)) - } + ClientCut = false }; var objectId = await client.PutObjectAsync(param); @@ -348,19 +346,19 @@ public class SmokeClientTests : SmokeTestsBase }; var createContainerParam = new PrmContainerCreate( - new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1)))) - { - Context = ctx - }; + new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1))), ctx); var container = await client.CreateContainerAsync(createContainerParam); - var containerInfo = await client.GetContainerAsync(new PrmContainerGet(container) { Context = ctx }); + var containerInfo = await client.GetContainerAsync(new PrmContainerGet(container, ctx)); Assert.NotNull(containerInfo); var bytes = GetRandomBytes(objectSize); - var param = new PrmObjectPut + var param = new PrmObjectPut(new CallContext + { + Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0)) + }) { Header = new FrostFsObjectHeader( containerId: container, @@ -368,10 +366,6 @@ public class SmokeClientTests : SmokeTestsBase [new FrostFsAttributePair("fileName", "test")]), Payload = new MemoryStream(bytes), ClientCut = false, - Context = new CallContext - { - Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0)) - }, SessionToken = token }; @@ -437,11 +431,12 @@ public class SmokeClientTests : SmokeTestsBase var ctx = new CallContext { - Timeout = TimeSpan.FromSeconds(10), - Interceptors = new([new MetricsInterceptor()]) + Timeout = TimeSpan.FromSeconds(10) }; - var container = await client.GetContainerAsync(new PrmContainerGet(containerId) { Context = ctx }); + ctx.Interceptors.Add(new CallbackInterceptor()); + + var container = await client.GetContainerAsync(new PrmContainerGet(containerId, ctx)); Assert.NotNull(container); @@ -462,7 +457,7 @@ public class SmokeClientTests : SmokeTestsBase var filter = new FilterByAttributePair(FrostFsMatchType.Equals, "fileName", "test"); bool hasObject = false; - await foreach (var objId in client.SearchObjectsAsync(new PrmObjectSearch(containerId, filter))) + await foreach (var objId in client.SearchObjectsAsync(new PrmObjectSearch(containerId, null, filter))) { hasObject = true; @@ -510,6 +505,38 @@ public class SmokeClientTests : SmokeTestsBase while (await enumerator!.MoveNextAsync()); } + [Fact] + public async void NodeInfoCallbackAndInterceptorTest() + { + using var client = FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.keyString, this.url)); + + bool callbackInvoked = false; + bool intercepterInvoked = false; + + var ctx = new CallContext + { + Callback = new((CallStatistics cs) => + { + callbackInvoked = true; + Assert.True(cs.ElapsedMicroSeconds > 0); + }) + }; + + ctx.Interceptors.Add(new CallbackInterceptor(s => intercepterInvoked = true)); + + var result = await client.GetNodeInfoAsync(new PrmNodeInfo(ctx)); + + Assert.True(callbackInvoked); + Assert.True(intercepterInvoked); + + Assert.Equal(2, result.Version.Major); + Assert.Equal(13, result.Version.Minor); + Assert.Equal(NodeState.Online, result.State); + Assert.Equal(33, result.PublicKey.Length); + Assert.Single(result.Addresses); + Assert.Equal(9, result.Attributes.Count); + } + private static byte[] GetRandomBytes(int size) { Random rnd = new(); -- 2.45.2 From bff8d6786767a8ece329676427a26004abb4685c Mon Sep 17 00:00:00 2001 From: Pavel Gross Date: Fri, 1 Nov 2024 10:41:17 +0300 Subject: [PATCH 3/3] [#24] Client: Implement pool part2 Unicode fix Signed-off-by: Pavel Gross --- src/FrostFS.SDK.ClientV2/Poll/Pool.cs | 39 ++++++++++++++------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/FrostFS.SDK.ClientV2/Poll/Pool.cs b/src/FrostFS.SDK.ClientV2/Poll/Pool.cs index 2602746..3e20634 100644 --- a/src/FrostFS.SDK.ClientV2/Poll/Pool.cs +++ b/src/FrostFS.SDK.ClientV2/Poll/Pool.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -304,7 +305,7 @@ public partial class Pool : IFrostFSClient ); } - private ClientWrapper Сonnection() + private ClientWrapper Connection() { foreach (var pool in InnerPools!) { @@ -518,7 +519,7 @@ public partial class Pool : IFrostFSClient public async Task GetNetmapSnapshotAsync(PrmNetmapSnapshot? args = null) { - var client = Сonnection(); + var client = Connection(); args ??= new(); args.Context.PoolErrorHandler = client.HandleError; @@ -528,7 +529,7 @@ public partial class Pool : IFrostFSClient public async Task GetNodeInfoAsync(PrmNodeInfo? args = null) { - var client = Сonnection(); + var client = Connection(); args ??= new(); args.Context.PoolErrorHandler = client.HandleError; @@ -538,7 +539,7 @@ public partial class Pool : IFrostFSClient public async Task GetNetworkSettingsAsync(PrmNetworkSettings? args = null) { - var client = Сonnection(); + var client = Connection(); args ??= new(); args.Context.PoolErrorHandler = client.HandleError; @@ -553,7 +554,7 @@ public partial class Pool : IFrostFSClient throw new ArgumentNullException(nameof(args)); } - var client = Сonnection(); + var client = Connection(); args.Context.PoolErrorHandler = client.HandleError; @@ -567,7 +568,7 @@ public partial class Pool : IFrostFSClient throw new ArgumentNullException(nameof(args)); } - var client = Сonnection(); + var client = Connection(); args.Context.PoolErrorHandler = client.HandleError; @@ -581,7 +582,7 @@ public partial class Pool : IFrostFSClient throw new ArgumentNullException(nameof(args)); } - var client = Сonnection(); + var client = Connection(); args.Context.PoolErrorHandler = client.HandleError; @@ -595,7 +596,7 @@ public partial class Pool : IFrostFSClient throw new ArgumentNullException(nameof(args)); } - var client = Сonnection(); + var client = Connection(); args.Context.PoolErrorHandler = client.HandleError; @@ -609,7 +610,7 @@ public partial class Pool : IFrostFSClient throw new ArgumentNullException(nameof(args)); } - var client = Сonnection(); + var client = Connection(); args.Context.PoolErrorHandler = client.HandleError; @@ -618,7 +619,7 @@ public partial class Pool : IFrostFSClient public IAsyncEnumerable ListContainersAsync(PrmContainerGetAll? args = null) { - var client = Сonnection(); + var client = Connection(); args ??= new(); args.Context.PoolErrorHandler = client.HandleError; @@ -633,7 +634,7 @@ public partial class Pool : IFrostFSClient throw new ArgumentNullException(nameof(args)); } - var client = Сonnection(); + var client = Connection(); args.Context.PoolErrorHandler = client.HandleError; @@ -647,7 +648,7 @@ public partial class Pool : IFrostFSClient throw new ArgumentNullException(nameof(args)); } - var client = Сonnection(); + var client = Connection(); args.Context.PoolErrorHandler = client.HandleError; @@ -661,7 +662,7 @@ public partial class Pool : IFrostFSClient throw new ArgumentNullException(nameof(args)); } - var client = Сonnection(); + var client = Connection(); args.Context.PoolErrorHandler = client.HandleError; @@ -675,7 +676,7 @@ public partial class Pool : IFrostFSClient throw new ArgumentNullException(nameof(args)); } - var client = Сonnection(); + var client = Connection(); args.Context.PoolErrorHandler = client.HandleError; @@ -689,7 +690,7 @@ public partial class Pool : IFrostFSClient throw new ArgumentNullException(nameof(args)); } - var client = Сonnection(); + var client = Connection(); args.Context.PoolErrorHandler = client.HandleError; @@ -703,7 +704,7 @@ public partial class Pool : IFrostFSClient throw new ArgumentNullException(nameof(args)); } - var client = Сonnection(); + var client = Connection(); args.Context.PoolErrorHandler = client.HandleError; @@ -717,7 +718,7 @@ public partial class Pool : IFrostFSClient throw new ArgumentNullException(nameof(args)); } - var client = Сonnection(); + var client = Connection(); args.Context.PoolErrorHandler = client.HandleError; @@ -731,7 +732,7 @@ public partial class Pool : IFrostFSClient throw new ArgumentNullException(nameof(args)); } - var client = Сonnection(); + var client = Connection(); args.Context.PoolErrorHandler = client.HandleError; @@ -740,7 +741,7 @@ public partial class Pool : IFrostFSClient public async Task GetBalanceAsync(PrmBalance? args) { - var client = Сonnection(); + var client = Connection(); args ??= new(); args.Context.PoolErrorHandler = client.HandleError; -- 2.45.2