[#26] All: Remove V2 from naming
All checks were successful
DCO / DCO (pull_request) Successful in 47s
lint-build / dotnet8.0 (pull_request) Successful in 47s
lint-build / dotnet8.0 (push) Successful in 1m0s

Rename project, namespaces and class names

Signed-off-by: Pavel Gross <p.gross@yadro.com>
This commit is contained in:
Pavel Gross 2024-11-18 11:11:17 +03:00
parent c406df1a78
commit 766f61a5f7
219 changed files with 219 additions and 974 deletions

View file

@ -0,0 +1,22 @@
using Microsoft.Extensions.Caching.Memory;
namespace FrostFS.SDK.Client;
internal static class Caches
{
private static readonly IMemoryCache _ownersCache = new MemoryCache(new MemoryCacheOptions
{
// TODO: get from options?
SizeLimit = 256
});
private static readonly IMemoryCache _containersCache = new MemoryCache(new MemoryCacheOptions
{
// TODO: get from options?
SizeLimit = 1024
});
internal static IMemoryCache Owners => _ownersCache;
internal static IMemoryCache Containers => _containersCache;
}

View file

@ -0,0 +1,14 @@
using System.Security.Cryptography;
using FrostFS.SDK.Cryptography;
using Google.Protobuf;
namespace FrostFS.SDK.Client;
public class ClientKey(ECDsa key)
{
internal ECDsa ECDsaKey { get; } = key;
internal ByteString PublicKeyProto { get; } = ByteString.CopyFrom(key.PublicKey());
}

View file

@ -0,0 +1,18 @@
using System;
namespace FrostFS.SDK.Client;
public class FrostFsException : Exception
{
public FrostFsException()
{
}
public FrostFsException(string message) : base(message)
{
}
public FrostFsException(string message, Exception innerException) : base(message, innerException)
{
}
}

View file

@ -0,0 +1,18 @@
using System;
namespace FrostFS.SDK.Client;
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.Client;
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.Client;
public class FrostFsStreamException : FrostFsException
{
public FrostFsStreamException()
{
}
public FrostFsStreamException(string message) : base(message)
{
}
public FrostFsStreamException(string message, Exception innerException) : base(message, innerException)
{
}
}

View file

@ -0,0 +1,18 @@
using System;
namespace FrostFS.SDK.Client;
public class SessionExpiredException : FrostFsException
{
public SessionExpiredException()
{
}
public SessionExpiredException(string message) : base(message)
{
}
public SessionExpiredException(string message, Exception innerException) : base(message, innerException)
{
}
}

View file

@ -0,0 +1,18 @@
using System;
namespace FrostFS.SDK.Client;
public class SessionNotFoundException : FrostFsException
{
public SessionNotFoundException()
{
}
public SessionNotFoundException(string message) : base(message)
{
}
public SessionNotFoundException(string message, Exception innerException) : base(message, innerException)
{
}
}

View file

@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>12.0</LangVersion>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
</PropertyGroup>
<PropertyGroup>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
</PropertyGroup>
<PropertyGroup>
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" 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="System.Diagnostics.DiagnosticSource" Version="8.0.1" />
<PackageReference Include="System.Runtime.Caching" Version="8.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FrostFS.SDK.Cryptography\FrostFS.SDK.Cryptography.csproj" />
<ProjectReference Include="..\FrostFS.SDK.Protos\FrostFS.SDK.Protos.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,577 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Frostfs.V2.Ape;
using Frostfs.V2.Apemanager;
using FrostFS.Accounting;
using FrostFS.Container;
using FrostFS.Netmap;
using FrostFS.Object;
using FrostFS.SDK.Client.Interfaces;
using FrostFS.SDK.Client.Services;
using FrostFS.SDK.Cryptography;
using FrostFS.Session;
using Grpc.Core;
using Grpc.Core.Interceptors;
using Grpc.Net.Client;
using Microsoft.Extensions.Options;
namespace FrostFS.SDK.Client;
public class FrostFSClient : IFrostFSClient
{
private bool isDisposed;
internal ContainerService.ContainerServiceClient? ContainerServiceClient { get; set; }
internal NetmapService.NetmapServiceClient? NetmapServiceClient { get; set; }
internal APEManagerService.APEManagerServiceClient? ApeManagerServiceClient { get; set; }
internal SessionService.SessionServiceClient? SessionServiceClient { get; set; }
internal ObjectService.ObjectServiceClient? ObjectServiceClient { get; set; }
internal AccountingService.AccountingServiceClient? AccountingServiceClient { get; set; }
internal ClientContext ClientCtx { get; set; }
public static IFrostFSClient GetInstance(IOptions<ClientSettings> clientOptions, GrpcChannelOptions? channelOptions = null)
{
return new FrostFSClient(clientOptions, channelOptions);
}
public static IFrostFSClient GetSingleOwnerInstance(IOptions<SingleOwnerClientSettings> clientOptions, GrpcChannelOptions? channelOptions = null)
{
return new FrostFSClient(clientOptions, channelOptions);
}
/// <summary>
/// For test only. Provide custom implementation or mock object to inject required logic instead of internal gRPC client.
/// </summary>
/// <param name="clientOptions">Global setting for client</param>
/// <param name="channelOptions">Setting for gRPC channel</param>
/// <param name="containerService">ContainerService.ContainerServiceClient implementation</param>
/// <param name="netmapService">Netmap.NetmapService.NetmapServiceClient implementation</param>
/// <param name="sessionService">Session.SessionService.SessionServiceClient implementation</param>
/// <param name="objectService">Object.ObjectService.ObjectServiceClient implementation</param>
/// <returns></returns>
public static IFrostFSClient GetTestInstance(
IOptions<SingleOwnerClientSettings> clientOptions,
GrpcChannelOptions? channelOptions,
NetmapService.NetmapServiceClient netmapService,
SessionService.SessionServiceClient sessionService,
ContainerService.ContainerServiceClient containerService,
ObjectService.ObjectServiceClient objectService)
{
if (clientOptions is null)
{
throw new ArgumentNullException(nameof(clientOptions));
}
return new FrostFSClient(clientOptions, channelOptions, containerService, netmapService, sessionService, objectService);
}
private FrostFSClient(
IOptions<SingleOwnerClientSettings> settings,
GrpcChannelOptions? channelOptions,
ContainerService.ContainerServiceClient containerService,
NetmapService.NetmapServiceClient netmapService,
SessionService.SessionServiceClient sessionService,
ObjectService.ObjectServiceClient objectService)
{
if (settings is null)
{
throw new ArgumentNullException(nameof(settings));
}
var ecdsaKey = settings.Value.Key.LoadWif();
FrostFsOwner.FromKey(ecdsaKey);
ClientCtx = new ClientContext(
client: this,
key: ecdsaKey,
owner: FrostFsOwner.FromKey(ecdsaKey),
channel: InitGrpcChannel(settings.Value.Host, channelOptions),
version: new FrostFsVersion(2, 13));
ContainerServiceClient = containerService ?? throw new ArgumentNullException(nameof(containerService));
NetmapServiceClient = netmapService ?? throw new ArgumentNullException(nameof(netmapService));
SessionServiceClient = sessionService ?? throw new ArgumentNullException(nameof(sessionService));
ObjectServiceClient = objectService ?? throw new ArgumentNullException(nameof(objectService));
}
private FrostFSClient(IOptions<ClientSettings> options, GrpcChannelOptions? channelOptions)
{
var clientSettings = (options?.Value) ?? throw new ArgumentNullException(nameof(options), "Options value must be initialized");
clientSettings.Validate();
var channel = InitGrpcChannel(clientSettings.Host, channelOptions);
ClientCtx = new ClientContext(
this,
key: null,
owner: null,
channel: channel,
version: new FrostFsVersion(2, 13));
// TODO: define timeout logic
// CheckFrostFsVersionSupport(new Context { Timeout = TimeSpan.FromSeconds(20) });
}
private FrostFSClient(IOptions<SingleOwnerClientSettings> options, GrpcChannelOptions? channelOptions)
{
var clientSettings = (options?.Value) ?? throw new ArgumentNullException(nameof(options), "Options value must be initialized");
clientSettings.Validate();
var ecdsaKey = clientSettings.Key.LoadWif();
var channel = InitGrpcChannel(clientSettings.Host, channelOptions);
ClientCtx = new ClientContext(
this,
key: ecdsaKey,
owner: FrostFsOwner.FromKey(ecdsaKey),
channel: channel,
version: new FrostFsVersion(2, 13));
// TODO: define timeout logic
// CheckFrostFsVersionSupport(new Context { Timeout = TimeSpan.FromSeconds(20) });
}
internal FrostFSClient(WrapperPrm prm, SessionCache cache)
{
ClientCtx = new ClientContext(
client: this,
key: prm.Key,
owner: FrostFsOwner.FromKey(prm.Key!),
channel: InitGrpcChannel(prm.Address, null), //prm.GrpcChannelOptions),
version: new FrostFsVersion(2, 13))
{
SessionCache = cache
};
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing && !isDisposed)
{
ClientCtx?.Dispose();
isDisposed = true;
}
}
#region ApeManagerImplementation
public Task<byte[]> AddChainAsync(PrmApeChainAdd args)
{
if (args is null)
{
throw new ArgumentNullException(nameof(args));
}
var service = GetApeManagerService(args);
return service.AddChainAsync(args);
}
public Task RemoveChainAsync(PrmApeChainRemove args)
{
if (args is null)
{
throw new ArgumentNullException(nameof(args));
}
var service = GetApeManagerService(args);
return service.RemoveChainAsync(args);
}
public Task<Chain[]> ListChainAsync(PrmApeChainList args)
{
if (args is null)
throw new ArgumentNullException(nameof(args));
var service = GetApeManagerService(args);
return service.ListChainAsync(args);
}
#endregion
#region ContainerImplementation
public Task<FrostFsContainerInfo> GetContainerAsync(PrmContainerGet args)
{
if (args is null)
throw new ArgumentNullException(nameof(args));
var service = GetContainerService(args);
return service.GetContainerAsync(args);
}
public IAsyncEnumerable<FrostFsContainerId> ListContainersAsync(PrmContainerGetAll? args = null)
{
args ??= new PrmContainerGetAll();
var service = GetContainerService(args);
return service.ListContainersAsync(args);
}
public Task<FrostFsContainerId> CreateContainerAsync(PrmContainerCreate args)
{
if (args is null)
throw new ArgumentNullException(nameof(args));
var service = GetContainerService(args);
return service.CreateContainerAsync(args);
}
public Task DeleteContainerAsync(PrmContainerDelete args)
{
if (args is null)
throw new ArgumentNullException(nameof(args));
var service = GetContainerService(args);
return service.DeleteContainerAsync(args);
}
#endregion
#region NetworkImplementation
public Task<FrostFsNetmapSnapshot> GetNetmapSnapshotAsync(PrmNetmapSnapshot? args)
{
args ??= new PrmNetmapSnapshot();
var service = GetNetmapService(args);
return service.GetNetmapSnapshotAsync(args);
}
public Task<FrostFsNodeInfo> GetNodeInfoAsync(PrmNodeInfo? args)
{
args ??= new PrmNodeInfo();
var service = GetNetmapService(args);
return service.GetLocalNodeInfoAsync(args);
}
public Task<NetworkSettings> GetNetworkSettingsAsync(PrmNetworkSettings? args)
{
args ??= new PrmNetworkSettings();
var service = GetNetmapService(args);
return service.GetNetworkSettingsAsync(args.Context!);
}
#endregion
#region ObjectImplementation
public Task<FrostFsObjectHeader> GetObjectHeadAsync(PrmObjectHeadGet args)
{
if (args is null)
throw new ArgumentNullException(nameof(args));
var service = GetObjectService(args);
return service.GetObjectHeadAsync(args);
}
public Task<FrostFsObject> GetObjectAsync(PrmObjectGet args)
{
if (args is null)
throw new ArgumentNullException(nameof(args));
var service = GetObjectService(args);
return service.GetObjectAsync(args);
}
public Task<RangeReader> GetRangeAsync(PrmRangeGet args)
{
if (args is null)
throw new ArgumentNullException(nameof(args));
var service = GetObjectService(args);
return service.GetRangeAsync(args);
}
public Task<ReadOnlyMemory<byte>[]> GetRangeHashAsync(PrmRangeHashGet args)
{
if (args is null)
throw new ArgumentNullException(nameof(args));
var service = GetObjectService(args);
return service.GetRangeHashAsync(args);
}
public Task<FrostFsObjectId> PutObjectAsync(PrmObjectPut args)
{
if (args is null)
throw new ArgumentNullException(nameof(args));
var service = GetObjectService(args);
return service.PutObjectAsync(args);
}
public Task<FrostFsObjectId> PutSingleObjectAsync(PrmSingleObjectPut args)
{
if (args is null)
throw new ArgumentNullException(nameof(args));
var service = GetObjectService(args);
return service.PutSingleObjectAsync(args);
}
public Task<FrostFsObjectId> PatchObjectAsync(PrmObjectPatch args)
{
if (args is null)
{
throw new ArgumentNullException(nameof(args));
}
var service = GetObjectService(args);
return service.PatchObjectAsync(args);
}
public Task DeleteObjectAsync(PrmObjectDelete args)
{
if (args is null)
throw new ArgumentNullException(nameof(args));
var service = GetObjectService(args);
return service.DeleteObjectAsync(args);
}
public IAsyncEnumerable<FrostFsObjectId> SearchObjectsAsync(PrmObjectSearch args)
{
if (args is null)
throw new ArgumentNullException(nameof(args));
var service = GetObjectService(args);
return service.SearchObjectsAsync(args);
}
#endregion
#region Session Implementation
public async Task<FrostFsSessionToken> CreateSessionAsync(PrmSessionCreate args)
{
if (args is null)
throw new ArgumentNullException(nameof(args));
var session = await CreateSessionInternalAsync(args).ConfigureAwait(false);
var token = session.Serialize();
return new FrostFsSessionToken(token, session.Body.Id.ToUuid());
}
internal Task<SessionToken> CreateSessionInternalAsync(PrmSessionCreate args)
{
if (args is null)
throw new ArgumentNullException(nameof(args));
var service = GetSessionService(args);
return service.CreateSessionAsync(args);
}
#endregion
#region Accounting Implementation
public async Task<Accounting.Decimal> GetBalanceAsync(PrmBalance? args)
{
args ??= new PrmBalance();
var service = GetAccouningService(args);
return await service.GetBallance(args).ConfigureAwait(false);
}
#endregion
#region ToolsImplementation
public FrostFsObjectId CalculateObjectId(FrostFsObjectHeader header, CallContext ctx)
{
if (header == null)
throw new ArgumentNullException(nameof(header));
return ObjectTools.CalculateObjectId(header, ctx);
}
#endregion
private async void CheckFrostFsVersionSupport(CallContext? ctx = default)
{
var args = new PrmNodeInfo(ctx);
if (ctx?.Version == null)
throw new ArgumentNullException(nameof(ctx), "Version must be initialized");
var service = GetNetmapService(args);
var localNodeInfo = await service.GetLocalNodeInfoAsync(args).ConfigureAwait(false);
if (!localNodeInfo.Version.IsSupported(ctx.Version))
{
var msg = $"FrostFS {localNodeInfo.Version} is not supported.";
throw new FrostFsException(msg);
}
}
private CallInvoker? SetupClientContext(IContext ctx)
{
if (isDisposed)
throw new FrostFsInvalidObjectException("Client is disposed.");
if (ctx.Context!.Key == null)
{
if (ClientCtx.Key == null)
{
throw new ArgumentNullException(nameof(ctx), "Key is not initialized.");
}
ctx.Context.Key = ClientCtx.Key.ECDsaKey;
}
if (ctx.Context.OwnerId == null)
{
ctx.Context.OwnerId = ClientCtx.Owner ?? FrostFsOwner.FromKey(ctx.Context.Key);
}
if (ctx.Context.Version == null)
{
if (ClientCtx.Version == null)
{
throw new ArgumentNullException(nameof(ctx), "Version is not initialized.");
}
ctx.Context.Version = ClientCtx.Version;
}
CallInvoker? callInvoker = null;
foreach (var interceptor in ctx.Context.Interceptors)
callInvoker = AddInvoker(callInvoker, interceptor);
if (ctx.Context.Callback != null)
callInvoker = AddInvoker(callInvoker, new MetricsInterceptor(ctx.Context.Callback));
if (ctx.Context.PoolErrorHandler != null)
callInvoker = AddInvoker(callInvoker, new ErrorInterceptor(ctx.Context.PoolErrorHandler));
return callInvoker;
CallInvoker AddInvoker(CallInvoker? callInvoker, Interceptor interceptor)
{
if (callInvoker == null)
callInvoker = ClientCtx.Channel.Intercept(interceptor);
else
callInvoker = callInvoker.Intercept(interceptor);
return callInvoker;
}
}
private NetmapServiceProvider GetNetmapService(IContext ctx)
{
var callInvoker = SetupClientContext(ctx);
var client = NetmapServiceClient ?? (callInvoker != null
? new NetmapService.NetmapServiceClient(callInvoker)
: new NetmapService.NetmapServiceClient(ClientCtx.Channel));
return new NetmapServiceProvider(client, ClientCtx);
}
private SessionServiceProvider GetSessionService(IContext ctx)
{
var callInvoker = SetupClientContext(ctx);
var client = SessionServiceClient ?? (callInvoker != null
? new SessionService.SessionServiceClient(callInvoker)
: new SessionService.SessionServiceClient(ClientCtx.Channel));
return new SessionServiceProvider(client, ClientCtx);
}
private ApeManagerServiceProvider GetApeManagerService(IContext ctx)
{
var callInvoker = SetupClientContext(ctx);
var client = ApeManagerServiceClient ?? (callInvoker != null
? new APEManagerService.APEManagerServiceClient(callInvoker)
: new APEManagerService.APEManagerServiceClient(ClientCtx.Channel));
return new ApeManagerServiceProvider(client, ClientCtx);
}
private AccountingServiceProvider GetAccouningService(IContext ctx)
{
var callInvoker = SetupClientContext(ctx);
var client = AccountingServiceClient ?? (callInvoker != null
? new AccountingService.AccountingServiceClient(callInvoker)
: new AccountingService.AccountingServiceClient(ClientCtx.Channel));
return new AccountingServiceProvider(client, ClientCtx);
}
private ContainerServiceProvider GetContainerService(IContext ctx)
{
var callInvoker = SetupClientContext(ctx);
var client = ContainerServiceClient ?? (callInvoker != null
? new ContainerService.ContainerServiceClient(callInvoker)
: new ContainerService.ContainerServiceClient(ClientCtx.Channel));
return new ContainerServiceProvider(client, ClientCtx);
}
private ObjectServiceProvider GetObjectService(IContext ctx)
{
var callInvoker = SetupClientContext(ctx);
var client = ObjectServiceClient ?? (callInvoker != null
? new ObjectService.ObjectServiceClient(callInvoker)
: new ObjectService.ObjectServiceClient(ClientCtx.Channel));
return new ObjectServiceProvider(client, ClientCtx);
}
private AccountingServiceProvider GetAccountService(IContext ctx)
{
var callInvoker = SetupClientContext(ctx);
var client = AccountingServiceClient ?? (callInvoker != null
? new AccountingService.AccountingServiceClient(callInvoker)
: new AccountingService.AccountingServiceClient(ClientCtx.Channel));
return new AccountingServiceProvider(client, ClientCtx);
}
private static GrpcChannel InitGrpcChannel(string host, GrpcChannelOptions? channelOptions)
{
try
{
var uri = new Uri(host);
if (channelOptions != null)
return GrpcChannel.ForAddress(uri, channelOptions);
return GrpcChannel.ForAddress(uri, new GrpcChannelOptions
{
HttpHandler = new HttpClientHandler()
});
}
catch (UriFormatException e)
{
throw new ArgumentException($"Host '{host}' has invalid format. Error: {e.Message}");
}
}
public async Task<string?> Dial(CallContext ctx)
{
var prm = new PrmBalance(ctx);
var service = GetAccouningService(prm);
_ = await service.GetBallance(prm).ConfigureAwait(false);
return null;
}
public bool RestartIfUnhealthy(CallContext ctx)
{
throw new NotImplementedException();
}
public void Close()
{
Dispose();
}
}

View file

@ -0,0 +1,8 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "<Pending>", Scope = "member", Target = "~M:FrostFS.SDK.Client.Sampler.Next~System.Int32")]

View file

@ -0,0 +1,68 @@
using System;
using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Core.Interceptors;
namespace FrostFS.SDK.Client;
[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

@ -0,0 +1,75 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Core.Interceptors;
namespace FrostFS.SDK.Client;
[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 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)
{
var watch = new Stopwatch();
watch.Start();
var response = await call;
watch.Stop();
var elapsed = watch.ElapsedTicks * 1_000_000 / Stopwatch.Frequency;
callback(new CallStatistics { MethodName = call.ToString(), ElapsedMicroSeconds = elapsed });
return response;
}
private async Task<TResponse> HandleStreamResponse<TRequest, TResponse>(AsyncClientStreamingCall<TRequest, TResponse> call)
{
var watch = new Stopwatch();
watch.Start();
var response = await call;
watch.Stop();
var elapsed = watch.ElapsedTicks * 1_000_000 / Stopwatch.Frequency;
callback(new CallStatistics { MethodName = call.ToString(), ElapsedMicroSeconds = elapsed });
return response;
}
}

View file

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Frostfs.V2.Ape;
namespace FrostFS.SDK.Client.Interfaces;
public interface IFrostFSClient : IDisposable
{
#region Network
Task<FrostFsNetmapSnapshot> GetNetmapSnapshotAsync(PrmNetmapSnapshot? args = null);
Task<FrostFsNodeInfo> GetNodeInfoAsync(PrmNodeInfo? args = null);
Task<NetworkSettings> GetNetworkSettingsAsync(PrmNetworkSettings? args = null);
#endregion
#region Session
Task<FrostFsSessionToken> CreateSessionAsync(PrmSessionCreate args);
#endregion
#region ApeManager
Task<byte[]> AddChainAsync(PrmApeChainAdd args);
Task RemoveChainAsync(PrmApeChainRemove args);
Task<Chain[]> ListChainAsync(PrmApeChainList args);
#endregion
#region Container
Task<FrostFsContainerInfo> GetContainerAsync(PrmContainerGet args);
IAsyncEnumerable<FrostFsContainerId> ListContainersAsync(PrmContainerGetAll? args = null);
Task<FrostFsContainerId> CreateContainerAsync(PrmContainerCreate args);
Task DeleteContainerAsync(PrmContainerDelete args);
#endregion
#region Object
Task<FrostFsObjectHeader> GetObjectHeadAsync(PrmObjectHeadGet args);
Task<FrostFsObject> GetObjectAsync(PrmObjectGet args);
Task<RangeReader> GetRangeAsync(PrmRangeGet args);
Task<ReadOnlyMemory<byte>[]> GetRangeHashAsync(PrmRangeHashGet args);
Task<FrostFsObjectId> PutObjectAsync(PrmObjectPut args);
Task<FrostFsObjectId> PutSingleObjectAsync(PrmSingleObjectPut args);
Task<FrostFsObjectId> PatchObjectAsync(PrmObjectPatch args);
Task DeleteObjectAsync(PrmObjectDelete args);
IAsyncEnumerable<FrostFsObjectId> SearchObjectsAsync(PrmObjectSearch args);
#endregion
#region Account
Task<Accounting.Decimal> GetBalanceAsync(PrmBalance? args = null);
#endregion
#region Tools
FrostFsObjectId CalculateObjectId(FrostFsObjectHeader header, CallContext ctx);
#endregion
public Task<string?> Dial(CallContext ctx);
public void Close();
}

View file

@ -0,0 +1,24 @@
using Microsoft.Extensions.Logging;
namespace FrostFS.SDK.Client;
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

@ -0,0 +1,22 @@
using System;
using System.Linq;
using FrostFS.SDK.Cryptography;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class ContainerMapper
{
public static FrostFsContainerInfo ToModel(this Container.Container container)
{
if (container == null)
throw new ArgumentNullException(nameof(container));
return new FrostFsContainerInfo(
container.PlacementPolicy.ToModel(),
container.Attributes?.Select(a => new FrostFsAttributePair(a.Key, a.Value)).ToArray(),
container.Version?.ToModel(),
container.OwnerId?.ToModel(),
container.Nonce?.ToUuid());
}
}

View file

@ -0,0 +1,48 @@
using System;
using FrostFS.Refs;
using FrostFS.SDK.Cryptography;
using Google.Protobuf;
using Microsoft.Extensions.Caching.Memory;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class ContainerIdMapper
{
private static readonly MemoryCacheEntryOptions _oneHourExpiration = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromHours(1))
.SetSize(1);
public static ContainerID ToMessage(this FrostFsContainerId model)
{
if (model is null)
{
throw new ArgumentNullException(nameof(model));
}
var containerId = model.GetValue() ?? throw new ArgumentNullException(nameof(model));
if (!Caches.Containers.TryGetValue(containerId, out ContainerID? message))
{
message = new ContainerID
{
Value = ByteString.CopyFrom(Base58.Decode(containerId))
};
Caches.Containers.Set(containerId, message, _oneHourExpiration);
}
return message!;
}
public static FrostFsContainerId ToModel(this ContainerID message)
{
if (message is null)
{
throw new ArgumentNullException(nameof(message));
}
return new FrostFsContainerId(Base58.Encode(message.Value.ToByteArray()));
}
}

View file

@ -0,0 +1,23 @@
using System;
using FrostFS.Session;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class MetaHeaderMapper
{
public static RequestMetaHeader ToMessage(this MetaHeader metaHeader)
{
if (metaHeader is null)
{
throw new ArgumentNullException(nameof(metaHeader));
}
return new RequestMetaHeader
{
Version = metaHeader.Version.ToMessage(),
Epoch = (uint)metaHeader.Epoch,
Ttl = (uint)metaHeader.Ttl
};
}
}

View file

@ -0,0 +1,23 @@
using System;
using System.Linq;
using FrostFS.Netmap;
namespace FrostFS.SDK.Client;
public static class NetmapMapper
{
public static FrostFsNetmapSnapshot ToModel(this NetmapSnapshotResponse netmap)
{
if (netmap is null)
{
throw new ArgumentNullException(nameof(netmap));
}
return new FrostFsNetmapSnapshot(
netmap.Body.Netmap.Epoch,
netmap.Body.Netmap.Nodes
.Select(n => n.ToModel(netmap.MetaHeader.Version))
.ToArray());
}
}

View file

@ -0,0 +1,45 @@
using System;
using System.Linq;
using FrostFS.Netmap;
using FrostFS.SDK.Client.Mappers.GRPC;
namespace FrostFS.SDK.Client;
public static class NodeInfoMapper
{
public static FrostFsNodeInfo ToModel(this LocalNodeInfoResponse.Types.Body node)
{
if (node is null)
{
throw new ArgumentNullException(nameof(node));
}
return node.NodeInfo.ToModel(node.Version);
}
public static FrostFsNodeInfo ToModel(this NodeInfo nodeInfo, Refs.Version version)
{
if (nodeInfo is null)
{
throw new ArgumentNullException(nameof(nodeInfo));
}
NodeState state = nodeInfo.State switch
{
NodeInfo.Types.State.Unspecified => NodeState.Unspecified,
NodeInfo.Types.State.Online => NodeState.Online,
NodeInfo.Types.State.Offline => NodeState.Offline,
NodeInfo.Types.State.Maintenance => NodeState.Maintenance,
_ => throw new ArgumentException($"Unknown NodeState. Value: '{nodeInfo.State}'.")
};
return new FrostFsNodeInfo(
version: version.ToModel(),
state: state,
addresses: [.. nodeInfo.Addresses],
attributes: nodeInfo.Attributes.ToDictionary(n => n.Key, n => n.Value),
publicKey: nodeInfo.PublicKey.ToByteArray()
);
}
}

View file

@ -0,0 +1,22 @@
using System;
using System.Linq;
using FrostFS.Netmap;
namespace FrostFS.SDK.Client;
public static class PlacementPolicyMapper
{
public static FrostFsPlacementPolicy ToModel(this PlacementPolicy placementPolicy)
{
if (placementPolicy is null)
{
throw new ArgumentNullException(nameof(placementPolicy));
}
return new FrostFsPlacementPolicy(
placementPolicy.Unique,
placementPolicy.Replicas.Select(replica => replica.ToModel()).ToArray()
);
}
}

View file

@ -0,0 +1,27 @@
using System;
using FrostFS.Netmap;
namespace FrostFS.SDK.Client;
public static class ReplicaMapper
{
public static Replica ToMessage(this FrostFsReplica replica)
{
return new Replica
{
Count = (uint)replica.Count,
Selector = replica.Selector
};
}
public static FrostFsReplica ToModel(this Replica replica)
{
if (replica is null)
{
throw new ArgumentNullException(nameof(replica));
}
return new FrostFsReplica((int)replica.Count, replica.Selector);
}
}

View file

@ -0,0 +1,12 @@
namespace FrostFS.SDK.Client.Mappers.GRPC;
internal static class ObjectMapper
{
internal static FrostFsObject ToModel(this Object.Object obj)
{
return new FrostFsObject(obj.Header.ToModel())
{
ObjectId = FrostFsObjectId.FromHash(obj.ObjectId.Value.ToByteArray())
};
}
}

View file

@ -0,0 +1,32 @@
using System;
using FrostFS.Object;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class ObjectAttributeMapper
{
public static Header.Types.Attribute ToMessage(this FrostFsAttributePair attribute)
{
if (attribute is null)
{
throw new ArgumentNullException(nameof(attribute));
}
return new Header.Types.Attribute
{
Key = attribute.Key,
Value = attribute.Value
};
}
public static FrostFsAttributePair ToModel(this Header.Types.Attribute attribute)
{
if (attribute is null)
{
throw new ArgumentNullException(nameof(attribute));
}
return new FrostFsAttributePair(attribute.Key, attribute.Value);
}
}

View file

@ -0,0 +1,34 @@
using System;
using FrostFS.Object;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class ObjectFilterMapper
{
public static SearchRequest.Types.Body.Types.Filter ToMessage(this IObjectFilter filter)
{
if (filter is null)
{
throw new ArgumentNullException(nameof(filter));
}
var objMatchTypeName = filter.MatchType switch
{
FrostFsMatchType.Unspecified => MatchType.Unspecified,
FrostFsMatchType.Equals => MatchType.StringEqual,
FrostFsMatchType.NotEquals => MatchType.StringNotEqual,
FrostFsMatchType.KeyAbsent => MatchType.NotPresent,
FrostFsMatchType.StartsWith => MatchType.CommonPrefix,
_ => throw new ArgumentException($"Unknown MatchType. Value: '{filter.MatchType}'.")
};
return new SearchRequest.Types.Body.Types.Filter
{
MatchType = objMatchTypeName,
Key = filter.Key,
Value = filter.GetSerializedValue()
};
}
}

View file

@ -0,0 +1,55 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using FrostFS.Object;
using FrostFS.SDK.Cryptography;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class ObjectHeaderMapper
{
public static FrostFsObjectHeader ToModel(this Header header)
{
if (header is null)
{
throw new ArgumentNullException(nameof(header));
}
var objTypeName = header.ObjectType switch
{
ObjectType.Regular => FrostFsObjectType.Regular,
ObjectType.Lock => FrostFsObjectType.Lock,
ObjectType.Tombstone => FrostFsObjectType.Tombstone,
_ => throw new ArgumentException($"Unknown ObjectType. Value: '{header.ObjectType}'.")
};
FrostFsSplit? split = null;
if (header.Split != null)
{
var children = header.Split.Children.Count != 0 ? new ReadOnlyCollection<FrostFsObjectId>(
header.Split.Children.Select(x => x.ToModel()).ToList()) : null;
split = new FrostFsSplit(new SplitId(header.Split.SplitId.ToUuid()),
header.Split.Previous?.ToModel(),
header.Split.Parent?.ToModel(),
header.Split.ParentHeader?.ToModel(),
null,
children);
}
var model = new FrostFsObjectHeader(
new FrostFsContainerId(Base58.Encode(header.ContainerId.Value.ToByteArray())),
objTypeName,
header.Attributes.Select(attribute => attribute.ToModel()).ToArray(),
split,
header.OwnerId.ToModel(),
header.Version.ToModel())
{
PayloadLength = header.PayloadLength,
};
return model;
}
}

View file

@ -0,0 +1,33 @@
using System;
using FrostFS.Refs;
using Google.Protobuf;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class ObjectIdMapper
{
public static ObjectID ToMessage(this FrostFsObjectId objectId)
{
if (objectId is null)
{
throw new ArgumentNullException(nameof(objectId));
}
return new ObjectID
{
Value = ByteString.CopyFrom(objectId.ToHash())
};
}
public static FrostFsObjectId ToModel(this ObjectID objectId)
{
if (objectId is null)
{
throw new ArgumentNullException(nameof(objectId));
}
return FrostFsObjectId.FromHash(objectId.Value.ToByteArray());
}
}

View file

@ -0,0 +1,54 @@
using System;
using FrostFS.Refs;
using FrostFS.SDK.Cryptography;
using Google.Protobuf;
using Microsoft.Extensions.Caching.Memory;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class OwnerIdMapper
{
private static readonly MemoryCacheEntryOptions _oneHourExpiration = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromHours(1))
.SetSize(1);
public static OwnerID ToMessage(this FrostFsOwner model)
{
if (model is null)
{
throw new ArgumentNullException(nameof(model));
}
if (!Caches.Owners.TryGetValue(model, out OwnerID? message))
{
message = new OwnerID
{
Value = ByteString.CopyFrom(model.ToHash())
};
Caches.Owners.Set(model, message, _oneHourExpiration);
}
return message!;
}
public static FrostFsOwner ToModel(this OwnerID message)
{
if (message is null)
{
throw new ArgumentNullException(nameof(message));
}
if (!Caches.Owners.TryGetValue(message, out FrostFsOwner? model))
{
model = new FrostFsOwner(Base58.Encode(message.Value.ToByteArray()));
Caches.Owners.Set(message, model, _oneHourExpiration);
}
return model!;
}
}

View file

@ -0,0 +1,28 @@
using System;
using Google.Protobuf;
namespace FrostFS.SDK.Client;
public static class SessionMapper
{
public static byte[] Serialize(this Session.SessionToken token)
{
if (token is null)
{
throw new ArgumentNullException(nameof(token));
}
byte[] bytes = new byte[token.CalculateSize()];
using CodedOutputStream stream = new(bytes);
token.WriteTo(stream);
return bytes;
}
public static Session.SessionToken Deserialize(this Session.SessionToken token, byte[] bytes)
{
token.MergeFrom(bytes);
return token;
}
}

View file

@ -0,0 +1,31 @@
using System;
using Google.Protobuf;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class SignatureMapper
{
public static Refs.Signature ToMessage(this FrostFsSignature signature)
{
if (signature is null)
{
throw new ArgumentNullException(nameof(signature));
}
var scheme = signature.Scheme switch
{
SignatureScheme.EcdsaRfc6979Sha256 => Refs.SignatureScheme.EcdsaRfc6979Sha256,
SignatureScheme.EcdsaRfc6979Sha256WalletConnect => Refs.SignatureScheme.EcdsaRfc6979Sha256WalletConnect,
SignatureScheme.EcdsaSha512 => Refs.SignatureScheme.EcdsaSha512,
_ => throw new ArgumentException(nameof(signature.Scheme), $"Unexpected enum value: {signature.Scheme}")
};
return new Refs.Signature
{
Key = ByteString.CopyFrom(signature.Key),
Scheme = scheme,
Sign = ByteString.CopyFrom(signature.Sign)
};
}
}

View file

@ -0,0 +1,18 @@
using System;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class StatusMapper
{
public static FrostFsResponseStatus ToModel(this Status.Status status)
{
if (status is null)
return new FrostFsResponseStatus(FrostFsStatusCode.Success);
var codeName = Enum.GetName(typeof(FrostFsStatusCode), status.Code);
return codeName is null
? throw new ArgumentException($"Unknown StatusCode. Value: '{status.Code}'.")
: new FrostFsResponseStatus((FrostFsStatusCode)status.Code, status.Message);
}
}

View file

@ -0,0 +1,85 @@
using System.Collections;
using System.Threading;
using FrostFS.Refs;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class VersionMapper
{
private static readonly Hashtable _cacheMessages = [];
private static readonly Hashtable _cacheModels = [];
private static SpinLock _spinlock;
public static Version ToMessage(this FrostFsVersion model)
{
if (model is null)
{
throw new System.ArgumentNullException(nameof(model));
}
var key = model.Major << 16 + model.Minor;
if (!_cacheMessages.ContainsKey(key))
{
bool lockTaken = false;
try
{
_spinlock.Enter(ref lockTaken);
var message = new Version
{
Major = (uint)model.Major,
Minor = (uint)model.Minor
};
_cacheMessages.Add(key, message);
return message;
}
catch (System.ArgumentException)
{
// ignore attempt to add duplicate
}
finally
{
if (lockTaken)
_spinlock.Exit(false);
}
}
return (Version)_cacheMessages[key];
}
public static FrostFsVersion ToModel(this Version message)
{
if (message is null)
{
throw new System.ArgumentNullException(nameof(message));
}
var key = (int)message.Major << 16 + (int)message.Minor;
if (!_cacheModels.ContainsKey(key))
{
bool lockTaken = false;
try
{
_spinlock.Enter(ref lockTaken);
var model = new FrostFsVersion((int)message.Major, (int)message.Minor);
_cacheModels.Add(key, model);
return model;
}
catch (System.ArgumentException)
{
// ignore attempt to add duplicate
}
finally
{
if (lockTaken)
_spinlock.Exit(false);
}
}
return (FrostFsVersion)_cacheModels[key];
}
}

View file

@ -0,0 +1,62 @@
using System;
using Frostfs.V2.Ape;
namespace FrostFS.SDK.Client;
public struct FrostFsChainTarget(FrostFsTargetType type, string name) : IEquatable<FrostFsChainTarget>
{
private ChainTarget? chainTarget;
public FrostFsTargetType Type { get; } = type;
public string Name { get; } = name;
internal ChainTarget GetChainTarget()
{
return chainTarget ??= new ChainTarget
{
Type = GetTargetType(Type),
Name = Name
};
}
private static TargetType GetTargetType(FrostFsTargetType type)
{
return type switch
{
FrostFsTargetType.Undefined => TargetType.Undefined,
FrostFsTargetType.Namespace => TargetType.Namespace,
FrostFsTargetType.Container => TargetType.Container,
FrostFsTargetType.User => TargetType.User,
FrostFsTargetType.Group => TargetType.Group,
_ => throw new ArgumentException("Unexpected value for TargetType", nameof(type)),
};
}
public override readonly bool Equals(object obj)
{
var target = (FrostFsChainTarget)obj;
return Equals(target);
}
public override readonly int GetHashCode()
{
return $"{Name}{Type}".GetHashCode();
}
public static bool operator ==(FrostFsChainTarget left, FrostFsChainTarget right)
{
return left.Equals(right);
}
public static bool operator !=(FrostFsChainTarget left, FrostFsChainTarget right)
{
return !(left == right);
}
public readonly bool Equals(FrostFsChainTarget other)
{
return Type == other.Type && Name.Equals(other.Name, StringComparison.Ordinal);
}
}

View file

@ -0,0 +1,41 @@
using Google.Protobuf;
namespace FrostFS.SDK.Client;
public struct FrostFsChain(byte[] raw) : System.IEquatable<FrostFsChain>
{
private ByteString? grpcRaw;
public byte[] Raw { get; } = raw;
internal ByteString GetRaw()
{
return grpcRaw ??= ByteString.CopyFrom(Raw);
}
public override readonly bool Equals(object obj)
{
var chain = (FrostFsChain)obj;
return Equals(chain);
}
public override readonly int GetHashCode()
{
return Raw.GetHashCode();
}
public static bool operator ==(FrostFsChain left, FrostFsChain right)
{
return left.Equals(right);
}
public static bool operator !=(FrostFsChain left, FrostFsChain right)
{
return !(left == right);
}
public readonly bool Equals(FrostFsChain other)
{
return Raw == other.Raw;
}
}

View file

@ -0,0 +1,10 @@
namespace FrostFS.SDK.Client;
public enum FrostFsTargetType
{
Undefined = 0,
Namespace,
Container,
User,
Group
}

View file

@ -0,0 +1,70 @@
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Text;
namespace FrostFS.SDK;
public class ClientSettings
{
protected static readonly string errorTemplate = "{0} is required parameter";
public string Host { get; set; } = string.Empty;
public virtual void Validate()
{
var errors = CheckFields();
if (errors != null)
ThrowSettingsException(errors);
}
protected Collection<string>? CheckFields()
{
if (string.IsNullOrWhiteSpace(Host))
{
var error = string.Format(CultureInfo.InvariantCulture, errorTemplate, nameof(Host));
return new Collection<string>([error]);
}
return null;
}
protected static void ThrowSettingsException(Collection<string> errors)
{
if (errors is null)
{
throw new ArgumentNullException(nameof(errors));
}
StringBuilder messages = new();
foreach (var error in errors)
{
messages.AppendLine(error);
}
throw new ArgumentException(messages.ToString());
}
}
public class SingleOwnerClientSettings : ClientSettings
{
public string Key { get; set; } = string.Empty;
public override void Validate()
{
var errors = CheckFields();
if (errors != null)
ThrowSettingsException(errors);
}
protected new Collection<string>? CheckFields()
{
Collection<string>? errors = base.CheckFields();
if (string.IsNullOrWhiteSpace(Key))
(errors ??= []).Add(string.Format(CultureInfo.InvariantCulture, errorTemplate, nameof(Key)));
return errors;
}
}

View file

@ -0,0 +1,58 @@
using FrostFS.Refs;
using FrostFS.SDK.Client;
using FrostFS.SDK.Client.Mappers.GRPC;
using FrostFS.SDK.Cryptography;
namespace FrostFS.SDK;
public class FrostFsContainerId
{
private string? modelId;
private ContainerID? containerID;
public FrostFsContainerId(string id)
{
this.modelId = id;
}
internal FrostFsContainerId(ContainerID id)
{
this.containerID = id;
}
public string GetValue()
{
if (this.modelId != null)
return this.modelId;
if (containerID != null)
{
this.modelId = Base58.Encode(containerID.Value.ToByteArray());
return this.modelId;
}
throw new FrostFsInvalidObjectException();
}
internal ContainerID ContainerID
{
get
{
if (this.containerID != null)
return this.containerID;
if (modelId != null)
{
this.containerID = this.ToMessage();
return this.containerID;
}
throw new FrostFsInvalidObjectException();
}
}
public override string ToString()
{
return GetValue();
}
}

View file

@ -0,0 +1,109 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using FrostFS.SDK.Client;
using FrostFS.SDK.Client.Mappers.GRPC;
using FrostFS.SDK.Cryptography;
using Google.Protobuf;
namespace FrostFS.SDK;
public class FrostFsContainerInfo
{
private Container.Container.Types.Attribute[]? grpsAttributes;
private ReadOnlyCollection<FrostFsAttributePair>? attributes;
private FrostFsPlacementPolicy? placementPolicy;
private Guid? nonce;
private Container.Container? container;
public FrostFsContainerInfo(
FrostFsPlacementPolicy placementPolicy,
FrostFsAttributePair[]? attributes = null,
FrostFsVersion? version = null,
FrostFsOwner? owner = null,
Guid? nonce = null)
{
this.placementPolicy = placementPolicy;
Version = version;
Owner = owner;
this.nonce = nonce;
if (attributes != null)
this.attributes = new ReadOnlyCollection<FrostFsAttributePair>(attributes);
}
internal FrostFsContainerInfo(Container.Container container)
{
this.container = container;
}
public Guid Nonce
{
get
{
nonce ??= container?.Nonce != null ? container.Nonce.ToUuid() : Guid.NewGuid();
return nonce.Value;
}
}
public FrostFsPlacementPolicy? PlacementPolicy
{
get
{
placementPolicy ??= container?.PlacementPolicy?.ToModel();
return placementPolicy;
}
}
public ReadOnlyCollection<FrostFsAttributePair>? Attributes
{
get
{
if (attributes == null && grpsAttributes != null)
attributes = new ReadOnlyCollection<FrostFsAttributePair>(grpsAttributes.Select(a => new FrostFsAttributePair(a.Key, a.Value)).ToList());
return attributes;
}
}
public FrostFsVersion? Version { get; private set; }
public FrostFsOwner? Owner { get; private set; }
internal Container.Container.Types.Attribute[]? GetGrpsAttributes()
{
grpsAttributes ??= Attributes?
.Select(a => new Container.Container.Types.Attribute { Key = a.Key, Value = a.Value })
.ToArray();
return grpsAttributes;
}
internal Container.Container GetContainer()
{
if (this.container == null)
{
if (PlacementPolicy == null)
{
throw new ArgumentNullException("PlacementPolicy is null");
}
this.container = new Container.Container()
{
PlacementPolicy = PlacementPolicy.Value.GetPolicy(),
Nonce = ByteString.CopyFrom(Nonce.ToBytes()),
OwnerId = Owner?.OwnerID,
Version = Version?.Version
};
var attribs = GetGrpsAttributes();
if (attribs != null)
this.container.Attributes.AddRange(attribs);
}
return this.container;
}
}

View file

@ -0,0 +1,10 @@
namespace FrostFS.SDK;
public enum FrostFsMatchType
{
Unspecified = 0,
Equals = 1,
NotEquals = 2,
KeyAbsent = 3,
StartsWith = 4
}

View file

@ -0,0 +1,8 @@
namespace FrostFS.SDK;
public enum FrostFsObjectType
{
Regular = 0,
Tombstone = 1,
Lock = 3
}

View file

@ -0,0 +1,22 @@
namespace FrostFS.SDK;
public enum FrostFsStatusCode
{
Success = 0,
Internal = 1024,
WrongMagicNumber = 1025,
SignatureVerificationFailure = 1026,
NodeUnderMaintenance = 1027,
ObjectAccessDenied = 2048,
ObjectNotFound = 2049,
ObjectLocked = 2050,
LockNotRegularObject = 2051,
ObjectAlreadyRemoved = 2052,
OutOfRange = 2053,
ContainerNotFound = 3072,
EAclNotFound = 3073,
ContainerAccessDenied = 3074,
TokenNotFound = 4096,
TokenExpired = 4097,
ApeManagerAccessDenied = 5120
}

View file

@ -0,0 +1,9 @@
namespace FrostFS.SDK;
public enum NodeState
{
Unspecified = 0,
Online = 1,
Offline = 2,
Maintenance = 3
}

View file

@ -0,0 +1,8 @@
namespace FrostFS.SDK;
public enum SignatureScheme
{
EcdsaSha512,
EcdsaRfc6979Sha256,
EcdsaRfc6979Sha256WalletConnect
}

View file

@ -0,0 +1,7 @@
namespace FrostFS.SDK;
public class CallStatistics
{
public string? MethodName { get; set; }
public long ElapsedMicroSeconds { get; set; }
}

View file

@ -0,0 +1,21 @@
using System;
using FrostFS.SDK.Cryptography;
namespace FrostFS.SDK;
public class CheckSum
{
private byte[]? hash;
private string? text;
public static CheckSum CreateCheckSum(byte[] content)
{
return new CheckSum { hash = content.Sha256() };
}
public override string ToString()
{
return text ??= BitConverter.ToString(hash).Replace("-", "");
}
}

View file

@ -0,0 +1,52 @@
namespace FrostFS.SDK;
public static class Constants
{
public const int ObjectChunkSize = 3 * (1 << 20);
public const int Sha256HashLength = 32;
// HeaderPrefix is a prefix of key to object header value or property.
public const string HeaderPrefix = "$Object:";
// FilterHeaderVersion is a filter key to "version" field of the object header.
public const string FilterHeaderVersion = HeaderPrefix + "version";
// FilterHeaderObjectID is a filter key to "object_id" field of the object.
public const string FilterHeaderObjectID = HeaderPrefix + "objectID";
// FilterHeaderContainerID is a filter key to "container_id" field of the object header.
public const string FilterHeaderContainerID = HeaderPrefix + "containerID";
// FilterHeaderOwnerID is a filter key to "owner_id" field of the object header.
public const string FilterHeaderOwnerID = HeaderPrefix + "ownerID";
// FilterHeaderCreationEpoch is a filter key to "creation_epoch" field of the object header.
public const string FilterHeaderCreationEpoch = HeaderPrefix + "creationEpoch";
// FilterHeaderPayloadLength is a filter key to "payload_length" field of the object header.
public const string FilterHeaderPayloadLength = HeaderPrefix + "payloadLength";
// FilterHeaderPayloadHash is a filter key to "payload_hash" field of the object header.
public const string FilterHeaderPayloadHash = HeaderPrefix + "payloadHash";
// FilterHeaderObjectType is a filter key to "object_type" field of the object header.
public const string FilterHeaderObjectType = HeaderPrefix + "objectType";
// FilterHeaderHomomorphicHash is a filter key to "homomorphic_hash" field of the object header.
public const string FilterHeaderHomomorphicHash = HeaderPrefix + "homomorphicHash";
// FilterHeaderParent is a filter key to "split.parent" field of the object header.
public const string FilterHeaderParent = HeaderPrefix + "split.parent";
// FilterHeaderSplitID is a filter key to "split.splitID" field of the object header.
public const string FilterHeaderSplitID = HeaderPrefix + "split.splitID";
// FilterHeaderECParent is a filter key to "ec.parent" field of the object header.
public const string FilterHeaderECParent = HeaderPrefix + "ec.parent";
// FilterPropertyRoot is a filter key to check if regular object is on top of split hierarchy.
public const string FilterHeaderRoot = HeaderPrefix + "ROOT";
// FilterPropertyPhy is a filter key to check if an object physically stored on a node.
public const string FilterHeaderPhy = HeaderPrefix + "PHY";
}

View file

@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace FrostFS.SDK;
public class FrostFsNetmapSnapshot(ulong epoch, IReadOnlyList<FrostFsNodeInfo> nodeInfoCollection)
{
public ulong Epoch { get; private set; } = epoch;
public IReadOnlyList<FrostFsNodeInfo> NodeInfoCollection { get; private set; } = nodeInfoCollection;
}

View file

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
namespace FrostFS.SDK;
public class FrostFsNodeInfo(
FrostFsVersion version,
NodeState state,
IReadOnlyCollection<string> addresses,
IReadOnlyDictionary<string, string> attributes,
ReadOnlyMemory<byte> publicKey)
{
public NodeState State { get; private set; } = state;
public FrostFsVersion Version { get; private set; } = version;
public IReadOnlyCollection<string> Addresses { get; private set; } = addresses;
public IReadOnlyDictionary<string, string> Attributes { get; private set; } = attributes;
public ReadOnlyMemory<byte> PublicKey { get; private set; } = publicKey;
}

View file

@ -0,0 +1,89 @@
using System;
using System.Linq;
using FrostFS.Netmap;
using FrostFS.SDK.Client;
namespace FrostFS.SDK;
public struct FrostFsPlacementPolicy(bool unique, params FrostFsReplica[] replicas)
: IEquatable<FrostFsPlacementPolicy>
{
private PlacementPolicy policy;
public FrostFsReplica[] Replicas { get; private set; } = replicas;
public bool Unique { get; private set; } = unique;
public override readonly bool Equals(object obj)
{
if (obj is null)
return false;
var other = (FrostFsPlacementPolicy)obj;
return Equals(other);
}
public PlacementPolicy GetPolicy()
{
if (policy == null)
{
policy = new PlacementPolicy
{
Filters = { },
Selectors = { },
Replicas = { },
Unique = Unique
};
foreach (var replica in Replicas)
{
policy.Replicas.Add(replica.ToMessage());
}
}
return policy;
}
//public static FrostFsPlacementPolicy ToModel(placementPolicy)
//{
// return new FrostFsPlacementPolicy(
// placementPolicy.Unique,
// placementPolicy.Replicas.Select(replica => replica.ToModel()).ToArray()
// );
//}
public override readonly int GetHashCode()
{
return Unique ? 17 : 0 + Replicas.GetHashCode();
}
public static bool operator ==(FrostFsPlacementPolicy left, FrostFsPlacementPolicy right)
{
return left.Equals(right);
}
public static bool operator !=(FrostFsPlacementPolicy left, FrostFsPlacementPolicy right)
{
return !(left == right);
}
public readonly bool Equals(FrostFsPlacementPolicy other)
{
var notEqual = Unique != other.Unique
|| Replicas.Length != other.Replicas.Length;
if (notEqual)
return false;
foreach (var replica in Replicas)
{
if (!other.Replicas.Any(r => r.Equals(replica)))
return false;
}
return true;
}
}

View file

@ -0,0 +1,47 @@
using System;
namespace FrostFS.SDK;
public struct FrostFsReplica : IEquatable<FrostFsReplica>
{
public int Count { get; set; }
public string Selector { get; set; }
public FrostFsReplica(int count, string? selector = null)
{
selector ??= string.Empty;
Count = count;
Selector = selector;
}
public override readonly bool Equals(object obj)
{
if (obj is null)
return false;
var other = (FrostFsReplica)obj;
return Count == other.Count && Selector == other.Selector;
}
public override readonly int GetHashCode()
{
return Count + Selector.GetHashCode();
}
public static bool operator ==(FrostFsReplica left, FrostFsReplica right)
{
return left.Equals(right);
}
public static bool operator !=(FrostFsReplica left, FrostFsReplica right)
{
return !(left == right);
}
public readonly bool Equals(FrostFsReplica other)
{
return Count == other.Count && Selector == other.Selector;
}
}

View file

@ -0,0 +1,36 @@
using FrostFS.Refs;
using FrostFS.SDK.Client.Mappers.GRPC;
namespace FrostFS.SDK;
public class FrostFsVersion(int major, int minor)
{
private Version? version;
public int Major { get; set; } = major;
public int Minor { get; set; } = minor;
internal Version Version
{
get
{
this.version ??= this.ToMessage();
return this.version;
}
}
public bool IsSupported(FrostFsVersion version)
{
if (version is null)
{
throw new System.ArgumentNullException(nameof(version));
}
return Major == version.Major;
}
public override string ToString()
{
return $"v{Major}.{Minor}";
}
}

View file

@ -0,0 +1,48 @@
using FrostFS.Refs;
using FrostFS.SDK.Client.Mappers.GRPC;
namespace FrostFS.SDK;
public class FrostFsAddress
{
private FrostFsObjectId? frostFsObjectId;
private FrostFsContainerId? frostFsContainerId;
private ObjectID? objectId;
private ContainerID? containerId;
public FrostFsAddress(FrostFsContainerId frostFsContainerId, FrostFsObjectId frostFsObjectId)
{
FrostFsObjectId = frostFsObjectId ?? throw new System.ArgumentNullException(nameof(frostFsObjectId));
FrostFsContainerId = frostFsContainerId ?? throw new System.ArgumentNullException(nameof(frostFsContainerId));
}
internal FrostFsAddress(ObjectID objectId, ContainerID containerId)
{
ObjectId = objectId ?? throw new System.ArgumentNullException(nameof(objectId));
ContainerId = containerId ?? throw new System.ArgumentNullException(nameof(containerId));
}
public FrostFsObjectId FrostFsObjectId
{
get => frostFsObjectId ??= objectId!.ToModel();
set => frostFsObjectId = value;
}
public FrostFsContainerId FrostFsContainerId
{
get => frostFsContainerId ??= containerId!.ToModel();
set => frostFsContainerId = value;
}
public ObjectID ObjectId
{
get => objectId ??= frostFsObjectId!.ToMessage();
set => objectId = value;
}
public ContainerID ContainerId
{
get => containerId ??= frostFsContainerId!.ToMessage();
set => containerId = value;
}
}

View file

@ -0,0 +1,8 @@
namespace FrostFS.SDK;
public class FrostFsAttributePair(string key, string value)
{
public string Key { get; set; } = key;
public string Value { get; set; } = value;
}

View file

@ -0,0 +1,9 @@
namespace FrostFS.SDK;
public class FrostFsLargeObject(FrostFsContainerId container) : FrostFsObject(container)
{
public ulong PayloadLength
{
get { return Header!.PayloadLength; }
}
}

View file

@ -0,0 +1,21 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace FrostFS.SDK;
public class FrostFsLinkObject : FrostFsObject
{
public FrostFsLinkObject(FrostFsContainerId containerId,
SplitId splitId,
FrostFsObjectHeader largeObjectHeader,
IList<FrostFsObjectId> children)
: base(containerId)
{
Header!.Split = new FrostFsSplit(splitId,
null,
null,
largeObjectHeader,
null,
new ReadOnlyCollection<FrostFsObjectId>(children));
}
}

View file

@ -0,0 +1,74 @@
using System;
namespace FrostFS.SDK;
public class FrostFsObject
{
private byte[]? bytes;
/// <summary>
/// Creates new instance from <c>ObjectHeader</c>
/// </summary>
/// <param name="header"></param> <summary>
public FrostFsObject(FrostFsObjectHeader header)
{
Header = header;
}
/// <summary>
/// Creates new instance with specified parameters
/// </summary>
/// <param name="container"></param>
/// <param name="objectType"></param>
public FrostFsObject(FrostFsContainerId container, FrostFsObjectType objectType = FrostFsObjectType.Regular)
{
Header = new FrostFsObjectHeader(containerId: container, type: objectType);
}
/// <summary>
/// Header contains metadata for the object
/// </summary>
/// <value></value>
public FrostFsObjectHeader Header { get; set; }
/// <summary>
/// The value is calculated internally as a hash of ObjectHeader. Do not use pre-calculated value is the object has been changed.
/// </summary>
public FrostFsObjectId? ObjectId
{
get; set;
}
/// <summary>
/// A payload is obtained via stream reader
/// </summary>
/// <value>Reader for received data</value>
public IObjectReader? ObjectReader { get; set; }
internal byte[] SingleObjectPayload
{
get { return bytes ?? []; }
}
/// <summary>
/// The size of payload cannot exceed <c>MaxObjectSize</c> value from <c>NetworkSettings</c>
/// Used only for PutSingleObject method
/// </summary>
/// <value>Buffer for output data</value>
public void SetSingleObjectPayload(byte[] bytes)
{
this.bytes = bytes;
}
/// <summary>
/// Applied only for the last Object in chain in case of manual multipart uploading
/// </summary>
/// <param name="largeObject">Parent for multipart object</param>
public void SetParent(FrostFsObjectHeader largeObjectHeader)
{
if (Header?.Split == null)
throw new ArgumentNullException(nameof(largeObjectHeader), "Split value must not be null");
Header.Split.ParentHeader = largeObjectHeader;
}
}

View file

@ -0,0 +1,111 @@
namespace FrostFS.SDK;
public interface IObjectFilter
{
public FrostFsMatchType MatchType { get; set; }
public string Key { get; set; }
string? GetSerializedValue();
}
public abstract class FrostFsObjectFilter<T>(FrostFsMatchType matchType, string key, T value) : IObjectFilter
{
public FrostFsMatchType MatchType { get; set; } = matchType;
public string Key { get; set; } = key;
public T Value { get; set; } = value;
public string? GetSerializedValue()
{
return Value?.ToString();
}
}
/// <summary>
/// Creates filter to search by Attribute
/// </summary>
/// <param name="matchType">Match type</param>
/// <param name="key">Attribute key</param>
/// <param name="value">Attribute value</param>
public class FilterByAttributePair(FrostFsMatchType matchType, string key, string value) : FrostFsObjectFilter<string>(matchType, key, value) { }
/// <summary>
/// Creates filter to search by ObjectId
/// </summary>
/// <param name="matchType">Match type</param>
/// <param name="objectId">ObjectId</param>
public class FilterByObjectId(FrostFsMatchType matchType, FrostFsObjectId objectId) : FrostFsObjectFilter<FrostFsObjectId>(matchType, Constants.FilterHeaderObjectID, objectId) { }
/// <summary>
/// Creates filter to search by OwnerId
/// </summary>
/// <param name="matchType">Match type</param>
/// <param name="ownerId">ObjectId</param>
public class FilterByOwnerId(FrostFsMatchType matchType, FrostFsOwner ownerId) : FrostFsObjectFilter<FrostFsOwner>(matchType, Constants.FilterHeaderOwnerID, ownerId) { }
/// <summary>
/// Creates filter to search by Version
/// </summary>
/// <param name="matchType">Match type</param>
/// <param name="version">Version</param>
public class FilterByVersion(FrostFsMatchType matchType, FrostFsVersion version) : FrostFsObjectFilter<FrostFsVersion>(matchType, Constants.FilterHeaderVersion, version) { }
/// <summary>
/// Creates filter to search by ContainerId
/// </summary>
/// <param name="matchType">Match type</param>
/// <param name="containerId">ContainerId</param>
public class FilterByContainerId(FrostFsMatchType matchType, FrostFsContainerId containerId) : FrostFsObjectFilter<FrostFsContainerId>(matchType, Constants.FilterHeaderContainerID, containerId) { }
/// <summary>
/// Creates filter to search by creation Epoch
/// </summary>
/// <param name="matchType">Match type</param>
/// <param name="epoch">Creation Epoch</param>
public class FilterByEpoch(FrostFsMatchType matchType, ulong epoch) : FrostFsObjectFilter<ulong>(matchType, Constants.FilterHeaderCreationEpoch, epoch) { }
/// <summary>
/// Creates filter to search by Payload Length
/// </summary>
/// <param name="matchType">Match type</param>
/// <param name="payloadLength">Payload Length</param>
public class FilterByPayloadLength(FrostFsMatchType matchType, ulong payloadLength) : FrostFsObjectFilter<ulong>(matchType, Constants.FilterHeaderPayloadLength, payloadLength) { }
/// <summary>
/// Creates filter to search by Payload Hash
/// </summary>
/// <param name="matchType">Match type</param>
/// <param name="payloadHash">Payload Hash</param>
public class FilterByPayloadHash(FrostFsMatchType matchType, CheckSum payloadHash) : FrostFsObjectFilter<CheckSum>(matchType, Constants.FilterHeaderPayloadHash, payloadHash) { }
/// <summary>
/// Creates filter to search by Parent
/// </summary>
/// <param name="matchType">Match type</param>
/// <param name="parentId">Parent</param>
public class FilterByParent(FrostFsMatchType matchType, FrostFsObjectId parentId) : FrostFsObjectFilter<FrostFsObjectId>(matchType, Constants.FilterHeaderParent, parentId) { }
/// <summary>
/// Creates filter to search by SplitId
/// </summary>
/// <param name="matchType">Match type</param>
/// <param name="splitId">SplitId</param>
public class FilterBySplitId(FrostFsMatchType matchType, SplitId splitId) : FrostFsObjectFilter<SplitId>(matchType, Constants.FilterHeaderSplitID, splitId) { }
/// <summary>
/// Creates filter to search by Payload Hash
/// </summary>
/// <param name="matchType">Match type</param>
/// <param name="ecParentId">Payload Hash</param>
public class FilterByECParent(FrostFsMatchType matchType, FrostFsObjectId ecParentId) : FrostFsObjectFilter<FrostFsObjectId>(matchType, Constants.FilterHeaderECParent, ecParentId) { }
/// <summary>
/// Creates filter to search Root objects
/// </summary>
public class FilterByRootObject() : FrostFsObjectFilter<string>(FrostFsMatchType.Unspecified, Constants.FilterHeaderRoot, string.Empty) { }
/// <summary>
/// Creates filter to search objects that are physically stored on the server
/// </summary
public class FilterByPhysicallyStored() : FrostFsObjectFilter<string>(FrostFsMatchType.Unspecified, Constants.FilterHeaderPhy, string.Empty) { }

View file

@ -0,0 +1,90 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using FrostFS.Object;
using FrostFS.SDK.Client.Mappers.GRPC;
namespace FrostFS.SDK;
public class FrostFsObjectHeader(
FrostFsContainerId containerId,
FrostFsObjectType type = FrostFsObjectType.Regular,
FrostFsAttributePair[]? attributes = null,
FrostFsSplit? split = null,
FrostFsOwner? owner = null,
FrostFsVersion? version = null)
{
private Header? header;
private Container.Container.Types.Attribute[]? grpsAttributes;
public ReadOnlyCollection<FrostFsAttributePair>? Attributes { get; internal set; } =
attributes == null ? null :
new ReadOnlyCollection<FrostFsAttributePair>(attributes);
public FrostFsContainerId ContainerId { get; } = containerId;
public ulong PayloadLength { get; set; }
public byte[]? PayloadCheckSum { get; set; }
public FrostFsObjectType ObjectType { get; } = type;
public FrostFsOwner? OwnerId { get; internal set; } = owner;
public FrostFsVersion? Version { get; internal set; } = version;
public FrostFsSplit? Split { get; internal set; } = split;
internal Container.Container.Types.Attribute[]? GetGrpsAttributes()
{
grpsAttributes ??= Attributes?
.Select(a => new Container.Container.Types.Attribute { Key = a.Key, Value = a.Value })
.ToArray();
return grpsAttributes;
}
public Header GetHeader()
{
if (header == null)
{
var objTypeName = ObjectType switch
{
FrostFsObjectType.Regular => Object.ObjectType.Regular,
FrostFsObjectType.Lock => Object.ObjectType.Lock,
FrostFsObjectType.Tombstone => Object.ObjectType.Tombstone,
_ => throw new ArgumentException($"Unknown ObjectType. Value: '{ObjectType}'.")
};
this.header = new Header
{
OwnerId = OwnerId?.ToMessage(),
Version = Version?.ToMessage(),
ContainerId = ContainerId.ToMessage(),
ObjectType = objTypeName,
PayloadLength = PayloadLength
};
if (Attributes != null)
{
foreach (var attribute in Attributes)
{
this.header.Attributes.Add(attribute.ToMessage());
}
}
var split = Split;
if (split != null)
{
this.header.Split = new Header.Types.Split
{
SplitId = split!.SplitId != null ? split.SplitId.GetSplitId() : null
};
}
}
return this.header;
}
}

View file

@ -0,0 +1,33 @@
using System;
using FrostFS.SDK.Cryptography;
namespace FrostFS.SDK;
public class FrostFsObjectId(string id)
{
public string Value { get; } = id;
public static FrostFsObjectId FromHash(byte[] hash)
{
if (hash is null)
{
throw new ArgumentNullException(nameof(hash));
}
if (hash.Length != Constants.Sha256HashLength)
throw new FormatException("ObjectID must be a sha256 hash.");
return new FrostFsObjectId(Base58.Encode(hash));
}
public byte[] ToHash()
{
return Base58.Decode(Value);
}
public override string ToString()
{
return Value;
}
}

View file

@ -0,0 +1,38 @@
using System.Security.Cryptography;
using FrostFS.Refs;
using FrostFS.SDK.Client.Mappers.GRPC;
using FrostFS.SDK.Cryptography;
namespace FrostFS.SDK;
public class FrostFsOwner(string id)
{
private OwnerID? ownerID;
public string Value { get; } = id;
public static FrostFsOwner FromKey(ECDsa key)
{
return new FrostFsOwner(key.PublicKey().PublicKeyToAddress());
}
internal OwnerID OwnerID
{
get
{
ownerID ??= this.ToMessage();
return ownerID;
}
}
public byte[] ToHash()
{
return Base58.Decode(Value);
}
public override string ToString()
{
return Value;
}
}

View file

@ -0,0 +1,27 @@
namespace FrostFS.SDK;
public readonly struct FrostFsRange(ulong offset, ulong length) : System.IEquatable<FrostFsRange>
{
public ulong Offset { get; } = offset;
public ulong Length { get; } = length;
public override readonly bool Equals(object obj) => this == (FrostFsRange)obj;
public override readonly int GetHashCode() => $"{Offset}{Length}".GetHashCode();
public static bool operator ==(FrostFsRange left, FrostFsRange right)
{
return left.Equals(right);
}
public static bool operator !=(FrostFsRange left, FrostFsRange right)
{
return !(left == right);
}
public readonly bool Equals(FrostFsRange other)
{
return this == other;
}
}

View file

@ -0,0 +1,28 @@
using System.Collections.ObjectModel;
namespace FrostFS.SDK;
public class FrostFsSplit(SplitId splitId,
FrostFsObjectId? previous = null,
FrostFsObjectId? parent = null,
FrostFsObjectHeader? parentHeader = null,
FrostFsSignature? parentSignature = null,
ReadOnlyCollection<FrostFsObjectId>? children = null)
{
public FrostFsSplit() : this(new SplitId())
{
}
public SplitId SplitId { get; private set; } = splitId;
public FrostFsObjectId? Previous { get; } = previous;
public FrostFsObjectId? Parent { get; } = parent;
public FrostFsSignature? ParentSignature { get; } = parentSignature;
public FrostFsObjectHeader? ParentHeader { get; set; } = parentHeader;
public ReadOnlyCollection<FrostFsObjectId>? Children { get; } = children;
}

View file

@ -0,0 +1,10 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace FrostFS.SDK;
public interface IObjectReader : IDisposable
{
ValueTask<ReadOnlyMemory<byte>?> ReadChunk(CancellationToken cancellationToken = default);
}

View file

@ -0,0 +1,62 @@
using System;
using FrostFS.SDK.Cryptography;
using Google.Protobuf;
namespace FrostFS.SDK;
public class SplitId
{
private readonly Guid id;
private ByteString? message;
public SplitId()
{
this.id = Guid.NewGuid();
}
public SplitId(Guid id)
{
this.id = id;
}
private SplitId(byte[] binary)
{
this.id = new Guid(binary);
}
private SplitId(string str)
{
this.id = new Guid(str);
}
public static SplitId CreateFromBinary(byte[] binaryData)
{
return new SplitId(binaryData);
}
public static SplitId CreateFromString(string stringData)
{
return new SplitId(stringData);
}
public override string ToString()
{
return this.id.ToString();
}
public byte[]? ToBinary()
{
if (this.id == Guid.Empty)
return null;
return this.id.ToBytes();
}
public ByteString? GetSplitId()
{
return this.message ??= ByteString.CopyFrom(ToBinary());
}
}

View file

@ -0,0 +1,14 @@
namespace FrostFS.SDK;
public class FrostFsResponseStatus(FrostFsStatusCode code, string? message = null)
{
public FrostFsStatusCode Code { get; set; } = code;
public string Message { get; set; } = message ?? string.Empty;
public bool IsSuccess => Code == FrostFsStatusCode.Success;
public override string ToString()
{
return $"Response status: {Code}. Message: {Message}.";
}
}

View file

@ -0,0 +1,10 @@
namespace FrostFS.SDK;
public class FrostFsSignature()
{
public byte[]? Key { get; set; }
public byte[]? Sign { get; set; }
public SignatureScheme Scheme { get; set; }
}

View file

@ -0,0 +1,20 @@
namespace FrostFS.SDK;
public class MetaHeader(FrostFsVersion version, int epoch, int ttl)
{
public FrostFsVersion Version { get; set; } = version;
public int Epoch { get; set; } = epoch;
public int Ttl { get; set; } = ttl;
public static MetaHeader Default()
{
return new MetaHeader(
new FrostFsVersion(
major: 2,
minor: 13
),
epoch: 0,
ttl: 2
);
}
}

View file

@ -0,0 +1,9 @@
using System;
namespace FrostFS.SDK;
public class FrostFsSessionToken(byte[] token, Guid id)
{
public Guid Id { get; private set; } = id;
public byte[] Token { get; private set; } = token;
}

View file

@ -0,0 +1,45 @@
using System;
using System.Collections.ObjectModel;
using System.Security.Cryptography;
using System.Threading;
using FrostFS.SDK.Cryptography;
using Google.Protobuf;
using Grpc.Core.Interceptors;
namespace FrostFS.SDK.Client;
public class CallContext()
{
private ByteString? publicKeyCache;
internal Action<Exception>? PoolErrorHandler { get; set; }
public ECDsa? Key { get; set; }
public FrostFsOwner? OwnerId { get; set; }
public FrostFsVersion? Version { get; set; }
public CancellationToken CancellationToken { get; set; }
public TimeSpan Timeout { get; set; }
public DateTime? Deadline => Timeout.Ticks > 0 ? DateTime.UtcNow.Add(Timeout) : null;
public Action<CallStatistics>? Callback { get; set; }
public Collection<Interceptor> Interceptors { get; } = [];
public ByteString? GetPublicKeyCache()
{
if (publicKeyCache == null && Key != null)
{
publicKeyCache = ByteString.CopyFrom(Key.PublicKey());
}
return publicKeyCache;
}
}

View file

@ -0,0 +1,10 @@
using System.Security.Cryptography;
namespace FrostFS.SDK.Client;
public class Credentials(ECDsa key, FrostFsOwner ownerId)
{
public ECDsa Key { get; } = key;
public FrostFsOwner OwnerId { get; } = ownerId;
}

View file

@ -0,0 +1,11 @@
namespace FrostFS.SDK.Client;
public interface IContext
{
/// <summary>
/// The method call can be extended with additional behavior like canceling by timeout or user's request,
/// callbacks, interceptors.
/// </summary>
/// <value>Additional parameters for calling the method</value>
CallContext? Context { get; }
}

View file

@ -0,0 +1,12 @@
namespace FrostFS.SDK.Client;
public interface ISessionToken
{
/// <summary>
/// Object represents token of the FrostFS Object session. A session is opened between any two sides of the
/// system, and implements a mechanism for transferring the power of attorney of actions to another network
/// member. The session has a limited validity period, and applies to a strictly defined set of operations.
/// </summary>
/// <value>Instance of the session obtained from the server</value>
FrostFsSessionToken? SessionToken { get; set; }
}

View file

@ -0,0 +1,6 @@
namespace FrostFS.SDK.Client;
public sealed class PrmApeChainList(FrostFsChainTarget target, CallContext? ctx = null) : PrmBase(ctx)
{
public FrostFsChainTarget Target { get; } = target;
}

View file

@ -0,0 +1,8 @@
namespace FrostFS.SDK.Client;
public sealed class PrmApeChainRemove(FrostFsChainTarget target, FrostFsChain chain, CallContext? ctx = null) : PrmBase(ctx)
{
public FrostFsChainTarget Target { get; } = target;
public FrostFsChain Chain { get; } = chain;
}

View file

@ -0,0 +1,8 @@
namespace FrostFS.SDK.Client;
public sealed class PrmApeChainAdd(FrostFsChainTarget target, FrostFsChain chain, CallContext? ctx = null) : PrmBase(ctx)
{
public FrostFsChainTarget Target { get; } = target;
public FrostFsChain Chain { get; } = chain;
}

View file

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

View file

@ -0,0 +1,14 @@
using System.Collections.Specialized;
namespace FrostFS.SDK.Client;
public class PrmBase(CallContext? ctx, NameValueCollection? xheaders = null) : IContext
{
/// <summary>
/// FrostFS request X-Headers
/// </summary>
public NameValueCollection XHeaders { get; } = xheaders ?? [];
/// <inheritdoc />
public CallContext Context { get; } = ctx ?? new CallContext();
}

View file

@ -0,0 +1,17 @@
namespace FrostFS.SDK.Client;
public sealed class PrmContainerCreate(FrostFsContainerInfo container, CallContext? ctx = null) : PrmBase(ctx), ISessionToken
{
public FrostFsContainerInfo Container { get; set; } = container;
/// <summary>
/// Since the container becomes available with some delay, it needs to poll the container status
/// </summary>
/// <value>Rules for polling the result</value>
public PrmWait? WaitParams { get; set; }
/// <summary>
/// Blank session token
/// </summary>
public FrostFsSessionToken? SessionToken { get; set; }
}

View file

@ -0,0 +1,14 @@
namespace FrostFS.SDK.Client;
public sealed class PrmContainerDelete(FrostFsContainerId containerId, CallContext? ctx = null) : PrmBase(ctx), ISessionToken
{
public FrostFsContainerId ContainerId { get; set; } = containerId;
/// <summary>
/// Since the container is removed with some delay, it needs to poll the container status
/// </summary>
/// <value>Rules for polling the result</value>
public PrmWait? WaitParams { get; set; }
public FrostFsSessionToken? SessionToken { get; set; }
}

View file

@ -0,0 +1,6 @@
namespace FrostFS.SDK.Client;
public sealed class PrmContainerGet(FrostFsContainerId container, CallContext? ctx = null) : PrmBase(ctx)
{
public FrostFsContainerId Container { get; set; } = container;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,11 @@
namespace FrostFS.SDK.Client;
public sealed class PrmObjectDelete(FrostFsContainerId containerId, FrostFsObjectId objectId, CallContext? ctx = null) : PrmBase(ctx), ISessionToken
{
public FrostFsContainerId ContainerId { get; set; } = containerId;
public FrostFsObjectId ObjectId { get; set; } = objectId;
/// <inheritdoc />
public FrostFsSessionToken? SessionToken { get; set; }
}

View file

@ -0,0 +1,11 @@
namespace FrostFS.SDK.Client;
public sealed class PrmObjectGet(FrostFsContainerId containerId, FrostFsObjectId objectId, CallContext? ctx = null) : PrmBase(ctx), ISessionToken
{
public FrostFsContainerId ContainerId { get; set; } = containerId;
public FrostFsObjectId ObjectId { get; set; } = objectId;
/// <inheritdoc />
public FrostFsSessionToken? SessionToken { get; set; }
}

View file

@ -0,0 +1,11 @@
namespace FrostFS.SDK.Client;
public sealed class PrmObjectHeadGet(FrostFsContainerId containerId, FrostFsObjectId objectId, CallContext? ctx = null) : PrmBase(ctx), ISessionToken
{
public FrostFsContainerId ContainerId { get; set; } = containerId;
public FrostFsObjectId ObjectId { get; set; } = objectId;
/// <inheritdoc />
public FrostFsSessionToken? SessionToken { get; set; }
}

View file

@ -0,0 +1,24 @@
using System.IO;
namespace FrostFS.SDK.Client;
public sealed class PrmObjectPatch(FrostFsAddress address, CallContext? ctx = null) : PrmBase(ctx), ISessionToken
{
public FrostFsAddress Address { get; } = address;
public FrostFsRange Range { get; set; }
/// <summary>
/// A stream with source data
/// </summary>
public Stream? Payload { get; set; }
public FrostFsAttributePair[]? NewAttributes { get; set; }
public bool ReplaceAttributes { get; set; }
public int MaxPayloadPatchChunkLength { get; set; }
/// <inheritdoc />
public FrostFsSessionToken? SessionToken { get; set; }
}

View file

@ -0,0 +1,46 @@
using System.IO;
namespace FrostFS.SDK.Client;
public sealed class PrmObjectPut(CallContext? ctx = null) : PrmBase(ctx), ISessionToken
{
/// <summary>
/// Need to provide values like <c>ContainerId</c> and <c>ObjectType</c> to create and object.
/// Optional parameters ike <c>Attributes</c> can be provided as well.
/// </summary>
/// <value>Header with required parameters to create an object</value>
public FrostFsObjectHeader? Header { get; set; }
/// <summary>
/// A stream with source data
/// </summary>
public Stream? Payload { get; set; }
/// <summary>
/// Object size is limited. In the data exceeds the limit, the object will be splitted.
/// If the parameter is <c>true</c>, the client side cut is applied. Otherwise, the data is transferred
/// as a stream and will be cut on server side.
/// </summary>
/// <value>Is client cut is applied</value>
public bool ClientCut { get; set; }
/// <summary>
/// Overrides default size of the buffer for stream transferring.
/// </summary>
/// <value>Size of the buffer</value>
public int BufferMaxSize { get; set; }
/// <summary>
/// Allows to define a buffer for chunks to manage by the memory allocation and releasing.
/// </summary>
public byte[]? CustomBuffer { get; set; }
/// <inheritdoc />
public FrostFsSessionToken? SessionToken { get; set; }
internal int MaxObjectSizeCache { get; set; }
internal ulong CurrentStreamPosition { get; set; }
internal ulong FullLength { get; set; }
}

View file

@ -0,0 +1,21 @@
using System.Collections.Generic;
namespace FrostFS.SDK.Client;
public sealed class PrmObjectSearch(FrostFsContainerId containerId, CallContext? ctx = null, params IObjectFilter[] filters) : PrmBase(ctx), ISessionToken
{
/// <summary>
/// Defines container for the search
/// </summary>
/// <value></value>
public FrostFsContainerId ContainerId { get; set; } = containerId;
/// <summary>
/// Defines the search criteria
/// </summary>
/// <value>Collection of filters</value>
public IEnumerable<IObjectFilter> Filters { get; set; } = filters;
/// <inheritdoc />
public FrostFsSessionToken? SessionToken { get; set; }
}

View file

@ -0,0 +1,20 @@
namespace FrostFS.SDK.Client;
public sealed class PrmRangeGet(
FrostFsContainerId containerId,
FrostFsObjectId objectId,
FrostFsRange range,
bool raw = false,
CallContext? ctx = null) : PrmBase(ctx), ISessionToken
{
public FrostFsContainerId ContainerId { get; } = containerId;
public FrostFsObjectId ObjectId { get; } = objectId;
public FrostFsRange Range { get; } = range;
public bool Raw { get; } = raw;
/// <inheritdoc />
public FrostFsSessionToken? SessionToken { get; set; }
}

View file

@ -0,0 +1,20 @@
namespace FrostFS.SDK.Client;
public sealed class PrmRangeHashGet(
FrostFsContainerId containerId,
FrostFsObjectId objectId,
FrostFsRange[] ranges,
byte[] salt,
CallContext? ctx = null) : PrmBase(ctx), ISessionToken
{
public FrostFsContainerId ContainerId { get; } = containerId;
public FrostFsObjectId ObjectId { get; } = objectId;
public FrostFsRange[] Ranges { get; } = ranges;
public byte[] Salt { get; } = salt;
/// <inheritdoc />
public FrostFsSessionToken? SessionToken { get; set; }
}

View file

@ -0,0 +1,6 @@
namespace FrostFS.SDK.Client;
public sealed class PrmSessionCreate(ulong expiration, CallContext? ctx = null) : PrmBase(ctx)
{
public ulong Expiration { get; set; } = expiration;
}

View file

@ -0,0 +1,9 @@
namespace FrostFS.SDK.Client;
public sealed class PrmSingleObjectPut(FrostFsObject frostFsObject, CallContext? ctx = null) : PrmBase(ctx), ISessionToken
{
public FrostFsObject FrostFsObject { get; set; } = frostFsObject;
/// <inheritdoc />
public FrostFsSessionToken? SessionToken { get; set; }
}

View file

@ -0,0 +1,24 @@
using System;
namespace FrostFS.SDK.Client;
public class PrmWait(TimeSpan timeout, TimeSpan pollInterval)
{
private static TimeSpan DefaultTimeout = TimeSpan.FromSeconds(120);
private static TimeSpan DefaultPollInterval = TimeSpan.FromSeconds(5);
public PrmWait(int timeout, int interval) : this(TimeSpan.FromSeconds(timeout), TimeSpan.FromSeconds(interval))
{
}
public static PrmWait DefaultParams { get; } = new PrmWait(DefaultTimeout, DefaultPollInterval);
public TimeSpan Timeout { get; set; } = timeout.Ticks == 0 ? DefaultTimeout : timeout;
public TimeSpan PollInterval { get; set; } = pollInterval.Ticks == 0 ? DefaultPollInterval : pollInterval;
public DateTime GetDeadline()
{
return DateTime.UtcNow.AddTicks(Timeout.Ticks);
}
}

View file

@ -0,0 +1,163 @@
using System;
using System.Threading;
using Microsoft.Extensions.Logging;
namespace FrostFS.SDK.Client;
// clientStatusMonitor count error rate and other statistics for connection.
public class ClientStatusMonitor : IClientStatus
{
private static readonly MethodIndex[] MethodIndexes =
[
MethodIndex.methodBalanceGet,
MethodIndex.methodContainerPut,
MethodIndex.methodContainerGet,
MethodIndex.methodContainerList,
MethodIndex.methodContainerDelete,
MethodIndex.methodEndpointInfo,
MethodIndex.methodNetworkInfo,
MethodIndex.methodNetMapSnapshot,
MethodIndex.methodObjectPut,
MethodIndex.methodObjectDelete,
MethodIndex.methodObjectGet,
MethodIndex.methodObjectHead,
MethodIndex.methodObjectRange,
MethodIndex.methodObjectPatch,
MethodIndex.methodSessionCreate,
MethodIndex.methodAPEManagerAddChain,
MethodIndex.methodAPEManagerRemoveChain,
MethodIndex.methodAPEManagerListChains
];
public static string GetMethodName(MethodIndex index)
{
return index switch
{
MethodIndex.methodBalanceGet => "BalanceGet",
MethodIndex.methodContainerPut => "ContainerPut",
MethodIndex.methodContainerGet => "ContainerGet",
MethodIndex.methodContainerList => "ContainerList",
MethodIndex.methodContainerDelete => "ContainerDelete",
MethodIndex.methodEndpointInfo => "EndpointInfo",
MethodIndex.methodNetworkInfo => "NetworkInfo",
MethodIndex.methodNetMapSnapshot => "NetMapSnapshot",
MethodIndex.methodObjectPut => "ObjectPut",
MethodIndex.methodObjectDelete => "ObjectDelete",
MethodIndex.methodObjectGet => "ObjectGet",
MethodIndex.methodObjectHead => "ObjectHead",
MethodIndex.methodObjectRange => "ObjectRange",
MethodIndex.methodObjectPatch => "ObjectPatch",
MethodIndex.methodSessionCreate => "SessionCreate",
MethodIndex.methodAPEManagerAddChain => "APEManagerAddChain",
MethodIndex.methodAPEManagerRemoveChain => "APEManagerRemoveChain",
MethodIndex.methodAPEManagerListChains => "APEManagerListChains",
_ => throw new ArgumentException("Unknown method", nameof(index)),
};
}
private readonly object _lock = new();
private readonly ILogger? logger;
private int healthy;
public ClientStatusMonitor(ILogger? logger, string address)
{
this.logger = logger;
healthy = (int)HealthyStatus.Healthy;
Address = address;
Methods = new MethodStatus[MethodIndexes.Length];
for (int i = 0; i < MethodIndexes.Length; i++)
{
Methods[i] = new MethodStatus(GetMethodName(MethodIndexes[i]));
}
}
public string Address { get; }
internal uint ErrorThreshold { get; set; }
public uint CurrentErrorCount { get; set; }
public ulong OverallErrorCount { get; set; }
public MethodStatus[] Methods { get; private set; }
public bool IsHealthy()
{
var res = Interlocked.CompareExchange(ref healthy, -1, -1) == (int)HealthyStatus.Healthy;
return res;
}
public bool IsDialed()
{
return Interlocked.CompareExchange(ref healthy, -1, -1) != (int)HealthyStatus.UnhealthyOnDial;
}
public void SetHealthy()
{
Interlocked.Exchange(ref healthy, (int)HealthyStatus.Healthy);
}
public void SetUnhealthy()
{
Interlocked.Exchange(ref healthy, (int)HealthyStatus.UnhealthyOnRequest);
}
public void SetUnhealthyOnDial()
{
Interlocked.Exchange(ref healthy, (int)HealthyStatus.UnhealthyOnDial);
}
public void IncErrorRate()
{
bool thresholdReached;
lock (_lock)
{
CurrentErrorCount++;
OverallErrorCount++;
thresholdReached = CurrentErrorCount >= ErrorThreshold;
if (thresholdReached)
{
SetUnhealthy();
CurrentErrorCount = 0;
}
}
if (thresholdReached && logger != null)
{
FrostFsMessages.ErrorЕhresholdReached(logger, Address, ErrorThreshold);
}
}
public uint GetCurrentErrorRate()
{
lock (_lock)
{
return CurrentErrorCount;
}
}
public ulong GetOverallErrorRate()
{
lock (_lock)
{
return OverallErrorCount;
}
}
public StatusSnapshot[] MethodsStatus()
{
var result = new StatusSnapshot[Methods.Length];
for (int i = 0; i < result.Length; i++)
{
result[i] = Methods[i].Snapshot!;
}
return result;
}
}

View file

@ -0,0 +1,157 @@
using System;
using System.Threading.Tasks;
using Grpc.Core;
namespace FrostFS.SDK.Client;
// clientWrapper is used by default, alternative implementations are intended for testing purposes only.
public class ClientWrapper : ClientStatusMonitor
{
private readonly object _lock = new();
private SessionCache sessionCache;
internal ClientWrapper(WrapperPrm wrapperPrm, Pool pool) : base(wrapperPrm.Logger, wrapperPrm.Address)
{
WrapperPrm = wrapperPrm;
ErrorThreshold = wrapperPrm.ErrorThreshold;
sessionCache = pool.SessionCache;
Client = new FrostFSClient(WrapperPrm, sessionCache);
}
internal FrostFSClient? Client { get; private set; }
internal WrapperPrm WrapperPrm { get; }
internal FrostFSClient? GetClient()
{
lock (_lock)
{
if (IsHealthy())
{
return Client;
}
return null;
}
}
// dial establishes a connection to the server from the FrostFS network.
// Returns an error describing failure reason. If failed, the client
// SHOULD NOT be used.
internal async Task Dial(CallContext ctx)
{
var client = GetClient() ?? throw new FrostFsInvalidObjectException("pool client unhealthy");
await client.Dial(ctx).ConfigureAwait(false);
}
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()
{
if (Client == null)
return;
await Task.Delay((int)WrapperPrm.GracefulCloseOnSwitchTimeout).ConfigureAwait(false);
Client.Close();
}
// restartIfUnhealthy checks healthy status of client and recreate it if status is unhealthy.
// Indicating if status was changed by this function call and returns error that caused unhealthy status.
internal async Task<bool> RestartIfUnhealthy(CallContext ctx)
{
bool wasHealthy;
try
{
var prmNodeInfo = new PrmNodeInfo(ctx);
var response = await Client!.GetNodeInfoAsync(prmNodeInfo).ConfigureAwait(false);
return false;
}
catch (RpcException)
{
wasHealthy = true;
}
// if connection is dialed before, to avoid routine/connection leak,
// pool has to close it and then initialize once again.
if (IsDialed())
{
await ScheduleGracefulClose().ConfigureAwait(false);
}
FrostFSClient? client = null;
try
{
client = new(WrapperPrm, sessionCache);
var dialCtx = new CallContext
{
Timeout = TimeSpan.FromTicks((long)WrapperPrm.DialTimeout),
CancellationToken = ctx.CancellationToken
};
var error = await client.Dial(ctx).ConfigureAwait(false);
if (!string.IsNullOrEmpty(error))
{
SetUnhealthyOnDial();
return wasHealthy;
}
lock (_lock)
{
Client = client;
}
client = null;
}
finally
{
client?.Dispose();
}
try
{
var prmNodeInfo = new PrmNodeInfo(ctx);
var res = await Client.GetNodeInfoAsync(prmNodeInfo).ConfigureAwait(false);
}
catch (FrostFsException)
{
SetUnhealthy();
return wasHealthy;
}
SetHealthy();
return !wasHealthy;
}
internal void IncRequests(ulong elapsed, MethodIndex method)
{
var methodStat = Methods[(int)method];
methodStat.IncRequests(elapsed);
}
}

View file

@ -0,0 +1,18 @@
namespace FrostFS.SDK.Client;
// values for healthy status of clientStatusMonitor.
public enum HealthyStatus
{
// statusUnhealthyOnDial is set when dialing to the endpoint is failed,
// so there is no connection to the endpoint, and pool should not close it
// before re-establishing connection once again.
UnhealthyOnDial,
// statusUnhealthyOnRequest is set when communication after dialing to the
// endpoint is failed due to immediate or accumulated errors, connection is
// available and pool should close it before re-establishing connection once again.
UnhealthyOnRequest,
// statusHealthy is set when connection is ready to be used by the pool.
Healthy
}

View file

@ -0,0 +1,28 @@
namespace FrostFS.SDK.Client;
public interface IClientStatus
{
// isHealthy checks if the connection can handle requests.
bool IsHealthy();
// isDialed checks if the connection was created.
bool IsDialed();
// setUnhealthy marks client as unhealthy.
void SetUnhealthy();
// address return address of endpoint.
string Address { get; }
// currentErrorRate returns current errors rate.
// After specific threshold connection is considered as unhealthy.
// Pool.startRebalance routine can make this connection healthy again.
uint GetCurrentErrorRate();
// overallErrorRate returns the number of all happened errors.
ulong GetOverallErrorRate();
// methodsStatus returns statistic for all used methods.
StatusSnapshot[] MethodsStatus();
}

View file

@ -0,0 +1,36 @@
using System;
using System.Security.Cryptography;
using Grpc.Net.Client;
using Microsoft.Extensions.Logging;
namespace FrostFS.SDK.Client;
// InitParameters contains values used to initialize connection Pool.
public class InitParameters
{
public ECDsa? Key { get; set; }
public ulong NodeDialTimeout { get; set; }
public ulong NodeStreamTimeout { get; set; }
public ulong HealthcheckTimeout { get; set; }
public ulong ClientRebalanceInterval { get; set; }
public ulong SessionExpirationDuration { get; set; }
public uint ErrorThreshold { get; set; }
public NodeParam[]? NodeParams { get; set; }
public GrpcChannelOptions[]? DialOptions { get; set; }
public Func<string, ClientWrapper>? ClientBuilder { get; set; }
public ulong GracefulCloseOnSwitchTimeout { get; set; }
public ILogger? Logger { get; set; }
}

Some files were not shown because too many files have changed in this diff Show more