From ee2079837906b274ccaf075a42cf5b55c5d1942b Mon Sep 17 00:00:00 2001 From: Pavel Gross Date: Fri, 1 Nov 2024 10:30:28 +0300 Subject: [PATCH] [#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();