Skip to content

Commit

Permalink
Exception serialization support for built-in messages (#6297) (#6300)
Browse files Browse the repository at this point in the history
* added end to end spec to validate error serialization for remote actor supervision

* defined `.proto`s for `Status.Failure` and `Status.Success`

* added `Status.,Failure` and `Status.Success` support to the `MiscMessageSerializer`

* added tests to `MiscMessageSerializerSpec`

* close #3903

(cherry picked from commit 998dcca)

Co-authored-by: Aaron Stannard <aaron@petabridge.com>
  • Loading branch information
Arkatufus and Aaronontheweb committed Dec 9, 2022
1 parent 4b7badc commit fddecf1
Show file tree
Hide file tree
Showing 8 changed files with 793 additions and 70 deletions.
124 changes: 124 additions & 0 deletions src/core/Akka.Remote.Tests/Serialization/Bugfix3903Spec.cs
@@ -0,0 +1,124 @@
//-----------------------------------------------------------------------
// <copyright file="Bugfix3903Specs.cs" company="Akka.NET Project">
// Copyright (C) 2009-2022 Lightbend Inc. <http://www.lightbend.com>
// Copyright (C) 2013-2022 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
//-----------------------------------------------------------------------

using System;
using System.Threading.Tasks;
using Akka.Actor;
using Akka.Configuration;
using Akka.TestKit;
using Akka.Util.Internal;
using Xunit;
using Xunit.Abstractions;
using FluentAssertions;

namespace Akka.Remote.Tests.Serialization
{
public class Bugfix3903Spec : AkkaSpec
{
// hocon config enabling akka.remote
private static readonly Config Config = @"akka.actor.provider = remote
akka.remote.dot-netty.tcp.hostname = localhost
akka.remote.dot-netty.tcp.port = 0";

public Bugfix3903Spec(ITestOutputHelper outputHelper) : base(Config, outputHelper)
{
}

#region Internal Types

// parent actor type that will remotely deploy a child actor type onto a specific address
private class ParentActor : ReceiveActor
{
// message type that includes an Address
public class DeployChild
{
public DeployChild(Address address)
{
Address = address;
}

public Address Address { get; }
}

public ParentActor()
{
Receive<DeployChild>(s =>
{
// props to deploy an EchoActor at the address specified in DeployChild
var props = Props.Create<EchoActor>().WithDeploy(new Deploy(new RemoteScope(s.Address)));
var child = Context.ActorOf(props, "child");
Sender.Tell(child);
});
}
}

internal class EchoActor : ReceiveActor
{
public class Fail
{
public static readonly Fail Instance = new Fail();
private Fail(){}
}

public EchoActor()
{
// receive message that will cause this actor to fail
Receive<Fail>(s =>
{
throw new ApplicationException("fail");
});
ReceiveAny(o => Sender.Tell(o));
}
}

#endregion

// a test where Sys starts a ParentActor and has it remotely deploy an EchoActor onto a second ActorSystem
[Fact]
public async Task ParentActor_should_be_able_to_deploy_EchoActor_to_remote_system()
{
// create a second ActorSystem
var system2 = ActorSystem.Create(Sys.Name, Sys.Settings.Config);
InitializeLogger(system2);
try
{
// create a supervision strategy that will send a message to the TestActor including the exception of the child that failed
var strategy = new OneForOneStrategy(ex =>
{
TestActor.Tell(ex);
return Directive.Stop;
});

// create a ParentActor in the first ActorSystem
var parent = Sys.ActorOf(Props.Create<ParentActor>().WithSupervisorStrategy(strategy), "parent");

// have the ParentActor remotely deploy an EchoActor onto the second ActorSystem
var child = await parent
.Ask<IActorRef>(new ParentActor.DeployChild(
system2.AsInstanceOf<ExtendedActorSystem>().Provider.DefaultAddress), RemainingOrDefault).ConfigureAwait(false);

// assert that Child is a remote actor reference
child.Should().BeOfType<RemoteActorRef>();
Watch(child);

// send a message to the EchoActor and verify that it is received
(await child.Ask<string>("hello", RemainingOrDefault).ConfigureAwait(false)).Should().Be("hello");

// cause the child to crash
child.Tell(EchoActor.Fail.Instance);
var exception = ExpectMsg<ApplicationException>();
exception.Message.Should().Be("fail");
ExpectTerminated(child);
}
finally
{
// shut down the second ActorSystem
Shutdown(system2);
}
}
}
}
Expand Up @@ -76,6 +76,30 @@ public void Can_serialize_IdentifyWithNull()
AssertEqual(identify);
}

[Theory]
[InlineData(null)]
[InlineData(1)]
[InlineData("hi")]
public void Can_serialize_StatusSuccess(object payload)
{
var success = new Status.Success(payload);
AssertEqual(success);
}

[Theory]
[InlineData(null)]
[InlineData(1)]
[InlineData("hi")]
public void Can_serialize_StatusFailure(object payload)
{
var success = new Status.Failure(new ApplicationException("foo"),payload);
// can't use AssertEqual here since the Exception data isn't 100% identical after round-trip serialization
var deserialized = AssertAndReturn(success);
deserialized.State.Should().BeEquivalentTo(success.State);
deserialized.Cause.Message.Should().BeEquivalentTo(success.Cause.Message);
deserialized.Cause.Should().BeOfType(success.Cause.GetType());
}

[Fact]
public void Can_serialize_ActorIdentity()
{
Expand Down Expand Up @@ -372,7 +396,7 @@ private T AssertAndReturn<T>(T message)
private void AssertEqual<T>(T message)
{
var deserialized = AssertAndReturn(message);
Assert.Equal(message, deserialized);
deserialized.Should().BeEquivalentTo(message);
}
}
}
2 changes: 2 additions & 0 deletions src/core/Akka.Remote/Configuration/Remote.conf
Expand Up @@ -39,6 +39,8 @@ akka {
"Akka.Actor.PoisonPill, Akka" = akka-misc
"Akka.Actor.Kill, Akka" = akka-misc
"Akka.Actor.PoisonPill, Akka" = akka-misc
"Akka.Actor.Status+Failure, Akka" = akka-misc
"Akka.Actor.Status+Success, Akka" = akka-misc
#"Akka.Actor.LocalScope, Akka" = akka-misc
"Akka.Actor.RemoteScope, Akka" = akka-misc
"Akka.Routing.FromConfig, Akka" = akka-misc
Expand Down
2 changes: 1 addition & 1 deletion src/core/Akka.Remote/Serialization/ExceptionSupport.cs
Expand Up @@ -150,7 +150,7 @@ public Exception ExceptionFromProtoNet(Proto.Msg.ExceptionData proto)
return obj;
}

private string ValueOrNull(string value)
private static string ValueOrNull(string value)
=> string.IsNullOrEmpty(value) ? null : value;
}
}
Expand Up @@ -9,7 +9,6 @@
using System.Linq;
using System.Runtime.Serialization;
using Akka.Actor;
using Akka.Remote.Serialization.Proto.Msg;
using Akka.Serialization;
using Akka.Util;
using Google.Protobuf;
Expand Down Expand Up @@ -108,7 +107,7 @@ public override object FromBinary(byte[] bytes, Type type)
return new ActorSelectionMessage(message, elements);
}

private Proto.Msg.Selection BuildPattern(string matcher, Proto.Msg.Selection.Types.PatternType tpe)
private static Proto.Msg.Selection BuildPattern(string matcher, Proto.Msg.Selection.Types.PatternType tpe)
{
var selection = new Proto.Msg.Selection { Type = tpe };
if (matcher != null)
Expand Down

0 comments on commit fddecf1

Please sign in to comment.