0

Disclaimer: I'm new to Akka :)

I'm trying to implement a router in Akka, that basically

  1. receives a message
  2. looks up in dictionary for IActorRef that handle type of message
  3. if no match is found, create one using Akka.DI as child actor and add to dictionary
  4. forward message to that actor

This works great - the first time, but if I try to Tell() or Ask() the router twice, the second message always ends up in the stream as unhandled

I've tried overriding Unhandled() in the child actor and putting a breakpoint there, and that is in fact being hit on the second message.

Router:

public class CommandRouter : UntypedActor
{
    protected readonly IActorResolver _resolver;
    private static readonly Dictionary<Type, IActorRef> _routees = new Dictionary<Type, IActorRef>();
    private ILoggingAdapter _log = Context.GetLogger(new SerilogLogMessageFormatter());

    public CommandRouter(IActorResolver resolver)
    {
        _resolver = resolver;
    }

    protected override void OnReceive(object message)
    {
        _log.Info("Routing command {cmd}", message);
        var typeKey = message.GetType();

        if (!_routees.ContainsKey(typeKey))
        {
            var props = CreateActorProps(typeKey);

            if (!props.Any())
            {
                Sender?.Tell(Response.WithException(
                    new RoutingException(
                        $"Could not route message to routee. No routees found for message type {typeKey.FullName}")));
                return;
            }

            if (props.Count() > 1)
            {
                Sender?.Tell(Response.WithException(
                    new RoutingException(
                        $"Multiple routees registered for message {typeKey.FullName}, which is not supported by this router. Did you want to publish stuff instead?")));
                return;
            }

            var prop = props.First();
            var routee = Context.ActorOf(prop, prop.Type.Name);
            _routees.Add(typeKey, routee);
        }

        _routees[typeKey].Forward(message);

    }

    private IEnumerable<Props> CreateActorProps(Type messageType)
    {
        return _resolver.TryCreateActorProps(typeof(IHandleCommand<>).MakeGenericType(messageType)).ToList();
    }

    protected override SupervisorStrategy SupervisorStrategy()
    {
        return new OneForOneStrategy(x => Directive.Restart);
    }
}

The ActorResolver-method, which uses the DependencyResolver from Akka.DI.StructureMap:

public IEnumerable<Props> TryCreateActorProps(Type actorType)
{
    foreach (var type in _container.GetAllInstances(actorType))
    {
        yield return _resolver.Create(type.GetType());
    }
}

The actual child actor is quite straigt forward:

public class ProductSubscriptionHandler : ReceiveActor, IHandleCommand<AddProductSubscription>
{
    public ProductSubscriptionHandler()
    {
        Receive<AddProductSubscription>(Handle);
    }

    protected bool Handle(AddProductSubscription command)
    {
        Sender?.Tell(Response.Empty);
        return true;
    }
}

The whole thing is called after the actor system has initialized, like so:

var router = Sys.ActorOf(resolver.Create<CommandRouter>(), ActorNames.CommandRouter);

router.Ask(new AddProductSubscription());
router.Ask(new AddProductSubscription());

I consistently get this error on the second (or any subsequent) message: "Unhandled message from...":

[INFO][17-07-2017 23:05:39][Thread 0003][[akka://pos-system/user/CommandRouter#676182398]] Routing command Commands.AddProductSubscription
[DEBUG][17-07-2017 23:05:39][Thread 0003][akka://pos-system/user/CommandRouter] now supervising akka://pos-system/user/CommandRouter/ProductSubscriptionHandler
[DEBUG][17-07-2017 23:05:39][Thread 0003][akka://pos-system/user/CommandRouter] *Unhandled message from akka://pos-system/temp/d* : Documents.Commands.AddProductSubscription
[DEBUG][17-07-2017 23:05:39][Thread 0007][akka://pos-system/user/CommandRouter/ProductSubscriptionHandler] Started (Consumers.Service.Commands.ProductSubscriptionHandler)
Lars Baunwall
  • 75
  • 1
  • 4
  • I'm afraid there's a bit too much in there that's unfamiliar to me. (I haven't used untyped actors and I don't use DI.) However, theoretically, you shouldn't have your _routees variable declared as *static* (because then you could be sharing it between actor-instances and threads). I doubt that's your problem, though. (If it were me I'd investigate by wrapping everything in try catch blocks, maybe override some lifecycle (prerestart?) methods, extra logging and thread.sleeps too.) It's probably something silly though :-) Don't your Ask calls need awaits on them? – mwardm Jul 18 '17 at 17:43
  • It's hard to say what's wrong from this snippet. Could you provide a reproduction example on github? I think, this would make things faster. Also if you have plans to go distributed with this design, you might be interested in using Akka.Cluster.Sharding plugin, which does the same thing as your code. – Bartosz Sypytkowski Jul 20 '17 at 09:22

1 Answers1

0

So, turns out there was a much simpler (and working) solution to my problem: Just register and start all routee actors in the CommandRouter constructor instead of per-receive.

So now my code looks much simpler too:

CommandRouter:

public class CommandRouterActor : UntypedActor
{
    public Dictionary<Type, IActorRef> RoutingTable { get; }
    private ILoggingAdapter _log = Context.GetLogger(new SerilogLogMessageFormatter());

    public CommandRouterActor(IActorResolver resolver)
    {
        var props = resolver.CreateCommandHandlerProps();
        RoutingTable = props.ToDictionary(k => k.Item1, v => Context.ActorOf(v.Item2, $"CommandHandler-{v.Item1.Name}"));
    }

    protected override void OnReceive(object message)
    {
        _log.Info("Routing command {cmd}", message);
        var typeKey = message.GetType();

        if (!RoutingTable.ContainsKey(typeKey))
        {
                Sender?.Tell(Response.WithException(
                    new RoutingException(
                        $"Could not route message to routee. No routees found for message type {typeKey.FullName}")));

                _log.Info("Could not route command {cmd}, no routes found", message);
        }

        RoutingTable[typeKey].Forward(message);
    }

    protected override SupervisorStrategy SupervisorStrategy()
    {
        return new OneForOneStrategy(x => Directive.Restart);
    }
}

And my ActorResolver (used in the ctor above) just queries the StructureMap model and ask for all registered instances of IHandleCommand<>:

    public IEnumerable<Tuple<Type, Props>> CreateCommandHandlerProps()
    {
        var handlerTypes =
            _container.Model.AllInstances.Where(
                    i =>
                        i.PluginType.IsGenericType && i.PluginType.GetGenericTypeDefinition() ==
                        typeof(IHandleCommand<>))
                .Select(m => m.PluginType);

        foreach (var handler in handlerTypes)
        {
            yield return new Tuple<Type, Props>(handler.GenericTypeArguments.First(), _resolver.Create(handler));
        }
    }
Lars Baunwall
  • 75
  • 1
  • 4