You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
InMemoryCacheProvider.GetOrAdd overwrites a concurrently Set value, and racing callers get an instance that isn't the cached one #85
InMemoryCacheProvider (Essentials.CacheProviders.InMemory/InMemoryCacheProvider.cs) doesn't declare GetOrAdd, so it inherits the default body at Essentials/ICacheProvider.cs:77-88:
Check and write are separate, and Set overwrites unconditionally:
A concurrent Set is lost. Thread A misses and runs a slow factory; thread B calls Set(key, fresh); A's Set then replaces B's value. Deterministic repro:
stringr=cache.GetOrAdd("k", key =>{cache.Set(key,"fresh-from-concurrent-writer");return"factory";});// cache now holds "factory"; the concurrent writer's value is gone
Racing callers diverge. Two threads calling GetOrAdd("k", _ => new object()) on a cold key (synchronised with a Barrier) each get a different instance; only the last writer's is cached. Measured: same instance: False; caller0 holds cached: False; caller1 holds cached: True.
Why it matters
The name and the ConcurrentDictionary backing both imply add-if-absent semantics: ConcurrentDictionary.GetOrAdd may run the factory twice but always returns the stored value and never overwrites. Callers using GetOrAdd to share a single instance (connections, locks, lazily built objects) silently get duplicates. #47 fixed the mirror-image race (expired-entry removal deleting a fresh Set) but did not touch GetOrAdd.
Suggested fix / acceptance criteria
Declare GetOrAdd on InMemoryCacheProvider: run the factory, then TryAdd; if an unexpired entry exists, return it and discard the factory result; if an expired entry exists, replace only that observed entry via TryUpdate(key, newEntry, observed) with retry.
GetOrAddAsync picks up the fix through its default.
Priority: Medium. This is a real concurrency correctness bug: a concurrent Set is lost, and racing callers get different instances. It only bites under contention, and callers relying on GetOrAdd for single-instance sharing are the ones affected.
Activity
matt-edmondson commentedon Oct 6, 2026
Triage
Setis lost, and racing callers get different instances. It only bites under contention, and callers relying onGetOrAddfor single-instance sharing are the ones affected.Essentials.CacheProviders.InMemory,ICacheProviderdefault interface methods).ICacheProvider.GetOrAdd.Generated by Claude Code