From 568bdc67e872ff5244154ad08861fa7b819f62a1 Mon Sep 17 00:00:00 2001
From: Pavel Gross
Date: Tue, 24 Dec 2024 17:32:29 +0300
Subject: [PATCH] [#29] Client: Add object placement methods
Signed-off-by: Pavel Gross
---
.../Models/Netmap/FrostFsFilter.cs | 10 +
.../Models/Netmap/FrostFsNetmapSnapshot.cs | 257 ++++++++++-
.../Models/Netmap/FrostFsNodeInfo.cs | 76 ++-
.../Models/Netmap/FrostFsPlacementPolicy.cs | 25 +-
.../Models/Netmap/FrostFsReplica.cs | 18 +-
.../Models/Netmap/FrostFsSelector.cs | 10 +
.../Models/Netmap/IFrostFsFilter.cs | 11 +
.../Models/Netmap/NodeAttrPair.cs | 7 +
.../Models/Netmap/Placement/Clause.cs | 8 +
.../Models/Netmap/Placement/Context.cs | 433 ++++++++++++++++++
.../Models/Netmap/Placement/HasherList.cs | 18 +
.../Models/Netmap/Placement/IAggregator.cs | 8 +
.../Models/Netmap/Placement/IHasher.cs | 6 +
.../Models/Netmap/Placement/INormalizer.cs | 6 +
.../Models/Netmap/Placement/MeanAgg.cs | 20 +
.../Models/Netmap/Placement/MeanIQRAgg.cs | 65 +++
.../Models/Netmap/Placement/MinAgg.cs | 27 ++
.../Models/Netmap/Placement/Operation.cs | 16 +
.../Models/Netmap/Placement/ReverseMinNorm.cs | 8 +
.../Netmap/Placement/SelectFilterExpr.cs | 10 +
.../Models/Netmap/Placement/SigmoidNorm.cs | 23 +
.../Models/Netmap/Placement/Tools.cs | 140 ++++++
src/FrostFS.SDK.Client/Tools/ObjectTools.cs | 23 +-
src/FrostFS.SDK.Cryptography/Murmur3_128.cs | 28 +-
.../Unit/PlacementVectorTests.cs | 161 +++++++
25 files changed, 1382 insertions(+), 32 deletions(-)
create mode 100644 src/FrostFS.SDK.Client/Models/Netmap/FrostFsFilter.cs
create mode 100644 src/FrostFS.SDK.Client/Models/Netmap/FrostFsSelector.cs
create mode 100644 src/FrostFS.SDK.Client/Models/Netmap/IFrostFsFilter.cs
create mode 100644 src/FrostFS.SDK.Client/Models/Netmap/NodeAttrPair.cs
create mode 100644 src/FrostFS.SDK.Client/Models/Netmap/Placement/Clause.cs
create mode 100644 src/FrostFS.SDK.Client/Models/Netmap/Placement/Context.cs
create mode 100644 src/FrostFS.SDK.Client/Models/Netmap/Placement/HasherList.cs
create mode 100644 src/FrostFS.SDK.Client/Models/Netmap/Placement/IAggregator.cs
create mode 100644 src/FrostFS.SDK.Client/Models/Netmap/Placement/IHasher.cs
create mode 100644 src/FrostFS.SDK.Client/Models/Netmap/Placement/INormalizer.cs
create mode 100644 src/FrostFS.SDK.Client/Models/Netmap/Placement/MeanAgg.cs
create mode 100644 src/FrostFS.SDK.Client/Models/Netmap/Placement/MeanIQRAgg.cs
create mode 100644 src/FrostFS.SDK.Client/Models/Netmap/Placement/MinAgg.cs
create mode 100644 src/FrostFS.SDK.Client/Models/Netmap/Placement/Operation.cs
create mode 100644 src/FrostFS.SDK.Client/Models/Netmap/Placement/ReverseMinNorm.cs
create mode 100644 src/FrostFS.SDK.Client/Models/Netmap/Placement/SelectFilterExpr.cs
create mode 100644 src/FrostFS.SDK.Client/Models/Netmap/Placement/SigmoidNorm.cs
create mode 100644 src/FrostFS.SDK.Client/Models/Netmap/Placement/Tools.cs
create mode 100644 src/FrostFS.SDK.Tests/Unit/PlacementVectorTests.cs
diff --git a/src/FrostFS.SDK.Client/Models/Netmap/FrostFsFilter.cs b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsFilter.cs
new file mode 100644
index 0000000..ebd3c20
--- /dev/null
+++ b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsFilter.cs
@@ -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;
+}
\ No newline at end of file
diff --git a/src/FrostFS.SDK.Client/Models/Netmap/FrostFsNetmapSnapshot.cs b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsNetmapSnapshot.cs
index 3033ec6..686c966 100644
--- a/src/FrostFS.SDK.Client/Models/Netmap/FrostFsNetmapSnapshot.cs
+++ b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsNetmapSnapshot.cs
@@ -1,4 +1,11 @@
+using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+
+using FrostFS.SDK.Client;
+using FrostFS.SDK.Client.Models.Netmap.Placement;
+using FrostFS.SDK.Cryptography;
namespace FrostFS.SDK;
@@ -7,4 +14,252 @@ public class FrostFsNetmapSnapshot(ulong epoch, IReadOnlyList n
public ulong Epoch { get; private set; } = epoch;
public IReadOnlyList NodeInfoCollection { get; private set; } = nodeInfoCollection;
-}
\ No newline at end of file
+
+ 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, spanWeigths);
+
+ Tools.SortHasherSliceByWeightValue(result[i].ToList(), spanWeigths, hash);
+ }
+
+ 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> SelectFilterNodes(SelectFilterExpr expr)
+ {
+ var policy = new FrostFsPlacementPolicy(false);
+
+ foreach (var f in expr.Filters)
+ policy.Filters.Add(f);
+
+ policy.Selectors.Add(expr.Selector);
+
+ var ctx = new Context(this)
+ {
+ Cbf = expr.Cbf
+ };
+
+ ctx.ProcessFilters(policy);
+ ctx.ProcessSelectors(policy);
+
+ var ret = new List>();
+
+ if (expr.Selector == null)
+ {
+ var lastFilter = expr.Filters[^1];
+
+ var subCollestion = new List();
+ 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();
+ ret.Add(subCollestion);
+ foreach (var n in ns)
+ {
+ subCollestion.Add(n);
+ }
+ }
+ }
+
+ return ret;
+ }
+
+ internal static Func NewWeightFunc(INormalizer capNorm, INormalizer priceNorm)
+ {
+ return new Func((FrostFsNodeInfo nodeInfo) =>
+ {
+ return capNorm.Normalize(nodeInfo.GetCapacity()) * priceNorm.Normalize(nodeInfo.Price);
+ });
+ }
+
+ private static FrostFsNodeInfo[] FlattenNodes(List> 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
+ };
+
+ 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>(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(Context.mainFilterName)
+ {
+ Count = p.Replicas[i].CountNodes()
+ };
+
+ var nodes = c.GetSelection(s);
+
+ var arg = new List>(nodes.Count);
+ for (int j = 0; j < nodes.Count; j++)
+ {
+ arg[i] = nodes[j];
+ }
+
+ result[i].AddRange(FlattenNodes(arg));
+
+ 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];
+
+ var arg = new List>(nodes.Count);
+ for (int j = 0; j < nodes.Count; j++)
+ {
+ arg[i] = nodes[j];
+ }
+
+ result[i].AddRange(FlattenNodes(arg));
+ }
+ }
+
+ var collection = new FrostFsNodeInfo[result.Count][];
+ for(int i =0; i < result.Count; i++)
+ {
+ collection[i] = [.. result[i]];
+ }
+
+ return collection;
+ }
+}
diff --git a/src/FrostFS.SDK.Client/Models/Netmap/FrostFsNodeInfo.cs b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsNodeInfo.cs
index 9581b21..ea7d7c5 100644
--- a/src/FrostFS.SDK.Client/Models/Netmap/FrostFsNodeInfo.cs
+++ b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsNodeInfo.cs
@@ -1,5 +1,9 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
+
+using FrostFS.SDK.Client.Models.Netmap.Placement;
+using FrostFS.SDK.Cryptography;
namespace FrostFS.SDK;
@@ -8,11 +12,69 @@ public class FrostFsNodeInfo(
NodeState state,
IReadOnlyCollection addresses,
IReadOnlyDictionary attributes,
- ReadOnlyMemory publicKey)
+ ReadOnlyMemory publicKey) : IHasher
{
- public NodeState State { get; private set; } = state;
- public FrostFsVersion Version { get; private set; } = version;
- public IReadOnlyCollection Addresses { get; private set; } = addresses;
- public IReadOnlyDictionary Attributes { get; private set; } = attributes;
- public ReadOnlyMemory PublicKey { get; private set; } = publicKey;
-}
\ No newline at end of file
+ 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 Addresses { get; } = addresses;
+
+ public IReadOnlyDictionary Attributes { get; } = attributes;
+
+ public ReadOnlyMemory 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;
+ }
+ }
+}
diff --git a/src/FrostFS.SDK.Client/Models/Netmap/FrostFsPlacementPolicy.cs b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsPlacementPolicy.cs
index 29c9310..19fe755 100644
--- a/src/FrostFS.SDK.Client/Models/Netmap/FrostFsPlacementPolicy.cs
+++ b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsPlacementPolicy.cs
@@ -1,5 +1,5 @@
-
using System;
+using System.Collections.ObjectModel;
using System.Linq;
using FrostFS.Netmap;
@@ -13,12 +13,21 @@ public struct FrostFsPlacementPolicy(bool unique, params FrostFsReplica[] replic
private PlacementPolicy policy;
public FrostFsReplica[] Replicas { get; private set; } = replicas;
+
+ public Collection Selectors { get; } = [];
+
+ public Collection Filters { get; } = [];
+
public bool Unique { get; private set; } = unique;
+ public uint BackupFactor { get; set; }
+
public override readonly bool Equals(object obj)
{
if (obj is null)
+ {
return false;
+ }
var other = (FrostFsPlacementPolicy)obj;
@@ -46,14 +55,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()
{
@@ -86,4 +91,4 @@ public struct FrostFsPlacementPolicy(bool unique, params FrostFsReplica[] replic
return true;
}
-}
\ No newline at end of file
+}
diff --git a/src/FrostFS.SDK.Client/Models/Netmap/FrostFsReplica.cs b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsReplica.cs
index fa2f8db..5ace048 100644
--- a/src/FrostFS.SDK.Client/Models/Netmap/FrostFsReplica.cs
+++ b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsReplica.cs
@@ -6,6 +6,8 @@ public struct FrostFsReplica : IEquatable
{
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
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
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;
}
-}
\ No newline at end of file
+}
diff --git a/src/FrostFS.SDK.Client/Models/Netmap/FrostFsSelector.cs b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsSelector.cs
new file mode 100644
index 0000000..74380f8
--- /dev/null
+++ b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsSelector.cs
@@ -0,0 +1,10 @@
+namespace FrostFS.SDK;
+
+public class FrostFsSelector(string name)
+{
+ public string Name { get; } = name;
+ public uint Count { get; set; }
+ public uint Clause { get; set; }
+ public string? Attribute { get; set; }
+ public string? Filter { get; set; }
+}
diff --git a/src/FrostFS.SDK.Client/Models/Netmap/IFrostFsFilter.cs b/src/FrostFS.SDK.Client/Models/Netmap/IFrostFsFilter.cs
new file mode 100644
index 0000000..e13875a
--- /dev/null
+++ b/src/FrostFS.SDK.Client/Models/Netmap/IFrostFsFilter.cs
@@ -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; }
+ }
+}
\ No newline at end of file
diff --git a/src/FrostFS.SDK.Client/Models/Netmap/NodeAttrPair.cs b/src/FrostFS.SDK.Client/Models/Netmap/NodeAttrPair.cs
new file mode 100644
index 0000000..3c014fc
--- /dev/null
+++ b/src/FrostFS.SDK.Client/Models/Netmap/NodeAttrPair.cs
@@ -0,0 +1,7 @@
+namespace FrostFS.SDK;
+
+struct NodeAttrPair
+{
+ internal string attr;
+ internal FrostFsNodeInfo[] nodes;
+}
diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/Clause.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/Clause.cs
new file mode 100644
index 0000000..b982027
--- /dev/null
+++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/Clause.cs
@@ -0,0 +1,8 @@
+namespace FrostFS.SDK.Client.Models.Netmap.Placement;
+
+public enum FrostFsClause
+{
+ Unspecified = 0,
+ Same,
+ Distinct
+}
diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/Context.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/Context.cs
new file mode 100644
index 0000000..fc6e80b
--- /dev/null
+++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/Context.cs
@@ -0,0 +1,433 @@
+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 ProcessedFilters { get; } = [];
+
+ // cache of processed selectors
+ internal Dictionary ProcessedSelectors { get; } = [];
+
+ // stores results of selector processing
+ internal Dictionary>> Selections { get; } = [];
+
+ // cache of parsed numeric values
+ internal Dictionary 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 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 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;
+ }
+
+ if (filter.Operation == (int)Operation.EQ ||
+ filter.Operation == (int)Operation.NE ||
+ filter.Operation == (int)Operation.LIKE ||
+ filter.Operation == (int)Operation.GT ||
+ filter.Operation == (int)Operation.GE ||
+ filter.Operation == (int)Operation.LT ||
+ filter.Operation == (int)Operation.LE)
+ {
+ var n = uint.Parse(filter.Value, CultureInfo.InvariantCulture);
+ NumCache[filter.Value] = n;
+ }
+ else
+ {
+ 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 == (uint)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();
+
+ var nodeMap = new Dictionary>();
+ 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 = [];
+
+ foreach (var res in result)
+ {
+ Tools.AppendWeightsTo(res.nodes, weightFunc, ws);
+ Tools.SortHasherSliceByWeightValue(res.nodes.ToList(), ws, HrwSeedHash);
+ }
+ }
+ return [.. result];
+ }
+
+ static double CalcBucketWeight(List ns, IAggregator a, Func 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> 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>(buckets.Length);
+ var fallback = new List>(buckets.Length);
+
+ for (int i = 0; i < buckets.Length; i++)
+ {
+ var ns = buckets[i].nodes;
+ if (ns.Length >= maxNodesInBucket)
+ {
+ res.Add(new List(ns[..maxNodesInBucket]));
+ }
+ else if (ns.Length >= nodesInBucket)
+ {
+ fallback.Add(new List(ns));
+ }
+ }
+
+ if (res.Count < bucketCount)
+ {
+ // Fallback to using minimum allowed backup factor (1).
+ res = 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();
+ Tools.SortHasherSliceByWeightValue(hashers, weights, HrwSeedHash);
+ }
+
+ 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[f.Key] == 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:
+ {
+ var attr = f.Key switch
+ {
+ FrostFsNodeInfo.AttrPrice => nodeInfo.Price,
+ FrostFsNodeInfo.AttrCapacity => nodeInfo.GetCapacity(),
+ _ => uint.Parse(nodeInfo.Attributes[f.Key], CultureInfo.InvariantCulture),
+ };
+
+ 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);
+ }
+ }
+}
diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/HasherList.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/HasherList.cs
new file mode 100644
index 0000000..117980c
--- /dev/null
+++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/HasherList.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+
+namespace FrostFS.SDK.Client.Models.Netmap.Placement;
+
+internal sealed class HasherList : IHasher
+{
+ private readonly List _nodes;
+
+ internal HasherList(List nodes)
+ {
+ _nodes = nodes;
+ }
+
+ public ulong Hash()
+ {
+ return _nodes.Count > 0 ? _nodes[0].Hash() : 0;
+ }
+}
diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/IAggregator.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/IAggregator.cs
new file mode 100644
index 0000000..774dbc2
--- /dev/null
+++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/IAggregator.cs
@@ -0,0 +1,8 @@
+namespace FrostFS.SDK.Client.Models.Netmap.Placement;
+
+internal interface IAggregator
+{
+ void Add(double d);
+
+ double Compute();
+}
diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/IHasher.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/IHasher.cs
new file mode 100644
index 0000000..6a0f536
--- /dev/null
+++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/IHasher.cs
@@ -0,0 +1,6 @@
+namespace FrostFS.SDK.Client.Models.Netmap.Placement;
+
+internal interface IHasher
+{
+ ulong Hash();
+}
diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/INormalizer.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/INormalizer.cs
new file mode 100644
index 0000000..ddf4169
--- /dev/null
+++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/INormalizer.cs
@@ -0,0 +1,6 @@
+namespace FrostFS.SDK.Client.Models.Netmap.Placement;
+
+interface INormalizer
+{
+ double Normalize(double w);
+}
diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/MeanAgg.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/MeanAgg.cs
new file mode 100644
index 0000000..0febba1
--- /dev/null
+++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/MeanAgg.cs
@@ -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 *= (double)count / c + n / c;
+
+ count++;
+ }
+
+ internal readonly double Compute()
+ {
+ return mean;
+ }
+}
diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/MeanIQRAgg.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/MeanIQRAgg.cs
new file mode 100644
index 0000000..e7f6fca
--- /dev/null
+++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/MeanIQRAgg.cs
@@ -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 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();
+ }
+}
diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/MinAgg.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/MinAgg.cs
new file mode 100644
index 0000000..7b68fc0
--- /dev/null
+++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/MinAgg.cs
@@ -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;
+ }
+}
diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/Operation.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/Operation.cs
new file mode 100644
index 0000000..705827e
--- /dev/null
+++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/Operation.cs
@@ -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
+}
diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/ReverseMinNorm.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/ReverseMinNorm.cs
new file mode 100644
index 0000000..d3ffe5c
--- /dev/null
+++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/ReverseMinNorm.cs
@@ -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);
+}
diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/SelectFilterExpr.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/SelectFilterExpr.cs
new file mode 100644
index 0000000..22cb961
--- /dev/null
+++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/SelectFilterExpr.cs
@@ -0,0 +1,10 @@
+using System.Collections.ObjectModel;
+
+namespace FrostFS.SDK.Client.Models.Netmap.Placement;
+
+internal struct SelectFilterExpr(uint cbf, FrostFsSelector selector, Collection filters)
+{
+ internal uint Cbf { get; } = cbf;
+ internal FrostFsSelector Selector { get; } = selector;
+ internal Collection Filters { get; } = filters;
+}
diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/SigmoidNorm.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/SigmoidNorm.cs
new file mode 100644
index 0000000..e4cd416
--- /dev/null
+++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/SigmoidNorm.cs
@@ -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);
+ }
+}
diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/Tools.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/Tools.cs
new file mode 100644
index 0000000..b6de113
--- /dev/null
+++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/Tools.cs
@@ -0,0 +1,140 @@
+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 wf, 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 void SortHasherSliceByWeightValue(List nodes, Span weights, ulong hash) where T : IHasher
+ {
+ if (nodes.Count == 0)
+ {
+ return;
+ }
+
+ 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 ulong[nodes.Count];
+
+ if (allEquals)
+ {
+ for (int i = 0; i < dist.Length; i++)
+ {
+ var x = nodes[i].Hash();
+ dist[i] = Distance(x, hash);
+ }
+
+ SortHasherByDistance(nodes, dist, true);
+ return;
+ }
+
+ for (int i = 0; i < dist.Length; i++)
+ {
+ var d = Distance(nodes[i].Hash(), hash);
+ dist[i] = ulong.MaxValue - (ulong)(d * weights[i]);
+ }
+
+ SortHasherByDistance(nodes, dist, false);
+ }
+
+ internal static void SortHasherByDistance(List nodes, N[] dist, bool asc)
+ {
+ IndexedValue[] indexes = new IndexedValue[nodes.Count];
+ for (int i = 0; i < dist.Length; i++)
+ {
+ indexes[i] = new IndexedValue() { nodeInfo = nodes[i], dist = dist[i] };
+ }
+
+ if (asc)
+ {
+ nodes = new List(indexes
+ .OrderBy(x => x.dist)
+ .Select(x => x.nodeInfo).ToArray());
+ }
+ else
+ {
+ nodes = new List(indexes
+ .OrderByDescending(x => x.dist)
+ .Select(x => x.nodeInfo)
+ .ToArray());
+ }
+ }
+
+ internal static Func DefaultWeightFunc(IReadOnlyList 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
+ {
+ internal T nodeInfo;
+ internal N dist;
+ }
+}
diff --git a/src/FrostFS.SDK.Client/Tools/ObjectTools.cs b/src/FrostFS.SDK.Client/Tools/ObjectTools.cs
index 36a22a1..40cf1ad 100644
--- a/src/FrostFS.SDK.Client/Tools/ObjectTools.cs
+++ b/src/FrostFS.SDK.Client/Tools/ObjectTools.cs
@@ -13,7 +13,7 @@ namespace FrostFS.SDK.Client;
public static class ObjectTools
{
public static FrostFsObjectId CalculateObjectId(
- FrostFsObjectHeader header,
+ FrostFsObjectHeader header,
ReadOnlyMemory payloadHash,
FrostFsOwner owner,
FrostFsVersion version,
@@ -58,6 +58,7 @@ public static class ObjectTools
grpcHeader.PayloadHash = Sha256Checksum(@object.SingleObjectPayload);
var split = @object.Header.Split;
+
if (split != null)
{
SetSplitValues(grpcHeader, split, ctx.Owner, ctx.Version, ctx.Key);
@@ -80,17 +81,21 @@ public static class ObjectTools
}
internal static void SetSplitValues(
- Header grpcHeader,
+ Header grpcHeader,
FrostFsSplit split,
- FrostFsOwner owner,
- FrostFsVersion version,
+ FrostFsOwner owner,
+ FrostFsVersion version,
ClientKey key)
{
if (split == null)
+ {
return;
+ }
if (key == null)
+ {
throw new FrostFsInvalidObjectException(nameof(key));
+ }
grpcHeader.Split = new Header.Types.Split
{
@@ -98,7 +103,9 @@ public static class ObjectTools
};
if (split.Children != null && split.Children.Count != 0)
+ {
grpcHeader.Split.Children.AddRange(split.Children.Select(id => id.ToMessage()));
+ }
if (split.ParentHeader is not null)
{
@@ -118,8 +125,8 @@ public static class ObjectTools
internal static Header CreateHeader(
FrostFsObjectHeader header,
- ReadOnlyMemory payloadChecksum,
- FrostFsOwner owner,
+ ReadOnlyMemory payloadChecksum,
+ FrostFsOwner owner,
FrostFsVersion version)
{
header.OwnerId ??= owner;
@@ -128,7 +135,7 @@ public static class ObjectTools
var grpcHeader = header.GetHeader();
grpcHeader.PayloadHash = ChecksumFromSha256(payloadChecksum);
-
+
return grpcHeader;
}
@@ -149,4 +156,4 @@ public static class ObjectTools
Sum = UnsafeByteOperations.UnsafeWrap(dataHash)
};
}
-}
\ No newline at end of file
+}
diff --git a/src/FrostFS.SDK.Cryptography/Murmur3_128.cs b/src/FrostFS.SDK.Cryptography/Murmur3_128.cs
index e8f52fc..3e0ae5f 100644
--- a/src/FrostFS.SDK.Cryptography/Murmur3_128.cs
+++ b/src/FrostFS.SDK.Cryptography/Murmur3_128.cs
@@ -4,7 +4,7 @@ using System.Security.Cryptography;
namespace FrostFS.SDK.Cryptography;
-internal class Murmur3_128 : HashAlgorithm
+public class Murmur3 : HashAlgorithm
{
private const ulong c1 = 0x87c37b91114253d5;
private const ulong c2 = 0x4cf5ad432745937f;
@@ -17,14 +17,31 @@ internal class Murmur3_128 : HashAlgorithm
private readonly uint seed;
private int length;
- public Murmur3_128(uint seed)
+ public Murmur3(uint seed)
{
this.seed = seed;
Initialize();
}
+ public ulong GetCheckSum64(byte[] bytes)
+ {
+ if (bytes is null)
+ {
+ throw new ArgumentNullException(nameof(bytes));
+ }
+
+ Initialize();
+ HashCore(bytes, 0, bytes.Length);
+ return HashFinalUlong();
+ }
+
protected override void HashCore(byte[] array, int ibStart, int cbSize)
{
+ if (array is null)
+ {
+ throw new ArgumentNullException(nameof(array));
+ }
+
length += cbSize;
int remainder = cbSize & 15;
int alignedLength = ibStart + (cbSize - remainder);
@@ -92,6 +109,11 @@ internal class Murmur3_128 : HashAlgorithm
}
protected override byte[] HashFinal()
+ {
+ return BitConverter.GetBytes(HashFinalUlong());
+ }
+
+ protected ulong HashFinalUlong()
{
h1 ^= (ulong)length;
h2 ^= (ulong)length;
@@ -102,7 +124,7 @@ internal class Murmur3_128 : HashAlgorithm
h1 += h2;
h2 += h1;
- return BitConverter.GetBytes(h1);
+ return h1;
}
public override void Initialize()
diff --git a/src/FrostFS.SDK.Tests/Unit/PlacementVectorTests.cs b/src/FrostFS.SDK.Tests/Unit/PlacementVectorTests.cs
new file mode 100644
index 0000000..e578f90
--- /dev/null
+++ b/src/FrostFS.SDK.Tests/Unit/PlacementVectorTests.cs
@@ -0,0 +1,161 @@
+using System.Diagnostics.CodeAnalysis;
+
+using FrostFS.SDK.Client.Models.Netmap.Placement;
+
+namespace FrostFS.SDK.Tests.Unit;
+
+[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")]
+public class PlacementVectorTests
+{
+ Dictionary[] attribs;
+
+ public PlacementVectorTests()
+ {
+ var attribs1 = new Dictionary
+ {
+ {"Country", "Germany" },
+ {"Price", "2" },
+ {"Capacity", "10000"}
+ };
+
+ var attribs2 = new Dictionary
+ {
+ {"Country", "Germany" },
+ {"Price", "4" },
+ {"Capacity", "1"}
+ };
+
+ var attribs3 = new Dictionary
+ {
+ {"Country", "France" },
+ {"Price", "3" },
+ {"Capacity", "10"}
+ };
+
+ var attribs4 = new Dictionary
+ {
+ {"Country", "Russia" },
+ {"Price", "2" },
+ {"Capacity", "10000"}
+ };
+
+ var attribs5 = new Dictionary
+ {
+ {"Country", "Russia" },
+ {"Price", "1" },
+ {"Capacity", "10000"}
+ };
+
+ var attribs6 = new Dictionary
+ {
+ {"Country", "Russia" },
+ {"Capacity", "10000"}
+ };
+ var attribs7 = new Dictionary
+ {
+ {"Country", "France" },
+ {"Price", "100" },
+ {"Capacity", "10000"}
+ };
+ var attribs8 = new Dictionary
+ {
+ {"Country", "France" },
+ {"Price", "7" },
+ {"Capacity", "10000"}
+ };
+ var attribs9 = new Dictionary
+ {
+ {"Country", "Russia" },
+ {"Price", "2" },
+ {"Capacity", "1"}
+ };
+
+ attribs = [attribs1, attribs2, attribs3, attribs4, attribs5, attribs6, attribs7, attribs8, attribs9];
+ }
+
+ [Fact]
+ public void PlacementVectorTest()
+ {
+ FrostFsVersion v = new(2, 13);
+ var addresses = new string[] { "localhost", "server1" };
+ var key1 = new byte[] { 1 };
+ var key2 = new byte[] { 2 };
+ var key3 = new byte[] { 3 };
+
+ var nodes = new List
+ {
+ new(v, NodeState.Online, addresses.AsReadOnly(), attribs[5].AsReadOnly(), key1),
+ new(v, NodeState.Online, addresses.AsReadOnly(), attribs[0].AsReadOnly(), key2),
+ new(v, NodeState.Online, addresses.AsReadOnly(), attribs[8].AsReadOnly(), key3)
+ };
+
+ var netmap = new FrostFsNetmapSnapshot(100, nodes.AsReadOnly());
+
+ var arg = new FrostFsNodeInfo[1][];
+ var pivot = "objectID"u8.ToArray();
+
+ arg[0] = [.. nodes];
+ var result = netmap.PlacementVectors(arg, pivot);
+
+ Assert.Single(result);
+ Assert.Equal(3, result[0].Length);
+ Assert.Equal(key1, result[0][0].PublicKey);
+ Assert.Equal(key2, result[0][1].PublicKey);
+ Assert.Equal(key3, result[0][2].PublicKey);
+ }
+
+ [Fact]
+ public void TestPlacementPolicyUnique()
+ {
+ FrostFsVersion version = new(2, 13);
+ var p = new FrostFsPlacementPolicy(true, [new FrostFsReplica(1, "S"), new FrostFsReplica(1, "S")])
+ {
+ BackupFactor = 2
+ };
+ p.Selectors.Add(new FrostFsSelector("S")
+ {
+ Attribute = "City",
+ Count = 1,
+ Filter = "*",
+ Clause = (int)FrostFsClause.Same
+ });
+
+ List nodes = [];
+
+ var cities = new string[] { "Moscow", "Berlin", "Shenzhen" };
+ for (int i = 0; i < 3; i++)
+ {
+ for (int j = 0; j < 3; j++)
+ {
+ var attr = new Dictionary { { "City", cities[i] } };
+ var key = new byte[] { (byte)(i * 4 + j) };
+ var node = new FrostFsNodeInfo(version, NodeState.Online, [], attr, key);
+
+ nodes.Add(node);
+ }
+ }
+
+ var netMap = new FrostFsNetmapSnapshot(100, nodes.AsReadOnly());
+
+ var v = netMap.ContainerNodes(p, null);
+
+ Assert.Equal(2, v.Length);
+ Assert.Equal(2, v[0].Length);
+ Assert.Equal(2, v[1].Length);
+
+
+ for (int i = 0; i < v.Length; i++)
+ {
+ foreach (var ni in v[i])
+ {
+ for (int j = 0; j < i; j++)
+ {
+ foreach (var nj in v[j])
+ {
+ Assert.NotEqual(ni.Hash, nj.Hash);
+ }
+ }
+ }
+ }
+ }
+}