[#24] Client: Implement pool part2
All checks were successful
DCO / DCO (pull_request) Successful in 46s

Signed-off-by: Pavel Gross <p.gross@yadro.com>
This commit is contained in:
Pavel Gross 2024-11-01 10:30:28 +03:00
parent c9a75ea025
commit ee20798379
63 changed files with 801 additions and 526 deletions

View file

@ -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)
{
}
}

View file

@ -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)
{
}
}

View file

@ -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)
{
}
}

View file

@ -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)
{
}
}

View file

@ -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)
{
}
}

View file

@ -22,10 +22,10 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" /> <PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="8.0.1" /> <PackageReference Include="System.Diagnostics.DiagnosticSource" Version="8.0.1" />
<PackageReference Include="System.Runtime.Caching" Version="8.0.0" /> <PackageReference Include="System.Runtime.Caching" Version="8.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -39,7 +39,7 @@ public class FrostFSClient : IFrostFSClient
internal AccountingService.AccountingServiceClient? AccountingServiceClient { get; set; } internal AccountingService.AccountingServiceClient? AccountingServiceClient { get; set; }
internal EnvironmentContext ClientCtx { get; set; } internal ClientContext ClientCtx { get; set; }
public static IFrostFSClient GetInstance(IOptions<ClientSettings> clientOptions, GrpcChannelOptions? channelOptions = null) public static IFrostFSClient GetInstance(IOptions<ClientSettings> clientOptions, GrpcChannelOptions? channelOptions = null)
{ {
@ -93,7 +93,7 @@ public class FrostFSClient : IFrostFSClient
var ecdsaKey = settings.Value.Key.LoadWif(); var ecdsaKey = settings.Value.Key.LoadWif();
FrostFsOwner.FromKey(ecdsaKey); FrostFsOwner.FromKey(ecdsaKey);
ClientCtx = new EnvironmentContext( ClientCtx = new ClientContext(
client: this, client: this,
key: ecdsaKey, key: ecdsaKey,
owner: FrostFsOwner.FromKey(ecdsaKey), owner: FrostFsOwner.FromKey(ecdsaKey),
@ -108,13 +108,13 @@ public class FrostFSClient : IFrostFSClient
private FrostFSClient(IOptions<ClientSettings> options, GrpcChannelOptions? channelOptions) private FrostFSClient(IOptions<ClientSettings> 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(); clientSettings.Validate();
var channel = InitGrpcChannel(clientSettings.Host, channelOptions); var channel = InitGrpcChannel(clientSettings.Host, channelOptions);
ClientCtx = new EnvironmentContext( ClientCtx = new ClientContext(
this, this,
key: null, key: null,
owner: null, owner: null,
@ -127,7 +127,7 @@ public class FrostFSClient : IFrostFSClient
private FrostFSClient(IOptions<SingleOwnerClientSettings> options, GrpcChannelOptions? channelOptions) private FrostFSClient(IOptions<SingleOwnerClientSettings> 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(); clientSettings.Validate();
@ -135,7 +135,7 @@ public class FrostFSClient : IFrostFSClient
var channel = InitGrpcChannel(clientSettings.Host, channelOptions); var channel = InitGrpcChannel(clientSettings.Host, channelOptions);
ClientCtx = new EnvironmentContext( ClientCtx = new ClientContext(
this, this,
key: ecdsaKey, key: ecdsaKey,
owner: FrostFsOwner.FromKey(ecdsaKey), owner: FrostFsOwner.FromKey(ecdsaKey),
@ -146,14 +146,17 @@ public class FrostFSClient : IFrostFSClient
// CheckFrostFsVersionSupport(new Context { Timeout = TimeSpan.FromSeconds(20) }); // 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, client: this,
key: prm.Key, key: prm.Key,
owner: FrostFsOwner.FromKey(prm.Key!), owner: FrostFsOwner.FromKey(prm.Key!),
channel: InitGrpcChannel(prm.Address, null), //prm.GrpcChannelOptions), channel: InitGrpcChannel(prm.Address, null), //prm.GrpcChannelOptions),
version: new FrostFsVersion(2, 13)); version: new FrostFsVersion(2, 13))
{
SessionCache = cache
};
} }
public void Dispose() public void Dispose()
@ -363,10 +366,10 @@ public class FrostFSClient : IFrostFSClient
private async void CheckFrostFsVersionSupport(CallContext? ctx = default) private async void CheckFrostFsVersionSupport(CallContext? ctx = default)
{ {
var args = new PrmNodeInfo { Context = ctx }; var args = new PrmNodeInfo(ctx);
if (ctx?.Version == null) if (ctx?.Version == null)
throw new InvalidObjectException(nameof(ctx.Version)); throw new ArgumentNullException(nameof(ctx), "Version must be initialized");
var service = GetNetmapService(args); var service = GetNetmapService(args);
var localNodeInfo = await service.GetLocalNodeInfoAsync(args).ConfigureAwait(false); 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) 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) 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; ctx.Context.Key = ClientCtx.Key.ECDsaKey;
@ -404,24 +405,23 @@ public class FrostFSClient : IFrostFSClient
{ {
if (ClientCtx.Version == null) 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; ctx.Context.Version = ClientCtx.Version;
} }
CallInvoker? callInvoker = null; CallInvoker? callInvoker = null;
if (ctx.Context.Interceptors != null && ctx.Context.Interceptors.Count > 0)
{ foreach (var interceptor in ctx.Context.Interceptors)
foreach (var interceptor in ctx.Context.Interceptors) callInvoker = AddInvoker(callInvoker, interceptor);
{
callInvoker = AddInvoker(callInvoker, interceptor);
}
}
if (ctx.Context.Callback != null) if (ctx.Context.Callback != null)
callInvoker = AddInvoker(callInvoker, new MetricsInterceptor(ctx.Context.Callback)); callInvoker = AddInvoker(callInvoker, new MetricsInterceptor(ctx.Context.Callback));
if (ctx.Context.PoolErrorHandler != null)
callInvoker = AddInvoker(callInvoker, new ErrorInterceptor(ctx.Context.PoolErrorHandler));
return callInvoker; return callInvoker;
CallInvoker AddInvoker(CallInvoker? callInvoker, Interceptor interceptor) CallInvoker AddInvoker(CallInvoker? callInvoker, Interceptor interceptor)
@ -429,7 +429,7 @@ public class FrostFSClient : IFrostFSClient
if (callInvoker == null) if (callInvoker == null)
callInvoker = ClientCtx.Channel.Intercept(interceptor); callInvoker = ClientCtx.Channel.Intercept(interceptor);
else else
callInvoker.Intercept(interceptor); callInvoker = callInvoker.Intercept(interceptor);
return callInvoker; return callInvoker;
} }
@ -437,7 +437,7 @@ public class FrostFSClient : IFrostFSClient
private NetmapServiceProvider GetNetmapService(IContext ctx) private NetmapServiceProvider GetNetmapService(IContext ctx)
{ {
var callInvoker = SetupEnvironment(ctx); var callInvoker = SetupClientContext(ctx);
var client = NetmapServiceClient ?? (callInvoker != null var client = NetmapServiceClient ?? (callInvoker != null
? new NetmapService.NetmapServiceClient(callInvoker) ? new NetmapService.NetmapServiceClient(callInvoker)
: new NetmapService.NetmapServiceClient(ClientCtx.Channel)); : new NetmapService.NetmapServiceClient(ClientCtx.Channel));
@ -447,7 +447,7 @@ public class FrostFSClient : IFrostFSClient
private SessionServiceProvider GetSessionService(IContext ctx) private SessionServiceProvider GetSessionService(IContext ctx)
{ {
var callInvoker = SetupEnvironment(ctx); var callInvoker = SetupClientContext(ctx);
var client = SessionServiceClient ?? (callInvoker != null var client = SessionServiceClient ?? (callInvoker != null
? new SessionService.SessionServiceClient(callInvoker) ? new SessionService.SessionServiceClient(callInvoker)
: new SessionService.SessionServiceClient(ClientCtx.Channel)); : new SessionService.SessionServiceClient(ClientCtx.Channel));
@ -457,7 +457,7 @@ public class FrostFSClient : IFrostFSClient
private ApeManagerServiceProvider GetApeManagerService(IContext ctx) private ApeManagerServiceProvider GetApeManagerService(IContext ctx)
{ {
var callInvoker = SetupEnvironment(ctx); var callInvoker = SetupClientContext(ctx);
var client = ApeManagerServiceClient ?? (callInvoker != null var client = ApeManagerServiceClient ?? (callInvoker != null
? new APEManagerService.APEManagerServiceClient(callInvoker) ? new APEManagerService.APEManagerServiceClient(callInvoker)
: new APEManagerService.APEManagerServiceClient(ClientCtx.Channel)); : new APEManagerService.APEManagerServiceClient(ClientCtx.Channel));
@ -467,7 +467,7 @@ public class FrostFSClient : IFrostFSClient
private AccountingServiceProvider GetAccouningService(IContext ctx) private AccountingServiceProvider GetAccouningService(IContext ctx)
{ {
var callInvoker = SetupEnvironment(ctx); var callInvoker = SetupClientContext(ctx);
var client = AccountingServiceClient ?? (callInvoker != null var client = AccountingServiceClient ?? (callInvoker != null
? new AccountingService.AccountingServiceClient(callInvoker) ? new AccountingService.AccountingServiceClient(callInvoker)
: new AccountingService.AccountingServiceClient(ClientCtx.Channel)); : new AccountingService.AccountingServiceClient(ClientCtx.Channel));
@ -477,7 +477,7 @@ public class FrostFSClient : IFrostFSClient
private ContainerServiceProvider GetContainerService(IContext ctx) private ContainerServiceProvider GetContainerService(IContext ctx)
{ {
var callInvoker = SetupEnvironment(ctx); var callInvoker = SetupClientContext(ctx);
var client = ContainerServiceClient ?? (callInvoker != null var client = ContainerServiceClient ?? (callInvoker != null
? new ContainerService.ContainerServiceClient(callInvoker) ? new ContainerService.ContainerServiceClient(callInvoker)
: new ContainerService.ContainerServiceClient(ClientCtx.Channel)); : new ContainerService.ContainerServiceClient(ClientCtx.Channel));
@ -487,7 +487,7 @@ public class FrostFSClient : IFrostFSClient
private ObjectServiceProvider GetObjectService(IContext ctx) private ObjectServiceProvider GetObjectService(IContext ctx)
{ {
var callInvoker = SetupEnvironment(ctx); var callInvoker = SetupClientContext(ctx);
var client = ObjectServiceClient ?? (callInvoker != null var client = ObjectServiceClient ?? (callInvoker != null
? new ObjectService.ObjectServiceClient(callInvoker) ? new ObjectService.ObjectServiceClient(callInvoker)
: new ObjectService.ObjectServiceClient(ClientCtx.Channel)); : new ObjectService.ObjectServiceClient(ClientCtx.Channel));
@ -497,7 +497,7 @@ public class FrostFSClient : IFrostFSClient
private AccountingServiceProvider GetAccountService(IContext ctx) private AccountingServiceProvider GetAccountService(IContext ctx)
{ {
var callInvoker = SetupEnvironment(ctx); var callInvoker = SetupClientContext(ctx);
var client = AccountingServiceClient ?? (callInvoker != null var client = AccountingServiceClient ?? (callInvoker != null
? new AccountingService.AccountingServiceClient(callInvoker) ? new AccountingService.AccountingServiceClient(callInvoker)
: new AccountingService.AccountingServiceClient(ClientCtx.Channel)); : new AccountingService.AccountingServiceClient(ClientCtx.Channel));
@ -527,19 +527,12 @@ public class FrostFSClient : IFrostFSClient
public async Task<string?> Dial(CallContext ctx) public async Task<string?> Dial(CallContext ctx)
{ {
try var prm = new PrmBalance(ctx);
{
var prm = new PrmBalance { Context = ctx };
var service = GetAccouningService(prm); var service = GetAccouningService(prm);
var balance = await service.GetBallance(prm).ConfigureAwait(false); _ = await service.GetBallance(prm).ConfigureAwait(false);
return null; return null;
}
catch (FrostFsException ex)
{
return ex.Message;
}
} }
public bool RestartIfUnhealthy(CallContext ctx) public bool RestartIfUnhealthy(CallContext ctx)

View file

@ -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<Exception> handler) : Interceptor
{
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
var call = continuation(request, context);
return new AsyncUnaryCall<TResponse>(
HandleUnaryResponse(call),
call.ResponseHeadersAsync,
call.GetStatus,
call.GetTrailers,
call.Dispose);
}
public override AsyncClientStreamingCall<TRequest, TResponse> AsyncClientStreamingCall<TRequest, TResponse>(
ClientInterceptorContext<TRequest, TResponse> context,
AsyncClientStreamingCallContinuation<TRequest, TResponse> continuation)
{
var call = continuation(context);
return new AsyncClientStreamingCall<TRequest, TResponse>(
call.RequestStream,
HandleStreamResponse(call),
call.ResponseHeadersAsync,
call.GetStatus,
call.GetTrailers,
call.Dispose);
}
private async Task<TResponse> HandleUnaryResponse<TResponse>(AsyncUnaryCall<TResponse> call)
{
try
{
return await call;
}
catch (Exception ex)
{
handler(ex);
throw;
}
}
private async Task<TResponse> HandleStreamResponse<TRequest, TResponse>(AsyncClientStreamingCall<TRequest, TResponse> call)
{
try
{
return await call;
}
catch (Exception ex)
{
handler(ex);
throw;
}
}
}

View file

@ -7,6 +7,8 @@ using Grpc.Core.Interceptors;
namespace FrostFS.SDK.ClientV2; 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<CallStatistics> callback) : Interceptor public class MetricsInterceptor(Action<CallStatistics> callback) : Interceptor
{ {
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>( public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
@ -14,11 +16,6 @@ public class MetricsInterceptor(Action<CallStatistics> callback) : Interceptor
ClientInterceptorContext<TRequest, TResponse> context, ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation) AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{ {
if (continuation is null)
{
throw new ArgumentNullException(nameof(continuation));
}
var call = continuation(request, context); var call = continuation(request, context);
return new AsyncUnaryCall<TResponse>( return new AsyncUnaryCall<TResponse>(
@ -33,9 +30,6 @@ public class MetricsInterceptor(Action<CallStatistics> callback) : Interceptor
ClientInterceptorContext<TRequest, TResponse> context, ClientInterceptorContext<TRequest, TResponse> context,
AsyncClientStreamingCallContinuation<TRequest, TResponse> continuation) AsyncClientStreamingCallContinuation<TRequest, TResponse> continuation)
{ {
if (continuation is null)
throw new ArgumentNullException(nameof(continuation));
var call = continuation(context); var call = continuation(context);
return new AsyncClientStreamingCall<TRequest, TResponse>( return new AsyncClientStreamingCall<TRequest, TResponse>(
@ -52,7 +46,7 @@ public class MetricsInterceptor(Action<CallStatistics> callback) : Interceptor
var watch = new Stopwatch(); var watch = new Stopwatch();
watch.Start(); watch.Start();
var response = await call.ResponseAsync.ConfigureAwait(false); var response = await call;
watch.Stop(); watch.Stop();
@ -68,7 +62,7 @@ public class MetricsInterceptor(Action<CallStatistics> callback) : Interceptor
var watch = new Stopwatch(); var watch = new Stopwatch();
watch.Start(); watch.Start();
var response = await call.ResponseAsync.ConfigureAwait(false); var response = await call;
watch.Stop(); watch.Stop();

View file

@ -61,7 +61,5 @@ public interface IFrostFSClient : IDisposable
public Task<string?> Dial(CallContext ctx); public Task<string?> Dial(CallContext ctx);
public bool RestartIfUnhealthy(CallContext ctx);
public void Close(); public void Close();
} }

View file

@ -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);
}

View file

@ -15,7 +15,7 @@ public class ClientSettings
{ {
var errors = CheckFields(); var errors = CheckFields();
if (errors != null) if (errors != null)
ThrowException(errors); ThrowSettingsException(errors);
} }
protected Collection<string>? CheckFields() protected Collection<string>? CheckFields()
@ -29,7 +29,7 @@ public class ClientSettings
return null; return null;
} }
protected static void ThrowException(Collection<string> errors) protected static void ThrowSettingsException(Collection<string> errors)
{ {
if (errors is null) if (errors is null)
{ {
@ -55,7 +55,7 @@ public class SingleOwnerClientSettings : ClientSettings
{ {
var errors = CheckFields(); var errors = CheckFields();
if (errors != null) if (errors != null)
ThrowException(errors); ThrowSettingsException(errors);
} }
protected new Collection<string>? CheckFields() protected new Collection<string>? CheckFields()

View file

@ -31,7 +31,7 @@ public class FrostFsContainerId
return this.modelId; return this.modelId;
} }
throw new InvalidObjectException(); throw new FrostFsInvalidObjectException();
} }
internal ContainerID ContainerID internal ContainerID ContainerID
@ -47,7 +47,7 @@ public class FrostFsContainerId
return this.containerID; return this.containerID;
} }
throw new InvalidObjectException(); throw new FrostFsInvalidObjectException();
} }
} }

View file

@ -88,7 +88,7 @@ public class FrostFsContainerInfo
{ {
if (PlacementPolicy == null) if (PlacementPolicy == null)
{ {
throw new InvalidObjectException("PlacementPolicy is null"); throw new ArgumentNullException("PlacementPolicy is null");
} }
this.container = new Container.Container() this.container = new Container.Container()

View file

@ -1,4 +1,4 @@
using FrostFS.SDK.ClientV2; using System;
namespace FrostFS.SDK; namespace FrostFS.SDK;
@ -67,7 +67,7 @@ public class FrostFsObject
public void SetParent(FrostFsObjectHeader largeObjectHeader) public void SetParent(FrostFsObjectHeader largeObjectHeader)
{ {
if (Header?.Split == null) 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; Header.Split.ParentHeader = largeObjectHeader;
} }

View file

@ -13,10 +13,10 @@ namespace FrostFS.SDK.ClientV2;
public class CallContext() public class CallContext()
{ {
private ReadOnlyCollection<Interceptor>? interceptors;
private ByteString? publicKeyCache; private ByteString? publicKeyCache;
internal Action<Exception>? PoolErrorHandler { get; set; }
public ECDsa? Key { get; set; } public ECDsa? Key { get; set; }
public FrostFsOwner? OwnerId { get; set; } public FrostFsOwner? OwnerId { get; set; }
@ -31,11 +31,7 @@ public class CallContext()
public Action<CallStatistics>? Callback { get; set; } public Action<CallStatistics>? Callback { get; set; }
public ReadOnlyCollection<Interceptor>? Interceptors public Collection<Interceptor> Interceptors { get; } = [];
{
get { return this.interceptors; }
set { this.interceptors = value; }
}
public ByteString? GetPublicKeyCache() public ByteString? GetPublicKeyCache()
{ {

View file

@ -7,5 +7,5 @@ public interface IContext
/// callbacks, interceptors. /// callbacks, interceptors.
/// </summary> /// </summary>
/// <value>Additional parameters for calling the method</value> /// <value>Additional parameters for calling the method</value>
CallContext? Context { get; set; } CallContext? Context { get; }
} }

View file

@ -1,6 +1,6 @@
namespace FrostFS.SDK.ClientV2; 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; public FrostFsChainTarget Target { get; } = target;
} }

View file

@ -1,6 +1,6 @@
namespace FrostFS.SDK.ClientV2; 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; public FrostFsChainTarget Target { get; } = target;

View file

@ -1,6 +1,6 @@
namespace FrostFS.SDK.ClientV2; 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; public FrostFsChainTarget Target { get; } = target;

View file

@ -1,5 +1,5 @@
namespace FrostFS.SDK.ClientV2; namespace FrostFS.SDK.ClientV2;
public sealed class PrmBalance() : PrmBase public sealed class PrmBalance(CallContext? ctx = null) : PrmBase(ctx)
{ {
} }

View file

@ -2,7 +2,7 @@
namespace FrostFS.SDK.ClientV2; namespace FrostFS.SDK.ClientV2;
public class PrmBase(NameValueCollection? xheaders = null) : IContext public class PrmBase(CallContext? ctx, NameValueCollection? xheaders = null) : IContext
{ {
/// <summary> /// <summary>
/// FrostFS request X-Headers /// FrostFS request X-Headers
@ -10,5 +10,5 @@ public class PrmBase(NameValueCollection? xheaders = null) : IContext
public NameValueCollection XHeaders { get; } = xheaders ?? []; public NameValueCollection XHeaders { get; } = xheaders ?? [];
/// <inheritdoc /> /// <inheritdoc />
public CallContext? Context { get; set; } public CallContext Context { get; } = ctx ?? new CallContext();
} }

View file

@ -1,6 +1,6 @@
namespace FrostFS.SDK.ClientV2; 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; public FrostFsContainerInfo Container { get; set; } = container;

View file

@ -1,6 +1,6 @@
namespace FrostFS.SDK.ClientV2; 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; public FrostFsContainerId ContainerId { get; set; } = containerId;

View file

@ -1,6 +1,6 @@
namespace FrostFS.SDK.ClientV2; 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; public FrostFsContainerId Container { get; set; } = container;
} }

View file

@ -1,5 +1,5 @@
namespace FrostFS.SDK.ClientV2; namespace FrostFS.SDK.ClientV2;
public sealed class PrmContainerGetAll() : PrmBase() public sealed class PrmContainerGetAll(CallContext? ctx = null) : PrmBase(ctx)
{ {
} }

View file

@ -1,5 +1,5 @@
namespace FrostFS.SDK.ClientV2; namespace FrostFS.SDK.ClientV2;
public sealed class PrmNetmapSnapshot() : PrmBase public sealed class PrmNetmapSnapshot(CallContext? ctx = null) : PrmBase(ctx)
{ {
} }

View file

@ -1,5 +1,5 @@
namespace FrostFS.SDK.ClientV2; namespace FrostFS.SDK.ClientV2;
public sealed class PrmNetworkSettings() : PrmBase public sealed class PrmNetworkSettings(CallContext? ctx = null) : PrmBase(ctx)
{ {
} }

View file

@ -1,5 +1,5 @@
namespace FrostFS.SDK.ClientV2; namespace FrostFS.SDK.ClientV2;
public sealed class PrmNodeInfo() : PrmBase public sealed class PrmNodeInfo(CallContext? ctx = null) : PrmBase(ctx)
{ {
} }

View file

@ -1,6 +1,6 @@
namespace FrostFS.SDK.ClientV2; 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; public FrostFsContainerId ContainerId { get; set; } = containerId;

View file

@ -1,6 +1,6 @@
namespace FrostFS.SDK.ClientV2; 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; public FrostFsContainerId ContainerId { get; set; } = containerId;

View file

@ -1,6 +1,6 @@
namespace FrostFS.SDK.ClientV2; 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; public FrostFsContainerId ContainerId { get; set; } = containerId;

View file

@ -2,7 +2,7 @@ using System.IO;
namespace FrostFS.SDK.ClientV2; namespace FrostFS.SDK.ClientV2;
public sealed class PrmObjectPut : PrmBase, ISessionToken public sealed class PrmObjectPut(CallContext? ctx = null) : PrmBase(ctx), ISessionToken
{ {
/// <summary> /// <summary>
/// Need to provide values like <c>ContainerId</c> and <c>ObjectType</c> to create and object. /// Need to provide values like <c>ContainerId</c> and <c>ObjectType</c> to create and object.

View file

@ -2,7 +2,7 @@
namespace FrostFS.SDK.ClientV2; 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
{ {
/// <summary> /// <summary>
/// Defines container for the search /// Defines container for the search

View file

@ -1,6 +1,6 @@
namespace FrostFS.SDK.ClientV2; 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; public ulong Expiration { get; set; } = expiration;
} }

View file

@ -1,6 +1,6 @@
namespace FrostFS.SDK.ClientV2; 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; public FrostFsObject FrostFsObject { get; set; } = frostFsObject;

View file

@ -27,8 +27,7 @@ public class ClientStatusMonitor : IClientStatus
MethodIndex.methodSessionCreate, MethodIndex.methodSessionCreate,
MethodIndex.methodAPEManagerAddChain, MethodIndex.methodAPEManagerAddChain,
MethodIndex.methodAPEManagerRemoveChain, MethodIndex.methodAPEManagerRemoveChain,
MethodIndex.methodAPEManagerListChains, MethodIndex.methodAPEManagerListChains
MethodIndex.methodLast
]; ];
public static string GetMethodName(MethodIndex index) public static string GetMethodName(MethodIndex index)
@ -53,7 +52,7 @@ public class ClientStatusMonitor : IClientStatus
MethodIndex.methodAPEManagerAddChain => "APEManagerAddChain", MethodIndex.methodAPEManagerAddChain => "APEManagerAddChain",
MethodIndex.methodAPEManagerRemoveChain => "APEManagerRemoveChain", MethodIndex.methodAPEManagerRemoveChain => "APEManagerRemoveChain",
MethodIndex.methodAPEManagerListChains => "APEManagerListChains", 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 readonly ILogger? logger;
private int healthy; private int healthy;
public ClientStatusMonitor(ILogger? logger, string address, uint errorThreshold) public ClientStatusMonitor(ILogger? logger, string address)
{ {
this.logger = logger; this.logger = logger;
healthy = (int)HealthyStatus.Healthy; healthy = (int)HealthyStatus.Healthy;
Address = address;
ErrorThreshold = errorThreshold;
Address = address;
Methods = new MethodStatus[MethodIndexes.Length]; Methods = new MethodStatus[MethodIndexes.Length];
for (int i = 0; i < MethodIndexes.Length; i++) for (int i = 0; i < MethodIndexes.Length; i++)
@ -79,7 +77,7 @@ public class ClientStatusMonitor : IClientStatus
public string Address { get; } public string Address { get; }
internal uint ErrorThreshold { get; } internal uint ErrorThreshold { get; set; }
public uint CurrentErrorCount { get; set; } public uint CurrentErrorCount { get; set; }
@ -89,7 +87,8 @@ public class ClientStatusMonitor : IClientStatus
public bool IsHealthy() 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() public bool IsDialed()
@ -124,14 +123,13 @@ public class ClientStatusMonitor : IClientStatus
if (thresholdReached) if (thresholdReached)
{ {
SetUnhealthy(); SetUnhealthy();
CurrentErrorCount = 0; 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);
} }
} }

View file

@ -1,38 +1,36 @@
using System.Threading.Tasks; using System;
using System.Threading.Tasks;
using Grpc.Core;
namespace FrostFS.SDK.ClientV2; namespace FrostFS.SDK.ClientV2;
// clientWrapper is used by default, alternative implementations are intended for testing purposes only. // 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(); 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; WrapperPrm = wrapperPrm;
StatusMonitor = new ClientStatusMonitor(wrapperPrm.Logger, wrapperPrm.Address, wrapperPrm.ErrorThreshold); ErrorThreshold = wrapperPrm.ErrorThreshold;
try sessionCache = pool.SessionCache;
{ Client = new FrostFSClient(WrapperPrm, sessionCache);
Client = new FrostFSClient(WrapperPrm);
StatusMonitor.SetHealthy();
}
catch (FrostFsException)
{
}
} }
internal FrostFSClient? Client { get; private set; } internal FrostFSClient? Client { get; private set; }
internal WrapperPrm WrapperPrm { get; } internal WrapperPrm WrapperPrm { get; }
internal ClientStatusMonitor StatusMonitor { get; }
internal FrostFSClient? GetClient() internal FrostFSClient? GetClient()
{ {
lock (_lock) lock (_lock)
{ {
if (StatusMonitor.IsHealthy()) if (IsHealthy())
{ {
return Client; return Client;
} }
@ -44,21 +42,29 @@ public class ClientWrapper
// dial establishes a connection to the server from the FrostFS network. // dial establishes a connection to the server from the FrostFS network.
// Returns an error describing failure reason. If failed, the client // Returns an error describing failure reason. If failed, the client
// SHOULD NOT be used. // SHOULD NOT be used.
internal async Task<string?> Dial(CallContext ctx) internal async Task Dial(CallContext ctx)
{ {
var client = GetClient(); var client = GetClient() ?? throw new FrostFsInvalidObjectException("pool client unhealthy");
if (client == null) await client.Dial(ctx).ConfigureAwait(false);
return "pool client unhealthy"; }
var result = await client.Dial(ctx).ConfigureAwait(false); internal void HandleError(Exception ex)
if (!string.IsNullOrEmpty(result)) {
if (ex is FrostFsResponseException responseException && responseException.Status != null)
{ {
StatusMonitor.SetUnhealthyOnDial(); switch (responseException.Status.Code)
return result; {
case FrostFsStatusCode.Internal:
case FrostFsStatusCode.WrongMagicNumber:
case FrostFsStatusCode.SignatureVerificationFailure:
case FrostFsStatusCode.NodeUnderMaintenance:
IncErrorRate();
return;
}
} }
return null; IncErrorRate();
} }
private async Task ScheduleGracefulClose() private async Task ScheduleGracefulClose()
@ -79,31 +85,31 @@ public class ClientWrapper
try try
{ {
var prmNodeInfo = new PrmNodeInfo { Context = ctx }; var prmNodeInfo = new PrmNodeInfo(ctx);
var response = await Client!.GetNodeInfoAsync(prmNodeInfo).ConfigureAwait(false); var response = await Client!.GetNodeInfoAsync(prmNodeInfo).ConfigureAwait(false);
return false; return false;
} }
catch (FrostFsException) catch (RpcException)
{ {
wasHealthy = true; wasHealthy = true;
} }
// if connection is dialed before, to avoid routine/connection leak, // if connection is dialed before, to avoid routine/connection leak,
// pool has to close it and then initialize once again. // pool has to close it and then initialize once again.
if (StatusMonitor.IsDialed()) if (IsDialed())
{ {
await ScheduleGracefulClose().ConfigureAwait(false); await ScheduleGracefulClose().ConfigureAwait(false);
} }
#pragma warning disable CA2000 // Dispose objects before losing scope: will be disposed manually #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 #pragma warning restore CA2000
//TODO: set additioanl params //TODO: set additioanl params
var error = await client.Dial(ctx).ConfigureAwait(false); var error = await client.Dial(ctx).ConfigureAwait(false);
if (!string.IsNullOrEmpty(error)) if (!string.IsNullOrEmpty(error))
{ {
StatusMonitor.SetUnhealthyOnDial(); SetUnhealthyOnDial();
return wasHealthy; return wasHealthy;
} }
@ -114,22 +120,22 @@ public class ClientWrapper
try try
{ {
var prmNodeInfo = new PrmNodeInfo { Context = ctx }; var prmNodeInfo = new PrmNodeInfo(ctx);
var res = await client.GetNodeInfoAsync(prmNodeInfo).ConfigureAwait(false); var res = await client.GetNodeInfoAsync(prmNodeInfo).ConfigureAwait(false);
} }
catch (FrostFsException) catch (FrostFsException)
{ {
StatusMonitor.SetUnhealthy(); SetUnhealthy();
return wasHealthy; return wasHealthy;
} }
StatusMonitor.SetHealthy(); SetHealthy();
return !wasHealthy; return !wasHealthy;
} }
internal void IncRequests(ulong elapsed, MethodIndex method) internal void IncRequests(ulong elapsed, MethodIndex method)
{ {
var methodStat = StatusMonitor.Methods[(int)method]; var methodStat = Methods[(int)method];
methodStat.IncRequests(elapsed); methodStat.IncRequests(elapsed);
} }

View file

@ -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; }
}

View file

@ -1,6 +1,8 @@
using System; using System;
using System.Security.Cryptography; using System.Security.Cryptography;
using Grpc.Net.Client;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace FrostFS.SDK.ClientV2; namespace FrostFS.SDK.ClientV2;
@ -24,7 +26,7 @@ public class InitParameters
public NodeParam[]? NodeParams { get; set; } public NodeParam[]? NodeParams { get; set; }
public DialOptions[]? DialOptions { get; set; } public GrpcChannelOptions[]? DialOptions { get; set; }
public Func<string, ClientWrapper>? ClientBuilder { get; set; } public Func<string, ClientWrapper>? ClientBuilder { get; set; }

View file

@ -21,7 +21,7 @@ internal sealed class InnerPool
if (Clients.Length == 1) if (Clients.Length == 1)
{ {
var client = Clients[0]; var client = Clients[0];
if (client.StatusMonitor.IsHealthy()) if (client.IsHealthy())
{ {
return client; return client;
} }
@ -34,7 +34,7 @@ internal sealed class InnerPool
{ {
int index = Sampler.Next(); int index = Sampler.Next();
if (Clients[index].StatusMonitor.IsHealthy()) if (Clients[index].IsHealthy())
{ {
return Clients[index]; return Clients[index];
} }

View file

@ -2,7 +2,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -13,6 +12,8 @@ using FrostFS.SDK.ClientV2.Interfaces;
using FrostFS.SDK.ClientV2.Mappers.GRPC; using FrostFS.SDK.ClientV2.Mappers.GRPC;
using FrostFS.SDK.Cryptography; using FrostFS.SDK.Cryptography;
using Grpc.Core;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -36,7 +37,7 @@ public partial class Pool : IFrostFSClient
private ECDsa Key { get; set; } private ECDsa Key { get; set; }
private byte[] PublicKey { get; } private string PublicKey { get; }
private OwnerID? _ownerId; private OwnerID? _ownerId;
private FrostFsOwner? _owner; private FrostFsOwner? _owner;
@ -65,7 +66,7 @@ public partial class Pool : IFrostFSClient
internal CancellationTokenSource CancellationTokenSource { get; } = new CancellationTokenSource(); internal CancellationTokenSource CancellationTokenSource { get; } = new CancellationTokenSource();
private SessionCache Cache { get; set; } internal SessionCache SessionCache { get; set; }
private ulong SessionTokenDuration { get; set; } private ulong SessionTokenDuration { get; set; }
@ -75,7 +76,7 @@ public partial class Pool : IFrostFSClient
private bool disposedValue; private bool disposedValue;
private ILogger? Logger { get; set; } private ILogger? logger { get; set; }
private ulong MaxObjectSize { get; set; } private ulong MaxObjectSize { get; set; }
@ -91,20 +92,20 @@ public partial class Pool : IFrostFSClient
if (options.Key == null) 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 nodesParams = AdjustNodeParams(options.NodeParams);
var cache = new SessionCache(options.SessionExpirationDuration); var cache = new SessionCache(options.SessionExpirationDuration);
FillDefaultInitParams(options, cache); FillDefaultInitParams(options, this);
Key = options.Key; Key = options.Key;
PublicKey = Key.PublicKey(); PublicKey = $"{Key.PublicKey()}";
Cache = cache; SessionCache = cache;
Logger = options.Logger; logger = options.Logger;
SessionTokenDuration = options.SessionExpirationDuration; SessionTokenDuration = options.SessionExpirationDuration;
RebalanceParams = new RebalanceParameters( RebalanceParams = new RebalanceParameters(
@ -148,47 +149,54 @@ public partial class Pool : IFrostFSClient
for (int j = 0; j < nodeParams.Addresses.Count; j++) for (int j = 0; j < nodeParams.Addresses.Count; j++)
{ {
var client = ClientBuilder(nodeParams.Addresses[j]); ClientWrapper? client = null;
clients[j] = client; bool dialed = false;
var error = await client.Dial(ctx).ConfigureAwait(false);
if (!string.IsNullOrEmpty(error))
{
Logger?.LogWarning("Failed to build client. Address {Address}, {Error})", client.WrapperPrm.Address, error);
continue;
}
try 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) var token = await InitSessionForDuration(ctx, client, RebalanceParams.SessionExpirationDuration, Key, false)
.ConfigureAwait(false); .ConfigureAwait(false);
var key = FormCacheKey(nodeParams.Addresses[j], Key, false); var key = FormCacheKey(nodeParams.Addresses[j], Key.PrivateKey().ToString());
_ = Cache.Cache[key] = token; _ = SessionCache.Cache[key] = token;
atLeastOneHealthy = true;
} }
catch (FrostFsException ex) catch (RpcException ex)
{ {
client.StatusMonitor.SetUnhealthy(); if (!dialed)
Logger?.LogWarning("Failed to create frostfs session token for client. Address {Address}, {Error})", client!.SetUnhealthyOnDial();
client.WrapperPrm.Address, ex.Message); 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()); var sampler = new Sampler(nodeParams.Weights.ToArray());
inner[i] = new InnerPool(sampler, clients); inner[i] = new InnerPool(sampler, clients);
i++;
} }
if (!atLeastOneHealthy) if (!atLeastOneHealthy)
return "at least one node must be healthy"; return "At least one node must be healthy";
InnerPools = inner; InnerPools = inner;
var res = await GetNetworkSettingsAsync(new PrmNetworkSettings { Context = ctx }).ConfigureAwait(false); var res = await GetNetworkSettingsAsync(new PrmNetworkSettings(ctx)).ConfigureAwait(false);
MaxObjectSize = res.MaxObjectSize; MaxObjectSize = res.MaxObjectSize;
@ -252,7 +260,7 @@ public partial class Pool : IFrostFSClient
return adjusted; return adjusted;
} }
private static void FillDefaultInitParams(InitParameters parameters, SessionCache cache) private static void FillDefaultInitParams(InitParameters parameters, Pool pool)
{ {
if (parameters.SessionExpirationDuration == 0) if (parameters.SessionExpirationDuration == 0)
parameters.SessionExpirationDuration = defaultSessionTokenExpirationDuration; parameters.SessionExpirationDuration = defaultSessionTokenExpirationDuration;
@ -275,8 +283,8 @@ public partial class Pool : IFrostFSClient
if (parameters.NodeStreamTimeout <= 0) if (parameters.NodeStreamTimeout <= 0)
parameters.NodeStreamTimeout = defaultStreamTimeout; parameters.NodeStreamTimeout = defaultStreamTimeout;
if (cache.TokenDuration == 0) if (parameters.SessionExpirationDuration == 0)
cache.TokenDuration = defaultSessionTokenExpirationDuration; parameters.SessionExpirationDuration = defaultSessionTokenExpirationDuration;
parameters.ClientBuilder ??= new Func<string, ClientWrapper>((address) => parameters.ClientBuilder ??= new Func<string, ClientWrapper>((address) =>
{ {
@ -291,29 +299,29 @@ public partial class Pool : IFrostFSClient
GracefulCloseOnSwitchTimeout = parameters.GracefulCloseOnSwitchTimeout GracefulCloseOnSwitchTimeout = parameters.GracefulCloseOnSwitchTimeout
}; };
return new ClientWrapper(wrapperPrm); return new ClientWrapper(wrapperPrm, pool);
} }
); );
} }
private FrostFSClient? Сonnection() private ClientWrapper Сonnection()
{ {
foreach (var pool in InnerPools!) foreach (var pool in InnerPools!)
{ {
var client = pool.Connection(); var client = pool.Connection();
if (client != null) if (client != null)
{ {
return client.Client; return client;
} }
} }
return null; throw new FrostFsException("Cannot find alive client");
} }
private static async Task<FrostFsSessionToken?> InitSessionForDuration(CallContext ctx, ClientWrapper cw, ulong duration, ECDsa key, bool clientCut) private static async Task<FrostFsSessionToken?> InitSessionForDuration(CallContext ctx, ClientWrapper cw, ulong duration, ECDsa key, bool clientCut)
{ {
var client = cw.Client; 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; var epoch = networkInfo.Epoch;
@ -321,17 +329,14 @@ public partial class Pool : IFrostFSClient
? ulong.MaxValue ? ulong.MaxValue
: epoch + duration; : epoch + duration;
var prmSessionCreate = new PrmSessionCreate(exp) { Context = ctx }; var prmSessionCreate = new PrmSessionCreate(exp, ctx);
return await client.CreateSessionAsync(prmSessionCreate).ConfigureAwait(false); 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; return $"{address}{key}";
var stype = clientCut ? "client" : "server";
return $"{address}{stype}{k}";
} }
public void Close() public void Close()
@ -343,7 +348,7 @@ public partial class Pool : IFrostFSClient
// close all clients // close all clients
foreach (var innerPool in InnerPools) foreach (var innerPool in InnerPools)
foreach (var client in innerPool.Clients) foreach (var client in innerPool.Clients)
if (client.StatusMonitor.IsDialed()) if (client.IsDialed())
client.Client?.Close(); client.Client?.Close();
} }
} }
@ -355,7 +360,7 @@ public partial class Pool : IFrostFSClient
for (int i = 0; i < RebalanceParams.NodesParams.Length; i++) 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]; buffers[i] = new double[parameters.Weights.Count];
Task.Run(async () => Task.Run(async () =>
@ -405,25 +410,27 @@ public partial class Pool : IFrostFSClient
try try
{ {
// check timeout settings // check timeout settings
changed = await client.RestartIfUnhealthy(ctx).ConfigureAwait(false); changed = await client!.RestartIfUnhealthy(ctx).ConfigureAwait(false);
healthy = true; healthy = true;
bufferWeights[j] = options.NodesParams[poolIndex].Weights[j]; bufferWeights[j] = options.NodesParams[poolIndex].Weights[j];
} }
// TODO: specify
catch (FrostFsException e) catch (FrostFsException e)
{ {
error = e.Message; error = e.Message;
bufferWeights[j] = 0; bufferWeights[j] = 0;
Cache.DeleteByPrefix(client.StatusMonitor.Address); SessionCache.DeleteByPrefix(client.Address);
} }
if (changed) if (changed)
{ {
StringBuilder fields = new($"address {client.StatusMonitor.Address}, healthy {healthy}"); if (!string.IsNullOrEmpty(error))
if (string.IsNullOrEmpty(error))
{ {
fields.Append($", reason {error}"); if (logger != null)
Logger?.Log(LogLevel.Warning, "Health has changed: {Fields}", fields.ToString()); {
FrostFsMessages.HealthChanged(logger, client.Address, healthy, error!);
}
Interlocked.Exchange(ref healthyChanged, 1); Interlocked.Exchange(ref healthyChanged, 1);
} }
@ -443,6 +450,8 @@ public partial class Pool : IFrostFSClient
} }
} }
// TODO: remove
private bool CheckSessionTokenErr(Exception error, string address) private bool CheckSessionTokenErr(Exception error, string address)
{ {
if (error == null) if (error == null)
@ -452,7 +461,7 @@ public partial class Pool : IFrostFSClient
if (error is SessionNotFoundException || error is SessionExpiredException) if (error is SessionNotFoundException || error is SessionExpiredException)
{ {
this.Cache.DeleteByPrefix(address); this.SessionCache.DeleteByPrefix(address);
return true; return true;
} }
@ -463,14 +472,13 @@ public partial class Pool : IFrostFSClient
{ {
if (InnerPools == null) if (InnerPools == null)
{ {
throw new InvalidObjectException(nameof(Pool)); throw new FrostFsInvalidObjectException(nameof(Pool));
} }
var statistics = new Statistic(); var statistics = new Statistic();
foreach (var inner in InnerPools) foreach (var inner in InnerPools)
{ {
int nodeIndex = 0;
int valueIndex = 0; int valueIndex = 0;
var nodes = new string[inner.Clients.Length]; var nodes = new string[inner.Clients.Length];
@ -478,20 +486,22 @@ public partial class Pool : IFrostFSClient
{ {
foreach (var client in inner.Clients) 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 var node = new NodeStatistic
{ {
Address = client.StatusMonitor.Address, Address = client.Address,
Methods = client.StatusMonitor.MethodsStatus(), Methods = client.MethodsStatus(),
OverallErrors = client.StatusMonitor.GetOverallErrorRate(), OverallErrors = client.GetOverallErrorRate(),
CurrentErrors = client.StatusMonitor.GetCurrentErrorRate() CurrentErrors = client.GetCurrentErrorRate()
}; };
statistics.Nodes[nodeIndex++] = node; statistics.Nodes.Add(node);
valueIndex++;
statistics.OverallErrors += node.OverallErrors; statistics.OverallErrors += node.OverallErrors;
} }
@ -508,120 +518,234 @@ public partial class Pool : IFrostFSClient
public async Task<FrostFsNetmapSnapshot> GetNetmapSnapshotAsync(PrmNetmapSnapshot? args = null) public async Task<FrostFsNetmapSnapshot> GetNetmapSnapshotAsync(PrmNetmapSnapshot? args = null)
{ {
var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); var client = Сonnection();
return await client.GetNetmapSnapshotAsync(args).ConfigureAwait(false);
args ??= new();
args.Context.PoolErrorHandler = client.HandleError;
return await client.Client!.GetNetmapSnapshotAsync(args).ConfigureAwait(false);
} }
public async Task<FrostFsNodeInfo> GetNodeInfoAsync(PrmNodeInfo? args = null) public async Task<FrostFsNodeInfo> GetNodeInfoAsync(PrmNodeInfo? args = null)
{ {
var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); var client = Сonnection();
return await client.GetNodeInfoAsync(args).ConfigureAwait(false);
args ??= new();
args.Context.PoolErrorHandler = client.HandleError;
return await client.Client!.GetNodeInfoAsync(args).ConfigureAwait(false);
} }
public async Task<NetworkSettings> GetNetworkSettingsAsync(PrmNetworkSettings? args = null) public async Task<NetworkSettings> GetNetworkSettingsAsync(PrmNetworkSettings? args = null)
{ {
var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); var client = Сonnection();
return await client.GetNetworkSettingsAsync(args).ConfigureAwait(false);
args ??= new();
args.Context.PoolErrorHandler = client.HandleError;
return await client.Client!.GetNetworkSettingsAsync(args).ConfigureAwait(false);
} }
public async Task<FrostFsSessionToken> CreateSessionAsync(PrmSessionCreate args) public async Task<FrostFsSessionToken> CreateSessionAsync(PrmSessionCreate args)
{ {
var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); if (args is null)
return await client.CreateSessionAsync(args).ConfigureAwait(false); {
throw new ArgumentNullException(nameof(args));
}
var client = Сonnection();
args.Context.PoolErrorHandler = client.HandleError;
return await client.Client!.CreateSessionAsync(args).ConfigureAwait(false);
} }
public async Task<byte[]> AddChainAsync(PrmApeChainAdd args) public async Task<byte[]> AddChainAsync(PrmApeChainAdd args)
{ {
var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); if (args is null)
return await client.AddChainAsync(args).ConfigureAwait(false); {
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) public async Task RemoveChainAsync(PrmApeChainRemove args)
{ {
var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); if (args is null)
await client.RemoveChainAsync(args).ConfigureAwait(false); {
throw new ArgumentNullException(nameof(args));
}
var client = Сonnection();
args.Context.PoolErrorHandler = client.HandleError;
await client.Client!.RemoveChainAsync(args).ConfigureAwait(false);
} }
public async Task<Chain[]> ListChainAsync(PrmApeChainList args) public async Task<Chain[]> ListChainAsync(PrmApeChainList args)
{ {
var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); if (args is null)
return await client.ListChainAsync(args).ConfigureAwait(false); {
throw new ArgumentNullException(nameof(args));
}
var client = Сonnection();
args.Context.PoolErrorHandler = client.HandleError;
return await client.Client!.ListChainAsync(args).ConfigureAwait(false);
} }
public async Task<FrostFsContainerInfo> GetContainerAsync(PrmContainerGet args) public async Task<FrostFsContainerInfo> GetContainerAsync(PrmContainerGet args)
{ {
var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); if (args is null)
return await client.GetContainerAsync(args).ConfigureAwait(false); {
throw new ArgumentNullException(nameof(args));
}
var client = Сonnection();
args.Context.PoolErrorHandler = client.HandleError;
return await client.Client!.GetContainerAsync(args).ConfigureAwait(false);
} }
public IAsyncEnumerable<FrostFsContainerId> ListContainersAsync(PrmContainerGetAll? args = null) public IAsyncEnumerable<FrostFsContainerId> ListContainersAsync(PrmContainerGetAll? args = null)
{ {
var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); var client = Сonnection();
return client.ListContainersAsync(args);
args ??= new();
args.Context.PoolErrorHandler = client.HandleError;
return client.Client!.ListContainersAsync(args);
} }
public async Task<FrostFsContainerId> CreateContainerAsync(PrmContainerCreate args) public async Task<FrostFsContainerId> CreateContainerAsync(PrmContainerCreate args)
{ {
var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); if (args is null)
return await client.CreateContainerAsync(args).ConfigureAwait(false); {
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) public async Task DeleteContainerAsync(PrmContainerDelete args)
{ {
var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); if (args is null)
await client.DeleteContainerAsync(args).ConfigureAwait(false); {
throw new ArgumentNullException(nameof(args));
}
var client = Сonnection();
args.Context.PoolErrorHandler = client.HandleError;
await client.Client!.DeleteContainerAsync(args).ConfigureAwait(false);
} }
public async Task<FrostFsObjectHeader> GetObjectHeadAsync(PrmObjectHeadGet args) public async Task<FrostFsObjectHeader> GetObjectHeadAsync(PrmObjectHeadGet args)
{ {
var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); if (args is null)
return await client.GetObjectHeadAsync(args).ConfigureAwait(false); {
throw new ArgumentNullException(nameof(args));
}
var client = Сonnection();
args.Context.PoolErrorHandler = client.HandleError;
return await client.Client!.GetObjectHeadAsync(args).ConfigureAwait(false);
} }
public async Task<FrostFsObject> GetObjectAsync(PrmObjectGet args) public async Task<FrostFsObject> GetObjectAsync(PrmObjectGet args)
{ {
var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); if (args is null)
return await client.GetObjectAsync(args).ConfigureAwait(false); {
throw new ArgumentNullException(nameof(args));
}
var client = Сonnection();
args.Context.PoolErrorHandler = client.HandleError;
return await client.Client!.GetObjectAsync(args).ConfigureAwait(false);
} }
public async Task<FrostFsObjectId> PutObjectAsync(PrmObjectPut args) public async Task<FrostFsObjectId> PutObjectAsync(PrmObjectPut args)
{ {
var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); if (args is null)
return await client.PutObjectAsync(args).ConfigureAwait(false); {
throw new ArgumentNullException(nameof(args));
}
var client = Сonnection();
args.Context.PoolErrorHandler = client.HandleError;
return await client.Client!.PutObjectAsync(args).ConfigureAwait(false);
} }
public async Task<FrostFsObjectId> PutSingleObjectAsync(PrmSingleObjectPut args) public async Task<FrostFsObjectId> PutSingleObjectAsync(PrmSingleObjectPut args)
{ {
var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); if (args is null)
return await client.PutSingleObjectAsync(args).ConfigureAwait(false); {
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) public async Task DeleteObjectAsync(PrmObjectDelete args)
{ {
var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); if (args is null)
await client.DeleteObjectAsync(args).ConfigureAwait(false); {
throw new ArgumentNullException(nameof(args));
}
var client = Сonnection();
args.Context.PoolErrorHandler = client.HandleError;
await client.Client!.DeleteObjectAsync(args).ConfigureAwait(false);
} }
public IAsyncEnumerable<FrostFsObjectId> SearchObjectsAsync(PrmObjectSearch args) public IAsyncEnumerable<FrostFsObjectId> SearchObjectsAsync(PrmObjectSearch args)
{ {
var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); if (args is null)
return client.SearchObjectsAsync(args); {
throw new ArgumentNullException(nameof(args));
}
var client = Сonnection();
args.Context.PoolErrorHandler = client.HandleError;
return client.Client!.SearchObjectsAsync(args);
} }
public async Task<Accounting.Decimal> GetBalanceAsync(PrmBalance? args = null) public async Task<Accounting.Decimal> GetBalanceAsync(PrmBalance? args)
{ {
var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); var client = Сonnection();
return await client.GetBalanceAsync(args).ConfigureAwait(false);
}
public bool RestartIfUnhealthy(CallContext ctx) args ??= new();
{ args.Context.PoolErrorHandler = client.HandleError;
throw new NotImplementedException();
}
public bool IsHealthy() return await client.Client!.GetBalanceAsync(args).ConfigureAwait(false);
{
throw new NotImplementedException();
} }
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)

View file

@ -3,7 +3,7 @@
public class RebalanceParameters( public class RebalanceParameters(
NodesParam[] nodesParams, NodesParam[] nodesParams,
ulong nodeRequestTimeout, ulong nodeRequestTimeout,
ulong clientRebalanceInterval, ulong clientRebalanceInterval,
ulong sessionExpirationDuration) ulong sessionExpirationDuration)
{ {
public NodesParam[] NodesParams { get; set; } = nodesParams; public NodesParam[] NodesParams { get; set; } = nodesParams;

View file

@ -3,18 +3,13 @@ using System.Collections;
namespace FrostFS.SDK.ClientV2; namespace FrostFS.SDK.ClientV2;
internal struct SessionCache internal struct SessionCache(ulong sessionExpirationDuration)
{ {
public SessionCache(ulong sessionExpirationDuration)
{
TokenDuration = sessionExpirationDuration;
}
internal Hashtable Cache { get; } = []; internal Hashtable Cache { get; } = [];
internal ulong CurrentEpoch { get; set; } internal ulong CurrentEpoch { get; set; }
internal ulong TokenDuration { get; set; } internal ulong TokenDuration { get; set; } = sessionExpirationDuration;
internal void DeleteByPrefix(string prefix) internal void DeleteByPrefix(string prefix)
{ {

View file

@ -10,7 +10,7 @@ internal sealed class AccountingServiceProvider : ContextAccessor
internal AccountingServiceProvider( internal AccountingServiceProvider(
AccountingService.AccountingServiceClient? accountingServiceClient, AccountingService.AccountingServiceClient? accountingServiceClient,
EnvironmentContext context) ClientContext context)
: base(context) : base(context)
{ {
_accountingServiceClient = accountingServiceClient; _accountingServiceClient = accountingServiceClient;

View file

@ -1,3 +1,4 @@
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Frostfs.V2.Ape; using Frostfs.V2.Ape;
@ -9,7 +10,7 @@ internal sealed class ApeManagerServiceProvider : ContextAccessor
{ {
private readonly APEManagerService.APEManagerServiceClient? _apeManagerServiceClient; private readonly APEManagerService.APEManagerServiceClient? _apeManagerServiceClient;
internal ApeManagerServiceProvider(APEManagerService.APEManagerServiceClient? apeManagerServiceClient, EnvironmentContext context) internal ApeManagerServiceProvider(APEManagerService.APEManagerServiceClient? apeManagerServiceClient, ClientContext context)
: base(context) : base(context)
{ {
_apeManagerServiceClient = apeManagerServiceClient; _apeManagerServiceClient = apeManagerServiceClient;
@ -18,10 +19,10 @@ internal sealed class ApeManagerServiceProvider : ContextAccessor
internal async Task<byte[]> AddChainAsync(PrmApeChainAdd args) internal async Task<byte[]> AddChainAsync(PrmApeChainAdd args)
{ {
var ctx = args.Context!; var ctx = args.Context!;
ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; ctx.Key ??= ClientContext.Key?.ECDsaKey;
if (ctx.Key == null) if (ctx.Key == null)
throw new InvalidObjectException(nameof(ctx.Key)); throw new ArgumentNullException(nameof(args), "Key is null");
AddChainRequest request = new() AddChainRequest request = new()
{ {
@ -45,10 +46,10 @@ internal sealed class ApeManagerServiceProvider : ContextAccessor
internal async Task RemoveChainAsync(PrmApeChainRemove args) internal async Task RemoveChainAsync(PrmApeChainRemove args)
{ {
var ctx = args.Context!; var ctx = args.Context!;
ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; ctx.Key ??= ClientContext.Key?.ECDsaKey;
if (ctx.Key == null) if (ctx.Key == null)
throw new InvalidObjectException(nameof(ctx.Key)); throw new ArgumentNullException(nameof(args), "Key is null");
RemoveChainRequest request = new() RemoveChainRequest request = new()
{ {
@ -70,10 +71,10 @@ internal sealed class ApeManagerServiceProvider : ContextAccessor
internal async Task<Chain[]> ListChainAsync(PrmApeChainList args) internal async Task<Chain[]> ListChainAsync(PrmApeChainList args)
{ {
var ctx = args.Context!; var ctx = args.Context!;
ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; ctx.Key ??= ClientContext.Key?.ECDsaKey;
if (ctx.Key == null) if (ctx.Key == null)
throw new InvalidObjectException(nameof(ctx.Key)); throw new ArgumentNullException(nameof(args), "Key is null");
ListChainsRequest request = new() ListChainsRequest request = new()
{ {

View file

@ -12,20 +12,28 @@ using FrostFS.Session;
namespace FrostFS.SDK.ClientV2; 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<SessionToken> GetOrCreateSession(ISessionToken args, CallContext ctx) public async ValueTask<SessionToken> 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); return await sessions.GetOrCreateSession(args, ctx).ConfigureAwait(false);
} }
internal async Task<FrostFsContainerInfo> GetContainerAsync(PrmContainerGet args) internal async Task<FrostFsContainerInfo> 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); Verifier.CheckResponse(response);
@ -35,13 +43,13 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService
internal async IAsyncEnumerable<FrostFsContainerId> ListContainersAsync(PrmContainerGetAll args) internal async IAsyncEnumerable<FrostFsContainerId> ListContainersAsync(PrmContainerGetAll args)
{ {
var ctx = args.Context!; var ctx = args.Context!;
ctx.OwnerId ??= EnvironmentContext.Owner; ctx.OwnerId ??= ClientContext.Owner;
ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; ctx.Key ??= ClientContext.Key?.ECDsaKey;
if (ctx.Key == null) if (ctx.Key == null)
throw new InvalidObjectException(nameof(ctx.Key)); throw new ArgumentNullException(nameof(args), "Key is null");
if (ctx.OwnerId == null) if (ctx.OwnerId == null)
throw new InvalidObjectException(nameof(ctx.OwnerId)); throw new ArgumentException(nameof(ctx.OwnerId));
var request = new ListRequest var request = new ListRequest
{ {
@ -74,11 +82,11 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService
grpcContainer.Version ??= ctx.Version?.ToMessage(); grpcContainer.Version ??= ctx.Version?.ToMessage();
if (ctx.Key == null) if (ctx.Key == null)
throw new InvalidObjectException(nameof(ctx.Key)); throw new ArgumentNullException(nameof(args), "Key is null");
if (grpcContainer.OwnerId == null) if (grpcContainer.OwnerId == null)
throw new InvalidObjectException(nameof(grpcContainer.OwnerId)); throw new ArgumentException(nameof(grpcContainer.OwnerId));
if (grpcContainer.Version == null) if (grpcContainer.Version == null)
throw new InvalidObjectException(nameof(grpcContainer.Version)); throw new ArgumentException(nameof(grpcContainer.Version));
var request = new PutRequest var request = new PutRequest
{ {
@ -114,7 +122,7 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService
{ {
var ctx = args.Context!; var ctx = args.Context!;
if (ctx.Key == null) if (ctx.Key == null)
throw new InvalidObjectException(nameof(ctx.Key)); throw new ArgumentNullException(nameof(args), "Key is null");
var request = new DeleteRequest var request = new DeleteRequest
{ {
@ -150,7 +158,7 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService
private static GetRequest GetContainerRequest(ContainerID id, NameValueCollection? xHeaders, CallContext ctx) private static GetRequest GetContainerRequest(ContainerID id, NameValueCollection? xHeaders, CallContext ctx)
{ {
if (ctx.Key == null) if (ctx.Key == null)
throw new InvalidObjectException(nameof(ctx.Key)); throw new ArgumentNullException(nameof(ctx), "Key is null");
var request = new GetRequest var request = new GetRequest
{ {
@ -207,7 +215,7 @@ internal sealed class ContainerServiceProvider(ContainerService.ContainerService
await Task.Delay(waitParams.PollInterval).ConfigureAwait(false); await Task.Delay(waitParams.PollInterval).ConfigureAwait(false);
} }
catch (ResponseException ex) catch (FrostFsResponseException ex)
{ {
if (DateTime.UtcNow >= deadLine) if (DateTime.UtcNow >= deadLine)
throw new TimeoutException(); throw new TimeoutException();

View file

@ -1,4 +1,5 @@
using System.Linq; using System;
using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -12,7 +13,7 @@ internal sealed class NetmapServiceProvider : ContextAccessor
{ {
private readonly NetmapService.NetmapServiceClient netmapServiceClient; private readonly NetmapService.NetmapServiceClient netmapServiceClient;
internal NetmapServiceProvider(NetmapService.NetmapServiceClient netmapServiceClient, EnvironmentContext context) internal NetmapServiceProvider(NetmapService.NetmapServiceClient netmapServiceClient, ClientContext context)
: base(context) : base(context)
{ {
this.netmapServiceClient = netmapServiceClient; this.netmapServiceClient = netmapServiceClient;
@ -20,8 +21,8 @@ internal sealed class NetmapServiceProvider : ContextAccessor
internal async Task<NetworkSettings> GetNetworkSettingsAsync(CallContext ctx) internal async Task<NetworkSettings> GetNetworkSettingsAsync(CallContext ctx)
{ {
if (EnvironmentContext.NetworkSettings != null) if (ClientContext.NetworkSettings != null)
return EnvironmentContext.NetworkSettings; return ClientContext.NetworkSettings;
var response = await GetNetworkInfoAsync(ctx).ConfigureAwait(false); var response = await GetNetworkInfoAsync(ctx).ConfigureAwait(false);
@ -38,7 +39,7 @@ internal sealed class NetmapServiceProvider : ContextAccessor
SetNetworksParam(param, settings); SetNetworksParam(param, settings);
} }
EnvironmentContext.NetworkSettings = settings; ClientContext.NetworkSettings = settings;
return settings; return settings;
} }
@ -46,10 +47,10 @@ internal sealed class NetmapServiceProvider : ContextAccessor
internal async Task<FrostFsNodeInfo> GetLocalNodeInfoAsync(PrmNodeInfo args) internal async Task<FrostFsNodeInfo> GetLocalNodeInfoAsync(PrmNodeInfo args)
{ {
var ctx = args.Context!; var ctx = args.Context!;
ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; ctx.Key ??= ClientContext.Key?.ECDsaKey;
if (ctx.Key == null) if (ctx.Key == null)
throw new InvalidObjectException(nameof(ctx.Key)); throw new ArgumentNullException(nameof(args), "Key is null");
var request = new LocalNodeInfoRequest var request = new LocalNodeInfoRequest
{ {
@ -59,8 +60,6 @@ internal sealed class NetmapServiceProvider : ContextAccessor
request.AddMetaHeader(args.XHeaders); request.AddMetaHeader(args.XHeaders);
request.Sign(ctx.Key); request.Sign(ctx.Key);
var response = await netmapServiceClient.LocalNodeInfoAsync(request, null, ctx.Deadline, ctx.CancellationToken); var response = await netmapServiceClient.LocalNodeInfoAsync(request, null, ctx.Deadline, ctx.CancellationToken);
Verifier.CheckResponse(response); Verifier.CheckResponse(response);
@ -70,10 +69,10 @@ internal sealed class NetmapServiceProvider : ContextAccessor
internal async Task<NetworkInfoResponse> GetNetworkInfoAsync(CallContext ctx) internal async Task<NetworkInfoResponse> GetNetworkInfoAsync(CallContext ctx)
{ {
ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; ctx.Key ??= ClientContext.Key?.ECDsaKey;
if (ctx.Key == null) if (ctx.Key == null)
throw new InvalidObjectException(nameof(ctx.Key)); throw new ArgumentNullException(nameof(ctx), "Key is null");
var request = new NetworkInfoRequest(); var request = new NetworkInfoRequest();
@ -91,10 +90,10 @@ internal sealed class NetmapServiceProvider : ContextAccessor
internal async Task<FrostFsNetmapSnapshot> GetNetmapSnapshotAsync(PrmNetmapSnapshot args) internal async Task<FrostFsNetmapSnapshot> GetNetmapSnapshotAsync(PrmNetmapSnapshot args)
{ {
var ctx = args.Context!; var ctx = args.Context!;
ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; ctx.Key ??= ClientContext.Key?.ECDsaKey;
if (ctx.Key == null) if (ctx.Key == null)
throw new InvalidObjectException(nameof(ctx.Key)); throw new ArgumentNullException(nameof(args), "Key is null");
var request = new NetmapSnapshotRequest(); var request = new NetmapSnapshotRequest();

View file

@ -15,30 +15,32 @@ using Google.Protobuf;
namespace FrostFS.SDK.ClientV2; 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 SessionProvider? sessions;
private ObjectService.ObjectServiceClient client; private readonly ObjectService.ObjectServiceClient client = client;
internal ObjectServiceProvider(ObjectService.ObjectServiceClient client, EnvironmentContext env)
: base(env)
{
this.sessions = new(EnvironmentContext);
this.client = client;
}
public async ValueTask<SessionToken> GetOrCreateSession(ISessionToken args, CallContext ctx) public async ValueTask<SessionToken> 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); return await sessions.GetOrCreateSession(args, ctx).ConfigureAwait(false);
} }
internal async Task<FrostFsObjectHeader> GetObjectHeadAsync(PrmObjectHeadGet args) internal async Task<FrostFsObjectHeader> GetObjectHeadAsync(PrmObjectHeadGet args)
{ {
var ctx = args.Context!; var ctx = args.Context!;
ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; ctx.Key ??= ClientContext.Key?.ECDsaKey;
if (ctx.Key == null) if (ctx.Key == null)
throw new InvalidObjectException(nameof(ctx.Key)); throw new ArgumentNullException(nameof(args), "Key is null");
var request = new HeadRequest var request = new HeadRequest
{ {
@ -74,10 +76,10 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider
{ {
var ctx = args.Context!; var ctx = args.Context!;
ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; ctx.Key ??= ClientContext.Key?.ECDsaKey;
if (ctx.Key == null) if (ctx.Key == null)
throw new InvalidObjectException(nameof(ctx.Key)); throw new ArgumentNullException(nameof(args), "Key is null");
var request = new GetRequest var request = new GetRequest
{ {
@ -108,10 +110,10 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider
internal async Task DeleteObjectAsync(PrmObjectDelete args) internal async Task DeleteObjectAsync(PrmObjectDelete args)
{ {
var ctx = args.Context!; var ctx = args.Context!;
ctx.Key ??= EnvironmentContext.Key?.ECDsaKey; ctx.Key ??= ClientContext.Key?.ECDsaKey;
if (ctx.Key == null) if (ctx.Key == null)
throw new InvalidObjectException(nameof(ctx.Key)); throw new ArgumentNullException(nameof(args), "Key is null");
var request = new DeleteRequest var request = new DeleteRequest
{ {
@ -145,7 +147,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider
var ctx = args.Context!; var ctx = args.Context!;
if (ctx.Key == null) if (ctx.Key == null)
throw new InvalidObjectException(nameof(ctx.Key)); throw new ArgumentNullException(nameof(args), "Key is null");
var request = new SearchRequest var request = new SearchRequest
{ {
@ -183,10 +185,10 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider
throw new ArgumentNullException(nameof(args)); throw new ArgumentNullException(nameof(args));
if (args.Header == null) if (args.Header == null)
throw new ArgumentException(nameof(args.Header)); throw new ArgumentNullException(nameof(args), "Header is null");
if (args.Payload == null) if (args.Payload == null)
throw new ArgumentException(nameof(args.Payload)); throw new ArgumentNullException(nameof(args), "Payload is null");
if (args.ClientCut) if (args.ClientCut)
return await PutClientCutObject(args).ConfigureAwait(false); return await PutClientCutObject(args).ConfigureAwait(false);
@ -206,7 +208,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider
var ctx = args.Context!; var ctx = args.Context!;
if (ctx.Key == null) 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); var grpcObject = ObjectTools.CreateObject(args.FrostFsObject, ctx);
@ -254,7 +256,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider
if (args.MaxObjectSizeCache == 0) 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); .ConfigureAwait(false);
args.MaxObjectSizeCache = (int)networkSettings.MaxObjectSize; 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); 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(); var parentHeader = args.Header.GetHeader();
@ -331,7 +333,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider
{ {
var ctx = args.Context!; var ctx = args.Context!;
if (ctx.Key == null) if (ctx.Key == null)
throw new InvalidObjectException(nameof(ctx.Key)); throw new ArgumentNullException(nameof(args), "Key is null");
var payload = args.Payload!; var payload = args.Payload!;
@ -352,7 +354,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider
} }
else else
{ {
chunkBuffer = EnvironmentContext.GetArrayPool(Constants.ObjectChunkSize).Rent(chunkSize); chunkBuffer = ClientContext.GetArrayPool(Constants.ObjectChunkSize).Rent(chunkSize);
isRentBuffer = true; isRentBuffer = true;
} }
@ -409,7 +411,7 @@ internal sealed class ObjectServiceProvider : ContextAccessor, ISessionProvider
var header = args.Header!; var header = args.Header!;
if (ctx.Key == null) if (ctx.Key == null)
throw new InvalidObjectException(nameof(ctx.Key)); throw new ArgumentNullException(nameof(args), "Key is null");
header.OwnerId ??= ctx.OwnerId; header.OwnerId ??= ctx.OwnerId;
header.Version ??= ctx.Version; header.Version ??= ctx.Version;

View file

@ -10,7 +10,7 @@ internal sealed class SessionServiceProvider : ContextAccessor
{ {
private readonly SessionService.SessionServiceClient? _sessionServiceClient; private readonly SessionService.SessionServiceClient? _sessionServiceClient;
internal SessionServiceProvider(SessionService.SessionServiceClient? sessionServiceClient, EnvironmentContext context) internal SessionServiceProvider(SessionService.SessionServiceClient? sessionServiceClient, ClientContext context)
: base(context) : base(context)
{ {
_sessionServiceClient = sessionServiceClient; _sessionServiceClient = sessionServiceClient;
@ -20,7 +20,7 @@ internal sealed class SessionServiceProvider : ContextAccessor
{ {
var ctx = args.Context!; var ctx = args.Context!;
ctx.OwnerId ??= EnvironmentContext.Owner; ctx.OwnerId ??= ClientContext.Owner;
var request = new CreateRequest var request = new CreateRequest
{ {

View file

@ -1,6 +1,6 @@
namespace FrostFS.SDK.ClientV2; 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;
} }

View file

@ -7,14 +7,13 @@ internal interface ISessionProvider
ValueTask<Session.SessionToken> GetOrCreateSession(ISessionToken args, CallContext ctx); ValueTask<Session.SessionToken> 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<Session.SessionToken> GetOrCreateSession(ISessionToken args, CallContext ctx) public async ValueTask<Session.SessionToken> GetOrCreateSession(ISessionToken args, CallContext ctx)
{ {
if (args.SessionToken is null) 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); .ConfigureAwait(false);
} }

View file

@ -2,16 +2,21 @@ using System;
using System.Buffers; using System.Buffers;
using System.Security.Cryptography; using System.Security.Cryptography;
using FrostFS.SDK.Cryptography;
using Grpc.Net.Client; using Grpc.Net.Client;
namespace FrostFS.SDK.ClientV2; 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<byte>? _arrayPool; private ArrayPool<byte>? _arrayPool;
private string? sessionKey;
internal FrostFsOwner? Owner { get; } = owner; internal FrostFsOwner? Owner { get; } = owner;
internal string? Address { get; } = channel.Target;
internal GrpcChannel Channel { get; private set; } = channel; internal GrpcChannel Channel { get; private set; } = channel;
internal FrostFsVersion Version { get; } = version; 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 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;
}
}
/// <summary> /// <summary>
/// Custom pool is used for predefined sizes of buffers like grpc chunk /// Custom pool is used for predefined sizes of buffers like grpc chunk
/// </summary> /// </summary>

View file

@ -17,13 +17,13 @@ public sealed class ObjectReader(AsyncServerStreamingCall<GetResponse> call) : I
internal async Task<Object.Object> ReadHeader() internal async Task<Object.Object> ReadHeader()
{ {
if (!await Call.ResponseStream.MoveNext().ConfigureAwait(false)) 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; var response = Call.ResponseStream.Current;
Verifier.CheckResponse(response); Verifier.CheckResponse(response);
if (response.Body.ObjectPartCase != GetResponse.Types.Body.ObjectPartOneofCase.Init) 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 return new Object.Object
{ {
@ -41,7 +41,7 @@ public sealed class ObjectReader(AsyncServerStreamingCall<GetResponse> call) : I
Verifier.CheckResponse(response); Verifier.CheckResponse(response);
if (response.Body.ObjectPartCase != GetResponse.Types.Body.ObjectPartOneofCase.Chunk) 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; return response.Body.Chunk.Memory;
} }

View file

@ -59,7 +59,7 @@ internal static class ObjectTools
return; return;
if (ctx.Key == null) if (ctx.Key == null)
throw new InvalidObjectException(nameof(ctx.Key)); throw new FrostFsInvalidObjectException(nameof(ctx.Key));
grpcHeader.Split = new Header.Types.Split grpcHeader.Split = new Header.Types.Split
{ {

View file

@ -122,7 +122,7 @@ public static class Verifier
var status = resp.MetaHeader.Status.ToModel(); var status = resp.MetaHeader.Status.ToModel();
if (status != null && !status.IsSuccess) if (status != null && !status.IsSuccess)
throw new ResponseException(status); throw new FrostFsResponseException(status);
} }
/// <summary> /// <summary>
@ -137,6 +137,6 @@ public static class Verifier
} }
if (!request.Verify()) if (!request.Verify())
throw new FormatException($"invalid response, type={request.GetType()}"); throw new FrostFsResponseException($"invalid response, type={request.GetType()}");
} }
} }

View file

@ -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<string>? callback = null) : Interceptor
{
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
var call = continuation(request, context);
return new AsyncUnaryCall<TResponse>(
HandleUnaryResponse(call),
call.ResponseHeadersAsync,
call.GetStatus,
call.GetTrailers,
call.Dispose);
}
private async Task<TResponse> HandleUnaryResponse<TResponse>(AsyncUnaryCall<TResponse> call)
{
var response = await call;
callback?.Invoke($"elapsed");
return response;
}
}

View file

@ -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<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
ArgumentNullException.ThrowIfNull(continuation);
using var call = continuation(request, context);
return new AsyncUnaryCall<TResponse>(
HandleUnaryResponse(call),
call.ResponseHeadersAsync,
call.GetStatus,
call.GetTrailers,
call.Dispose);
}
private static async Task<TResponse> HandleUnaryResponse<TResponse>(AsyncUnaryCall<TResponse> 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;
}
}

View file

@ -28,19 +28,16 @@ public class NetworkTest : NetworkTestsBase
Mocker.Parameters.Add("HomomorphicHashingDisabled", [1]); Mocker.Parameters.Add("HomomorphicHashingDisabled", [1]);
Mocker.Parameters.Add("MaintenanceModeAllowed", [1]); Mocker.Parameters.Add("MaintenanceModeAllowed", [1]);
var param = new PrmNetworkSettings(); var param = useContext ?
new PrmNetworkSettings(new CallContext
if (useContext)
{
param.Context = new CallContext
{ {
CancellationToken = Mocker.CancellationTokenSource.Token, CancellationToken = Mocker.CancellationTokenSource.Token,
Timeout = TimeSpan.FromSeconds(20), Timeout = TimeSpan.FromSeconds(20),
OwnerId = OwnerId, OwnerId = OwnerId,
Key = ECDsaKey, Key = ECDsaKey,
Version = Version Version = Version
}; })
} : new PrmNetworkSettings();
var validTimeoutFrom = DateTime.UtcNow.AddSeconds(20); var validTimeoutFrom = DateTime.UtcNow.AddSeconds(20);
@ -116,12 +113,11 @@ public class NetworkTest : NetworkTestsBase
Mocker.NetmapSnapshotResponse = new NetmapSnapshotResponse { Body = body }; Mocker.NetmapSnapshotResponse = new NetmapSnapshotResponse { Body = body };
var param = new PrmNetmapSnapshot(); PrmNetmapSnapshot param;
if (useContext) if (useContext)
{ {
param.XHeaders.Add("headerKey1", "headerValue1"); var ctx = new CallContext
param.Context = new CallContext
{ {
CancellationToken = Mocker.CancellationTokenSource.Token, CancellationToken = Mocker.CancellationTokenSource.Token,
Timeout = TimeSpan.FromSeconds(20), Timeout = TimeSpan.FromSeconds(20),
@ -129,6 +125,14 @@ public class NetworkTest : NetworkTestsBase
Key = ECDsaKey, Key = ECDsaKey,
Version = Version Version = Version
}; };
param = new(ctx);
param.XHeaders.Add("headerKey1", "headerValue1");
}
else
{
param = new();
} }
var validTimeoutFrom = DateTime.UtcNow.AddSeconds(20); var validTimeoutFrom = DateTime.UtcNow.AddSeconds(20);
@ -208,12 +212,11 @@ public class NetworkTest : NetworkTestsBase
Mocker.NodeInfoResponse = new LocalNodeInfoResponse { Body = body }; Mocker.NodeInfoResponse = new LocalNodeInfoResponse { Body = body };
var param = new PrmNodeInfo(); PrmNodeInfo param;
if (useContext) if (useContext)
{ {
param.XHeaders.Add("headerKey1", "headerValue1"); var ctx = new CallContext
param.Context = new CallContext
{ {
CancellationToken = Mocker.CancellationTokenSource.Token, CancellationToken = Mocker.CancellationTokenSource.Token,
Timeout = TimeSpan.FromSeconds(20), Timeout = TimeSpan.FromSeconds(20),
@ -221,6 +224,14 @@ public class NetworkTest : NetworkTestsBase
Key = ECDsaKey, Key = ECDsaKey,
Version = Version Version = Version
}; };
param = new(ctx);
param.XHeaders.Add("headerKey1", "headerValue1");
}
else
{
param = new();
} }
var validTimeoutFrom = DateTime.UtcNow.AddSeconds(20); var validTimeoutFrom = DateTime.UtcNow.AddSeconds(20);

View file

@ -32,7 +32,7 @@ public class ObjectTest : ObjectTestsBase
var objectId = client.CalculateObjectId(Mocker.ObjectHeader!, ctx); 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); Assert.NotNull(result);
@ -50,7 +50,7 @@ public class ObjectTest : ObjectTestsBase
[Fact] [Fact]
public async void PutObjectTest() public async void PutObjectTest()
{ {
Mocker.ResultObjectIds.Add(SHA256.HashData([])); Mocker.ResultObjectIds!.Add(SHA256.HashData([]));
Random rnd = new(); Random rnd = new();
var bytes = new byte[1024]; var bytes = new byte[1024];
@ -107,7 +107,7 @@ public class ObjectTest : ObjectTestsBase
rnd.NextBytes(objIds.ElementAt(2)); rnd.NextBytes(objIds.ElementAt(2));
foreach (var objId in objIds) foreach (var objId in objIds)
Mocker.ResultObjectIds.Add(objId); Mocker.ResultObjectIds!.Add(objId);
var result = await GetClient().PutObjectAsync(param); var result = await GetClient().PutObjectAsync(param);

View file

@ -23,16 +23,6 @@ public class PoolSmokeTests : SmokeTestsBase
Key = keyString.LoadWif(), Key = keyString.LoadWif(),
NodeParams = [new(1, this.url, 100.0f)], NodeParams = [new(1, this.url, 100.0f)],
DialOptions = [new()
{
Authority = "",
Block = false,
DisableHealthCheck = false,
DisableRetry = false,
ReturnLastError = true,
Timeout = 30_000_000
}
],
ClientBuilder = null, ClientBuilder = null,
GracefulCloseOnSwitchTimeout = 30_000_000, GracefulCloseOnSwitchTimeout = 30_000_000,
Logger = null Logger = null
@ -85,6 +75,44 @@ public class PoolSmokeTests : SmokeTestsBase
Assert.Equal(9, result.Attributes.Count); 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] [Fact]
public async void NodeInfoStatisticsTest() public async void NodeInfoStatisticsTest()
{ {
@ -308,31 +336,29 @@ public class PoolSmokeTests : SmokeTestsBase
}; };
var createContainerParam = new PrmContainerCreate( var createContainerParam = new PrmContainerCreate(
new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1)), [new("testKey", "testValue")])) new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1)), [new("testKey", "testValue")]), ctx);
{
Context = ctx
};
var createdContainer = await pool.CreateContainerAsync(createContainerParam); 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.NotNull(container);
Assert.True(callbackInvoked); Assert.True(callbackInvoked);
var bytes = GetRandomBytes(objectSize); 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( Header = new FrostFsObjectHeader(
containerId: createdContainer, containerId: createdContainer,
type: FrostFsObjectType.Regular, type: FrostFsObjectType.Regular,
[new FrostFsAttributePair("fileName", "test")]), [new FrostFsAttributePair("fileName", "test")]),
Payload = new MemoryStream(bytes), Payload = new MemoryStream(bytes),
ClientCut = false, ClientCut = false
Context = new CallContext
{
Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0))
}
}; };
var objectId = await pool.PutObjectAsync(param); var objectId = await pool.PutObjectAsync(param);
@ -400,19 +426,16 @@ public class PoolSmokeTests : SmokeTestsBase
}; };
var createContainerParam = new PrmContainerCreate( var createContainerParam = new PrmContainerCreate(
new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1)))) new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1))), ctx);
{
Context = ctx
};
var container = await pool.CreateContainerAsync(createContainerParam); 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); Assert.NotNull(containerInfo);
var bytes = GetRandomBytes(objectSize); 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( Header = new FrostFsObjectHeader(
containerId: container, containerId: container,
@ -420,10 +443,6 @@ public class PoolSmokeTests : SmokeTestsBase
[new FrostFsAttributePair("fileName", "test")]), [new FrostFsAttributePair("fileName", "test")]),
Payload = new MemoryStream(bytes), Payload = new MemoryStream(bytes),
ClientCut = false, ClientCut = false,
Context = new CallContext
{
Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0))
},
SessionToken = token SessionToken = token
}; };
@ -496,10 +515,11 @@ public class PoolSmokeTests : SmokeTestsBase
var ctx = new CallContext var ctx = new CallContext
{ {
Timeout = TimeSpan.FromSeconds(10), 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); Assert.NotNull(container);
@ -520,7 +540,7 @@ public class PoolSmokeTests : SmokeTestsBase
var filter = new FilterByAttributePair(FrostFsMatchType.Equals, "fileName", "test"); var filter = new FilterByAttributePair(FrostFsMatchType.Equals, "fileName", "test");
bool hasObject = false; 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; hasObject = true;
@ -551,21 +571,10 @@ public class PoolSmokeTests : SmokeTestsBase
await Cleanup(pool); await Cleanup(pool);
var deadline = DateTime.UtcNow.Add(TimeSpan.FromSeconds(5)); await foreach (var cid in pool.ListContainersAsync())
IAsyncEnumerator<FrostFsContainerId>? enumerator = null;
do
{ {
if (deadline <= DateTime.UtcNow) Assert.Fail($"Container {cid.GetValue()} exist");
{
Assert.Fail("Containers exist");
break;
}
enumerator = pool.ListContainersAsync().GetAsyncEnumerator();
await Task.Delay(500);
} }
while (await enumerator!.MoveNextAsync());
} }
private static byte[] GetRandomBytes(int size) private static byte[] GetRandomBytes(int size)

View file

@ -14,12 +14,11 @@ public class SessionTest : SessionTestsBase
public async void CreateSessionTest(bool useContext) public async void CreateSessionTest(bool useContext)
{ {
var exp = 100u; var exp = 100u;
var param = new PrmSessionCreate(exp); PrmSessionCreate param;
if (useContext) if (useContext)
{ {
param.XHeaders.Add("headerKey1", "headerValue1"); var ctx = new CallContext
param.Context = new CallContext
{ {
CancellationToken = Mocker.CancellationTokenSource.Token, CancellationToken = Mocker.CancellationTokenSource.Token,
Timeout = TimeSpan.FromSeconds(20), Timeout = TimeSpan.FromSeconds(20),
@ -27,6 +26,14 @@ public class SessionTest : SessionTestsBase
Key = ECDsaKey, Key = ECDsaKey,
Version = Mocker.Version Version = Mocker.Version
}; };
param = new PrmSessionCreate(exp, ctx);
param.XHeaders.Add("headerKey1", "headerValue1");
}
else
{
param = new PrmSessionCreate(exp);
} }
var validTimeoutFrom = DateTime.UtcNow.AddSeconds(20); var validTimeoutFrom = DateTime.UtcNow.AddSeconds(20);

View file

@ -26,7 +26,7 @@ public class SmokeClientTests : SmokeTestsBase
? FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.keyString, this.url)) ? FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.keyString, this.url))
: FrostFSClient.GetInstance(GetOptions(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); 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)); 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); var result = await client.GetNetmapSnapshotAsync(prm);
Assert.True(result.Epoch > 0); Assert.True(result.Epoch > 0);
@ -59,9 +59,11 @@ public class SmokeClientTests : SmokeTestsBase
[InlineData(true)] [InlineData(true)]
public async void NodeInfoTest(bool isSingleOnwerClient) 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); 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)); 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); var token = await client.CreateSessionAsync(prm);
@ -262,31 +264,27 @@ public class SmokeClientTests : SmokeTestsBase
}; };
var createContainerParam = new PrmContainerCreate( var createContainerParam = new PrmContainerCreate(
new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1)), [new("testKey", "testValue")])) new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1)), [new("testKey", "testValue")]), ctx);
{
Context = ctx
};
var createdContainer = await client.CreateContainerAsync(createContainerParam); 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.NotNull(container);
Assert.True(callbackInvoked); Assert.True(callbackInvoked);
var bytes = GetRandomBytes(objectSize); 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( Header = new FrostFsObjectHeader(
containerId: createdContainer, containerId: createdContainer,
type: FrostFsObjectType.Regular, type: FrostFsObjectType.Regular,
[new FrostFsAttributePair("fileName", "test")]), [new FrostFsAttributePair("fileName", "test")]),
Payload = new MemoryStream(bytes), Payload = new MemoryStream(bytes),
ClientCut = false, ClientCut = false
Context = new CallContext
{
Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0))
}
}; };
var objectId = await client.PutObjectAsync(param); var objectId = await client.PutObjectAsync(param);
@ -348,19 +346,19 @@ public class SmokeClientTests : SmokeTestsBase
}; };
var createContainerParam = new PrmContainerCreate( var createContainerParam = new PrmContainerCreate(
new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1)))) new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1))), ctx);
{
Context = ctx
};
var container = await client.CreateContainerAsync(createContainerParam); 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); Assert.NotNull(containerInfo);
var bytes = GetRandomBytes(objectSize); 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( Header = new FrostFsObjectHeader(
containerId: container, containerId: container,
@ -368,10 +366,6 @@ public class SmokeClientTests : SmokeTestsBase
[new FrostFsAttributePair("fileName", "test")]), [new FrostFsAttributePair("fileName", "test")]),
Payload = new MemoryStream(bytes), Payload = new MemoryStream(bytes),
ClientCut = false, ClientCut = false,
Context = new CallContext
{
Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0))
},
SessionToken = token SessionToken = token
}; };
@ -437,11 +431,12 @@ public class SmokeClientTests : SmokeTestsBase
var ctx = new CallContext var ctx = new CallContext
{ {
Timeout = TimeSpan.FromSeconds(10), Timeout = TimeSpan.FromSeconds(10)
Interceptors = new([new MetricsInterceptor()])
}; };
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); Assert.NotNull(container);
@ -462,7 +457,7 @@ public class SmokeClientTests : SmokeTestsBase
var filter = new FilterByAttributePair(FrostFsMatchType.Equals, "fileName", "test"); var filter = new FilterByAttributePair(FrostFsMatchType.Equals, "fileName", "test");
bool hasObject = false; 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; hasObject = true;
@ -510,6 +505,38 @@ public class SmokeClientTests : SmokeTestsBase
while (await enumerator!.MoveNextAsync()); 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) private static byte[] GetRandomBytes(int size)
{ {
Random rnd = new(); Random rnd = new();