Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions src/Orleans.Core.Abstractions/Logging/ErrorCodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,8 @@ public enum ErrorCode
Catalog_DeactivateAllActivations = CatalogBase + 45,
Catalog_ActivationCollector_BadState_3 = CatalogBase + 46,
Catalog_UnregisterAsync = CatalogBase + 47,
Catalog_CancelledActivate = CatalogBase + 48,
Catalog_DisposedObjectAccess = CatalogBase + 49,

MembershipBase = Runtime + 600,
MembershipCantWriteLivenessDisabled = Runtime_Error_100225, // Backward compatability
Expand Down
43 changes: 41 additions & 2 deletions src/Orleans.Runtime/Catalog/ActivationData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -805,7 +805,7 @@ public async ValueTask DisposeAsync()
await DisposeAsync(_serviceScope);
}

private static async ValueTask DisposeAsync(object obj)
private static async ValueTask DisposeAsync(object? obj)
{
try
{
Expand Down Expand Up @@ -1580,6 +1580,32 @@ private async Task ActivateAsync(Dictionary<string, object>? requestContextData,
{
await grainBase.OnActivateAsync(cancellationToken).WaitAsync(cancellationToken);
}
// this captures the case where user code in OnActivateAsync doesn't use the passed cancellation token
// and makes a call that tries to resolve the scoped IServiceProvider or other type that has been disposed because of cancellation.
catch (ObjectDisposedException ode) when (cancellationToken.IsCancellationRequested)
{
LogActivationDisposedObjectAccessed(_shared.Logger, ode.ObjectName, this);
CatalogInstruments.ActivationFailedToActivate.Add(1);
Deactivate(new(DeactivationReason.ReasonCode, ode, DeactivationReason.Description), CancellationToken.None);
// TODO: after the PR for activation data activity is in, re-enable this
// SetActivityError(_activationActivity, ode, ActivityErrorEvents.ActivationCancelled);
LogActivationCancelled(_shared.Logger, this, cancellationToken.IsCancellationRequested, DeactivationReason.ReasonCode.ToString(), ForwardingAddress);
// TODO: after the PR for activation data activity is in, re-enable this
// _activationActivity?.Stop();
return;
}
// catch OperationCanceledException only if it wasn't for a timeout.
catch (OperationCanceledException oce) when (cancellationToken.IsCancellationRequested && DeactivationReason.ReasonCode != DeactivationReasonCode.ActivationUnresponsive)
{
CatalogInstruments.ActivationFailedToActivate.Add(1);
Deactivate(new(DeactivationReason.ReasonCode, oce, DeactivationReason.Description), CancellationToken.None);
// TODO: after the PR for activation data activity is in, re-enable this
// SetActivityError(_activationActivity, oce, ActivityErrorEvents.ActivationCancelled);
LogActivationCancelled(_shared.Logger, this, cancellationToken.IsCancellationRequested, DeactivationReason.ReasonCode.ToString(), ForwardingAddress);
// TODO: after the PR for activation data activity is in, re-enable this
// _activationActivity?.Stop();
return;
}
catch (Exception exception)
{
LogErrorInGrainMethod(_shared.Logger, exception, nameof(IGrainBase.OnActivateAsync), this);
Expand Down Expand Up @@ -2361,11 +2387,24 @@ private readonly struct ActivationDataLogValue(ActivationData activation, bool i
Message = "Error activating grain {Grain}")]
private static partial void LogErrorActivatingGrain(ILogger logger, Exception exception, ActivationData grain);

[LoggerMessage(
EventId = (int)ErrorCode.Catalog_DisposedObjectAccess,
Level = LogLevel.Warning,
Message = "Disposed object {ObjectName} accessed in OnActivateAsync for grain {Grain}. Ensure the cancellationToken is passed to all async methods or they have .WaitAsync(cancellationToken) called on them.")]
private static partial void LogActivationDisposedObjectAccessed(ILogger logger, string objectName, ActivationData grain);

[LoggerMessage(
EventId = (int)ErrorCode.Catalog_CancelledActivate,
Level = LogLevel.Information,
Message = "Activation was cancelled for {Grain}. CancellationRequested={CancellationRequested}, DeactivationReason={DeactivationReason}, ForwardingAddress={ForwardingAddress}"
)]
private static partial void LogActivationCancelled(ILogger logger, ActivationData grain, bool cancellationRequested, string? deactivationReason, SiloAddress? forwardingAddress);

[LoggerMessage(
Level = LogLevel.Error,
Message = "Activation of grain {Grain} failed")]
private static partial void LogActivationFailed(ILogger logger, Exception exception, ActivationData grain);

[LoggerMessage(
Level = LogLevel.Trace,
Message = "Completing deactivation of '{Activation}'")]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#nullable enable

namespace UnitTests.GrainInterfaces;

/// <summary>
/// Interface for testing activation cancellation scenarios.
/// These grains are used to verify the proper handling of cancellation during grain activation.
/// </summary>
public interface IActivationCancellationTestGrain : IGrainWithGuidKey
{
/// <summary>
/// A simple method to test that the grain is activated and working.
/// </summary>
Task<string> GetActivationId();

/// <summary>
/// Checks if the activation was successful.
/// </summary>
Task<bool> IsActivated();
}

/// <summary>
/// Grain that throws OperationCanceledException during OnActivateAsync when the cancellation token is triggered.
/// This simulates code that properly observes the cancellation token.
/// </summary>
public interface IActivationCancellation_ThrowsOperationCancelledGrain : IActivationCancellationTestGrain;

/// <summary>
/// Grain that throws ObjectDisposedException during OnActivateAsync when trying to access disposed services.
/// This simulates code that doesn't observe the cancellation token but tries to access services that have been disposed.
/// </summary>
public interface IActivationCancellation_ThrowsObjectDisposedGrain : IActivationCancellationTestGrain;

/// <summary>
/// Grain that throws a generic exception during OnActivateAsync (not related to cancellation).
/// This is used to verify that non-cancellation exceptions are still handled properly.
/// </summary>
public interface IActivationCancellation_ThrowsGenericExceptionGrain : IActivationCancellationTestGrain;

/// <summary>
/// Grain that activates successfully without any issues.
/// This is a baseline to verify normal activation continues to work.
/// </summary>
public interface IActivationCancellation_SuccessfulActivationGrain : IActivationCancellationTestGrain;

/// <summary>
/// Grain that throws TaskCanceledException during OnActivateAsync.
/// TaskCanceledException inherits from OperationCanceledException and should be handled the same way.
/// </summary>
public interface IActivationCancellation_ThrowsTaskCancelledGrain : IActivationCancellationTestGrain;

/// <summary>
/// Grain that throws ObjectDisposedException unconditionally (not due to cancellation).
/// This tests that ObjectDisposedException thrown for other reasons is NOT treated as cancellation.
/// </summary>
public interface IActivationCancellation_ThrowsObjectDisposedUnconditionallyGrain : IActivationCancellationTestGrain;

/// <summary>
/// Grain that throws OperationCanceledException unconditionally (not due to cancellation).
/// This tests that OperationCanceledException thrown for other reasons is NOT treated as cancellation.
/// </summary>
public interface IActivationCancellation_ThrowsOperationCancelledUnconditionallyGrain : IActivationCancellationTestGrain;
Loading
Loading