Compare commits

...

13 commits

Author SHA1 Message Date
43e300c773 [#29] Client: Add PlacementVector unit tests
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2025-01-13 10:34:44 +03:00
568bdc67e8 [#29] Client: Add object placement methods
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-12-24 17:32:29 +03:00
8637515869 [#28] Client: Move CalculateObjectId from IFrostFsClient to statis tools
Get payload hash as an argument

Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-12-12 12:28:45 +00:00
db9b93b2e6 [#28] Client: Move CalculateObjectId from IFrostFsClient to statis tools
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-12-12 12:28:45 +00:00
543247e4d9 [#28] Client: add method to calculate ObjectId
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-12-12 12:28:45 +00:00
abd9b5d0d0 [#39] Add CODEOWNERS
Signed-off-by: Vitaliy Potyarkin <v.potyarkin@yadro.com>
2024-12-10 18:52:18 +03:00
c9418a1894 [#28] Client: Use external GRPC Channnel
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-12-06 09:01:33 +03:00
9bb7b5eff8 [#28] Clients: Make immutable parameters
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-12-02 19:33:45 +03:00
749000a090 [#28] Client: Apply code optimizations
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-11-18 17:00:19 +03:00
766f61a5f7 [#26] All: Remove V2 from naming
Rename project, namespaces and class names

Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-11-18 11:33:50 +03:00
c406df1a78 [#13] Execute .NET code analyzers in CI
Signed-off-by: Vitaliy Potyarkin <v.potyarkin@yadro.com>
2024-11-18 07:07:57 +00:00
14dc76898e [#25] Client Implement PAtch and Range methods
Fix the specific type in interfaces

Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-11-14 11:24:14 +03:00
003b7fdfdd [#25] Client: Implement Patch and Range methods
Signed-off-by: Pavel Gross <p.gross@yadro.com>
2024-11-08 10:38:50 +03:00
304 changed files with 10764 additions and 4474 deletions

View file

@ -214,7 +214,7 @@ dotnet_diagnostic.CA1700.severity = warning
dotnet_diagnostic.CA1707.severity = warning
# CA1708: Identifiers should differ by more than case
dotnet_diagnostic.CA1708.severity = warning
dotnet_diagnostic.CA1708.severity = none
# CA1710: Identifiers should have correct suffix
dotnet_diagnostic.CA1710.severity = warning
@ -232,7 +232,7 @@ dotnet_diagnostic.CA1713.severity = warning
dotnet_diagnostic.CA1715.severity = warning
# CA1716: Identifiers should not match keywords
dotnet_diagnostic.CA1716.severity = warning
dotnet_diagnostic.CA1716.severity = none
# CA1720: Identifier contains type name
dotnet_diagnostic.CA1720.severity = warning

View file

@ -0,0 +1,22 @@
name: lint-build
on:
push:
pull_request:
jobs:
lint-build:
name: dotnet${{ matrix.dotnet }}
runs-on: docker
container: git.frostfs.info/truecloudlab/env:dotnet-${{ matrix.dotnet }}
strategy:
matrix:
dotnet:
- '8.0'
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
# Dotnet runs code analyzers on build (if configured):
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview?tabs=net-8#enable-on-build
- run: dotnet build

3
CODEOWNERS Normal file
View file

@ -0,0 +1,3 @@
.* @PavelGrossSpb
.forgejo/.* @potyarkin
Makefile @potyarkin

View file

@ -3,11 +3,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrostFS.SDK.ClientV2", "src\FrostFS.SDK.ClientV2\FrostFS.SDK.ClientV2.csproj", "{50D8F61F-C302-4AC9-8D8A-AB0B8C0988C3}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrostFS.SDK.Client", "src\FrostFS.SDK.Client\FrostFS.SDK.Client.csproj", "{50D8F61F-C302-4AC9-8D8A-AB0B8C0988C3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrostFS.SDK.Cryptography", "src\FrostFS.SDK.Cryptography\FrostFS.SDK.Cryptography.csproj", "{3D804F4A-B0B2-47A5-B006-BE447BE64B50}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrostFS.SDK.ProtosV2", "src\FrostFS.SDK.ProtosV2\FrostFS.SDK.ProtosV2.csproj", "{5012EF96-9C9E-4E77-BC78-B4111EE54107}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrostFS.SDK.Protos", "src\FrostFS.SDK.Protos\FrostFS.SDK.Protos.csproj", "{5012EF96-9C9E-4E77-BC78-B4111EE54107}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrostFS.SDK.Tests", "src\FrostFS.SDK.Tests\FrostFS.SDK.Tests.csproj", "{8FDA7E0D-9C75-4874-988E-6592CD28F76C}"
EndProject

View file

@ -1,6 +1,6 @@
using Microsoft.Extensions.Caching.Memory;
namespace FrostFS.SDK.ClientV2;
namespace FrostFS.SDK.Client;
internal static class Caches
{

View file

@ -4,11 +4,15 @@ using FrostFS.SDK.Cryptography;
using Google.Protobuf;
namespace FrostFS.SDK.ClientV2;
namespace FrostFS.SDK.Client;
public class ClientKey(ECDsa key)
{
internal ECDsa ECDsaKey { get; } = key;
internal ByteString PublicKeyProto { get; } = ByteString.CopyFrom(key.PublicKey());
internal string PublicKey { get; } = key.PublicKey().ToString();
internal FrostFsOwner Owner { get; } = new FrostFsOwner(key.PublicKey().PublicKeyToAddress());
}

View file

@ -1,6 +1,6 @@
using System;
namespace FrostFS.SDK.ClientV2;
namespace FrostFS.SDK.Client;
public class FrostFsException : Exception
{

View file

@ -1,6 +1,6 @@
using System;
namespace FrostFS.SDK.ClientV2;
namespace FrostFS.SDK.Client;
public class FrostFsInvalidObjectException : FrostFsException
{

View file

@ -1,6 +1,6 @@
using System;
namespace FrostFS.SDK.ClientV2;
namespace FrostFS.SDK.Client;
public class FrostFsResponseException : FrostFsException
{

View file

@ -1,6 +1,6 @@
using System;
namespace FrostFS.SDK.ClientV2;
namespace FrostFS.SDK.Client;
public class FrostFsStreamException : FrostFsException
{

View file

@ -1,6 +1,6 @@
using System;
namespace FrostFS.SDK.ClientV2;
namespace FrostFS.SDK.Client;
public class SessionExpiredException : FrostFsException
{

View file

@ -1,6 +1,6 @@
using System;
namespace FrostFS.SDK.ClientV2;
namespace FrostFS.SDK.Client;
public class SessionNotFoundException : FrostFsException
{

View file

@ -4,16 +4,19 @@ using Google.Protobuf;
namespace FrostFS.SDK.Cryptography;
public static class UUIDExtension
public static class FrostFsExtensions
{
public static ByteString Sha256(this IMessage data)
{
return ByteString.CopyFrom(data.ToByteArray().Sha256());
}
public static Guid ToUuid(this ByteString id)
{
if (id == null)
throw new ArgumentNullException(nameof(id));
var bytes = id.ToByteArray();
var orderedBytes = GetGuidBytesDirectOrder(bytes);
var orderedBytes = GetGuidBytesDirectOrder(id.Span);
return new Guid(orderedBytes);
}
@ -32,7 +35,7 @@ public static class UUIDExtension
return orderedBytes;
}
private static byte[] GetGuidBytesDirectOrder(byte[] source)
private static byte[] GetGuidBytesDirectOrder(ReadOnlySpan<byte> source)
{
if (source.Length != 16)
throw new ArgumentException("Wrong uuid binary format");

View file

@ -15,22 +15,30 @@
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup>
<_SkipUpgradeNetAnalyzersNuGetWarning>true</_SkipUpgradeNetAnalyzersNuGetWarning>
</PropertyGroup>
<PropertyGroup>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0">
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="7.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="7.0.4">
<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" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="7.0.0" />
<PackageReference Include="System.Runtime.Caching" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FrostFS.SDK.Cryptography\FrostFS.SDK.Cryptography.csproj" />
<ProjectReference Include="..\FrostFS.SDK.ProtosV2\FrostFS.SDK.ProtosV2.csproj" />
<ProjectReference Include="..\FrostFS.SDK.Protos\FrostFS.SDK.Protos.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,473 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Frostfs.V2.Ape;
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 Microsoft.Extensions.Options;
using static Frostfs.V2.Apemanager.APEManagerService;
using static FrostFS.Accounting.AccountingService;
using static FrostFS.Container.ContainerService;
using static FrostFS.Netmap.NetmapService;
using static FrostFS.Object.ObjectService;
using static FrostFS.Session.SessionService;
namespace FrostFS.SDK.Client;
public class FrostFSClient : IFrostFSClient
{
internal ContainerServiceClient? ContainerServiceClient { get; set; }
internal ContainerServiceProvider? ContainerServiceProvider { get; set; }
internal NetmapServiceClient? NetmapServiceClient { get; set; }
internal NetmapServiceProvider? NetmapServiceProvider { get; set; }
internal APEManagerServiceClient? ApeManagerServiceClient { get; set; }
internal ApeManagerServiceProvider? ApeManagerServiceProvider { get; set; }
internal SessionServiceClient? SessionServiceClient { get; set; }
internal SessionServiceProvider? SessionServiceProvider { get; set; }
internal ObjectServiceClient? ObjectServiceClient { get; set; }
internal ObjectServiceProvider? ObjectServiceProvider { get; set; }
internal AccountingServiceClient? AccountingServiceClient { get; set; }
internal AccountingServiceProvider? AccountingServiceProvider { get; set; }
internal ClientContext ClientCtx { get; set; }
public static IFrostFSClient GetInstance(IOptions<ClientSettings> clientOptions, Func<string, ChannelBase> grpcChannelFactory)
{
if (clientOptions is null)
{
throw new ArgumentNullException(nameof(clientOptions));
}
if (grpcChannelFactory is null)
{
throw new ArgumentNullException(nameof(grpcChannelFactory));
}
return new FrostFSClient(clientOptions, grpcChannelFactory);
}
/// <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<ClientSettings> settings,
Func<string, ChannelBase> grpcChannelFactory,
NetmapServiceClient netmapService,
SessionServiceClient sessionService,
ContainerServiceClient containerService,
ObjectServiceClient objectService)
{
if (settings is null)
{
throw new ArgumentNullException(nameof(settings));
}
if (grpcChannelFactory is null)
{
throw new ArgumentNullException(nameof(grpcChannelFactory));
}
if (netmapService is null)
{
throw new ArgumentNullException(nameof(netmapService));
}
if (sessionService is null)
{
throw new ArgumentNullException(nameof(sessionService));
}
if (containerService is null)
{
throw new ArgumentNullException(nameof(containerService));
}
if (objectService is null)
{
throw new ArgumentNullException(nameof(objectService));
}
return new FrostFSClient(
settings, channel: grpcChannelFactory(settings.Value.Host), containerService, netmapService, sessionService, objectService);
}
private FrostFSClient(
IOptions<ClientSettings> settings,
ChannelBase channel,
ContainerServiceClient containerService,
NetmapServiceClient netmapService,
SessionServiceClient sessionService,
ObjectServiceClient objectService)
{
if (settings is null)
{
throw new ArgumentNullException(nameof(settings));
}
var ecdsaKey = settings.Value.Key.LoadWif();
ClientCtx = new ClientContext(
client: this,
key: new ClientKey(ecdsaKey),
owner: FrostFsOwner.FromKey(ecdsaKey),
channel: channel,
version: new FrostFsVersion(2, 13))
{
SessionCache = new SessionCache(0),
Callback = settings.Value.Callback,
Interceptors = settings.Value.Interceptors
};
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> settings, Func<string, ChannelBase> grpcChannelFactory)
{
var clientSettings = (settings?.Value) ?? throw new ArgumentNullException(nameof(settings), "Options value must be initialized");
clientSettings.Validate();
var ecdsaKey = clientSettings.Key.LoadWif();
ClientCtx = new ClientContext(
this,
key: new ClientKey(ecdsaKey),
owner: FrostFsOwner.FromKey(ecdsaKey),
channel: grpcChannelFactory(settings.Value.Host),
version: new FrostFsVersion(2, 13))
{
SessionCache = new SessionCache(0),
Callback = settings.Value.Callback,
Interceptors = settings.Value.Interceptors
};
// TODO: define timeout logic
// CheckFrostFsVersionSupport(new Context { Timeout = TimeSpan.FromSeconds(20) });
}
internal FrostFSClient(WrapperPrm prm, SessionCache cache)
{
ClientCtx = new ClientContext(
client: this,
key: new ClientKey(prm.Key),
owner: FrostFsOwner.FromKey(prm.Key!),
channel: prm.GrpcChannelFactory(prm.Address),
version: new FrostFsVersion(2, 13))
{
SessionCache = cache,
Interceptors = prm.Interceptors,
Callback = prm.Callback
};
}
#region ApeManagerImplementation
public Task<ReadOnlyMemory<byte>> AddChainAsync(PrmApeChainAdd args, CallContext ctx)
{
return GetApeManagerService().AddChainAsync(args, ctx);
}
public Task RemoveChainAsync(PrmApeChainRemove args, CallContext ctx)
{
return GetApeManagerService().RemoveChainAsync(args, ctx);
}
public Task<Chain[]> ListChainAsync(PrmApeChainList args, CallContext ctx)
{
return GetApeManagerService().ListChainAsync(args, ctx);
}
#endregion
#region ContainerImplementation
public Task<FrostFsContainerInfo> GetContainerAsync(PrmContainerGet args, CallContext ctx)
{
return GetContainerService().GetContainerAsync(args, ctx);
}
public IAsyncEnumerable<FrostFsContainerId> ListContainersAsync(PrmContainerGetAll args, CallContext ctx)
{
return GetContainerService().ListContainersAsync(args, ctx);
}
public Task<FrostFsContainerId> CreateContainerAsync(PrmContainerCreate args, CallContext ctx)
{
return GetContainerService().CreateContainerAsync(args, ctx);
}
public Task DeleteContainerAsync(PrmContainerDelete args, CallContext ctx)
{
return GetContainerService().DeleteContainerAsync(args, ctx);
}
#endregion
#region NetworkImplementation
public Task<FrostFsNetmapSnapshot> GetNetmapSnapshotAsync(CallContext ctx)
{
return GetNetmapService().GetNetmapSnapshotAsync(ctx);
}
public Task<FrostFsNodeInfo> GetNodeInfoAsync(CallContext ctx)
{
return GetNetmapService().GetLocalNodeInfoAsync(ctx);
}
public Task<NetworkSettings> GetNetworkSettingsAsync(CallContext ctx)
{
return GetNetmapService().GetNetworkSettingsAsync(ctx);
}
#endregion
#region ObjectImplementation
public Task<FrostFsHeaderResult> GetObjectHeadAsync(PrmObjectHeadGet args, CallContext ctx)
{
return GetObjectService().GetObjectHeadAsync(args, ctx);
}
public Task<FrostFsObject> GetObjectAsync(PrmObjectGet args, CallContext ctx)
{
return GetObjectService().GetObjectAsync(args, ctx);
}
public Task<RangeReader> GetRangeAsync(PrmRangeGet args, CallContext ctx)
{
return GetObjectService().GetRangeAsync(args, ctx);
}
public Task<ReadOnlyMemory<byte>[]> GetRangeHashAsync(PrmRangeHashGet args, CallContext ctx)
{
return GetObjectService().GetRangeHashAsync(args, ctx);
}
public Task<IObjectWriter> PutObjectAsync(PrmObjectPut args, CallContext ctx)
{
return GetObjectService().PutStreamObjectAsync(args, ctx);
}
public Task<FrostFsObjectId> PutClientCutObjectAsync(PrmObjectClientCutPut args, CallContext ctx)
{
return GetObjectService().PutClientCutObjectAsync(args, ctx);
}
public Task<FrostFsObjectId> PutSingleObjectAsync(PrmSingleObjectPut args, CallContext ctx)
{
return GetObjectService().PutSingleObjectAsync(args, ctx);
}
public Task<FrostFsObjectId> PatchObjectAsync(PrmObjectPatch args, CallContext ctx)
{
return GetObjectService().PatchObjectAsync(args, ctx);
}
public Task DeleteObjectAsync(PrmObjectDelete args, CallContext ctx)
{
return GetObjectService().DeleteObjectAsync(args, ctx);
}
public IAsyncEnumerable<FrostFsObjectId> SearchObjectsAsync(PrmObjectSearch args, CallContext ctx)
{
return GetObjectService().SearchObjectsAsync(args, ctx);
}
#endregion
#region Session Implementation
public async Task<FrostFsSessionToken> CreateSessionAsync(PrmSessionCreate args, CallContext ctx)
{
var token = await CreateSessionInternalAsync(args, ctx).ConfigureAwait(false);
return new FrostFsSessionToken(token);
}
internal Task<SessionToken> CreateSessionInternalAsync(PrmSessionCreate args, CallContext ctx)
{
var service = GetSessionService();
return service.CreateSessionAsync(args, ctx);
}
#endregion
#region Accounting Implementation
public async Task<Accounting.Decimal> GetBalanceAsync(CallContext ctx)
{
return await GetAccouningService().GetBallance(ctx).ConfigureAwait(false);
}
#endregion
private async void CheckFrostFsVersionSupport(CallContext ctx)
{
var service = GetNetmapService();
var localNodeInfo = await service.GetLocalNodeInfoAsync(ctx).ConfigureAwait(false);
if (!localNodeInfo.Version.IsSupported(ClientCtx.Version))
{
var msg = $"FrostFS {localNodeInfo.Version} is not supported.";
throw new FrostFsException(msg);
}
}
private CallInvoker? CreateInvoker()
{
CallInvoker? callInvoker = null;
if (ClientCtx.Interceptors != null)
{
foreach (var interceptor in ClientCtx.Interceptors)
callInvoker = AddInvoker(callInvoker, interceptor);
}
if (ClientCtx.Callback != null)
callInvoker = AddInvoker(callInvoker, new MetricsInterceptor(ClientCtx.Callback));
if (ClientCtx.PoolErrorHandler != null)
callInvoker = AddInvoker(callInvoker, new ErrorInterceptor(ClientCtx.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()
{
if (NetmapServiceProvider == null)
{
var invoker = CreateInvoker();
NetmapServiceClient = NetmapServiceClient ?? (
invoker != null
? new NetmapServiceClient(invoker)
: new NetmapServiceClient(ClientCtx.Channel));
NetmapServiceProvider = new NetmapServiceProvider(NetmapServiceClient, ClientCtx);
}
return NetmapServiceProvider;
}
private SessionServiceProvider GetSessionService()
{
if (SessionServiceProvider == null)
{
var invoker = CreateInvoker();
SessionServiceClient = SessionServiceClient ?? (
invoker != null
? new SessionServiceClient(invoker)
: new SessionServiceClient(ClientCtx.Channel));
SessionServiceProvider = new SessionServiceProvider(SessionServiceClient, ClientCtx);
}
return SessionServiceProvider;
}
private ApeManagerServiceProvider GetApeManagerService()
{
if (ApeManagerServiceProvider == null)
{
var invoker = CreateInvoker();
ApeManagerServiceClient = ApeManagerServiceClient ?? (
invoker != null
? new APEManagerServiceClient(invoker)
: new APEManagerServiceClient(ClientCtx.Channel));
ApeManagerServiceProvider = new ApeManagerServiceProvider(ApeManagerServiceClient, ClientCtx);
}
return ApeManagerServiceProvider;
}
private AccountingServiceProvider GetAccouningService()
{
if (this.AccountingServiceProvider == null)
{
var invoker = CreateInvoker();
AccountingServiceClient = AccountingServiceClient ?? (
invoker != null
? new AccountingServiceClient(invoker)
: new AccountingServiceClient(ClientCtx.Channel));
AccountingServiceProvider = new AccountingServiceProvider(AccountingServiceClient, ClientCtx);
}
return AccountingServiceProvider;
}
private ContainerServiceProvider GetContainerService()
{
if (this.ContainerServiceProvider == null)
{
var invoker = CreateInvoker();
ContainerServiceClient = ContainerServiceClient ?? (
invoker != null
? new ContainerServiceClient(invoker)
: new ContainerServiceClient(ClientCtx.Channel));
ContainerServiceProvider = new ContainerServiceProvider(ContainerServiceClient, ClientCtx);
}
return ContainerServiceProvider;
}
private ObjectServiceProvider GetObjectService()
{
if (this.ObjectServiceProvider == null)
{
var invoker = CreateInvoker();
ObjectServiceClient = ObjectServiceClient ?? (
invoker != null
? new ObjectServiceClient(invoker)
: new ObjectServiceClient(ClientCtx.Channel));
ObjectServiceProvider = new ObjectServiceProvider(ObjectServiceClient, ClientCtx);
}
return ObjectServiceProvider;
}
public async Task<string?> Dial(CallContext ctx)
{
var service = GetAccouningService();
_ = await service.GetBallance(ctx).ConfigureAwait(false);
return null;
}
public bool RestartIfUnhealthy(CallContext ctx)
{
throw new NotImplementedException();
}
}

View file

@ -5,4 +5,4 @@
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "<Pending>", Scope = "member", Target = "~M:FrostFS.SDK.ClientV2.Sampler.Next~System.Int32")]
[assembly: SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "<Pending>", Scope = "member", Target = "~M:FrostFS.SDK.Client.Sampler.Next~System.Int32")]

View file

@ -4,7 +4,7 @@ using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Core.Interceptors;
namespace FrostFS.SDK.ClientV2;
namespace FrostFS.SDK.Client;
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods",
Justification = "parameters are provided by GRPC infrastructure")]

View file

@ -5,7 +5,7 @@ using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Core.Interceptors;
namespace FrostFS.SDK.ClientV2;
namespace FrostFS.SDK.Client;
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods",
Justification = "parameters are provided by GRPC infrastructure")]

View file

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

View file

@ -0,0 +1,12 @@
using System;
using System.Threading.Tasks;
namespace FrostFS.SDK.Client.Interfaces
{
public interface IObjectWriter : IDisposable
{
Task WriteAsync(ReadOnlyMemory<byte> memory);
Task<FrostFsObjectId> CompleteAsync();
}
}

View file

@ -1,6 +1,6 @@
using Microsoft.Extensions.Logging;
namespace FrostFS.SDK.ClientV2;
namespace FrostFS.SDK.Client;
internal static partial class FrostFsMessages
{

View file

@ -3,7 +3,7 @@ using System.Linq;
using FrostFS.SDK.Cryptography;
namespace FrostFS.SDK.ClientV2.Mappers.GRPC;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class ContainerMapper
{

View file

@ -7,7 +7,7 @@ using Google.Protobuf;
using Microsoft.Extensions.Caching.Memory;
namespace FrostFS.SDK.ClientV2.Mappers.GRPC;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class ContainerIdMapper
{
@ -33,7 +33,16 @@ public static class ContainerIdMapper
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.Span));
}
}

View file

@ -2,7 +2,7 @@ using System;
using FrostFS.Session;
namespace FrostFS.SDK.ClientV2.Mappers.GRPC;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class MetaHeaderMapper
{

View file

@ -3,7 +3,7 @@ using System.Linq;
using FrostFS.Netmap;
namespace FrostFS.SDK.ClientV2;
namespace FrostFS.SDK.Client;
public static class NetmapMapper
{

View file

@ -2,9 +2,9 @@ using System;
using System.Linq;
using FrostFS.Netmap;
using FrostFS.SDK.ClientV2.Mappers.GRPC;
using FrostFS.SDK.Client.Mappers.GRPC;
namespace FrostFS.SDK.ClientV2;
namespace FrostFS.SDK.Client;
public static class NodeInfoMapper
{
@ -39,7 +39,7 @@ public static class NodeInfoMapper
state: state,
addresses: [.. nodeInfo.Addresses],
attributes: nodeInfo.Attributes.ToDictionary(n => n.Key, n => n.Value),
publicKey: nodeInfo.PublicKey.ToByteArray()
publicKey: nodeInfo.PublicKey.Memory
);
}
}

View file

@ -3,7 +3,7 @@ using System.Linq;
using FrostFS.Netmap;
namespace FrostFS.SDK.ClientV2;
namespace FrostFS.SDK.Client;
public static class PlacementPolicyMapper
{
@ -16,6 +16,9 @@ public static class PlacementPolicyMapper
return new FrostFsPlacementPolicy(
placementPolicy.Unique,
placementPolicy.ContainerBackupFactor,
new System.Collections.ObjectModel.Collection<FrostFsSelector>(placementPolicy.Selectors.Select(selector => selector.ToModel()).ToList()),
new System.Collections.ObjectModel.Collection<FrostFsFilter>(placementPolicy.Filters.Select(filter => filter.ToModel()).ToList()),
placementPolicy.Replicas.Select(replica => replica.ToModel()).ToArray()
);
}

View file

@ -0,0 +1,91 @@
using System;
using System.Linq;
using FrostFS.Netmap;
namespace FrostFS.SDK.Client;
public static class PolicyMapper
{
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);
}
public static Selector ToMessage(this FrostFsSelector selector)
{
if (selector is null)
{
throw new ArgumentNullException(nameof(selector));
}
return new Selector
{
Name = selector.Name,
Count = selector.Count,
Clause = (Clause)selector.Clause,
Attribute = selector.Attribute,
Filter = selector.Filter
};
}
public static FrostFsSelector ToModel(this Selector selector)
{
if (selector is null)
{
throw new ArgumentNullException(nameof(selector));
}
return new FrostFsSelector(selector.Name)
{
Count = selector.Count,
Clause = (int)selector.Clause,
Attribute = selector.Attribute,
Filter = selector.Filter
};
}
public static Filter ToMessage(this FrostFsFilter filter)
{
if (filter is null)
{
throw new ArgumentNullException(nameof(filter));
}
var message = new Filter
{
Name = filter.Name,
Key = filter.Key,
Op = (Operation)filter.Operation,
Value = filter.Value,
};
message.Filters.AddRange(filter.Filters.Select(f => f.ToMessage()));
return message;
}
public static FrostFsFilter ToModel(this Filter filter)
{
if (filter is null)
{
throw new ArgumentNullException(nameof(filter));
}
return new FrostFsFilter(filter.Name, filter.Key, (int)filter.Op, filter.Value, filter.Filters.Select(f => f.ToModel()).ToArray());
}
}

View file

@ -1,4 +1,4 @@
namespace FrostFS.SDK.ClientV2.Mappers.GRPC;
namespace FrostFS.SDK.Client.Mappers.GRPC;
internal static class ObjectMapper
{
@ -6,7 +6,7 @@ internal static class ObjectMapper
{
return new FrostFsObject(obj.Header.ToModel())
{
ObjectId = FrostFsObjectId.FromHash(obj.ObjectId.Value.ToByteArray())
ObjectId = FrostFsObjectId.FromHash(obj.ObjectId.Value.Span)
};
}
}

View file

@ -2,7 +2,7 @@ using System;
using FrostFS.Object;
namespace FrostFS.SDK.ClientV2.Mappers.GRPC;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class ObjectAttributeMapper
{

View file

@ -2,7 +2,7 @@ using System;
using FrostFS.Object;
namespace FrostFS.SDK.ClientV2.Mappers.GRPC;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class ObjectFilterMapper
{

View file

@ -5,7 +5,7 @@ using System.Linq;
using FrostFS.Object;
using FrostFS.SDK.Cryptography;
namespace FrostFS.SDK.ClientV2.Mappers.GRPC;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class ObjectHeaderMapper
{
@ -40,7 +40,7 @@ public static class ObjectHeaderMapper
}
var model = new FrostFsObjectHeader(
new FrostFsContainerId(Base58.Encode(header.ContainerId.Value.ToByteArray())),
new FrostFsContainerId(Base58.Encode(header.ContainerId.Value.Span)),
objTypeName,
header.Attributes.Select(attribute => attribute.ToModel()).ToArray(),
split,

View file

@ -4,7 +4,7 @@ using FrostFS.Refs;
using Google.Protobuf;
namespace FrostFS.SDK.ClientV2.Mappers.GRPC;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class ObjectIdMapper
{
@ -28,6 +28,6 @@ public static class ObjectIdMapper
throw new ArgumentNullException(nameof(objectId));
}
return FrostFsObjectId.FromHash(objectId.Value.ToByteArray());
return FrostFsObjectId.FromHash(objectId.Value.Span);
}
}

View file

@ -7,7 +7,7 @@ using Google.Protobuf;
using Microsoft.Extensions.Caching.Memory;
namespace FrostFS.SDK.ClientV2.Mappers.GRPC;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class OwnerIdMapper
{
@ -44,7 +44,7 @@ public static class OwnerIdMapper
if (!Caches.Owners.TryGetValue(message, out FrostFsOwner? model))
{
model = new FrostFsOwner(Base58.Encode(message.Value.ToByteArray()));
model = new FrostFsOwner(Base58.Encode(message.Value.Span));
Caches.Owners.Set(message, model, _oneHourExpiration);
}

View file

@ -2,7 +2,7 @@ using System;
using Google.Protobuf;
namespace FrostFS.SDK.ClientV2;
namespace FrostFS.SDK.Client;
public static class SessionMapper
{

View file

@ -2,7 +2,7 @@ using System;
using Google.Protobuf;
namespace FrostFS.SDK.ClientV2.Mappers.GRPC;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class SignatureMapper
{

View file

@ -1,6 +1,6 @@
using System;
namespace FrostFS.SDK.ClientV2.Mappers.GRPC;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class StatusMapper
{

View file

@ -3,7 +3,7 @@ using System.Threading;
using FrostFS.Refs;
namespace FrostFS.SDK.ClientV2.Mappers.GRPC;
namespace FrostFS.SDK.Client.Mappers.GRPC;
public static class VersionMapper
{

View file

@ -2,7 +2,7 @@
using Frostfs.V2.Ape;
namespace FrostFS.SDK.ClientV2;
namespace FrostFS.SDK.Client;
public struct FrostFsChainTarget(FrostFsTargetType type, string name) : IEquatable<FrostFsChainTarget>
{
@ -36,13 +36,19 @@ public struct FrostFsChainTarget(FrostFsTargetType type, string name) : IEquatab
public override readonly bool Equals(object obj)
{
var target = (FrostFsChainTarget)obj;
return Equals(target);
if (obj == null || obj is not FrostFsChainTarget)
return false;
return Equals((FrostFsChainTarget)obj);
}
public readonly bool Equals(FrostFsChainTarget other)
{
return Type == other.Type && Name.Equals(other.Name, StringComparison.Ordinal);
}
public override readonly int GetHashCode()
{
return $"{Name}{Type}".GetHashCode();
return Name.GetHashCode() ^ (int)Type;
}
public static bool operator ==(FrostFsChainTarget left, FrostFsChainTarget right)
@ -54,9 +60,4 @@ public struct FrostFsChainTarget(FrostFsTargetType type, string name) : IEquatab
{
return !(left == right);
}
public readonly bool Equals(FrostFsChainTarget other)
{
return Type == other.Type && Name.Equals(other.Name, StringComparison.Ordinal);
}
}

View file

@ -1,6 +1,6 @@
using Google.Protobuf;
namespace FrostFS.SDK.ClientV2;
namespace FrostFS.SDK.Client;
public struct FrostFsChain(byte[] raw) : System.IEquatable<FrostFsChain>
{

View file

@ -1,4 +1,4 @@
namespace FrostFS.SDK.ClientV2;
namespace FrostFS.SDK.Client;
public enum FrostFsTargetType
{

View file

@ -0,0 +1,37 @@
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Text;
using Grpc.Core.Interceptors;
namespace FrostFS.SDK;
public class ClientSettings
{
protected static readonly string errorTemplate = "{0} is required parameter";
public string Host { get; set; } = string.Empty;
public string Key { get; set; } = string.Empty;
public Action<CallStatistics>? Callback { get; set; }
public Collection<Interceptor> Interceptors { get; } = [];
public void Validate()
{
StringBuilder? errors = null;
if (string.IsNullOrWhiteSpace(Host))
(errors = new StringBuilder(128)).AppendLine(string.Format(CultureInfo.InvariantCulture, errorTemplate, nameof(Host)));
if (string.IsNullOrWhiteSpace(Key))
(errors ??= new StringBuilder(128)).AppendLine(string.Format(CultureInfo.InvariantCulture, errorTemplate, nameof(Key)));
if (errors != null)
{
throw new ArgumentException(errors.ToString());
}
}
}

View file

@ -1,6 +1,6 @@
using FrostFS.Refs;
using FrostFS.SDK.ClientV2;
using FrostFS.SDK.ClientV2.Mappers.GRPC;
using FrostFS.SDK.Client;
using FrostFS.SDK.Client.Mappers.GRPC;
using FrostFS.SDK.Cryptography;
namespace FrostFS.SDK;
@ -27,7 +27,7 @@ public class FrostFsContainerId
if (containerID != null)
{
this.modelId = Base58.Encode(containerID.Value.ToByteArray());
this.modelId = Base58.Encode(containerID.Value.Span);
return this.modelId;
}

View file

@ -2,8 +2,8 @@ using System;
using System.Collections.ObjectModel;
using System.Linq;
using FrostFS.SDK.ClientV2;
using FrostFS.SDK.ClientV2.Mappers.GRPC;
using FrostFS.SDK.Client;
using FrostFS.SDK.Client.Mappers.GRPC;
using FrostFS.SDK.Cryptography;
using Google.Protobuf;
@ -96,7 +96,7 @@ public class FrostFsContainerInfo
PlacementPolicy = PlacementPolicy.Value.GetPolicy(),
Nonce = ByteString.CopyFrom(Nonce.ToBytes()),
OwnerId = Owner?.OwnerID,
Version = Version?.Version
Version = Version?.VersionID
};
var attribs = GetGrpsAttributes();

View file

@ -0,0 +1,10 @@
namespace FrostFS.SDK;
public class FrostFsFilter(string name, string key, int operation, string value, FrostFsFilter[] filters) : IFrostFsFilter
{
public string Name { get; } = name;
public string Key { get; } = key;
public int Operation { get; } = operation;
public string Value { get; } = value;
public FrostFsFilter[] Filters { get; } = filters;
}

View file

@ -0,0 +1,246 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FrostFS.SDK.Client;
using FrostFS.SDK.Client.Models.Netmap.Placement;
using FrostFS.SDK.Cryptography;
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;
internal static INormalizer NewReverseMinNorm(double minV)
{
return new ReverseMinNorm { min = minV };
}
// newSigmoidNorm returns a normalizer which
// normalize values in range of 0.0 to 1.0 to a scaled sigmoid.
internal static INormalizer NewSigmoidNorm(double scale)
{
return new SigmoidNorm(scale);
}
// PlacementVectors sorts container nodes returned by ContainerNodes method
// and returns placement vectors for the entity identified by the given pivot.
// For example, in order to build node list to store the object, binary-encoded
// object identifier can be used as pivot. Result is deterministic for
// the fixed NetMap and parameters.
public FrostFsNodeInfo[][] PlacementVectors(FrostFsNodeInfo[][] vectors, byte[] pivot)
{
if (vectors is null)
{
throw new ArgumentNullException(nameof(vectors));
}
using var murmur3 = new Murmur3(0);
var hash = murmur3.GetCheckSum64(pivot);
var wf = Tools.DefaultWeightFunc(NodeInfoCollection.ToArray());
var result = new FrostFsNodeInfo[vectors.Length][];
var maxSize = vectors.Max(x => x.Length);
var spanWeigths = new double[maxSize];
for (int i = 0; i < vectors.Length; i++)
{
result[i] = new FrostFsNodeInfo[vectors[i].Length];
for (int j = 0; j < vectors[i].Length; j++)
{
result[i][j] = vectors[i][j];
}
Tools.AppendWeightsTo(result[i], wf, ref spanWeigths);
result[i] = Tools.SortHasherSliceByWeightValue(result[i].ToList<FrostFsNodeInfo>(), spanWeigths, hash).ToArray();
}
return result;
}
// SelectFilterNodes returns a two-dimensional list of nodes as a result of applying the
// given SelectFilterExpr to the NetMap.
// If the SelectFilterExpr contains only filters, the result contains a single row with the
// result of the last filter application.
// If the SelectFilterExpr contains only selectors, the result contains the selection rows
// of the last select application.
List<List<FrostFsNodeInfo>> SelectFilterNodes(SelectFilterExpr expr)
{
var policy = new FrostFsPlacementPolicy(false, expr.Cbf, [expr.Selector], expr.Filters);
var ctx = new Context(this)
{
Cbf = expr.Cbf
};
ctx.ProcessFilters(policy);
ctx.ProcessSelectors(policy);
var ret = new List<List<FrostFsNodeInfo>>();
if (expr.Selector == null)
{
var lastFilter = expr.Filters[^1];
var subCollestion = new List<FrostFsNodeInfo>();
ret.Add(subCollestion);
foreach (var nodeInfo in NodeInfoCollection)
{
if (ctx.Match(ctx.ProcessedFilters[lastFilter.Name], nodeInfo))
{
subCollestion.Add(nodeInfo);
}
}
}
else if (expr.Selector.Name != null)
{
var sel = ctx.GetSelection(ctx.ProcessedSelectors[expr.Selector.Name]);
foreach (var ns in sel)
{
var subCollestion = new List<FrostFsNodeInfo>();
ret.Add(subCollestion);
foreach (var n in ns)
{
subCollestion.Add(n);
}
}
}
return ret;
}
internal static Func<FrostFsNodeInfo, double> NewWeightFunc(INormalizer capNorm, INormalizer priceNorm)
{
return new Func<FrostFsNodeInfo, double>((FrostFsNodeInfo nodeInfo) =>
{
return capNorm.Normalize(nodeInfo.GetCapacity()) * priceNorm.Normalize(nodeInfo.Price);
});
}
private static FrostFsNodeInfo[] FlattenNodes(List<List<FrostFsNodeInfo>> nodes)
{
int sz = 0;
foreach (var ns in nodes)
{
sz += ns.Count;
}
var result = new FrostFsNodeInfo[sz];
int i = 0;
foreach (var ns in nodes)
{
foreach (var n in ns)
{
result[i++] = n;
}
}
return result;
}
// ContainerNodes returns two-dimensional list of nodes as a result of applying
// given PlacementPolicy to the NetMap. Each line of the list corresponds to a
// replica descriptor. Line order corresponds to order of ReplicaDescriptor list
// in the policy. Nodes are pre-filtered according to the Filter list from
// the policy, and then selected by Selector list. Result is deterministic for
// the fixed NetMap and parameters.
//
// Result can be used in PlacementVectors.
public FrostFsNodeInfo[][] ContainerNodes(FrostFsPlacementPolicy p, byte[]? pivot)
{
var c = new Context(this)
{
Cbf = p.BackupFactor == 0 ? 3 : p.BackupFactor
};
if (pivot != null && pivot.Length > 0)
{
c.HrwSeed = pivot;
using var murmur = new Murmur3(0);
c.HrwSeedHash = murmur.GetCheckSum64(pivot);
}
c.ProcessFilters(p);
c.ProcessSelectors(p);
var unique = p.IsUnique();
var result = new List<List<FrostFsNodeInfo>>(p.Replicas.Length);
for (int i = 0; i < p.Replicas.Length; i++)
{
result.Add([]);
}
// Note that the cached selectors are not used when the policy contains the UNIQUE flag.
// This is necessary because each selection vector affects potentially the subsequent vectors
// and thus we call getSelection in such case, in order to take into account nodes previously
// marked as used by earlier replicas.
for (int i = 0; i < p.Replicas.Length; i++)
{
var sName = p.Replicas[i].Selector;
if (string.IsNullOrEmpty(sName) && !(p.Replicas.Length == 1 && p.Selectors.Count == 1))
{
var s = new FrostFsSelector(string.Empty)
{
Count = p.Replicas[i].CountNodes(),
Filter = Context.mainFilterName
};
var nodes = c.GetSelection(s);
result[i].AddRange(FlattenNodes(nodes));
if (unique)
{
foreach (var n in result[i])
{
c.UsedNodes[n.Hash()] = true;
}
}
continue;
}
if (unique)
{
if (!c.ProcessedSelectors.TryGetValue(sName, out var s) || s == null)
{
throw new FrostFsException($"selector not found: {sName}");
}
var nodes = c.GetSelection(c.ProcessedSelectors[sName]);
result[i].AddRange(FlattenNodes(nodes));
foreach (var n in result[i])
{
c.UsedNodes[n.Hash()] = true;
}
}
else
{
var nodes = c.Selections[sName];
result[i].AddRange(FlattenNodes(nodes));
}
}
var collection = new FrostFsNodeInfo[result.Count][];
for (int i = 0; i < result.Count; i++)
{
collection[i] = [.. result[i]];
}
return collection;
}
}

View file

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using FrostFS.SDK.Client.Models.Netmap.Placement;
using FrostFS.SDK.Cryptography;
namespace FrostFS.SDK;
public class FrostFsNodeInfo(
FrostFsVersion version,
NodeState state,
IReadOnlyCollection<string> addresses,
IReadOnlyDictionary<string, string> attributes,
ReadOnlyMemory<byte> publicKey) : IHasher
{
private ulong _hash;
// attrPrice is a key to the node attribute that indicates the
// price in GAS tokens for storing one GB of data during one Epoch.
internal const string AttrPrice = "Price";
// attrCapacity is a key to the node attribute that indicates the
// total available disk space in Gigabytes.
internal const string AttrCapacity = "Capacity";
// attrExternalAddr is a key for the attribute storing node external addresses.
internal const string AttrExternalAddr = "ExternalAddr";
// sepExternalAddr is a separator for multi-value ExternalAddr attribute.
internal const string SepExternalAddr = ",";
private ulong price = ulong.MaxValue;
public NodeState State { get; } = state;
public FrostFsVersion Version { get; } = version;
public IReadOnlyCollection<string> Addresses { get; } = addresses;
public IReadOnlyDictionary<string, string> Attributes { get; } = attributes;
public ReadOnlyMemory<byte> PublicKey { get; } = publicKey;
public ulong Hash()
{
if (_hash == 0)
{
using var murmur3 = new Murmur3(0);
murmur3.Initialize();
_hash = murmur3.GetCheckSum64(PublicKey.ToArray());
}
return _hash;
}
internal ulong GetCapacity()
{
if (!Attributes.TryGetValue(AttrCapacity, out var val))
return 0;
return ulong.Parse(val, CultureInfo.InvariantCulture);
}
internal ulong Price
{
get
{
if (price == ulong.MaxValue)
{
if (!Attributes.TryGetValue(AttrPrice, out var val))
price = 0;
else
price = uint.Parse(val, CultureInfo.InvariantCulture);
}
return price;
}
}
}

View file

@ -1,24 +1,37 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using FrostFS.Netmap;
using FrostFS.SDK.ClientV2;
using FrostFS.SDK.Client;
namespace FrostFS.SDK;
public struct FrostFsPlacementPolicy(bool unique, params FrostFsReplica[] replicas)
public struct FrostFsPlacementPolicy(bool unique,
uint backupFactor,
Collection<FrostFsSelector> selectors,
Collection<FrostFsFilter> filters,
params FrostFsReplica[] replicas)
: IEquatable<FrostFsPlacementPolicy>
{
private PlacementPolicy policy;
public FrostFsReplica[] Replicas { get; private set; } = replicas;
public bool Unique { get; private set; } = unique;
public FrostFsReplica[] Replicas { get; } = replicas;
public Collection<FrostFsSelector> Selectors { get; } = selectors;
public Collection<FrostFsFilter> Filters { get; } = filters;
public bool Unique { get; } = unique;
public uint BackupFactor { get; } = backupFactor;
public override readonly bool Equals(object obj)
{
if (obj is null)
{
return false;
}
var other = (FrostFsPlacementPolicy)obj;
@ -46,14 +59,10 @@ public struct FrostFsPlacementPolicy(bool unique, params FrostFsReplica[] replic
return policy;
}
//public static FrostFsPlacementPolicy ToModel(placementPolicy)
//{
// return new FrostFsPlacementPolicy(
// placementPolicy.Unique,
// placementPolicy.Replicas.Select(replica => replica.ToModel()).ToArray()
// );
//}
internal readonly bool IsUnique()
{
return Unique || Replicas.Any(r => r.EcDataCount != 0 || r.EcParityCount != 0);
}
public override readonly int GetHashCode()
{

View file

@ -6,6 +6,8 @@ public struct FrostFsReplica : IEquatable<FrostFsReplica>
{
public int Count { get; set; }
public string Selector { get; set; }
public uint EcDataCount { get; set; }
public uint EcParityCount { get; set; }
public FrostFsReplica(int count, string? selector = null)
{
@ -18,16 +20,23 @@ public struct FrostFsReplica : IEquatable<FrostFsReplica>
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 readonly uint CountNodes()
{
return Count != 0 ? (uint)Count : EcDataCount + EcParityCount;
}
public override readonly int GetHashCode()
{
return Count + Selector.GetHashCode();
return (Count + Selector.GetHashCode()) ^ (int)EcDataCount ^ (int)EcParityCount;
}
public static bool operator ==(FrostFsReplica left, FrostFsReplica right)
@ -42,6 +51,9 @@ public struct FrostFsReplica : IEquatable<FrostFsReplica>
public readonly bool Equals(FrostFsReplica other)
{
return Count == other.Count && Selector == other.Selector;
return Count == other.Count
&& Selector == other.Selector
&& EcDataCount == other.EcDataCount
&& EcParityCount == other.EcParityCount;
}
}

View file

@ -0,0 +1,10 @@
namespace FrostFS.SDK;
public class FrostFsSelector(string name)
{
public string Name { get; set; } = name;
public uint Count { get; set; }
public int Clause { get; set; }
public string? Attribute { get; set; }
public string? Filter { get; set; }
}

View file

@ -1,5 +1,5 @@
using FrostFS.Refs;
using FrostFS.SDK.ClientV2.Mappers.GRPC;
using FrostFS.SDK.Client.Mappers.GRPC;
namespace FrostFS.SDK;
@ -10,7 +10,7 @@ public class FrostFsVersion(int major, int minor)
public int Major { get; set; } = major;
public int Minor { get; set; } = minor;
internal Version Version
internal Version VersionID
{
get
{

View file

@ -0,0 +1,11 @@
namespace FrostFS.SDK
{
public interface IFrostFsFilter
{
FrostFsFilter[] Filters { get; }
string Key { get; }
string Name { get; }
int Operation { get; }
string Value { get; }
}
}

View file

@ -0,0 +1,7 @@
namespace FrostFS.SDK;
struct NodeAttrPair
{
internal string attr;
internal FrostFsNodeInfo[] nodes;
}

View file

@ -0,0 +1,8 @@
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
public enum FrostFsClause
{
Unspecified = 0,
Same,
Distinct
}

View file

@ -0,0 +1,456 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
internal struct Context
{
private const string errInvalidFilterName = "filter name is invalid";
private const string errInvalidFilterOp = "invalid filter operation";
private const string errFilterNotFound = "filter not found";
private const string errNonEmptyFilters = "simple filter contains sub-filters";
private const string errNotEnoughNodes = "not enough nodes to SELECT from";
private const string errUnnamedTopFilter = "unnamed top-level filter";
internal const string mainFilterName = "*";
internal const string likeWildcard = "*";
// network map to operate on
internal FrostFsNetmapSnapshot NetMap { get; }
// cache of processed filters
internal Dictionary<string, FrostFsFilter> ProcessedFilters { get; } = [];
// cache of processed selectors
internal Dictionary<string, FrostFsSelector> ProcessedSelectors { get; } = [];
// stores results of selector processing
internal Dictionary<string, List<List<FrostFsNodeInfo>>> Selections { get; } = [];
// cache of parsed numeric values
internal Dictionary<string, ulong> NumCache { get; } = [];
internal byte[]? HrwSeed { get; set; }
// hrw.Hash of hrwSeed
internal ulong HrwSeedHash { get; set; }
// container backup factor
internal uint Cbf { get; set; }
// nodes already used in previous selections, which is needed when the placement
// policy uses the UNIQUE flag. Nodes marked as used are not used in subsequent
// base selections.
internal Dictionary<ulong, bool> UsedNodes { get; } = [];
// If true, returns an error when netmap does not contain enough nodes for selection.
// By default best effort is taken.
internal bool Strict { get; set; }
// weightFunc is a weighting function for determining node priority
// which combines low price and high performance
private readonly Func<FrostFsNodeInfo, double> weightFunc;
public Context(FrostFsNetmapSnapshot netMap)
{
NetMap = netMap;
weightFunc = Tools.DefaultWeightFunc(NetMap.NodeInfoCollection);
}
internal void ProcessFilters(FrostFsPlacementPolicy policy)
{
foreach (var filter in policy.Filters)
{
ProcessFilter(filter, true);
}
}
readonly void ProcessFilter(FrostFsFilter filter, bool top)
{
var filterName = filter.Name;
if (filterName == mainFilterName)
{
throw new FrostFsException($"{errInvalidFilterName}: '{errInvalidFilterName}' is reserved");
}
if (top && string.IsNullOrEmpty(filterName))
{
throw new FrostFsException(errUnnamedTopFilter);
}
if (!top && !string.IsNullOrEmpty(filterName) && !ProcessedFilters.ContainsKey(filterName))
{
throw new FrostFsException(errFilterNotFound);
}
if (filter.Operation == (int)Operation.AND ||
filter.Operation == (int)Operation.OR ||
filter.Operation == (int)Operation.NOT)
{
foreach (var f in filter.Filters)
ProcessFilter(f, false);
}
else
{
if (filter.Filters.Length != 0)
{
throw new FrostFsException(errNonEmptyFilters);
}
else if (!top && !string.IsNullOrEmpty(filterName))
{
// named reference
return;
}
switch (filter.Operation)
{
case (int)Operation.EQ:
case (int)Operation.NE:
case (int)Operation.LIKE:
break;
case (int)Operation.GT:
case (int)Operation.GE:
case (int)Operation.LT:
case (int)Operation.LE:
{
var n = uint.Parse(filter.Value, CultureInfo.InvariantCulture);
NumCache[filter.Value] = n;
break;
}
default:
throw new FrostFsException($"{errInvalidFilterOp}: {filter.Operation}");
}
}
if (top)
{
ProcessedFilters[filterName] = filter;
}
}
// processSelectors processes selectors and returns error is any of them is invalid.
internal void ProcessSelectors(FrostFsPlacementPolicy policy)
{
foreach (var selector in policy.Selectors)
{
var filterName = selector.Filter;
if (filterName != mainFilterName)
{
if (selector.Filter == null || !ProcessedFilters.ContainsKey(selector.Filter))
{
throw new FrostFsException($"{errFilterNotFound}: SELECT FROM '{filterName}'");
}
}
ProcessedSelectors[selector.Name] = selector;
var selection = GetSelection(selector);
Selections[selector.Name] = selection;
}
}
// calcNodesCount returns number of buckets and minimum number of nodes in every bucket
// for the given selector.
static (int bucketCount, int nodesInBucket) CalcNodesCount(FrostFsSelector selector)
{
return selector.Clause == (int)FrostFsClause.Same
? (1, (int)selector.Count)
: ((int)selector.Count, 1);
}
// getSelectionBase returns nodes grouped by selector attribute.
// It it guaranteed that each pair will contain at least one node.
internal NodeAttrPair[] GetSelectionBase(FrostFsSelector selector)
{
var fName = selector.Filter ?? throw new FrostFsException("Filter name for selector is empty");
_ = ProcessedFilters.TryGetValue(fName, out var f);
var isMain = fName == mainFilterName;
var result = new List<NodeAttrPair>();
var nodeMap = new Dictionary<string, List<FrostFsNodeInfo>>();
var attr = selector.Attribute;
foreach (var node in NetMap.NodeInfoCollection)
{
if (UsedNodes.ContainsKey(node.Hash()))
{
continue;
}
if (isMain || Match(f, node))
{
if (attr == null)
{
// Default attribute is transparent identifier which is different for every node.
result.Add(new NodeAttrPair { attr = "", nodes = [node] });
}
else
{
var v = node.Attributes[attr];
if (!nodeMap.TryGetValue(v, out var nodes) || nodes == null)
{
nodeMap[v] = [];
}
nodeMap[v].Add(node);
}
}
}
if (!string.IsNullOrEmpty(attr))
{
foreach (var v in nodeMap)
{
result.Add(new NodeAttrPair() { attr = v.Key, nodes = [.. v.Value] });
}
}
if (HrwSeed != null && HrwSeed.Length != 0)
{
double[] ws = [];
var sortedNodes = new NodeAttrPair[result.Count];
for (int i = 0; i < result.Count; i++)
{
var res = result[i];
Tools.AppendWeightsTo(res.nodes, weightFunc, ref ws);
sortedNodes[i].nodes = Tools.SortHasherSliceByWeightValue(res.nodes.ToList(), ws, HrwSeedHash).ToArray();
sortedNodes[i].attr = result[i].attr;
}
return sortedNodes;
}
return [.. result];
}
static double CalcBucketWeight(List<FrostFsNodeInfo> ns, IAggregator a, Func<FrostFsNodeInfo, double> wf)
{
foreach (var node in ns)
{
a.Add(wf(node));
}
return a.Compute();
}
// getSelection returns nodes grouped by s.attribute.
// Last argument specifies if more buckets can be used to fulfill CBF.
internal List<List<FrostFsNodeInfo>> GetSelection(FrostFsSelector s)
{
var (bucketCount, nodesInBucket) = CalcNodesCount(s);
var buckets = GetSelectionBase(s);
if (Strict && buckets.Length < bucketCount)
throw new FrostFsException($"errNotEnoughNodes: '{s.Name}'");
// We need deterministic output in case there is no pivot.
// If pivot is set, buckets are sorted by HRW.
// However, because initial order influences HRW order for buckets with equal weights,
// we also need to have deterministic input to HRW sorting routine.
if (HrwSeed == null || HrwSeed.Length == 0)
{
buckets = string.IsNullOrEmpty(s.Attribute)
? [.. buckets.OrderBy(b => b.nodes[0].Hash())]
: [.. buckets.OrderBy(b => b.attr)];
}
var maxNodesInBucket = nodesInBucket * (int)Cbf;
var res = new List<List<FrostFsNodeInfo>>(buckets.Length);
var fallback = new List<List<FrostFsNodeInfo>>(buckets.Length);
for (int i = 0; i < buckets.Length; i++)
{
var ns = buckets[i].nodes;
if (ns.Length >= maxNodesInBucket)
{
res.Add(new List<FrostFsNodeInfo>(ns[..maxNodesInBucket]));
}
else if (ns.Length >= nodesInBucket)
{
fallback.Add(new List<FrostFsNodeInfo>(ns));
}
}
if (res.Count < bucketCount)
{
// Fallback to using minimum allowed backup factor (1).
res.AddRange(fallback);
if (Strict && res.Count < bucketCount)
{
throw new FrostFsException($"{errNotEnoughNodes}: {s}");
}
}
if (HrwSeed != null && HrwSeed.Length != 0)
{
var weights = new double[res.Count];
var a = new MeanIQRAgg();
for (int i = 0; i < res.Count; i++)
{
a.Clear();
weights[i] = CalcBucketWeight(res[i], a, weightFunc);
}
var hashers = res.Select(r => new HasherList(r)).ToList();
hashers = Tools.SortHasherSliceByWeightValue(hashers, weights, HrwSeedHash);
for (int i = 0; i < res.Count; i++)
{
res[i] = hashers[i].Nodes;
}
}
if (res.Count < bucketCount)
{
if (Strict && res.Count == 0)
{
throw new FrostFsException(errNotEnoughNodes);
}
bucketCount = res.Count;
}
if (string.IsNullOrEmpty(s.Attribute))
{
fallback = res.Skip(bucketCount).ToList();
res = res.Take(bucketCount).ToList();
for (int i = 0; i < fallback.Count; i++)
{
var index = i % bucketCount;
if (res[index].Count >= maxNodesInBucket)
{
break;
}
res[index].AddRange(fallback[i]);
}
}
return res.Take(bucketCount).ToList();
}
internal bool MatchKeyValue(FrostFsFilter f, FrostFsNodeInfo nodeInfo)
{
switch (f.Operation)
{
case (int)Operation.EQ:
return nodeInfo.Attributes.TryGetValue(f.Key, out var val) && val == f.Value;
case (int)Operation.LIKE:
{
var hasPrefix = f.Value.StartsWith(likeWildcard, StringComparison.Ordinal);
var hasSuffix = f.Value.EndsWith(likeWildcard, StringComparison.Ordinal);
var start = hasPrefix ? likeWildcard.Length : 0;
var end = hasSuffix ? f.Value.Length - likeWildcard.Length : f.Value.Length;
var str = f.Value[start..end];
if (hasPrefix && hasSuffix)
return nodeInfo.Attributes[f.Key].Contains(str);
if (hasPrefix && !hasSuffix)
return nodeInfo.Attributes[f.Key].EndsWith(str, StringComparison.Ordinal);
if (!hasPrefix && hasSuffix)
return nodeInfo.Attributes[f.Key].StartsWith(str, StringComparison.Ordinal);
return nodeInfo.Attributes[f.Key] == f.Value;
}
case (int)Operation.NE:
return nodeInfo.Attributes[f.Key] != f.Value;
default:
{
ulong attr;
switch (f.Key)
{
case FrostFsNodeInfo.AttrPrice:
attr = nodeInfo.Price;
break;
case FrostFsNodeInfo.AttrCapacity:
attr = nodeInfo.GetCapacity();
break;
default:
if (!ulong.TryParse(nodeInfo.Attributes[f.Key], NumberStyles.Integer, CultureInfo.InvariantCulture, out attr))
return false;
break;
}
switch (f.Operation)
{
case (int)Operation.GT:
return attr > NumCache[f.Value];
case (int)Operation.GE:
return attr >= NumCache[f.Value];
case (int)Operation.LT:
return attr < NumCache[f.Value];
case (int)Operation.LE:
return attr <= NumCache[f.Value];
default:
// do nothing and return false
break;
}
}
break;
}
// will not happen if context was created from f (maybe panic?)
return false;
}
// match matches f against b. It returns no errors because
// filter should have been parsed during context creation
// and missing node properties are considered as a regular fail.
internal bool Match(FrostFsFilter f, FrostFsNodeInfo nodeInfo)
{
switch (f.Operation)
{
case (int)Operation.NOT:
{
var inner = f.Filters;
var fSub = inner[0];
if (!string.IsNullOrEmpty(inner[0].Name))
{
fSub = ProcessedFilters[inner[0].Name];
}
return !Match(fSub, nodeInfo);
}
case (int)Operation.AND:
case (int)Operation.OR:
{
for (int i = 0; i < f.Filters.Length; i++)
{
var fSub = f.Filters[i];
if (!string.IsNullOrEmpty(f.Filters[i].Name))
{
fSub = ProcessedFilters[f.Filters[i].Name];
}
var ok = Match(fSub, nodeInfo);
if (ok == (f.Operation == (int)Operation.OR))
{
return ok;
}
}
return f.Operation == (int)Operation.AND;
}
default:
return MatchKeyValue(f, nodeInfo);
}
}
}

View file

@ -0,0 +1,26 @@
using System.Collections.Generic;
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
internal sealed class HasherList : IHasher
{
private readonly List<FrostFsNodeInfo> _nodes;
internal HasherList(List<FrostFsNodeInfo> nodes)
{
_nodes = nodes;
}
internal List<FrostFsNodeInfo> Nodes
{
get
{
return _nodes;
}
}
public ulong Hash()
{
return _nodes.Count > 0 ? _nodes[0].Hash() : 0;
}
}

View file

@ -0,0 +1,8 @@
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
internal interface IAggregator
{
void Add(double d);
double Compute();
}

View file

@ -0,0 +1,6 @@
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
internal interface IHasher
{
ulong Hash();
}

View file

@ -0,0 +1,6 @@
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
interface INormalizer
{
double Normalize(double w);
}

View file

@ -0,0 +1,20 @@
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
internal struct MeanAgg
{
private double mean;
private int count;
internal void Add(double n)
{
int c = count + 1;
mean = mean * count / c + n / c;
count++;
}
internal readonly double Compute()
{
return mean;
}
}

View file

@ -0,0 +1,65 @@
using System.Collections.ObjectModel;
using System.Linq;
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
internal struct MeanIQRAgg : IAggregator
{
private const int minLn = 4;
internal Collection<double> arr = [];
public MeanIQRAgg()
{
}
public readonly void Add(double d)
{
arr.Add(d);
}
public readonly double Compute()
{
var length = arr.Count;
if (length == 0)
{
return 0;
}
var sorted = arr.OrderBy(p => p).ToArray();
double minV, maxV;
if (arr.Count < minLn)
{
minV = sorted[0];
maxV = sorted[length - 1];
}
else
{
var start = length / minLn;
var end = length * 3 / minLn - 1;
minV = sorted[start];
maxV = sorted[end];
}
var count = 0;
double sum = 0;
foreach (var e in sorted)
{
if (e >= minV && e <= maxV)
{
sum += e;
count++;
}
}
return sum / count;
}
internal readonly void Clear()
{
arr.Clear();
}
}

View file

@ -0,0 +1,27 @@
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
internal struct MinAgg
{
private double min;
private bool minFound;
internal void Add(double n)
{
if (!minFound)
{
min = n;
minFound = true;
return;
}
if (n < min)
{
min = n;
}
}
internal readonly double Compute()
{
return min;
}
}

View file

@ -0,0 +1,16 @@
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
public enum Operation
{
Unspecified = 0,
EQ,
NE,
GT,
GE,
LT,
LE,
OR,
AND,
NOT,
LIKE
}

View file

@ -0,0 +1,8 @@
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
internal struct ReverseMinNorm : INormalizer
{
internal double min;
public readonly double Normalize(double w) => (min + 1) / (w + 1);
}

View file

@ -0,0 +1,10 @@
using System.Collections.ObjectModel;
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
internal struct SelectFilterExpr(uint cbf, FrostFsSelector selector, Collection<FrostFsFilter> filters)
{
internal uint Cbf { get; } = cbf;
internal FrostFsSelector Selector { get; } = selector;
internal Collection<FrostFsFilter> Filters { get; } = filters;
}

View file

@ -0,0 +1,23 @@
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
internal readonly struct SigmoidNorm : INormalizer
{
private readonly double _scale;
internal SigmoidNorm(double scale)
{
_scale = scale;
}
public readonly double Normalize(double w)
{
if (_scale == 0)
{
return 0;
}
var x = w / _scale;
return x / (1 + x);
}
}

View file

@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Linq;
using static FrostFS.SDK.FrostFsNetmapSnapshot;
namespace FrostFS.SDK.Client.Models.Netmap.Placement;
public static class Tools
{
internal static ulong Distance(ulong x, ulong y)
{
var acc = x ^ y;
acc ^= acc >> 33;
acc *= 0xff51afd7ed558ccd;
acc ^= acc >> 33;
acc *= 0xc4ceb9fe1a85ec53;
acc ^= acc >> 33;
return acc;
}
internal static double ReverceNormalize(double r, double w)
{
return (r + 1) / (w + 1);
}
internal static double Normalize(double r, double w)
{
if (r == 0)
{
return 0;
}
var x = w / r;
return x / (1 + x);
}
internal static void AppendWeightsTo(FrostFsNodeInfo[] nodes, Func<FrostFsNodeInfo, double> wf, ref double[] weights)
{
if (weights.Length < nodes.Length)
{
weights = new double[nodes.Length];
}
for (int i = 0; i < nodes.Length; i++)
{
weights[i] = wf(nodes[i]);
}
}
internal static List<T> SortHasherSliceByWeightValue<T>(List<T> nodes, Span<double> weights, ulong hash) where T : IHasher
{
if (nodes.Count == 0)
{
return nodes;
}
var allEquals = true;
if (weights.Length > 1)
{
for (int i = 1; i < weights.Length; i++)
{
if (weights[i] != weights[0])
{
allEquals = false;
break;
}
}
}
var dist = new double[nodes.Count];
if (allEquals)
{
for (int i = 0; i < dist.Length; i++)
{
var x = nodes[i].Hash();
dist[i] = Distance(x, hash);
}
return SortHasherByDistance(nodes, dist, true);
}
for (int i = 0; i < dist.Length; i++)
{
var d = Distance(nodes[i].Hash(), hash);
dist[i] = (ulong.MaxValue - d) * weights[i];
}
return SortHasherByDistance(nodes, dist, false);
}
internal static List<T> SortHasherByDistance<T, N>(List<T> nodes, N[] dist, bool asc)
{
IndexedValue<T, N>[] indexes = new IndexedValue<T, N>[nodes.Count];
for (int i = 0; i < dist.Length; i++)
{
indexes[i] = new IndexedValue<T, N>() { nodeInfo = nodes[i], dist = dist[i] };
}
if (asc)
{
return new List<T>(indexes
.OrderBy(x => x.dist)
.Select(x => x.nodeInfo).ToArray());
}
else
{
return new List<T>(indexes
.OrderByDescending(x => x.dist)
.Select(x => x.nodeInfo));
}
}
internal static Func<FrostFsNodeInfo, double> DefaultWeightFunc(IReadOnlyList<FrostFsNodeInfo> nodes)
{
MeanAgg mean = new();
MinAgg minV = new();
foreach (var node in nodes)
{
mean.Add(node.GetCapacity());
minV.Add(node.Price);
}
return NewWeightFunc(
NewSigmoidNorm(mean.Compute()),
NewReverseMinNorm(minV.Compute()));
}
private struct IndexedValue<T, N>
{
internal T nodeInfo;
internal N dist;
}
}

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 FrostFsHeaderResult
{
public FrostFsObjectHeader? HeaderInfo { get; internal set; }
public FrostFsSplitInfo? SplitInfo { get; internal set; }
}

View file

@ -4,7 +4,7 @@ using System.Linq;
using FrostFS.Object;
using FrostFS.SDK.ClientV2.Mappers.GRPC;
using FrostFS.SDK.Client.Mappers.GRPC;
namespace FrostFS.SDK;

View file

@ -8,13 +8,8 @@ public class FrostFsObjectId(string id)
{
public string Value { get; } = id;
public static FrostFsObjectId FromHash(byte[] hash)
public static FrostFsObjectId FromHash(ReadOnlySpan<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.");

View file

@ -1,9 +1,10 @@
using System.Security.Cryptography;
using FrostFS.Refs;
using FrostFS.SDK.ClientV2.Mappers.GRPC;
using FrostFS.SDK.Client.Mappers.GRPC;
using FrostFS.SDK.Cryptography;
namespace FrostFS.SDK;
public class FrostFsOwner(string id)

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

@ -24,5 +24,4 @@ public class FrostFsSplit(SplitId splitId,
public FrostFsObjectHeader? ParentHeader { get; set; } = parentHeader;
public ReadOnlyCollection<FrostFsObjectId>? Children { get; } = children;
}

View file

@ -0,0 +1,28 @@
using FrostFS.Object;
using FrostFS.SDK.Cryptography;
namespace FrostFS.SDK;
public class FrostFsSplitInfo
{
private readonly SplitInfo _splitInfo;
private SplitId? _splitId;
private FrostFsObjectId? _link;
private FrostFsObjectId? _lastPart;
internal FrostFsSplitInfo(SplitInfo splitInfo)
{
_splitInfo = splitInfo;
}
public SplitId SplitId => _splitId ??= new SplitId(_splitInfo.SplitId.ToUuid());
public FrostFsObjectId? Link => _link ??= _splitInfo.Link == null
? null : FrostFsObjectId.FromHash(_splitInfo.Link.Value.Span);
public FrostFsObjectId? LastPart => _lastPart ??= _splitInfo.LastPart == null
? null : FrostFsObjectId.FromHash(_splitInfo.LastPart.Value.Span);
}

View file

@ -6,5 +6,5 @@ namespace FrostFS.SDK;
public interface IObjectReader : IDisposable
{
Task<ReadOnlyMemory<byte>?> ReadChunk(CancellationToken cancellationToken = default);
ValueTask<ReadOnlyMemory<byte>?> ReadChunk(CancellationToken cancellationToken = default);
}

View file

@ -0,0 +1,117 @@
using System;
using FrostFS.Refs;
using FrostFS.SDK.Client;
using FrostFS.SDK.Cryptography;
using FrostFS.Session;
using Google.Protobuf;
namespace FrostFS.SDK;
public class FrostFsSessionToken
{
private Guid _id;
private ReadOnlyMemory<byte> _sessionKey;
private readonly SessionToken.Types.Body _body;
private FrostFsSessionToken()
{
ProtoId = ByteString.Empty;
ProtoSessionKey = ByteString.Empty;
_body = new SessionToken.Types.Body();
}
internal FrostFsSessionToken(SessionToken token)
{
ProtoId = token.Body.Id;
ProtoSessionKey = token.Body.SessionKey;
_body = token.Body;
}
public Guid Id
{
get
{
if (_id == Guid.Empty)
_id = ProtoId.ToUuid();
return _id;
}
}
public ReadOnlyMemory<byte> SessionKey
{
get
{
if (_sessionKey.IsEmpty)
_sessionKey = ProtoSessionKey.Memory;
return _sessionKey;
}
}
internal ByteString ProtoId { get; }
internal ByteString ProtoSessionKey { get; }
public SessionToken CreateContainerToken(ContainerID? containerId, ContainerSessionContext.Types.Verb verb, ClientKey key)
{
if (key is null)
{
throw new ArgumentNullException(nameof(key));
}
SessionToken sessionToken = new() { Body = _body.Clone() };
sessionToken.Body.Container = new() { Verb = verb };
if (containerId != null)
sessionToken.Body.Container.ContainerId = containerId;
else
sessionToken.Body.Container.Wildcard = true;
sessionToken.Body.SessionKey = key.PublicKeyProto;
sessionToken.Signature = key.ECDsaKey.SignMessagePart(sessionToken.Body);
return sessionToken;
}
public SessionToken CreateObjectTokenContext(Address address, ObjectSessionContext.Types.Verb verb, ClientKey key)
{
if (address is null)
{
throw new ArgumentNullException(nameof(address));
}
if (key is null)
{
throw new ArgumentNullException(nameof(key));
}
SessionToken sessionToken = new()
{
Body = _body.Clone()
};
ObjectSessionContext.Types.Target target = new() { Container = address.ContainerId };
if (address.ObjectId != null)
target.Objects.Add(address.ObjectId);
sessionToken.Body.Object = new()
{
Target = target,
Verb = verb
};
sessionToken.Body.SessionKey = key.PublicKeyProto;
sessionToken.Signature = key.ECDsaKey.SignMessagePart(sessionToken.Body);
return sessionToken;
}
}

View file

@ -0,0 +1,68 @@
using System;
using System.Threading.Tasks;
using FrostFS.Object;
using FrostFS.SDK.Client.Interfaces;
using Google.Protobuf;
namespace FrostFS.SDK.Client
{
internal sealed class ObjectWriter : IObjectWriter
{
private readonly ClientContext ctx;
private readonly PrmObjectPutBase args;
private readonly ObjectStreamer<PutRequest, PutResponse> streamer;
private bool disposedValue;
internal ObjectWriter(ClientContext ctx, PrmObjectPutBase args, ObjectStreamer<PutRequest, PutResponse> streamer)
{
this.ctx = ctx;
this.args = args;
this.streamer = streamer;
}
public async Task WriteAsync(ReadOnlyMemory<byte> memory)
{
var chunkRequest = new PutRequest
{
Body = new PutRequest.Types.Body
{
Chunk = UnsafeByteOperations.UnsafeWrap(memory)
}
};
chunkRequest.Sign(this.ctx.Key.ECDsaKey);
await streamer.Write(chunkRequest).ConfigureAwait(false);
}
public async Task<FrostFsObjectId> CompleteAsync()
{
var response = await streamer.Close().ConfigureAwait(false);
Verifier.CheckResponse(response);
return FrostFsObjectId.FromHash(response.Body.ObjectId.Value.Span);
}
private void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
streamer.Dispose();
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View file

@ -0,0 +1,44 @@
using System;
using System.Threading;
namespace FrostFS.SDK.Client;
public readonly struct CallContext(TimeSpan timeout, CancellationToken cancellationToken = default) : IEquatable<CallContext>
{
public CancellationToken CancellationToken { get; } = cancellationToken;
public TimeSpan Timeout { get; } = timeout;
internal readonly DateTime? GetDeadline()
{
return Timeout.Ticks > 0 ? DateTime.UtcNow.Add(Timeout) : null;
}
public override readonly bool Equals(object obj)
{
if (obj == null || obj is not CallContext)
return false;
return Equals((CallContext)obj);
}
public bool Equals(CallContext other)
{
return Timeout == other.Timeout && CancellationToken.Equals(other.CancellationToken);
}
public override int GetHashCode()
{
return CancellationToken.GetHashCode() ^ Timeout.GetHashCode();
}
public static bool operator ==(CallContext left, CallContext right)
{
return left.Equals(right);
}
public static bool operator !=(CallContext left, CallContext right)
{
return !(left == right);
}
}

View file

@ -1,4 +1,4 @@
namespace FrostFS.SDK.ClientV2;
namespace FrostFS.SDK.Client;
public interface ISessionToken
{
@ -8,5 +8,5 @@ public interface ISessionToken
/// 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; }
FrostFsSessionToken? SessionToken { get; }
}

View file

@ -0,0 +1,43 @@
namespace FrostFS.SDK.Client;
public readonly struct PrmApeChainAdd(FrostFsChainTarget target, FrostFsChain chain, string[]? xheaders = null) : System.IEquatable<PrmApeChainAdd>
{
public FrostFsChainTarget Target { get; } = target;
public FrostFsChain Chain { get; } = chain;
/// <summary>
/// FrostFS request X-Headers
/// </summary>
public string[] XHeaders { get; } = xheaders ?? [];
public override readonly bool Equals(object obj)
{
if (obj == null || obj is not PrmApeChainAdd)
return false;
return Equals((PrmApeChainAdd)obj);
}
public readonly bool Equals(PrmApeChainAdd other)
{
return Target == other.Target
&& Chain == other.Chain
&& XHeaders == other.XHeaders;
}
public override readonly int GetHashCode()
{
return Chain.GetHashCode() ^ Target.GetHashCode() ^ XHeaders.GetHashCode();
}
public static bool operator ==(PrmApeChainAdd left, PrmApeChainAdd right)
{
return left.Equals(right);
}
public static bool operator !=(PrmApeChainAdd left, PrmApeChainAdd right)
{
return !(left == right);
}
}

View file

@ -0,0 +1,40 @@
namespace FrostFS.SDK.Client;
public readonly struct PrmApeChainList(FrostFsChainTarget target, string[]? xheaders = null) : System.IEquatable<PrmApeChainList>
{
public FrostFsChainTarget Target { get; } = target;
/// <summary>
/// FrostFS request X-Headers
/// </summary>
public string[] XHeaders { get; } = xheaders ?? [];
public override readonly bool Equals(object obj)
{
if (obj == null || obj is not PrmApeChainList)
return false;
return Equals((PrmApeChainList)obj);
}
public override readonly int GetHashCode()
{
return Target.GetHashCode() ^ XHeaders.GetHashCode();
}
public readonly bool Equals(PrmApeChainList other)
{
return Target == other.Target
&& XHeaders == other.XHeaders;
}
public static bool operator ==(PrmApeChainList left, PrmApeChainList right)
{
return left.Equals(right);
}
public static bool operator !=(PrmApeChainList left, PrmApeChainList right)
{
return !(left == right);
}
}

View file

@ -0,0 +1,46 @@
namespace FrostFS.SDK.Client;
public readonly struct PrmApeChainRemove(
FrostFsChainTarget target,
FrostFsChain chain,
string[]? xheaders = null) : System.IEquatable<PrmApeChainRemove>
{
public FrostFsChainTarget Target { get; } = target;
public FrostFsChain Chain { get; } = chain;
/// <summary>
/// FrostFS request X-Headers
/// </summary>
public string[] XHeaders { get; } = xheaders ?? [];
public override readonly bool Equals(object obj)
{
if (obj == null || obj is not PrmApeChainRemove)
return false;
return Equals((PrmApeChainRemove)obj);
}
public readonly bool Equals(PrmApeChainRemove other)
{
return Target == other.Target
&& Chain == other.Chain
&& XHeaders == other.XHeaders;
}
public override readonly int GetHashCode()
{
return Chain.GetHashCode() ^ Target.GetHashCode() ^ XHeaders.GetHashCode();
}
public static bool operator ==(PrmApeChainRemove left, PrmApeChainRemove right)
{
return left.Equals(right);
}
public static bool operator !=(PrmApeChainRemove left, PrmApeChainRemove right)
{
return !(left == right);
}
}

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