615b487a77
Adds missing <summary>/<param> XML docs across 99 server, worker, and test files so CommentChecker reports zero issues (TreatWarningsAsErrors needs the analyzer clean). Bundles in WIP dashboard work: NavSection extraction, MainLayout/site.css/js styling alignment, and DashboardOptions/Auth tweaks.
127 lines
4.9 KiB
C#
127 lines
4.9 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Server.Galaxy;
|
|
|
|
public static class GalaxyGlobMatcher
|
|
{
|
|
/// <summary>
|
|
/// Maximum number of compiled-regex entries retained in <see cref="RegexCache"/>.
|
|
/// The cache is keyed by glob pattern and patterns flow in from two sources:
|
|
/// admin-controlled API-key constraints (naturally bounded) and the
|
|
/// client-supplied <c>DiscoverHierarchyRequest.TagNameGlob</c> (unbounded — a
|
|
/// client can iterate through generated names and create millions of distinct
|
|
/// globs over the process lifetime). Capping the cache bounds memory while
|
|
/// keeping the hot working set hit-cached.
|
|
/// </summary>
|
|
internal const int RegexCacheCapacity = 256;
|
|
|
|
/// <summary>
|
|
/// Bounded compiled-regex cache keyed by glob pattern. <c>IsMatch</c> is called
|
|
/// once per object per <c>DiscoverHierarchy</c>/<c>WatchDeployEvents</c>
|
|
/// evaluation, so the same handful of glob patterns are translated
|
|
/// repeatedly; caching avoids rebuilding and recompiling the regex on every
|
|
/// call. Beyond <see cref="RegexCacheCapacity"/> entries the oldest insertion
|
|
/// is evicted so a client cannot grow the cache without bound by submitting
|
|
/// unique patterns. Eviction is approximate (FIFO over insertion order, not
|
|
/// true LRU) because we only need the bound, not exact recency tracking.
|
|
/// </summary>
|
|
private static readonly ConcurrentDictionary<string, Regex> RegexCache = new(StringComparer.Ordinal);
|
|
|
|
/// <summary>
|
|
/// Insertion-order queue used to evict the oldest cache entry when the cache
|
|
/// exceeds <see cref="RegexCacheCapacity"/>. A separate queue keeps the
|
|
/// <see cref="RegexCache"/> reads lock-free; the lock below only guards the
|
|
/// eviction path.
|
|
/// </summary>
|
|
private static readonly ConcurrentQueue<string> InsertionOrder = new();
|
|
private static readonly object EvictionLock = new();
|
|
|
|
/// <summary>
|
|
/// Current cache size, exposed for tests asserting the cap is honoured.
|
|
/// </summary>
|
|
internal static int CurrentCacheSize => RegexCache.Count;
|
|
|
|
/// <summary>Determines whether a value matches a glob pattern (with * and ? wildcards).</summary>
|
|
/// <param name="value">The value to test against the glob pattern.</param>
|
|
/// <param name="glob">The glob pattern with * and ? wildcards.</param>
|
|
public static bool IsMatch(string value, string glob)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(glob))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return GetOrCreateRegex(glob).IsMatch(value ?? string.Empty);
|
|
}
|
|
|
|
private static Regex GetOrCreateRegex(string glob)
|
|
{
|
|
if (RegexCache.TryGetValue(glob, out Regex? existing))
|
|
{
|
|
return existing;
|
|
}
|
|
|
|
Regex compiled = new(
|
|
BuildRegex(glob),
|
|
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled,
|
|
TimeSpan.FromMilliseconds(100));
|
|
|
|
// GetOrAdd atomically returns whichever instance is in the cache after the
|
|
// call — either the locally-compiled regex (we won the race) or the regex
|
|
// another thread inserted (we lost). It also avoids the TryAdd-then-indexer
|
|
// pattern where the key could be evicted between the failed TryAdd and the
|
|
// indexer read, producing a KeyNotFoundException under contention near the
|
|
// cap (Server-024).
|
|
Regex result = RegexCache.GetOrAdd(glob, compiled);
|
|
if (ReferenceEquals(result, compiled))
|
|
{
|
|
// We were the inserter — track for FIFO eviction and bound the cache.
|
|
InsertionOrder.Enqueue(glob);
|
|
EvictIfOverCapacity();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static void EvictIfOverCapacity()
|
|
{
|
|
if (RegexCache.Count <= RegexCacheCapacity)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Serialize eviction so two threads do not race past the cap together.
|
|
lock (EvictionLock)
|
|
{
|
|
while (RegexCache.Count > RegexCacheCapacity && InsertionOrder.TryDequeue(out string? oldest))
|
|
{
|
|
RegexCache.TryRemove(oldest, out _);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string BuildRegex(string glob)
|
|
{
|
|
StringBuilder builder = new("^", glob.Length + 2);
|
|
foreach (char character in glob)
|
|
{
|
|
switch (character)
|
|
{
|
|
case '*':
|
|
builder.Append(".*");
|
|
break;
|
|
case '?':
|
|
builder.Append('.');
|
|
break;
|
|
default:
|
|
builder.Append(Regex.Escape(character.ToString()));
|
|
break;
|
|
}
|
|
}
|
|
|
|
builder.Append('$');
|
|
return builder.ToString();
|
|
}
|
|
}
|