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