| | | 1 | | using MongoDB.Driver; |
| | | 2 | | using System.Text; |
| | | 3 | | |
| | | 4 | | namespace AsyncResponse.Internal; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// Container-scoped ownership ledger for MongoDB collections. The channel, transport, and |
| | | 8 | | /// durable-flow stores validate their own collection plans, but none can see the others' — |
| | | 9 | | /// and MongoDB has no catalog "relation kind" to verify against after the fact: a durable-flow |
| | | 10 | | /// store configured onto the channel's derived <c>{MessageCollection}_counters</c> collection |
| | | 11 | | /// would happily create flow documents there, and its TTL index would then silently delete the |
| | | 12 | | /// ack-sequence counter. Each store claims its effective collections (derived ones included) at |
| | | 13 | | /// construction, keyed by cluster + database, so whichever component starts second fails with an |
| | | 14 | | /// actionable error naming both claimants — in either startup order. Registered per container |
| | | 15 | | /// (no static state), so independent hosts and test fixtures never see each other. |
| | | 16 | | /// </summary> |
| | | 17 | | /// <remarks> |
| | | 18 | | /// Source-linked into the channel, transport, and durable-flow packages (matching |
| | | 19 | | /// <c>MongoOwnershipLedger</c>), but the <see cref="IMongoNamespaceRegistry"/> seam it implements |
| | | 20 | | /// lives in Core: registering <em>this</em> type directly under <c>TryAddSingleton</c> would key |
| | | 21 | | /// the DI container on a per-package-compiled type, so two MongoDB packages sharing one container |
| | | 22 | | /// would each install their own instance instead of sharing one — silently splitting the registry |
| | | 23 | | /// and defeating cross-component collision detection. Resolving through the Core-defined interface |
| | | 24 | | /// keeps one shared singleton regardless of which package's registration runs first. |
| | | 25 | | /// </remarks> |
| | | 26 | | internal sealed class MongoNamespaceRegistry : IMongoNamespaceRegistry |
| | | 27 | | { |
| | 156 | 28 | | private readonly object _gate = new(); |
| | 156 | 29 | | private readonly Dictionary<string, (string Component, string Purpose)> _claims = new(StringComparer.Ordinal); |
| | | 30 | | |
| | | 31 | | /// <inheritdoc /> |
| | | 32 | | public void Claim( |
| | | 33 | | string clusterKey, |
| | | 34 | | string databaseName, |
| | | 35 | | string componentName, |
| | | 36 | | IReadOnlyList<(string Collection, string Purpose)> collections) |
| | | 37 | | { |
| | 156 | 38 | | lock (_gate) |
| | | 39 | | { |
| | 624 | 40 | | foreach (var (collection, purpose) in collections) |
| | | 41 | | { |
| | 156 | 42 | | var key = $"{clusterKey}|{databaseName}|{collection}"; |
| | 156 | 43 | | if (_claims.TryGetValue(key, out var existing) |
| | 156 | 44 | | && !string.Equals(existing.Component, componentName, StringComparison.Ordinal)) |
| | | 45 | | { |
| | 0 | 46 | | throw new InvalidOperationException( |
| | 0 | 47 | | $"MongoDB collection '{databaseName}.{collection}' is used by both the {existing.Component} " + |
| | 0 | 48 | | $"({existing.Purpose}) and the {componentName} ({purpose}). Components sharing a database must u |
| | 0 | 49 | | "distinct collections — including derived ones such as the channel's '{MessageCollection}_counte |
| | 0 | 50 | | "ack-sequence counter, whose documents another component's TTL index would silently delete. " + |
| | 0 | 51 | | "Rename one of the configured collection names."); |
| | | 52 | | } |
| | | 53 | | |
| | 156 | 54 | | _claims[key] = (componentName, purpose); |
| | | 55 | | } |
| | | 56 | | } |
| | 156 | 57 | | } |
| | | 58 | | |
| | | 59 | | /// <summary> |
| | | 60 | | /// Stable identity of the cluster a database handle points at, for cross-component |
| | | 61 | | /// collection-ownership claims: same servers + same database name = same namespace space. |
| | | 62 | | /// The one implementation every store's ownership claim calls, so a derivation drift (SRV |
| | | 63 | | /// seedlist normalization, <c>DirectConnection</c>, host casing) can no longer desync the |
| | | 64 | | /// keys and silently turn collision detection into a no-op. |
| | | 65 | | /// </summary> |
| | | 66 | | internal static string ClusterKey(IMongoDatabase database) |
| | 630 | 67 | | => string.Join(",", database.Client.Settings.Servers.Select(static s => s.ToString()).OrderBy(static s => s, Str |
| | | 68 | | |
| | | 69 | | /// <summary>MongoDB's SHARDED namespace byte limit; see <see cref="ValidateEffectiveNamespace"/>.</summary> |
| | | 70 | | internal const int ShardedNamespaceByteLimit = 235; |
| | | 71 | | |
| | | 72 | | /// <summary> |
| | | 73 | | /// Validates an effective namespace ("database.collection") against MongoDB's 235-byte |
| | | 74 | | /// SHARDED namespace limit — tighter than the 255-byte limit on an unsharded namespace, and |
| | | 75 | | /// enforced here even while unsharded so a later <c>shardCollection</c> cannot strand an |
| | | 76 | | /// already-created collection whose namespace fit under 255 but not 235. Only the store |
| | | 77 | | /// constructor knows the actual database name, so this cannot live in options validation. |
| | | 78 | | /// </summary> |
| | | 79 | | internal static void ValidateEffectiveNamespace(IMongoDatabase database, string collectionName, string description) |
| | | 80 | | { |
| | 245 | 81 | | var ns = $"{database.DatabaseNamespace.DatabaseName}.{collectionName}"; |
| | 245 | 82 | | var byteLength = Encoding.UTF8.GetByteCount(ns); |
| | 245 | 83 | | if (byteLength > ShardedNamespaceByteLimit) |
| | 2 | 84 | | throw new InvalidOperationException( |
| | 2 | 85 | | $"The MongoDB namespace '{ns}' ({description}) is {byteLength} UTF-8 bytes; the store enforces MongoDB's |
| | 2 | 86 | | "namespace limit of 235 bytes (unsharded allows 255) so a later shard-enable cannot strand the collectio |
| | 2 | 87 | | "Shorten the database or collection name."); |
| | 243 | 88 | | } |
| | | 89 | | } |