From 87fd00db6e5fce67e222bcaba2461be3ab0ad8a0 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Wed, 20 May 2026 10:51:11 -0700 Subject: [PATCH 1/3] Wire grain directory metrics Reconnect grain directory cache lookup counters to the current lookup paths, restore LRU directory cache size observation, and remove the obsolete LocalLookup path which held stale metric call sites. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Metrics/DirectoryInstruments.cs | 64 +++++++++++- .../Diagnostics/Metrics/InstrumentNames.cs | 4 - .../GrainDirectory/CachedGrainLocator.cs | 2 + .../GrainDirectory/DhtGrainLocator.cs | 12 ++- .../DistributedRemoteGrainDirectory.cs | 1 + .../GrainDirectory/ILocalGrainDirectory.cs | 10 -- .../GrainDirectory/LocalGrainDirectory.cs | 99 +++---------------- .../GrainDirectory/LruGrainDirectoryCache.cs | 32 ++++-- .../Directory/MockLocalGrainDirectory.cs | 5 - 9 files changed, 110 insertions(+), 119 deletions(-) diff --git a/src/Orleans.Core/Diagnostics/Metrics/DirectoryInstruments.cs b/src/Orleans.Core/Diagnostics/Metrics/DirectoryInstruments.cs index dc2ffc10190..00555008225 100644 --- a/src/Orleans.Core/Diagnostics/Metrics/DirectoryInstruments.cs +++ b/src/Orleans.Core/Diagnostics/Metrics/DirectoryInstruments.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics.Metrics; #nullable disable @@ -6,6 +7,8 @@ namespace Orleans.Runtime; internal static class DirectoryInstruments { + private static readonly List CacheSizeObservers = new(); + internal static readonly Counter LookupsLocalIssued = Instruments.Meter.CreateCounter(InstrumentNames.DIRECTORY_LOOKUPS_LOCAL_ISSUED); internal static readonly Counter LookupsLocalSuccesses = Instruments.Meter.CreateCounter(InstrumentNames.DIRECTORY_LOOKUPS_LOCAL_SUCCESSES); @@ -34,10 +37,18 @@ internal static void RegisterDirectoryPartitionSizeObserve(Func observeValu DirectoryPartitionSize = Instruments.Meter.CreateObservableGauge(InstrumentNames.DIRECTORY_PARTITION_SIZE, observeValue); } - internal static ObservableGauge CacheSize; - internal static void RegisterCacheSizeObserve(Func observeValue) + internal static readonly ObservableGauge CacheSize = Instruments.Meter.CreateObservableGauge(InstrumentNames.DIRECTORY_CACHE_SIZE, ObserveCacheSize); + internal static IDisposable RegisterCacheSizeObserve(Func observeValue) { - CacheSize = Instruments.Meter.CreateObservableGauge(InstrumentNames.DIRECTORY_CACHE_SIZE, observeValue); + ArgumentNullException.ThrowIfNull(observeValue); + + var registration = new CacheSizeObserverRegistration(observeValue); + lock (CacheSizeObservers) + { + CacheSizeObservers.Add(registration); + } + + return registration; } internal static ObservableGauge RingSize; @@ -75,4 +86,51 @@ internal static void RegisterMyPortionAverageRingPercentageObserve(Func o internal static readonly Counter UnregistrationsManyIssued = Instruments.Meter.CreateCounter(InstrumentNames.DIRECTORY_UNREGISTRATIONS_MANY_ISSUED); internal static readonly Counter UnregistrationsManyRemoteSent = Instruments.Meter.CreateCounter(InstrumentNames.DIRECTORY_UNREGISTRATIONS_MANY_REMOTE_SENT); internal static readonly Counter UnregistrationsManyRemoteReceived = Instruments.Meter.CreateCounter(InstrumentNames.DIRECTORY_UNREGISTRATIONS_MANY_REMOTE_RECEIVED); + + private static int ObserveCacheSize() + { + CacheSizeObserverRegistration[] observers; + lock (CacheSizeObservers) + { + observers = CacheSizeObservers.ToArray(); + } + + var result = 0; + foreach (var observer in observers) + { + result += observer.Observe(); + } + + return result; + } + + private sealed class CacheSizeObserverRegistration : IDisposable + { + private Func observeValue; + + public CacheSizeObserverRegistration(Func observeValue) + { + this.observeValue = observeValue; + } + + public int Observe() + { + var observer = observeValue; + return observer is null ? 0 : observer(); + } + + public void Dispose() + { + lock (CacheSizeObservers) + { + if (observeValue is null) + { + return; + } + + observeValue = null; + CacheSizeObservers.Remove(this); + } + } + } } diff --git a/src/Orleans.Core/Diagnostics/Metrics/InstrumentNames.cs b/src/Orleans.Core/Diagnostics/Metrics/InstrumentNames.cs index 93c7f0d9b64..51e952ff2e5 100644 --- a/src/Orleans.Core/Diagnostics/Metrics/InstrumentNames.cs +++ b/src/Orleans.Core/Diagnostics/Metrics/InstrumentNames.cs @@ -50,18 +50,14 @@ internal static class InstrumentNames public const string CATALOG_ACTIVATION_CONCURRENT_REGISTRATION_ATTEMPTS = "orleans-catalog-activation-concurrent-registration-attempts"; // Directory - // not used... public const string DIRECTORY_LOOKUPS_LOCAL_ISSUED = "orleans-directory-lookups-local-issued"; - // not used... public const string DIRECTORY_LOOKUPS_LOCAL_SUCCESSES = "orleans-directory-lookups-local-successes"; public const string DIRECTORY_LOOKUPS_FULL_ISSUED = "orleans-directory-lookups-full-issued"; public const string DIRECTORY_LOOKUPS_REMOTE_SENT = "orleans-directory-lookups-remote-sent"; public const string DIRECTORY_LOOKUPS_REMOTE_RECEIVED = "orleans-directory-lookups-remote-received"; public const string DIRECTORY_LOOKUPS_LOCALDIRECTORY_ISSUED = "orleans-directory-lookups-local-directory-issued"; public const string DIRECTORY_LOOKUPS_LOCALDIRECTORY_SUCCESSES = "orleans-directory-lookups-local-directory-successes"; - // not used public const string DIRECTORY_LOOKUPS_CACHE_ISSUED = "orleans-directory-lookups-cache-issued"; - // not used public const string DIRECTORY_LOOKUPS_CACHE_SUCCESSES = "orleans-directory-lookups-cache-successes"; public const string DIRECTORY_VALIDATIONS_CACHE_SENT = "orleans-directory-validations-cache-sent"; public const string DIRECTORY_VALIDATIONS_CACHE_RECEIVED = "orleans-directory-validations-cache-received"; diff --git a/src/Orleans.Runtime/GrainDirectory/CachedGrainLocator.cs b/src/Orleans.Runtime/GrainDirectory/CachedGrainLocator.cs index 32cd76dc558..3f03e60ee3b 100644 --- a/src/Orleans.Runtime/GrainDirectory/CachedGrainLocator.cs +++ b/src/Orleans.Runtime/GrainDirectory/CachedGrainLocator.cs @@ -217,6 +217,7 @@ public bool TryLookupInCache(GrainId grainId, out GrainAddress address) ThrowUnsupportedGrainType(grainId); } + DirectoryInstruments.LookupsCacheIssued.Add(1); if (this.cache.LookUp(grainId, out address, out _)) { // If the silo is dead, remove the entry @@ -228,6 +229,7 @@ public bool TryLookupInCache(GrainId grainId, out GrainAddress address) else { // Entry found and valid -> return it + DirectoryInstruments.LookupsCacheSuccesses.Add(1); return true; } } diff --git a/src/Orleans.Runtime/GrainDirectory/DhtGrainLocator.cs b/src/Orleans.Runtime/GrainDirectory/DhtGrainLocator.cs index 1ad5994ceed..455d0820581 100644 --- a/src/Orleans.Runtime/GrainDirectory/DhtGrainLocator.cs +++ b/src/Orleans.Runtime/GrainDirectory/DhtGrainLocator.cs @@ -79,7 +79,17 @@ public static DhtGrainLocator FromLocalGrainDirectory(LocalGrainDirectory localG public void UpdateCache(GrainId grainId, SiloAddress siloAddress) => _localGrainDirectory.AddOrUpdateCacheEntry(grainId, siloAddress); public void InvalidateCache(GrainId grainId) => _localGrainDirectory.InvalidateCacheEntry(grainId); public void InvalidateCache(GrainAddress address) => _localGrainDirectory.InvalidateCacheEntry(address); - public bool TryLookupInCache(GrainId grainId, out GrainAddress address) => _localGrainDirectory.TryCachedLookup(grainId, out address); + public bool TryLookupInCache(GrainId grainId, out GrainAddress address) + { + DirectoryInstruments.LookupsLocalIssued.Add(1); + if (!_localGrainDirectory.TryCachedLookup(grainId, out address)) + { + return false; + } + + DirectoryInstruments.LookupsLocalSuccesses.Add(1); + return true; + } private class BatchedDeregistrationWorker { diff --git a/src/Orleans.Runtime/GrainDirectory/DistributedRemoteGrainDirectory.cs b/src/Orleans.Runtime/GrainDirectory/DistributedRemoteGrainDirectory.cs index 1db448f1b8a..d3692674fa3 100644 --- a/src/Orleans.Runtime/GrainDirectory/DistributedRemoteGrainDirectory.cs +++ b/src/Orleans.Runtime/GrainDirectory/DistributedRemoteGrainDirectory.cs @@ -312,6 +312,7 @@ public async Task RegisterMany(List addresses) public async Task> LookUpMany(List<(GrainId GrainId, int Version)> grainAndETagList) { + DirectoryInstruments.ValidationsCacheReceived.Add(1); LogInformationLookUpManyReceived(_logger, Silo, grainAndETagList.Count); using (var cts = CreateTimeoutCts(_directory.OnStoppedToken)) diff --git a/src/Orleans.Runtime/GrainDirectory/ILocalGrainDirectory.cs b/src/Orleans.Runtime/GrainDirectory/ILocalGrainDirectory.cs index c5e0f07f44f..036d21547c7 100644 --- a/src/Orleans.Runtime/GrainDirectory/ILocalGrainDirectory.cs +++ b/src/Orleans.Runtime/GrainDirectory/ILocalGrainDirectory.cs @@ -35,16 +35,6 @@ internal interface ILocalGrainDirectory : IDhtGrainDirectory /// the silo from which the message to the non-existing activation was sent Task UnregisterAfterNonexistingActivation(GrainAddress address, SiloAddress origin); - /// - /// Fetches locally known directory information for a grain. - /// If there is no local information, either in the cache or in this node's directory partition, - /// then this method will return false and leave the list empty. - /// - /// The ID of the grain to look up. - /// An output parameter that receives the list of locally-known activations of the grain. - /// True if remote addresses are complete within freshness constraint - bool LocalLookup(GrainId grain, out AddressAndTag addresses); - /// /// Invalidates cache entry for the given activation address. /// This method is intended to be called whenever a directory client tries to access diff --git a/src/Orleans.Runtime/GrainDirectory/LocalGrainDirectory.cs b/src/Orleans.Runtime/GrainDirectory/LocalGrainDirectory.cs index e8ffae913fc..a3e1e0ebb34 100644 --- a/src/Orleans.Runtime/GrainDirectory/LocalGrainDirectory.cs +++ b/src/Orleans.Runtime/GrainDirectory/LocalGrainDirectory.cs @@ -788,63 +788,6 @@ public async Task UnregisterManyAsync(List addresses, Unregistrati } - public bool LocalLookup(GrainId grain, out AddressAndTag result) - { - DirectoryInstruments.LookupsLocalIssued.Add(1); - - var silo = CalculateGrainDirectoryPartition(grain); - - LogDebugLocalLookupAttempt( - MyAddress, - grain, - silo, - new(grain), - new(silo)); - - //this will only happen if I'm the only silo in the cluster and I'm shutting down - if (silo == null) - { - LogTraceLocalLookupMineNull(grain); - result = default; - return false; - } - - // handle cache - DirectoryInstruments.LookupsCacheIssued.Add(1); - var address = GetLocalCacheData(grain); - if (address != default) - { - result = new(address, 0); - - LogTraceLocalLookupCache(grain, result.Address); - DirectoryInstruments.LookupsCacheSuccesses.Add(1); - DirectoryInstruments.LookupsLocalSuccesses.Add(1); - return true; - } - - // check if we own the grain - if (silo.Equals(MyAddress)) - { - DirectoryInstruments.LookupsLocalDirectoryIssued.Add(1); - result = GetLocalDirectoryData(grain); - if (result.Address == null) - { - // it can happen that we cannot find the grain in our partition if there were - // some recent changes in the membership - LogTraceLocalLookupMineNull(grain); - return false; - } - LogTraceLocalLookupMine(grain, result.Address); - DirectoryInstruments.LookupsLocalDirectorySuccesses.Add(1); - DirectoryInstruments.LookupsLocalSuccesses.Add(1); - return true; - } - - LogTraceTryFullLookupElse(grain); - result = default; - return false; - } - public AddressAndTag GetLocalDirectoryData(GrainId grain) => DirectoryPartition.LookUpActivation(grain); public GrainAddress? GetLocalCacheData(GrainId grain) @@ -1008,7 +951,17 @@ private static int CompareSiloAddress(SiloAddress left, SiloAddress right) } public void AddOrUpdateCacheEntry(GrainId grainId, SiloAddress siloAddress) => this.DirectoryCache.AddOrUpdate(new GrainAddress { GrainId = grainId, SiloAddress = siloAddress }, 0); - public bool TryCachedLookup(GrainId grainId, [NotNullWhen(true)] out GrainAddress? address) => (address = GetLocalCacheData(grainId)) is not null; + public bool TryCachedLookup(GrainId grainId, [NotNullWhen(true)] out GrainAddress? address) + { + DirectoryInstruments.LookupsCacheIssued.Add(1); + if ((address = GetLocalCacheData(grainId)) is null) + { + return false; + } + + DirectoryInstruments.LookupsCacheSuccesses.Add(1); + return true; + } void ILifecycleParticipant.Participate(ISiloLifecycle lifecycle) { lifecycle.Subscribe(ServiceLifecycleStage.RuntimeServices, (ct) => Task.Run(() => Start()), (ct) => Task.Run(() => StopAsync())); @@ -1136,36 +1089,6 @@ private readonly struct SiloHashLogValue(SiloAddress? silo) )] private partial void LogWarningUnregisterManyAsyncNotOwner(int count, int hopCount); - [LoggerMessage( - Level = LogLevel.Debug, - Message = "Silo {SiloAddress} tries to lookup for {Grain}-->{PartitionOwner} ({GrainHashCode}-->{PartitionOwnerHashCode})" - )] - private partial void LogDebugLocalLookupAttempt(SiloAddress siloAddress, GrainId grain, SiloAddress? partitionOwner, GrainHashLogValue grainHashCode, SiloHashLogValue partitionOwnerHashCode); - - [LoggerMessage( - Level = LogLevel.Trace, - Message = "LocalLookup mine {GrainId}=null" - )] - private partial void LogTraceLocalLookupMineNull(GrainId grainId); - - [LoggerMessage( - Level = LogLevel.Trace, - Message = "LocalLookup cache {GrainId}={TargetAddress}" - )] - private partial void LogTraceLocalLookupCache(GrainId grainId, GrainAddress? targetAddress); - - [LoggerMessage( - Level = LogLevel.Trace, - Message = "LocalLookup mine {GrainId}={Address}" - )] - private partial void LogTraceLocalLookupMine(GrainId grainId, GrainAddress? address); - - [LoggerMessage( - Level = LogLevel.Trace, - Message = "TryFullLookup else {GrainId}=null" - )] - private partial void LogTraceTryFullLookupElse(GrainId grainId); - [LoggerMessage( Level = LogLevel.Warning, Message = "LookupAsync - It seems we are not the owner of grain {GrainId} (hash: {Hash:X}), trying to forward it to {ForwardAddress} (hopCount={HopCount})" diff --git a/src/Orleans.Runtime/GrainDirectory/LruGrainDirectoryCache.cs b/src/Orleans.Runtime/GrainDirectory/LruGrainDirectoryCache.cs index f89a835fd35..5b15cf8d8a4 100644 --- a/src/Orleans.Runtime/GrainDirectory/LruGrainDirectoryCache.cs +++ b/src/Orleans.Runtime/GrainDirectory/LruGrainDirectoryCache.cs @@ -1,20 +1,28 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Orleans.Caching; #nullable disable namespace Orleans.Runtime.GrainDirectory; -internal sealed class LruGrainDirectoryCache( - int maxCacheSize, - TimeSpan maxCacheTTL, - TimeProvider timeProvider) : ConcurrentLruCache( - capacity: maxCacheSize, - comparer: null, - timeToLive: maxCacheTTL, - timeProvider: timeProvider), IGrainDirectoryCache +internal sealed class LruGrainDirectoryCache : ConcurrentLruCache, IGrainDirectoryCache, IAsyncDisposable { private static readonly Func<(GrainAddress Address, int Version), GrainAddress, bool> ActivationAddressesMatch = (value, state) => GrainAddress.MatchesGrainIdAndSilo(state, value.Address); + private readonly IDisposable _cacheSizeRegistration; + + public LruGrainDirectoryCache( + int maxCacheSize, + TimeSpan maxCacheTTL, + TimeProvider timeProvider) + : base( + capacity: maxCacheSize, + comparer: null, + timeToLive: maxCacheTTL, + timeProvider: timeProvider) + { + _cacheSizeRegistration = DirectoryInstruments.RegisterCacheSizeObserve(() => Count); + } public void AddOrUpdate(GrainAddress activationAddress, int version) => AddOrUpdate(activationAddress.GrainId, (activationAddress, version)); @@ -46,4 +54,12 @@ public bool LookUp(GrainId key, out GrainAddress result, out int version) } } } + + public new async ValueTask DisposeAsync() + { + _cacheSizeRegistration.Dispose(); + await base.DisposeAsync(); + } + + async ValueTask IAsyncDisposable.DisposeAsync() => await DisposeAsync(); } diff --git a/test/Orleans.Core.Tests/Directory/MockLocalGrainDirectory.cs b/test/Orleans.Core.Tests/Directory/MockLocalGrainDirectory.cs index f72b9123195..32308e886c3 100644 --- a/test/Orleans.Core.Tests/Directory/MockLocalGrainDirectory.cs +++ b/test/Orleans.Core.Tests/Directory/MockLocalGrainDirectory.cs @@ -78,11 +78,6 @@ public bool IsSiloInCluster(SiloAddress silo) throw new NotImplementedException(); } - public bool LocalLookup(GrainId grain, out AddressAndTag addresses) - { - throw new NotImplementedException(); - } - public Task LookupAsync(GrainId grainId, int hopCount = 0) { throw new NotImplementedException(); From 5efd728820f0814351db9b4e8d230d3a36a61aa1 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Wed, 20 May 2026 10:53:18 -0700 Subject: [PATCH 2/3] Remove stale cache validation sent metric Remove the grain directory cache validation sent instrument since the current runtime no longer has an adaptive cache validation sender. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Orleans.Core/Diagnostics/Metrics/DirectoryInstruments.cs | 1 - src/Orleans.Core/Diagnostics/Metrics/InstrumentNames.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Orleans.Core/Diagnostics/Metrics/DirectoryInstruments.cs b/src/Orleans.Core/Diagnostics/Metrics/DirectoryInstruments.cs index 00555008225..53c190d9c82 100644 --- a/src/Orleans.Core/Diagnostics/Metrics/DirectoryInstruments.cs +++ b/src/Orleans.Core/Diagnostics/Metrics/DirectoryInstruments.cs @@ -22,7 +22,6 @@ internal static class DirectoryInstruments internal static readonly Counter LookupsCacheIssued = Instruments.Meter.CreateCounter(InstrumentNames.DIRECTORY_LOOKUPS_CACHE_ISSUED); internal static readonly Counter LookupsCacheSuccesses = Instruments.Meter.CreateCounter(InstrumentNames.DIRECTORY_LOOKUPS_CACHE_SUCCESSES); - internal static readonly Counter ValidationsCacheSent = Instruments.Meter.CreateCounter(InstrumentNames.DIRECTORY_VALIDATIONS_CACHE_SENT); internal static readonly Counter ValidationsCacheReceived = Instruments.Meter.CreateCounter(InstrumentNames.DIRECTORY_VALIDATIONS_CACHE_RECEIVED); internal static readonly Counter SnapshotTransferCount = Instruments.Meter.CreateCounter(InstrumentNames.DIRECTORY_RANGE_SNAPSHOT_TRANSFER_COUNT); diff --git a/src/Orleans.Core/Diagnostics/Metrics/InstrumentNames.cs b/src/Orleans.Core/Diagnostics/Metrics/InstrumentNames.cs index 51e952ff2e5..7c71651cbf1 100644 --- a/src/Orleans.Core/Diagnostics/Metrics/InstrumentNames.cs +++ b/src/Orleans.Core/Diagnostics/Metrics/InstrumentNames.cs @@ -59,7 +59,6 @@ internal static class InstrumentNames public const string DIRECTORY_LOOKUPS_LOCALDIRECTORY_SUCCESSES = "orleans-directory-lookups-local-directory-successes"; public const string DIRECTORY_LOOKUPS_CACHE_ISSUED = "orleans-directory-lookups-cache-issued"; public const string DIRECTORY_LOOKUPS_CACHE_SUCCESSES = "orleans-directory-lookups-cache-successes"; - public const string DIRECTORY_VALIDATIONS_CACHE_SENT = "orleans-directory-validations-cache-sent"; public const string DIRECTORY_VALIDATIONS_CACHE_RECEIVED = "orleans-directory-validations-cache-received"; public const string DIRECTORY_PARTITION_SIZE = "orleans-directory-partition-size"; public const string DIRECTORY_CACHE_SIZE = "orleans-directory-cache-size"; From d0bd4222274c48608784bd7bf2dd6958bb91f400 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Wed, 20 May 2026 11:08:51 -0700 Subject: [PATCH 3/3] Optimize directory cache size observer registry Use immutable copy-on-write updates for cache size observers so observation can iterate without locks or per-observation array allocations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Metrics/DirectoryInstruments.cs | 31 +++++-------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/src/Orleans.Core/Diagnostics/Metrics/DirectoryInstruments.cs b/src/Orleans.Core/Diagnostics/Metrics/DirectoryInstruments.cs index 53c190d9c82..3028827f8ba 100644 --- a/src/Orleans.Core/Diagnostics/Metrics/DirectoryInstruments.cs +++ b/src/Orleans.Core/Diagnostics/Metrics/DirectoryInstruments.cs @@ -1,13 +1,14 @@ using System; -using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics.Metrics; +using System.Threading; #nullable disable namespace Orleans.Runtime; internal static class DirectoryInstruments { - private static readonly List CacheSizeObservers = new(); + private static ImmutableArray CacheSizeObservers = []; internal static readonly Counter LookupsLocalIssued = Instruments.Meter.CreateCounter(InstrumentNames.DIRECTORY_LOOKUPS_LOCAL_ISSUED); internal static readonly Counter LookupsLocalSuccesses = Instruments.Meter.CreateCounter(InstrumentNames.DIRECTORY_LOOKUPS_LOCAL_SUCCESSES); @@ -42,11 +43,7 @@ internal static IDisposable RegisterCacheSizeObserve(Func observeValue) ArgumentNullException.ThrowIfNull(observeValue); var registration = new CacheSizeObserverRegistration(observeValue); - lock (CacheSizeObservers) - { - CacheSizeObservers.Add(registration); - } - + ImmutableInterlocked.Update(ref CacheSizeObservers, static (observers, registration) => observers.Add(registration), registration); return registration; } @@ -88,14 +85,8 @@ internal static void RegisterMyPortionAverageRingPercentageObserve(Func o private static int ObserveCacheSize() { - CacheSizeObserverRegistration[] observers; - lock (CacheSizeObservers) - { - observers = CacheSizeObservers.ToArray(); - } - var result = 0; - foreach (var observer in observers) + foreach (var observer in CacheSizeObservers) { result += observer.Observe(); } @@ -114,21 +105,15 @@ public CacheSizeObserverRegistration(Func observeValue) public int Observe() { - var observer = observeValue; + var observer = Volatile.Read(ref observeValue); return observer is null ? 0 : observer(); } public void Dispose() { - lock (CacheSizeObservers) + if (Interlocked.Exchange(ref observeValue, null) is not null) { - if (observeValue is null) - { - return; - } - - observeValue = null; - CacheSizeObservers.Remove(this); + ImmutableInterlocked.Update(ref CacheSizeObservers, static (observers, registration) => observers.Remove(registration), this); } } }