Skip to content

Commit c6301c5

Browse files
committed
fix(runtime): guard pooled observer cancellation
1 parent f667a7b commit c6301c5

6 files changed

Lines changed: 1058 additions & 25 deletions

File tree

src/Orleans.CodeGenerator/InvokableGenerator.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -911,8 +911,7 @@ static bool IsPoolableBaseType(INamedTypeSymbol type)
911911
&& type.MetadataName is "Request"
912912
or "Request`1"
913913
or "TaskRequest"
914-
or "TaskRequest`1"
915-
or "VoidRequest";
914+
or "TaskRequest`1";
916915
}
917916

918917
internal abstract class InvokerFieldDescription(ITypeSymbol fieldType, string fieldName)

src/Orleans.Core/Runtime/InvokableObjectManager.cs

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ public sealed partial class LocalObjectData : IGrainContext, IGrainCallCancellat
9090
private static readonly Func<object?, Task> HandleFunc = self => ((LocalObjectData)self!).LocalObjectMessagePumpAsync();
9191
private readonly InvokableObjectManager _manager;
9292
private readonly Dictionary<Message, Task?> _runningRequests = [];
93+
private readonly Dictionary<Message, int> _cancellationLeaseCounts = [];
94+
private readonly HashSet<Message> _completedRequests = [];
9395
private Task? _messagePumpTask;
9496

9597
internal LocalObjectData(IAddressable obj, ObserverGrainId observerId, InvokableObjectManager manager)
@@ -324,12 +326,23 @@ private async Task ProcessMessageAsync(Message message)
324326
}
325327
finally
326328
{
327-
message.DisposeOwnedBody();
328-
329-
// Clear the running request when done.
329+
var disposeMessage = false;
330330
lock (Messages)
331331
{
332332
_runningRequests.Remove(message);
333+
if (_cancellationLeaseCounts.ContainsKey(message))
334+
{
335+
_completedRequests.Add(message);
336+
}
337+
else
338+
{
339+
disposeMessage = true;
340+
}
341+
}
342+
343+
if (disposeMessage)
344+
{
345+
message.DisposeOwnedBody();
333346
}
334347
}
335348
}
@@ -437,6 +450,7 @@ bool TryCancelRequest()
437450
{
438451
Message? message = null;
439452
var wasWaiting = false;
453+
var hasCancellationLease = false;
440454
lock (Messages)
441455
{
442456
// Check the running requests.
@@ -445,6 +459,9 @@ bool TryCancelRequest()
445459
if (runningRequest.Id == messageId && runningRequest.SendingGrain == senderGrainId)
446460
{
447461
message = runningRequest;
462+
_cancellationLeaseCounts.TryGetValue(message, out var leaseCount);
463+
_cancellationLeaseCounts[message] = leaseCount + 1;
464+
hasCancellationLease = true;
448465
break;
449466
}
450467
}
@@ -478,34 +495,67 @@ bool TryCancelRequest()
478495
}
479496

480497
var didCancel = false;
481-
if (message is not null)
498+
try
482499
{
483-
// The message never began executing, so send a canceled response immediately.
484-
// If the message did begin executing, wait for it to observe the cancellation token and respond itself.
485-
if (wasWaiting)
500+
if (message is not null)
486501
{
487-
try
502+
// The message never began executing, so send a canceled response immediately.
503+
// If the message did begin executing, wait for it to observe the cancellation token and respond itself.
504+
if (wasWaiting)
488505
{
489-
_manager.runtimeClient.SendResponse(message, Response.FromException(new OperationCanceledException()));
490-
didCancel = true;
506+
try
507+
{
508+
_manager.runtimeClient.SendResponse(message, Response.FromException(new OperationCanceledException()));
509+
didCancel = true;
510+
}
511+
finally
512+
{
513+
message.DisposeOwnedBody();
514+
}
515+
}
516+
else if (message.BodyObject is IInvokable invokableRequest)
517+
{
518+
didCancel = TryCancelInvokable(invokableRequest) || !invokableRequest.IsCancellable;
491519
}
492-
finally
520+
else
493521
{
494-
message.DisposeOwnedBody();
522+
// Assume the request is not cancellable.
523+
didCancel = true;
495524
}
496525
}
497-
else if (message.BodyObject is IInvokable invokableRequest)
526+
}
527+
finally
528+
{
529+
if (hasCancellationLease)
498530
{
499-
didCancel = TryCancelInvokable(invokableRequest) || !invokableRequest.IsCancellable;
531+
ReleaseCancellationLease(message!);
532+
}
533+
}
534+
535+
return didCancel;
536+
}
537+
538+
void ReleaseCancellationLease(Message message)
539+
{
540+
var disposeMessage = false;
541+
lock (Messages)
542+
{
543+
var leaseCount = _cancellationLeaseCounts[message];
544+
if (leaseCount == 1)
545+
{
546+
_cancellationLeaseCounts.Remove(message);
547+
disposeMessage = _completedRequests.Remove(message);
500548
}
501549
else
502550
{
503-
// Assume the request is not cancellable.
504-
didCancel = true;
551+
_cancellationLeaseCounts[message] = leaseCount - 1;
505552
}
506553
}
507554

508-
return didCancel;
555+
if (disposeMessage)
556+
{
557+
message.DisposeOwnedBody();
558+
}
509559
}
510560

511561
bool TryCancelInvokable(IInvokable request)

src/Orleans.Serialization/Invocation/Pools/InvokablePool.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ namespace Orleans.Serialization.Invocation;
1010
/// Provides bounded thread-local reuse for <see cref="IInvokable"/> implementations.
1111
/// </summary>
1212
/// <typeparam name="T">The invokable type.</typeparam>
13+
/// <remarks>
14+
/// A returned instance becomes available to one subsequent rental on the current thread.
15+
/// Callers reset mutable state before returning an instance and transfer exclusive ownership to the pool.
16+
/// </remarks>
1317
public sealed class InvokablePool<T> : IDisposable where T : class, IInvokable
1418
{
1519
private const int MaxPoolSizePerThread = 128;
@@ -49,7 +53,7 @@ public bool TryGet([NotNullWhen(true)] out T? item)
4953
/// <summary>
5054
/// Makes an instance available for reuse by the current thread.
5155
/// </summary>
52-
/// <param name="item">The instance to return.</param>
56+
/// <param name="item">The reset instance whose ownership is transferred to the pool.</param>
5357
public void Return(T item)
5458
{
5559
if (TryGetStack(out var stack) && stack.Count < MaxPoolSizePerThread)

test/Orleans.CodeGenerator.Tests/OrleansSourceGeneratorTests.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,28 @@ public Task<string> SayHello(string name)
739739
}
740740
}");
741741

742+
[Fact]
743+
public Task TestGrainRequestReturnShapes() => AssertSuccessfulSourceGeneration(
744+
@"using Orleans;
745+
using Orleans.Concurrency;
746+
using System.Threading.Tasks;
747+
748+
namespace TestProject;
749+
750+
public interface IRequestShapeGrain : IGrainWithIntegerKey
751+
{
752+
ValueTask ValueTaskMethod(int value);
753+
754+
ValueTask<int> ValueTaskOfTMethod(string value);
755+
756+
Task TaskMethod(object value);
757+
758+
Task<int> TaskOfTMethod(byte[] value);
759+
760+
[OneWay]
761+
void OneWayMethod(long value);
762+
}");
763+
742764
/// <summary>
743765
/// Tests proxy generation for grains with different key types.
744766
/// Orleans supports multiple grain key types:

0 commit comments

Comments
 (0)