-
Notifications
You must be signed in to change notification settings - Fork 2.1k
perf(messaging): add ref-counted message pooling #10072
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,120 @@ | ||||||||
| #nullable enable | ||||||||
| using System; | ||||||||
| using System.Collections.Concurrent; | ||||||||
| using System.Collections.Generic; | ||||||||
| using System.Diagnostics; | ||||||||
| using System.Threading; | ||||||||
|
|
||||||||
| namespace Orleans.Runtime | ||||||||
| { | ||||||||
| /// <summary> | ||||||||
| /// A thread-local object pool for <see cref="Message"/> instances. | ||||||||
| /// </summary> | ||||||||
| internal static class MessagePool | ||||||||
| { | ||||||||
| private static readonly ThreadLocal<Stack<Message>> _messages = new(() => new()); | ||||||||
|
|
||||||||
| #if DEBUG | ||||||||
| /// <summary> | ||||||||
| /// Tracks all messages that have been allocated but not returned to the pool. | ||||||||
| /// Only available in DEBUG builds. Must be enabled via <see cref="EnableLeakTracking"/>. | ||||||||
| /// </summary> | ||||||||
| private static readonly ConcurrentDictionary<Message, MessageAllocationInfo> _outstandingMessages = new(); | ||||||||
|
|
||||||||
| /// <summary> | ||||||||
| /// When true, tracks all message allocations for leak detection. | ||||||||
| /// Only available in DEBUG builds. | ||||||||
| /// </summary> | ||||||||
| public static bool EnableLeakTracking { get; set; } | ||||||||
|
|
||||||||
| /// <summary> | ||||||||
| /// Gets all messages that have been allocated but not returned to the pool. | ||||||||
| /// Only available in DEBUG builds and when <see cref="EnableLeakTracking"/> is true. | ||||||||
| /// </summary> | ||||||||
| public static IReadOnlyCollection<MessageAllocationInfo> GetOutstandingMessages() | ||||||||
| { | ||||||||
| return _outstandingMessages.Values.ToArray(); | ||||||||
| } | ||||||||
|
|
||||||||
| /// <summary> | ||||||||
| /// Clears the outstanding messages tracking. Call this at the start of a test. | ||||||||
| /// </summary> | ||||||||
| public static void ClearLeakTracking() | ||||||||
| { | ||||||||
| _outstandingMessages.Clear(); | ||||||||
| } | ||||||||
|
|
||||||||
| /// <summary> | ||||||||
| /// Information about a message allocation for leak tracking. | ||||||||
| /// </summary> | ||||||||
| public sealed class MessageAllocationInfo | ||||||||
| { | ||||||||
| public Message Message { get; } | ||||||||
| public string AllocationStack { get; } | ||||||||
| public DateTime AllocationTime { get; } | ||||||||
|
|
||||||||
| public MessageAllocationInfo(Message message, string allocationStack) | ||||||||
| { | ||||||||
| Message = message; | ||||||||
| AllocationStack = allocationStack; | ||||||||
| AllocationTime = DateTime.UtcNow; | ||||||||
| } | ||||||||
|
|
||||||||
| public override string ToString() => | ||||||||
| $"Message allocated at {AllocationTime:HH:mm:ss.fff}, Direction={Message.Direction}, Id={Message.Id}\nStack:\n{AllocationStack}"; | ||||||||
| } | ||||||||
| #endif | ||||||||
|
|
||||||||
| /// <summary> | ||||||||
| /// The maximum number of messages to keep per thread. | ||||||||
| /// </summary> | ||||||||
| public static int MaxPoolSizePerThread { get; set; } = 128; | ||||||||
|
|
||||||||
| /// <summary> | ||||||||
| /// Gets a message from the pool, or creates a new one if the pool is empty. | ||||||||
| /// </summary> | ||||||||
| public static Message Get() | ||||||||
| { | ||||||||
| var stack = _messages.Value!; | ||||||||
| if (!stack.TryPop(out var message)) | ||||||||
| { | ||||||||
| message = new Message(); | ||||||||
| } | ||||||||
|
|
||||||||
| message.InitializeRefCount(); | ||||||||
|
|
||||||||
| #if DEBUG | ||||||||
| if (EnableLeakTracking) | ||||||||
| { | ||||||||
| var info = new MessageAllocationInfo(message, Environment.StackTrace); | ||||||||
| _outstandingMessages[message] = info; | ||||||||
| } | ||||||||
| #endif | ||||||||
|
|
||||||||
| return message; | ||||||||
| } | ||||||||
|
|
||||||||
| /// <summary> | ||||||||
| /// Returns a message to the pool after resetting it. | ||||||||
|
||||||||
| /// Returns a message to the pool after resetting it. | |
| /// Releases the caller's reference to a message. | |
| /// The message is reset and returned to the pool only when its reference count reaches zero. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -374,6 +374,12 @@ private async Task ProcessOutgoing() | |
| { | ||
| throw; | ||
| } | ||
|
|
||
| if (message is not null) | ||
| { | ||
| inflight.Remove(message); | ||
| message = null; | ||
| } | ||
| } | ||
|
|
||
| var flushResult = await output.FlushAsync(); | ||
|
|
@@ -382,6 +388,13 @@ private async Task ProcessOutgoing() | |
| break; | ||
| } | ||
|
|
||
| // Release the send pipeline's reference after bytes have been flushed. | ||
| foreach (var msg in inflight) | ||
| { | ||
| msg.MarkTransferred("Connection.ProcessOutgoing:Sent"); | ||
| msg.Release(); | ||
| } | ||
|
|
||
| inflight.Clear(); | ||
| } | ||
|
Comment on lines
385
to
399
|
||
| } | ||
|
|
@@ -490,6 +503,8 @@ private bool HandleSendMessageFailure(Message message, Exception exception) | |
| response.BodyObject = Response.FromException(exception); | ||
|
|
||
| this.MessageCenter.DispatchLocalMessage(response); | ||
| message.MarkTransferred("Connection.HandleSendMessageFailure:RequestFailed"); | ||
| message.Release(); | ||
| } | ||
| else if (message.Direction == Message.Directions.Response && message.RetryCount < MessagingOptions.DEFAULT_MAX_MESSAGE_SEND_RETRIES) | ||
| { | ||
|
|
@@ -509,6 +524,8 @@ private bool HandleSendMessageFailure(Message message, Exception exception) | |
| message); | ||
|
|
||
| MessagingInstruments.OnDroppedSentMessage(message); | ||
| message.MarkTransferred("Connection.HandleSendMessageFailure:Dropped"); | ||
| message.Release(); | ||
| } | ||
|
|
||
| return true; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Release()only callsDebug.Failwhen the ref count goes negative. In non-DEBUG builds that check is effectively a no-op, so double-release/use-after-release can silently corrupt the pool (eg, decrementing a message which has been re-acquired by another owner). Consider throwing/fail-fast (or at least gating return-to-pool with an additional state/version check) whennewRefCount < 0to prevent silent corruption in production.