From c100a0faeea426d68f125b69163153dafe209b22 Mon Sep 17 00:00:00 2001 From: Marius Bughiu Date: Thu, 30 Jul 2026 03:15:40 +0300 Subject: [PATCH 1/2] docs(LruCache): note that a hit on the already-MRU entry keeps enumerators valid The class remarks, the TryGet remarks, the GetEnumerator summary, and the API reference all stated unconditionally that a mutating read invalidates active enumerators. MoveToHeadIfNeeded deliberately skips the relink and the version bump when the entry is already the head, which LruCacheEnumerationTests pins as GetOfAlreadyMostRecentEntry_DoesNotInvalidateEnumerator. Documentation only; no behavioural change. --- docs/api/collections.md | 4 +++- src/Celerity/Collections/LruCache.cs | 14 ++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/api/collections.md b/docs/api/collections.md index 0025bc1c..7aa7ecf2 100644 --- a/docs/api/collections.md +++ b/docs/api/collections.md @@ -3335,7 +3335,9 @@ frees. LRU semantics require a lookup to count as a *use*. The indexer getter and `TryGet` therefore **promote the entry to most-recently-used**, which reorders the recency list and invalidates any -in-progress enumerator (matching "collection was modified" semantics). To inspect the cache without +in-progress enumerator (matching "collection was modified" semantics). The one exception is a hit on +the entry that is *already* most-recently-used: there is nothing to reorder, so the promotion is a +no-op and active enumerators stay valid. To inspect the cache without disturbing recency order — and without invalidating an active enumerator — use `TryPeek`, `ContainsKey`, or the `TryPeekLeastRecentlyUsed` / `TryPeekMostRecentlyUsed` inspectors. diff --git a/src/Celerity/Collections/LruCache.cs b/src/Celerity/Collections/LruCache.cs index 8812db66..6cb162bf 100644 --- a/src/Celerity/Collections/LruCache.cs +++ b/src/Celerity/Collections/LruCache.cs @@ -31,7 +31,9 @@ namespace Celerity.Collections; /// /// Reads are mutating. LRU semantics require a lookup to count as a use, so the indexer getter /// and promote the entry to most-recently-used and therefore -/// invalidate any in-progress enumerator (matching "collection was modified" semantics). Use +/// invalidate any in-progress enumerator (matching "collection was modified" semantics) — except when +/// the entry is already the most-recently-used one, where the promotion is a no-op that leaves +/// the recency order and any active enumerator untouched. Use /// or to inspect the cache /// without disturbing recency order. /// @@ -155,7 +157,10 @@ public TValue? this[TKey key] /// value of . /// /// true if the key was found; otherwise false. - /// A hit reorders the recency list and therefore invalidates active enumerators. + /// + /// A hit reorders the recency list and therefore invalidates active enumerators, unless the entry + /// is already the most-recently-used one — that promotion is a no-op and leaves them valid. + /// public bool TryGet(TKey key, out TValue? value) { if (!_index.TryGetValue(key, out int node)) @@ -347,8 +352,9 @@ public bool TryPeekMostRecentlyUsed(out TKey? key, out TValue? value) /// Returns an allocation-free struct enumerator that yields each entry in /// most-recently-used to least-recently-used order. Enumeration is a peek: it does not /// change recency. If the cache is modified during enumeration — including by a mutating read - /// ( or the indexer getter) — - /// throws . + /// ( or the indexer getter) that promotes an entry which was + /// not already most-recently-used — throws + /// . /// /// A struct enumerator over this cache. public Enumerator GetEnumerator() => new Enumerator(this); From 02f92c8351dbd62386696a296b41cc4293dbfeed Mon Sep 17 00:00:00 2001 From: Marius Bughiu Date: Thu, 30 Jul 2026 03:22:14 +0300 Subject: [PATCH 2/2] docs(LruCache): state the enumerator's actual invalidation condition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Modified during enumeration" was broader than the code: AddOrUpdate on an entry that is already the head overwrites _nodeValues without bumping _version, so an in-place value overwrite leaves enumerators valid. Name the real condition — a change to the entry set or the recency order — instead. --- src/Celerity/Collections/LruCache.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Celerity/Collections/LruCache.cs b/src/Celerity/Collections/LruCache.cs index 6cb162bf..5e1c0e9a 100644 --- a/src/Celerity/Collections/LruCache.cs +++ b/src/Celerity/Collections/LruCache.cs @@ -351,10 +351,12 @@ public bool TryPeekMostRecentlyUsed(out TKey? key, out TValue? value) /// /// Returns an allocation-free struct enumerator that yields each entry in /// most-recently-used to least-recently-used order. Enumeration is a peek: it does not - /// change recency. If the cache is modified during enumeration — including by a mutating read - /// ( or the indexer getter) that promotes an entry which was - /// not already most-recently-used — throws - /// . + /// change recency. throws + /// when the entry set or the recency order changed since the enumerator was taken: an insert, an + /// eviction, a , a , or any read or write that + /// promoted an entry which was not already most-recently-used. Overwriting the value of an entry + /// without moving it is not a structural change and leaves active enumerators valid, matching the + /// rest of the library. /// /// A struct enumerator over this cache. public Enumerator GetEnumerator() => new Enumerator(this);