Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d079563
perf(runtime): cache local grain message targets
ReubenBond Aug 27, 2026
59514a1
perf(runtime): streamline cached local dispatch
ReubenBond Aug 27, 2026
a018630
perf(benchmarks): add deterministic ping matrix
ReubenBond Aug 27, 2026
d4923a0
perf(benchmarks): add focused routing measurements
ReubenBond Aug 28, 2026
19d236d
perf(runtime): embed message target in directory cache node
ReubenBond Aug 28, 2026
2b88b54
refactor(runtime): generalize message target cache handle
ReubenBond Aug 28, 2026
e654f1e
perf(runtime): cache remote connection groups
ReubenBond Aug 28, 2026
bfb902b
chore: remove test planning artifacts
ReubenBond Aug 28, 2026
471ba70
fix(runtime): prevent stale target binding during cache updates
ReubenBond Aug 28, 2026
086c2ed
perf(benchmarks): strengthen deterministic ping measurements
ReubenBond Aug 28, 2026
270a77f
fix(runtime): bound cached message target handles
ReubenBond Aug 29, 2026
d587fc6
perf(benchmarks): measure retained directory handles
ReubenBond Aug 29, 2026
fd3c3ac
perf(runtime): limit target caching to local activations
ReubenBond Aug 29, 2026
bf421fc
perf(benchmarks): measure directory entry allocation cost
ReubenBond Aug 29, 2026
f2d2168
fix(runtime): refresh retained directory cache entries
ReubenBond Aug 29, 2026
c83c32d
fix(benchmarks): honor deterministic run cancellation
ReubenBond Aug 29, 2026
c774bd0
perf(runtime): retain only local directory entries
ReubenBond Aug 29, 2026
eec280c
perf(benchmarks): include cache touch in handle cost
ReubenBond Aug 29, 2026
d4059ab
test(runtime): stabilize activation replacement coverage
ReubenBond Aug 29, 2026
7bab3d1
fix(runtime): release cached targets on cache disposal
ReubenBond Aug 29, 2026
bad19e7
test(runtime): clarify latency samples and GC checks
ReubenBond Aug 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/Orleans.Core.Abstractions/Runtime/GrainReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,18 @@ public class GrainReference : IAddressable, IEquatable<GrainReference>, ISpanFor
[NonSerialized]
private readonly IdSpan _key;

[NonSerialized]
private object? _messageTargetCache;

internal object? MessageTargetCache
{
get => Volatile.Read(ref _messageTargetCache);
set => Volatile.Write(ref _messageTargetCache, value);
}

internal void ClearMessageTargetCache(object expected)
=> Interlocked.CompareExchange(ref _messageTargetCache, null, expected);

/// <summary>
/// Gets the grain reference functionality which is shared by all grain references of a given type.
/// </summary>
Expand Down
35 changes: 27 additions & 8 deletions src/Orleans.Core/Caching/ConcurrentLruCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ public ConcurrentLruCache(int capacity) : this(capacity, comparer: null)
{
}

protected virtual void UpdateItem(LruItem item, V value) => item.Value = value;

protected virtual void OnItemRemoved(LruItem item)
{
}

/// <summary>
/// Initializes a new instance of the ConcurrentLruCore class with the specified capacity and expire-after-access time to live.
/// </summary>
Expand Down Expand Up @@ -161,15 +167,25 @@ public V Get(K key)
///<inheritdoc/>
public bool TryGet(K key, [MaybeNullWhen(false)] out V value)
{
if (_dictionary.TryGetValue(key, out var item))
if (TryGetItem(key, out var item))
{
value = item.Value;
Touch(item);
_telemetryPolicy.IncrementHit();
return true;
}

value = default;
return false;
}

protected bool TryGetItem(K key, [NotNullWhen(true)] out LruItem? item)
{
if (_dictionary.TryGetValue(key, out item))
{
TouchItem(item);
_telemetryPolicy.IncrementHit();
return true;
}

_telemetryPolicy.IncrementMiss();
return false;
}
Expand Down Expand Up @@ -326,6 +342,7 @@ private void OnRemove(LruItem item, ItemRemovedReason reason)
// from the queue.
item.WasAccessed = false;
item.WasRemoved = true;
OnItemRemoved(item);

if (reason == ItemRemovedReason.Evicted)
{
Expand All @@ -351,7 +368,7 @@ public bool TryUpdate(K key, V value)
{
var oldValue = existing.Value;

existing.Value = value;
UpdateItem(existing, value);
UpdateTimestamp(existing);

_telemetryPolicy.IncrementUpdated();
Expand Down Expand Up @@ -853,7 +870,7 @@ private static ItemDestination RouteCold(LruItem item)
/// <param name="value">The value.</param>
// NOTE: Internal for testing
[DebuggerDisplay("[{Key}] = {Value}")]
internal sealed class LruItem(K key, V value, long timestamp = 0)
internal class LruItem(K key, V value, long timestamp = 0)
{
private V _data = value;

Expand Down Expand Up @@ -1057,11 +1074,13 @@ private async Task RunExpirationLoop()
}
}

private LruItem CreateItem(K key, V value) =>
new(key, value, _expiresAfterAccess ? _timeProvider.GetTimestamp() : 0);
protected virtual LruItem CreateItem(K key, V value) =>
new(key, value, GetCurrentTimestamp());

protected long GetCurrentTimestamp() => _expiresAfterAccess ? _timeProvider.GetTimestamp() : 0;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Touch(LruItem item)
protected void TouchItem(LruItem item)
{
if (_expiresAfterAccess)
{
Expand Down
8 changes: 7 additions & 1 deletion src/Orleans.Core/Runtime/GrainReferenceRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,13 @@ public object Cast(IAddressable grain, Type grainInterface)
}

var interfaceType = this.interfaceTypeResolver.GetGrainInterfaceType(grainInterface);
return this.referenceActivator.CreateReference(grainId, interfaceType);
var result = this.referenceActivator.CreateReference(grainId, interfaceType);
if (grain is GrainReference source)
{
result.MessageTargetCache = source.MessageTargetCache;
}

return result;
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Orleans.Runtime/Core/InsideRuntimeClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ public void SendRequest(
}

this.messagingTrace.OnSendRequest(message);
this.MessageCenter.AddressAndSendMessage(message);
this.MessageCenter.AddressAndSendMessage(message, target);
}

public void SendResponse(Message request, Response response)
Expand Down
14 changes: 14 additions & 0 deletions src/Orleans.Runtime/GrainDirectory/CachedGrainLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,20 @@ public void UpdateCache(GrainId grainId, SiloAddress siloAddress)
}
public void InvalidateCache(GrainId grainId) => cache.Remove(grainId);
public void InvalidateCache(GrainAddress address) => cache.Remove(address);

internal bool TryGetCacheEntry(GrainId grainId, SiloAddress siloAddress, [NotNullWhen(true)] out GrainDirectoryCacheEntry? entry)
{
if (cache is IGrainDirectoryCacheEntrySource entrySource
&& entrySource.TryGetEntry(grainId, out entry)
&& entry.Address.SiloAddress?.Equals(siloAddress) == true)
{
return true;
}

entry = null;
return false;
}

public bool TryLookupInCache(GrainId grainId, [NotNullWhen(true)] out GrainAddress? address)
{
var grainType = grainId.Type;
Expand Down
11 changes: 11 additions & 0 deletions src/Orleans.Runtime/GrainDirectory/DhtGrainLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,17 @@ public static DhtGrainLocator FromLocalGrainDirectory(LocalGrainDirectory localG
public void InvalidateCache(GrainAddress address) => _localGrainDirectory.InvalidateCacheEntry(address);
public bool TryLookupInCache(GrainId grainId, [NotNullWhen(true)] out GrainAddress? address) => _localGrainDirectory.TryLocalLookup(grainId, out address);

internal bool TryGetCacheEntry(GrainId grainId, SiloAddress siloAddress, [NotNullWhen(true)] out GrainDirectoryCacheEntry? entry)
{
if (_localGrainDirectory is LocalGrainDirectory directory)
{
return directory.TryGetCacheEntry(grainId, siloAddress, out entry);
}

entry = null;
return false;
}

private class BatchedDeregistrationWorker
{
private const int OperationBatchSizeLimit = 2_000;
Expand Down
180 changes: 180 additions & 0 deletions src/Orleans.Runtime/GrainDirectory/GrainDirectoryCacheEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
using System.Threading;
using Orleans.Caching;

namespace Orleans.Runtime.GrainDirectory;

internal sealed class GrainDirectoryCacheEntry
: ConcurrentLruCache<GrainId, (GrainAddress ActivationAddress, int Version)>.LruItem,
IDisposable
{
private static readonly object Invalidated = new();
private static readonly object Updating = new();
private readonly LruGrainDirectoryCache? _owner;
private readonly WeakReference<GrainDirectoryCacheEntry> _referenceHandle;
private object? _messageTarget;

public GrainDirectoryCacheEntry(GrainAddress address, int version)
: this(owner: null, address.GrainId, (address, version), timestamp: 0)
{
}

public GrainDirectoryCacheEntry(
LruGrainDirectoryCache? owner,
GrainId grainId,
(GrainAddress ActivationAddress, int Version) value,
long timestamp)
: base(grainId, value, timestamp)
{
_owner = owner;
_referenceHandle = new(this);
}

public GrainAddress Address => Value.ActivationAddress;

public int Version => Value.Version;

public WeakReference<GrainDirectoryCacheEntry> ReferenceHandle => _referenceHandle;

public bool TryTouch()
{
if (!IsValid)
{
return false;
}

_owner?.Touch(this);
return IsValid;
}

public bool IsValid
{
get
{
var target = Volatile.Read(ref _messageTarget);
return !ReferenceEquals(target, Invalidated) && !ReferenceEquals(target, Updating);
}
}

public bool TryGetMessageTarget(out object? messageTarget)
{
var target = Volatile.Read(ref _messageTarget);
if (ReferenceEquals(target, Invalidated) || ReferenceEquals(target, Updating))
{
messageTarget = null;
return false;
}

messageTarget = target;
return messageTarget is not null;
}

public bool TrySetMessageTarget(object messageTarget, GrainAddress expectedAddress)
{
ArgumentNullException.ThrowIfNull(messageTarget);
ArgumentNullException.ThrowIfNull(expectedAddress);
if (!Address.Matches(expectedAddress) || !TrySetMessageTargetCore(messageTarget))
{
return false;
}

if (Address.Matches(expectedAddress))
{
return true;
}

ClearMessageTarget(messageTarget);
return false;
}

public bool TrySetMessageTarget(object messageTarget, SiloAddress expectedSilo)
{
ArgumentNullException.ThrowIfNull(messageTarget);
ArgumentNullException.ThrowIfNull(expectedSilo);
if (Address.SiloAddress?.Equals(expectedSilo) != true || !TrySetMessageTargetCore(messageTarget))
{
return false;
}

if (Address.SiloAddress?.Equals(expectedSilo) == true)
{
return true;
}

ClearMessageTarget(messageTarget);
return false;
}

private bool TrySetMessageTargetCore(object messageTarget)
{
var current = Volatile.Read(ref _messageTarget);
if (ReferenceEquals(current, Invalidated) || ReferenceEquals(current, Updating))
{
return false;
}

return ReferenceEquals(current, messageTarget)
|| current is null && Interlocked.CompareExchange(ref _messageTarget, messageTarget, null) is null;
}

public void ClearMessageTarget(object messageTarget)
{
ArgumentNullException.ThrowIfNull(messageTarget);
Interlocked.CompareExchange(ref _messageTarget, null, messageTarget);
}

public void Invalidate() => Interlocked.Exchange(ref _messageTarget, Invalidated);

public void Dispose() => Invalidate();

internal void Update((GrainAddress ActivationAddress, int Version) value)
{
var updateStarted = TryBeginUpdate();
try
{
Value = value;
}
finally
{
if (updateStarted)
{
EndUpdate();
}
}
}

public void ClearMessageTarget()
{
while (true)
{
var current = Volatile.Read(ref _messageTarget);
if (current is null || ReferenceEquals(current, Invalidated))
{
return;
}

if (ReferenceEquals(Interlocked.CompareExchange(ref _messageTarget, null, current), current))
{
return;
}
}
}

internal bool TryBeginUpdate()
{
while (true)
{
var current = Volatile.Read(ref _messageTarget);
if (ReferenceEquals(current, Invalidated))
{
return false;
}

if (ReferenceEquals(Interlocked.CompareExchange(ref _messageTarget, Updating, current), current))
{
return true;
}
}
}

internal void EndUpdate() => Interlocked.CompareExchange(ref _messageTarget, null, Updating);
}
20 changes: 20 additions & 0 deletions src/Orleans.Runtime/GrainDirectory/GrainLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,26 @@ public GrainLocator(GrainLocatorResolver grainLocatorResolver, DirectoryInstrume

public bool TryLookupInCache(GrainId grainId, [NotNullWhen(true)] out GrainAddress? address) => GetGrainLocator(grainId.Type).TryLookupInCache(grainId, out address);

internal bool TryGetCacheEntry(
GrainId grainId,
SiloAddress siloAddress,
[NotNullWhen(true)] out GrainDirectoryCacheEntry? entry)
{
var grainLocator = GetGrainLocator(grainId.Type);
return grainLocator switch
{
CachedGrainLocator cached => cached.TryGetCacheEntry(grainId, siloAddress, out entry),
DhtGrainLocator dht => dht.TryGetCacheEntry(grainId, siloAddress, out entry),
_ => ReturnFalse(out entry),
};

static bool ReturnFalse(out GrainDirectoryCacheEntry? result)
{
result = null;
return false;
}
}

public void InvalidateCache(GrainId grainId) => GetGrainLocator(grainId.Type).InvalidateCache(grainId);

public void InvalidateCache(GrainAddress address) => GetGrainLocator(address.GrainId.Type).InvalidateCache(address);
Expand Down
5 changes: 5 additions & 0 deletions src/Orleans.Runtime/GrainDirectory/IGrainDirectoryCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,9 @@ public static bool LookUp(this IGrainDirectoryCache cache, GrainId key, [NotNull
return cache.LookUp(key, out result, out _);
}
}

internal interface IGrainDirectoryCacheEntrySource
{
bool TryGetEntry(GrainId key, [NotNullWhen(true)] out GrainDirectoryCacheEntry? entry);
}
}
Loading