Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,7 @@ namespace Akka.Actor
public override Akka.Actor.ActorPath Path { get; }
public override Akka.Actor.IActorRefProvider Provider { get; }
public virtual void DeliverAsk(object message, Akka.Actor.ICanTell destination) { }
public override void SendSystemMessage(Akka.Dispatch.SysMsg.ISystemMessage message) { }
protected override void TellInternal(object message, Akka.Actor.IActorRef sender) { }
}
public class static Futures
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,7 @@ namespace Akka.Actor
public override Akka.Actor.ActorPath Path { get; }
public override Akka.Actor.IActorRefProvider Provider { get; }
public virtual void DeliverAsk(object message, Akka.Actor.ICanTell destination) { }
public override void SendSystemMessage(Akka.Dispatch.SysMsg.ISystemMessage message) { }
protected override void TellInternal(object message, Akka.Actor.IActorRef sender) { }
}
public class static Futures
Expand Down
2 changes: 1 addition & 1 deletion src/core/Akka.Tests/Actor/AskSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public SomeActor()
Sender.Tell(123);
break;
case "system":
Sender.Tell(new DummySystemMessage());
Sender.As<IInternalActorRef>().SendSystemMessage(new DummySystemMessage());
break;
}

Expand Down
64 changes: 64 additions & 0 deletions src/core/Akka.Tests/Actor/Bugfix7501Specs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// -----------------------------------------------------------------------
// <copyright file="Bugfix7501Specs.cs" company="Akka.NET Project">
// Copyright (C) 2009-2025 Lightbend Inc. <http://www.lightbend.com>
// Copyright (C) 2013-2025 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
// -----------------------------------------------------------------------

using System.Threading.Tasks;
using Akka.Actor;
using Akka.Actor.Dsl;
using Akka.TestKit;
using Akka.TestKit.TestActors;
using Xunit;
using Xunit.Abstractions;

namespace Akka.Tests.Actor;

public class Bugfix7501Specs : AkkaSpec
{
public Bugfix7501Specs(ITestOutputHelper output) : base(output)
{

}

[Fact]
public async Task FutureActorRefShouldSupportDeathWatch()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Handles both scenarios:

  1. Context.Watch before the FutureActorRef<T> completes
  2. Context.Watch after the FutureActorRef<T> has already completed, which should immediately report back with a Terminated message

{
// arrange
var customDeathWatchProbe = CreateTestProbe();
var watcher = Sys.ActorOf(act =>
{
act.Receive<string>((_, context) =>
{
// complete the Ask
context.Sender.Tell("hi");

// DeathWatch the FutureActorRef<T> BEFORE it completes
context.Watch(context.Sender);

// deliver the IActorRef of the Ask-er to TestActor
TestActor.Tell(context.Sender);
});

act.Receive<Terminated>((terminated, context) =>
{
// shut ourselves down to signal that we got our Terminated from FutureActorRef
context.Stop(context.Self);
});
});

// act
await customDeathWatchProbe.WatchAsync(watcher);
await watcher.Ask<string>("boo", RemainingOrDefault);
var futureActorRef = await ExpectMsgAsync<IActorRef>();
await WatchAsync(futureActorRef); // Ask is finished - should immediately dead-letter

// assert
await ExpectTerminatedAsync(futureActorRef);

// get the DeathWatch notification from the original actor
// this can only be received if the original actor got a Terminated message from FutureActorRef
await customDeathWatchProbe.ExpectTerminatedAsync(watcher);
}
}
62 changes: 58 additions & 4 deletions src/core/Akka/Actor/ActorRef.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,35 @@ public interface IRepointableRef : IActorRefScope
bool IsStarted { get; }
}

/// <summary>
/// INTERNAL API - didn't want static helper methods declared inside generic class
/// </summary>
internal static class FutureActorRefDeathWatchSupport

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Helper class for sending back ISystemMsgs from FutureActorRef<T>

{
internal static async Task ScheduleDeathWatch(IInternalActorRef notifier, IActorRef self, Task completionTask)
{
try
{
await completionTask;
}
catch
{
// we don't do error handling for this - we do not care

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Error-handling is the job of the user code await-ing on the Ask - not ours.

}
finally
{
// regardless of whether we succeeded or failed, we notify watchers
notifier.SendSystemMessage(TerminatedFor(self));
}

}

internal static DeathWatchNotification TerminatedFor(IActorRef self)
{
return new DeathWatchNotification(self, true, false);
}
}

/// <summary>
/// INTERNAL API.
///
Expand Down Expand Up @@ -110,9 +139,6 @@ protected override void TellInternal(object message, IActorRef sender)

switch (message)
{
case ISystemMessage msg:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

As mentioned in #7501, this code never worked because it was in the wrong method. Removing it should actually speed up Ask<T> processing slightly.

handled = _result.TrySetException(new InvalidOperationException($"system message of type '{msg.GetType().Name}' is invalid for {nameof(FutureActorRef<T>)}"));
break;
case T t:
handled = _result.TrySetResult(t);
break;
Expand Down Expand Up @@ -140,7 +166,35 @@ protected override void TellInternal(object message, IActorRef sender)
if (!handled && !_result.Task.IsCanceled)
_provider.DeadLetters.Tell(message ?? default(T), this);
}


public override void SendSystemMessage(ISystemMessage message)
{
if (message is Watch watch)
{
if (_result.Task.IsCompleted)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Automatically covers any possible completion state for the task: cancelled, faulted, or ran to completion

{
watch.Watcher.SendSystemMessage(FutureActorRefDeathWatchSupport.TerminatedFor(this));

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fast path: this actor is already "finished"

}
else
{
_ = FutureActorRefDeathWatchSupport.ScheduleDeathWatch(watch.Watcher, watch.Watchee, _result.Task);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Slow path - have to wait for actor to finish

}

}
else if (message is Unwatch unwatch)
{
// we're not going to support Unwatch - watchers
// already have to handle scenarios where the Unwatch arrives too late
// anyway, so we're just going to treat this like that in order to keep
// state management as simple as possible
}
else
{
// TODO: blow up the caller here by just throwing the exception at the callsite?
_result.TrySetException(new InvalidOperationException($"system message of type '{message.GetType().Name}' is invalid for {nameof(FutureActorRef<T>)}"));
}
}

public virtual void DeliverAsk(object message, ICanTell destination){
destination.Tell(message, this);
}
Expand Down