[#24] Client: Implement pool part1 #31

Merged
PavelGrossSpb merged 3 commits from PavelGrossSpb/frostfs-sdk-csharp:poll into master 2024-11-20 15:44:04 +00:00
63 changed files with 801 additions and 526 deletions
Showing only changes of commit ee20798379 - Show all commits

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,20 +527,13 @@ 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)
{ {
dstepanov-yadro marked this conversation as resolved
Review

To tell the truth, it doesn't look pretty. Without examining the source code, it is unclear what the returned string means.

To tell the truth, it doesn't look pretty. Without examining the source code, it is unclear what the returned string means.
Review

refactored

refactored

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);
if (!string.IsNullOrEmpty(result))
{
StatusMonitor.SetUnhealthyOnDial();
return result;
} }
return null; internal void HandleError(Exception ex)
{
if (ex is FrostFsResponseException responseException && responseException.Status != null)
{
switch (responseException.Status.Code)
{
case FrostFsStatusCode.Internal:
case FrostFsStatusCode.WrongMagicNumber:
case FrostFsStatusCode.SignatureVerificationFailure:
case FrostFsStatusCode.NodeUnderMaintenance:
IncErrorRate();
return;
}
}
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.
Review

Could be resolved with

var client FrostFSClient
try {
client = new(WrapperPrm);
...
Client = client;
client = null;
}
finally {
client?.Dispose();
}
Could be resolved with ``` var client FrostFSClient try { client = new(WrapperPrm); ... Client = client; client = null; } finally { client?.Dispose(); } ```
Review

and why do I need a reference to disposed object?

and why do I need a reference to disposed object?
Review

I did not understand the question: where is the reference to disposed object?
There are 4 main steps:

  1. Create temp reference to disposable object
  2. Inside try block create an disposable object to this reference
  3. Inside try block transfer ownership of disposable object from temp reference to new owner and set null to temp reference: now Client field holds reference to disposable object, not temp reference
  4. Dispose disposable object if it is not null
I did not understand the question: where is the reference to disposed object? There are 4 main steps: 1. Create temp reference to disposable object 2. Inside `try` block create an disposable object to this reference 3. Inside `try` block transfer ownership of disposable object from temp reference to new owner and set `null` to temp reference: now `Client` field holds reference to disposable object, not temp reference 4. Dispose disposable object if it is not `null`
Review

Got it. But it looks like a strange trick to avoid dispose pattern warning that follows another analizer warning about "always null variable". The suppressing comment is a good reminder about dispose necessity and keeps the code clean and clear.

Got it. But it looks like a strange trick to avoid dispose pattern warning that follows another analizer warning about "always null variable". The suppressing comment is a good reminder about dispose necessity and keeps the code clean and clear.
Review

I don't think it is a trick, but if you ok with it, I'm ok too.

I don't think it is a trick, but if you ok with it, I'm ok too.
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;
}
catch (FrostFsException ex)
{
client.StatusMonitor.SetUnhealthy();
Logger?.LogWarning("Failed to create frostfs session token for client. Address {Address}, {Error})",
client.WrapperPrm.Address, ex.Message);
continue;
}
atLeastOneHealthy = true; atLeastOneHealthy = true;
} }
catch (RpcException ex)
{
if (!dialed)
client!.SetUnhealthyOnDial();
else
client!.SetUnhealthy();
if (logger != null)
{
FrostFsMessages.SessionCreationError(logger, client!.WrapperPrm.Address, ex.Message);
}
}
catch (FrostFsInvalidObjectException)
{
break;
}
}
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
dstepanov-yadro marked this conversation as resolved
Review

See warning

See warning
Review

fixed

fixed
}; };
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));
} }
public async Task<Accounting.Decimal> GetBalanceAsync(PrmBalance? args = null) var client = Сonnection();
{
var client = Сonnection() ?? throw new FrostFsException("Cannot find alive client"); args.Context.PoolErrorHandler = client.HandleError;
return await client.GetBalanceAsync(args).ConfigureAwait(false);
return client.Client!.SearchObjectsAsync(args);
} }
public bool RestartIfUnhealthy(CallContext ctx) public async Task<Accounting.Decimal> GetBalanceAsync(PrmBalance? args)
{ {
throw new NotImplementedException(); var client = Сonnection();
}
public bool IsHealthy() args ??= new();
{ args.Context.PoolErrorHandler = client.HandleError;
throw new NotImplementedException();
return await client.Client!.GetBalanceAsync(args).ConfigureAwait(false);
} }
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)

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();