using System.Runtime.CompilerServices; namespace Nop.Core.Infrastructure; /// /// A thread-safe implementation of a radix tree /// public partial class ConcurrentTrie : IConcurrentCollection { #region Fields protected volatile TrieNode _root = new(); protected readonly StripedReaderWriterLock _locks = new(); protected readonly ReaderWriterLockSlim _structureLock = new(); #endregion #region Ctor /// /// Initializes a new instance of /// protected ConcurrentTrie(TrieNode subtreeRoot) { _root.Children[subtreeRoot.Label[0]] = subtreeRoot; } /// /// Initializes a new empty instance of /// public ConcurrentTrie() { } #endregion #region Utilities [MethodImpl(MethodImplOptions.AggressiveInlining)] protected static int GetCommonPrefixLength(ReadOnlySpan s1, ReadOnlySpan s2) { var i = 0; var minLength = Math.Min(s1.Length, s2.Length); while (i < minLength && s2[i] == s1[i]) i++; return i; } /// /// Gets a lock on the node's children /// /// /// May return the same lock for two different nodes, so the user needs to check to avoid lock recursion exceptions /// protected virtual ReaderWriterLockSlim GetLock(TrieNode node) { return _locks.GetLock(node.Children); } protected virtual bool Find(string key, TrieNode subtreeRoot, out TrieNode node) { node = subtreeRoot; if (key.Length == 0) return true; var suffix = key.AsSpan(); while (true) { var nodeLock = GetLock(node); nodeLock.EnterReadLock(); try { if (!node.Children.TryGetValue(suffix[0], out node)) return false; } finally { nodeLock.ExitReadLock(); } var span = node.Label.AsSpan(); var i = GetCommonPrefixLength(suffix, span); if (i != span.Length) return false; if (i == suffix.Length) return node.HasValue; suffix = suffix[i..]; } } protected virtual TrieNode GetOrAddNode(ReadOnlySpan key, TValue value, bool overwrite = false) { var node = _root; var suffix = key; ReaderWriterLockSlim nodeLock; char c; TrieNode nextNode; _structureLock.EnterReadLock(); try { while (true) { c = suffix[0]; nodeLock = GetLock(node); nodeLock.EnterUpgradeableReadLock(); try { if (node.Children.TryGetValue(c, out nextNode)) { var label = nextNode.Label.AsSpan(); var i = GetCommonPrefixLength(label, suffix); // suffix starts with label if (i == label.Length) { // keys are equal - this is the node we're looking for if (i == suffix.Length) { if (overwrite) nextNode.SetValue(value); else nextNode.GetOrAddValue(value); return nextNode; } // advance the suffix and continue the search from nextNode suffix = suffix[label.Length..]; node = nextNode; continue; } // we need to add a node, but don't want to hold an upgradeable read lock on _structureLock // since only one can be held at a time, so we break, release the lock and reacquire a write lock break; } // if there is no child starting with c, we can just add and return one nodeLock.EnterWriteLock(); try { var suffixNode = new TrieNode(suffix); suffixNode.SetValue(value); return node.Children[c] = suffixNode; } finally { nodeLock.ExitWriteLock(); } } finally { nodeLock.ExitUpgradeableReadLock(); } } } finally { _structureLock.ExitReadLock(); } // if we need to restructure the tree, we do it after releasing and reacquiring the lock. // however, another thread may have restructured around the node we're on in the meantime, // and in that case we need to retry the insertion _structureLock.EnterWriteLock(); nodeLock.EnterUpgradeableReadLock(); try { // we use while instead of if so we can break // and put a recursive call at the end of the method to enable tail-recursion optimization while (!node.IsDeleted && node.Children.TryGetValue(c, out nextNode)) { var label = nextNode.Label.AsSpan(); var i = GetCommonPrefixLength(label, suffix); // suffix starts with label? if (i == label.Length) { // if the keys are equal, the key has already been inserted if (i == suffix.Length) { if (overwrite) nextNode.SetValue(value); return nextNode; } // structure has changed since last; try again break; } var splitNode = new TrieNode(suffix[..i]) { Children = { [label[i]] = new TrieNode(label[i..], nextNode) } }; TrieNode outNode; // label starts with suffix, so we can return splitNode if (i == suffix.Length) outNode = splitNode; // the keys diverge, so we need to branch from splitNode else splitNode.Children[suffix[i]] = outNode = new TrieNode(suffix[i..]); outNode.SetValue(value); nodeLock.EnterWriteLock(); try { node.Children[c] = splitNode; } finally { nodeLock.ExitWriteLock(); } return outNode; } } finally { nodeLock.ExitUpgradeableReadLock(); _structureLock.ExitWriteLock(); } // we failed to add a node, so we have to retry; // the recursive call is placed at the end to enable tail-recursion optimization return GetOrAddNode(key, value, overwrite); } protected virtual void Remove(TrieNode subtreeRoot, ReadOnlySpan key) { TrieNode node = null, grandparent = null; var parent = subtreeRoot; var i = 0; _structureLock.EnterReadLock(); try { while (i < key.Length) { var c = key[i]; var parentLock = GetLock(parent); parentLock.EnterReadLock(); try { if (!parent.Children.TryGetValue(c, out node)) return; } finally { parentLock.ExitReadLock(); } var label = node.Label.AsSpan(); var k = GetCommonPrefixLength(key[i..], label); // is this the node we're looking for? if (k == label.Length && k == key.Length - i) { // this node has to be removed or merged if (node.TryRemoveValue(out _)) break; // the node is either already removed, or it is a branching node return; } if (k < label.Length) return; i += label.Length; grandparent = parent; parent = node; } } finally { _structureLock.ExitReadLock(); } if (node == null) return; // if we need to delete a node, the tree has to be restructured to remove empty leaves or merge // single children with branching node parents, and other threads may be currently on these nodes _structureLock.EnterWriteLock(); try { var nodeLock = GetLock(node); var parentLock = GetLock(parent); var grandparentLock = grandparent != null ? GetLock(grandparent) : null; var lockAlreadyHeld = nodeLock == parentLock || nodeLock == grandparentLock; if (lockAlreadyHeld) nodeLock.EnterUpgradeableReadLock(); else nodeLock.EnterReadLock(); try { // another thread has written a value to the node while we were waiting if (node.HasValue) return; var c = node.Label[0]; var nChildren = node.Children.Count; // if the node has no children, we can just remove it if (nChildren == 0) { parentLock.EnterWriteLock(); try { // was removed or replaced by another thread if (!parent.Children.TryGetValue(c, out var n) || n != node) return; parent.Children.Remove(c); node.Delete(); // since we removed a node, we may be able to merge a lone sibling with the parent if (parent.Children.Count == 1 && grandparent != null && !parent.HasValue) { var grandparentLockAlreadyHeld = grandparentLock == parentLock; if (!grandparentLockAlreadyHeld) grandparentLock.EnterWriteLock(); try { c = parent.Label[0]; if (!grandparent.Children.TryGetValue(c, out n) || n != parent || parent.HasValue) return; var child = parent.Children.First().Value; grandparent.Children[c] = new TrieNode(parent.Label + child.Label, child); parent.Delete(); } finally { if (!grandparentLockAlreadyHeld) grandparentLock.ExitWriteLock(); } } } finally { parentLock.ExitWriteLock(); } } // if there is a single child, we can merge it with node else if (nChildren == 1) { parentLock.EnterWriteLock(); try { // was removed or replaced by another thread if (!parent.Children.TryGetValue(c, out var n) || n != node) return; var child = node.Children.FirstOrDefault().Value; parent.Children[c] = new TrieNode(node.Label + child.Label, child); node.Delete(); } finally { parentLock.ExitWriteLock(); } } } finally { if (lockAlreadyHeld) nodeLock.ExitUpgradeableReadLock(); else nodeLock.ExitReadLock(); } } finally { _structureLock.ExitWriteLock(); } } #endregion #region Methods /// /// Attempts to get the value associated with the specified key /// /// The key of the item to get (case-sensitive) /// The value associated with , if found /// /// True if the key was found, otherwise false /// public virtual bool TryGetValue(string key, out TValue value) { ArgumentNullException.ThrowIfNull(key); value = default; return Find(key, _root, out var node) && node.TryGetValue(out value); } /// /// Adds a key-value pair to the trie /// /// The key of the new item (case-sensitive) /// The value to be associated with public virtual void Add(string key, TValue value) { if (string.IsNullOrEmpty(key)) throw new ArgumentException($"'{nameof(key)}' cannot be null or empty.", nameof(key)); GetOrAddNode(key, value, true); } /// /// Clears the trie /// public virtual void Clear() { _root = new TrieNode(); } /// /// Gets all key-value pairs for keys starting with the given prefix /// /// The prefix (case-sensitive) to search for /// /// All key-value pairs for keys starting with /// public virtual IEnumerable> Search(string prefix) { ArgumentNullException.ThrowIfNull(prefix); if (!SearchOrPrune(prefix, false, out var node)) return Enumerable.Empty>(); // depth-first traversal IEnumerable> traverse(TrieNode n, string s) { if (n.TryGetValue(out var value)) yield return new KeyValuePair(s, value); var nLock = GetLock(n); nLock.EnterReadLock(); List children; try { // we can't know what is done during enumeration, so we need to make a copy of the children children = n.Children.Values.ToList(); } finally { nLock.ExitReadLock(); } foreach (var child in children) foreach (var kv in traverse(child, s + child.Label)) yield return kv; } return traverse(node, node.Label); } /// /// Removes the item with the given key, if present /// /// The key (case-sensitive) of the item to be removed public void Remove(string key) { if (string.IsNullOrEmpty(key)) throw new ArgumentException($"'{nameof(key)}' cannot be null or empty.", nameof(key)); Remove(_root, key); } /// /// Attempts to remove all items with keys starting with the specified prefix /// /// The prefix (case-sensitive) of the items to be deleted /// The sub-collection containing all deleted items, if found /// /// True if the prefix was successfully removed from the trie, otherwise false /// public virtual bool Prune(string prefix, out IConcurrentCollection subCollection) { var succeeded = SearchOrPrune(prefix, true, out var subtreeRoot); subCollection = succeeded ? new ConcurrentTrie(subtreeRoot) : default; return succeeded; } protected bool SearchOrPrune(string prefix, bool prune, out TrieNode subtreeRoot) { ArgumentNullException.ThrowIfNull(prefix); if (prefix.Length == 0) { subtreeRoot = _root; return true; } subtreeRoot = default; var node = _root; var parent = node; var span = prefix.AsSpan(); var i = 0; while (i < span.Length) { var c = span[i]; var parentLock = GetLock(parent); parentLock.EnterUpgradeableReadLock(); try { if (!parent.Children.TryGetValue(c, out node)) return false; var label = node.Label.AsSpan(); var k = GetCommonPrefixLength(span[i..], label); if (k == span.Length - i) { subtreeRoot = new TrieNode(prefix[..i] + node.Label, node); if (!prune) return true; parentLock.EnterWriteLock(); try { if (parent.Children.Remove(c, out node)) return true; } finally { parentLock.ExitWriteLock(); } // was removed by another thread return false; } if (k < label.Length) return false; i += label.Length; } finally { parentLock.ExitUpgradeableReadLock(); } parent = node; } return false; } #endregion #region Properties /// /// Gets a collection that contains the keys in the /// public IEnumerable Keys => Search(string.Empty).Select(t => t.Key); #endregion #region Nested classes /// /// A striped ReaderWriterLock wrapper /// protected class StripedReaderWriterLock { #region Fields protected const int MULTIPLIER = 8; protected readonly ReaderWriterLockSlim[] _locks; #endregion #region Ctor // defaults to 8 times the number of processor cores public StripedReaderWriterLock(int nLocks = 0) { if (nLocks == 0) nLocks = Environment.ProcessorCount * MULTIPLIER; _locks = new ReaderWriterLockSlim[nLocks]; for (var i = 0; i < nLocks; i++) _locks[i] = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); } #endregion #region Methods /// /// Gets a lock on the object /// public ReaderWriterLockSlim GetLock(object obj) { return _locks[obj.GetHashCode() % _locks.Length]; } #endregion } /// /// An implementation of a trie node /// protected class TrieNode { #region Fields // used to avoid keeping a separate boolean flag that would use another byte per node protected static readonly ValueWrapper _deleted = new(default); protected volatile ValueWrapper _value; #endregion #region Ctor public TrieNode(string label = "") { Label = label; Children = new Dictionary(); } public TrieNode(ReadOnlySpan label) : this(label.ToString()) { } public TrieNode(string label, TrieNode node) : this(label) { Children = node.Children; _value = node._value; } public TrieNode(ReadOnlySpan label, TrieNode node) : this(label) { Children = node.Children; _value = node._value; } #endregion #region Methods /// /// Attempts to get the node value /// /// The node value, if value exists /// /// True if value exists, otherwise false /// public bool TryGetValue(out TValue value) { var wrapper = _value; value = default; if (wrapper == null) return false; value = wrapper.Value; return true; } public bool TryRemoveValue(out TValue value) { var wrapper = Interlocked.Exchange(ref _value, null); value = default; if (wrapper == null) return false; value = wrapper.Value; return true; } public void SetValue(TValue value) { _value = new ValueWrapper(value); } public TValue GetOrAddValue(TValue value) { var wrapper = Interlocked.CompareExchange(ref _value, new ValueWrapper(value), null); return wrapper != null ? wrapper.Value : value; } public void Delete() { _value = _deleted; } #endregion #region Properties public Dictionary Children { get; } public string Label { get; } public bool IsDeleted => _value == _deleted; public bool HasValue => _value != null && !IsDeleted; #endregion #region Nested class protected class ValueWrapper { public readonly TValue Value; public ValueWrapper(TValue value) { Value = value; } } #endregion } #endregion }