Skip to content

Commit

Permalink
Ask should push unhandled answers into deadletter 2 (#5259)
Browse files Browse the repository at this point in the history
* Ask should push unhandled answers into deadletter

* update future handler

* fix unit test

* remove not needed return

* remove redundant sync lock

* remove redudant code and seal class

* update api spec

* update api spec 2

* handle of Status.Failure

* ask should fail on system messages
  • Loading branch information
Zetanova authored Sep 7, 2021
1 parent 1f779fe commit 48a704c
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 74 deletions.
5 changes: 2 additions & 3 deletions src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt
Original file line number Diff line number Diff line change
Expand Up @@ -862,12 +862,11 @@ namespace Akka.Actor
public Failures() { }
public System.Collections.Generic.List<Akka.Actor.Failure> Entries { get; }
}
public class FutureActorRef<T> : Akka.Actor.MinimalActorRef
public sealed class FutureActorRef<T> : Akka.Actor.MinimalActorRef
{
public FutureActorRef(System.Threading.Tasks.TaskCompletionSource<T> result, System.Action<System.Threading.Tasks.Task> unregister, Akka.Actor.ActorPath path) { }
public FutureActorRef(System.Threading.Tasks.TaskCompletionSource<T> result, Akka.Actor.ActorPath path, Akka.Actor.IActorRefProvider provider) { }
public override Akka.Actor.ActorPath Path { get; }
public override Akka.Actor.IActorRefProvider Provider { get; }
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
82 changes: 82 additions & 0 deletions src/core/Akka.Tests/Actor/AskSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Akka.Util.Internal;
using FluentAssertions;
using Nito.AsyncEx;
using Akka.Dispatch.SysMsg;

namespace Akka.Tests.Actor
{
Expand All @@ -37,6 +38,29 @@ protected override void OnReceive(object message)
{
Sender.Tell("answer");
}

if (message.Equals("delay"))
{
Thread.Sleep(3000);
Sender.Tell("answer");
}

if (message.Equals("many"))
{
Sender.Tell("answer1");
Sender.Tell("answer2");
Sender.Tell("answer2");
}

if (message.Equals("invalid"))
{
Sender.Tell(123);
}

if (message.Equals("system"))
{
Sender.Tell(new DummySystemMessage());
}
}
}

Expand Down Expand Up @@ -83,6 +107,10 @@ protected override void OnReceive(object message)
}
}

public sealed class DummySystemMessage : ISystemMessage
{
}

[Fact]
public async Task Can_Ask_Response_actor()
{
Expand Down Expand Up @@ -114,6 +142,60 @@ public async Task Can_get_timeout_when_asking_actor()
await Assert.ThrowsAsync<AskTimeoutException>(async () => await actor.Ask<string>("timeout", TimeSpan.FromSeconds(3)));
}

[Fact]
public async Task Ask_should_put_timeout_answer_into_deadletter()
{
var actor = Sys.ActorOf<SomeActor>();

await EventFilter.DeadLetter<object>().ExpectOneAsync(TimeSpan.FromSeconds(5), async () =>
{
await Assert.ThrowsAsync<AskTimeoutException>(async () => await actor.Ask<string>("delay", TimeSpan.FromSeconds(1)));
});
}

[Fact]
public async Task Ask_should_put_too_many_answers_into_deadletter()
{
var actor = Sys.ActorOf<SomeActor>();

await EventFilter.DeadLetter<object>().ExpectAsync(2, async () =>
{
var result = await actor.Ask<string>("many", TimeSpan.FromSeconds(1));
result.ShouldBe("answer1");
});
}

[Fact]
public async Task Ask_should_not_put_canceled_answer_into_deadletter()
{
var actor = Sys.ActorOf<SomeActor>();

await EventFilter.DeadLetter<object>().ExpectAsync(0, async () =>
{
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)))
await Assert.ThrowsAsync<TaskCanceledException>(async () => await actor.Ask<string>("delay", Timeout.InfiniteTimeSpan, cts.Token));
});
}

[Fact]
public async Task Ask_should_put_invalid_answer_into_deadletter()
{
var actor = Sys.ActorOf<SomeActor>();

await EventFilter.DeadLetter<object>().ExpectOne(async () =>
{
await Assert.ThrowsAsync<ArgumentException>(async () => await actor.Ask<string>("invalid", TimeSpan.FromSeconds(1)));
});
}

[Fact]
public async Task Ask_should_fail_on_system_message()
{
var actor = Sys.ActorOf<SomeActor>();

await Assert.ThrowsAsync<InvalidOperationException>(async () => await actor.Ask<ISystemMessage>("system", TimeSpan.FromSeconds(1)));
}

[Fact]
public async Task Can_cancel_when_asking_actor()
{
Expand Down
112 changes: 44 additions & 68 deletions src/core/Akka/Actor/ActorRef.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,51 +69,34 @@ public interface IRepointableRef : IActorRefScope
///
/// ActorRef implementation used for one-off tasks.
/// </summary>
public class FutureActorRef<T> : MinimalActorRef
public sealed class FutureActorRef<T> : MinimalActorRef
{
private readonly TaskCompletionSource<T> _result;
private readonly ActorPath _path;
private readonly IActorRefProvider _provider;

/// <summary>
/// INTERNAL API
/// </summary>
/// <param name="result">TBD</param>
/// <param name="unregister">TBD</param>
/// <param name="path">TBD</param>
public FutureActorRef(TaskCompletionSource<T> result, Action<Task> unregister, ActorPath path)
/// <param name="provider">TBD</param>
public FutureActorRef(TaskCompletionSource<T> result, ActorPath path, IActorRefProvider provider)
{
if (ActorCell.Current != null)
{
_actorAwaitingResultSender = ActorCell.Current.Sender;
}
_result = result;
_path = path;

_result.Task.ContinueWith(unregister);
_provider = provider;
}

/// <summary>
/// TBD
/// </summary>
public override ActorPath Path
{
get { return _path; }
}
public override ActorPath Path => _path;

/// <summary>
/// TBD
/// </summary>
/// <exception cref="System.NotImplementedException">TBD</exception>
public override IActorRefProvider Provider
{
get { throw new NotImplementedException(); }
}


private const int INITIATED = 0;
private const int COMPLETED = 1;
private int status = INITIATED;
private readonly IActorRef _actorAwaitingResultSender;
public override IActorRefProvider Provider => _provider;

/// <summary>
/// TBD
Expand All @@ -122,43 +105,36 @@ public override IActorRefProvider Provider
/// <param name="sender">TBD</param>
protected override void TellInternal(object message, IActorRef sender)
{
var handled = false;

if (message is ISystemMessage sysM) //we have special handling for system messages
{
SendSystemMessage(sysM);
}
else
switch (message)
{
if (Interlocked.Exchange(ref status, COMPLETED) == INITIATED)
{
if (message is T t)
{
_result.TrySetResult(t);
}
else if (message == null) //special case: https://github.com/akkadotnet/akka.net/issues/5204
{
_result.TrySetResult(default);
}
else if (message is Failure f)
{
_result.TrySetException(f.Exception ?? new TaskCanceledException("Task cancelled by actor via Failure message."));
}
else
{
_result.TrySetException(new ArgumentException(
$"Received message of type [{message.GetType()}] - Ask expected message of type [{typeof(T)}]"));
}
}
case ISystemMessage msg:
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;
case null:
handled = _result.TrySetResult(default);
break;
case Status.Failure f:
handled = _result.TrySetException(f.Cause
?? new TaskCanceledException("Task cancelled by actor via Failure message."));
break;
case Failure f:
handled = _result.TrySetException(f.Exception
?? new TaskCanceledException("Task cancelled by actor via Failure message."));
break;
default:
_ = _result.TrySetException(new ArgumentException(
$"Received message of type [{message.GetType()}] - Ask expected message of type [{typeof(T)}]"));
break;
}
}

/// <summary>
/// TBD
/// </summary>
/// <param name="message">TBD</param>
public override void SendSystemMessage(ISystemMessage message)
{
base.SendSystemMessage(message);

//ignore canceled ask and put unhandled answers into deadletter
if (!handled && !_result.Task.IsCanceled)
_provider.DeadLetters.Tell(message ?? default(T), this);
}
}

Expand Down Expand Up @@ -727,16 +703,16 @@ public abstract class ActorRefWithCell : InternalActorRefBase
private IEnumerable<IActorRef> SelfAndChildren()
{
yield return this;
foreach(var child in Children.SelectMany(x =>
{
switch(x)
{
case ActorRefWithCell cell:
return cell.SelfAndChildren();
default:
return new[] { x };
}
}))
foreach (var child in Children.SelectMany(x =>
{
switch (x)
{
case ActorRefWithCell cell:
return cell.SelfAndChildren();
default:
return new[] { x };
}
}))
{
yield return child;
}
Expand Down
9 changes: 6 additions & 3 deletions src/core/Akka/Actor/Futures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,16 +149,19 @@ public static Task<T> Ask<T>(this ICanTell self, Func<IActorRef, object> message
}

//create a new tempcontainer path
ActorPath path = provider.TempPath();
var path = provider.TempPath();

var future = new FutureActorRef<T>(result, t =>
var future = new FutureActorRef<T>(result, path, provider);

//The future actor needs to be unregistered in the temp container
_ = result.Task.ContinueWith(t =>
{
provider.UnregisterTempActor(path);

ctr1?.Dispose();
ctr2?.Dispose();
timeoutCancellation?.Dispose();
}, path);
}, TaskContinuationOptions.ExecuteSynchronously);

//The future actor needs to be registered in the temp container
provider.RegisterTempActor(future, path);
Expand Down

0 comments on commit 48a704c

Please sign in to comment.