Skip to content

perf(runtime): add safe pooled CallbackData reuse#10067

Draft
ReubenBond wants to merge 1 commit into
dotnet:mainfrom
ReubenBond:split/callbackdata-pooling
Draft

perf(runtime): add safe pooled CallbackData reuse#10067
ReubenBond wants to merge 1 commit into
dotnet:mainfrom
ReubenBond:split/callbackdata-pooling

Conversation

@ReubenBond

@ReubenBond ReubenBond commented Apr 30, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds pooled CallbackData reuse via CallbackDataPool, CallbackDataOwner, and lease-based lifetime management.
  • Updates outside and inside runtime client callback registration, response completion, cancellation, unregister, failover, and timeout paths to return callbacks to the pool safely.

Validation

  • git diff --check
  • conflict-marker scan
  • dotnet build src\Orleans.Core\Orleans.Core.csproj -m
  • dotnet build src\Orleans.Runtime\Orleans.Runtime.csproj -m
  • gh pr checks 10067 --repo dotnet/orleans (62 checks passing)

Notes

  • This branch was adapted to current dictionaries and does not require the striped callback dictionary PR.
  • No focused pooling/concurrency tests were added. Follow-up coverage should include response/status/cancel/unregister races, reuse-after-return, wrong-correlation status updates, double-release/ref-count underflow, and cancellation registration disposal ordering.
Microsoft Reviewers: Open in CodeFlow

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a safe, pooled reuse mechanism for CallbackData to reduce allocations in Orleans runtime/client request tracking, using ref-counted ownership + lease-based access to avoid reuse races across response, cancellation, timeout, and failover paths.

Changes:

  • Introduces CallbackDataPool (thread-local pooling) and adds ref-count/lease lifetime management to CallbackData via CallbackDataOwner/CallbackDataLease.
  • Updates OutsideRuntimeClient and InsideRuntimeClient callback registration/removal and response/status/timeout/failover paths to use leases and return callbacks to the pool safely.
  • Adjusts InsideRuntimeClient.Invoke around invokable disposal (currently leaving disposal out of the method body).
Show a summary per file
File Description
src/Orleans.Runtime/Core/InsideRuntimeClient.cs Switches callback dictionary values to pooled CallbackDataOwner with lease-based access in response/status/failover/expiry paths.
src/Orleans.Core/Runtime/OutsideRuntimeClient.cs Same pooled callback owner/lease approach for the external client runtime callback lifecycle.
src/Orleans.Core/Runtime/CallbackDataPool.cs Adds thread-local CallbackData pooling and centralized return/reset via ref counting.
src/Orleans.Core/Runtime/CallbackData.cs Adds pooling support (Initialize/Reset), ref counting, correlation validation for status updates, and introduces owner/lease structs.

Copilot's findings

  • Files reviewed: 4/4 changed files
  • Comments generated: 2

Comment on lines 315 to 321
response = await invokable.Invoke();
response = this.responseCopier.Copy(response);
}

invokable.Dispose();
// Note: invokable disposal happens at the end of Invoke to return it to its pool
break;
}
Comment on lines +10 to +57
internal static class CallbackDataPool
{
private static readonly ThreadLocal<Stack<CallbackData>> _callbacks = new(() => new());

/// <summary>
/// The maximum number of callbacks to keep per thread.
/// </summary>
public static int MaxPoolSizePerThread { get; set; } = 128;

/// <summary>
/// Gets a callback from the pool, or creates a new one if the pool is empty.
/// </summary>
public static CallbackData Get()
{
var stack = _callbacks.Value!;
if (stack.TryPop(out var callback))
{
return callback;
}

return new CallbackData();
}

/// <summary>
/// Returns a callback to the pool via the reference counting system.
/// This decrements the owner's reference count. The callback will be returned
/// to the pool when all leases have been released.
/// </summary>
/// <param name="owner">The owner of the callback.</param>
public static void Return(CallbackDataOwner owner)
{
owner.Release();
}

/// <summary>
/// Internal method called by <see cref="CallbackData.ReleaseLease"/> when ref count reaches zero.
/// Actually returns the callback to the pool after resetting it.
/// </summary>
internal static void ReturnCore(CallbackData callback)
{
callback.Reset();

var stack = _callbacks.Value!;
if (stack.Count < MaxPoolSizePerThread)
{
stack.Push(callback);
}
}
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ReubenBond ReubenBond force-pushed the split/callbackdata-pooling branch from 59f1538 to aef33c8 Compare April 30, 2026 15:33
@ReubenBond ReubenBond changed the title Add safe pooled CallbackData reuse perf(runtime): add safe pooled CallbackData reuse May 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants