Skip to content

It's all about messages and pattern matching

My inability to cleanly insert an intercepting actor to spawn new actors on demand discussed in my previous post about Calculon illustrated tunnelvision in my expression based messages. While these are a great way to express the message contract, they are finally just one way of defining message payloads. The pipeline should be ignorant of message payloads and be able to route any payload based on meta-data alone.

Routing revisited

I had to stop hiding the IMessage format under hood and just accept that ExpressionTransport really represents a convenience extension for generating message payloads. This change not only makes it possible for any actor to accept any kind of message, but also to makes the routing a lot simpler and flexible. Now there is only a single, much simpler ITransport:

public interface ITransport {
    ActorAddress Sender { get; }
    void Send(IMessage message);
}

I've also attached ther sender onto the transport, which means there's one less dependency to inject. Given this, ExpressionTransport and MessageTransport simply becomes extension methods on ITransport:

public static class TransportEx {
    public static void Send<TRecipient>(
       this ITransport transport,
       Expression<Action<TRecipient>> message
    ) {
       transport.Send(new ExpressionActionMessage<TRecipient>(
          transport.Sender,
          ActorAddress.Create<TRecipient>(), message
       ));
    }
    // etc.
}

This provides a lot more flexibility for messaging, since new payloads can be created by implementing IMessage. It does mean that dynamic dispatch would be nice for dispatching the different payloads against their respective receivers. Oh well, we can work around that.

Determining what messages to accept

Now it's possible for an actor not of type TRecipient to receive an ExpressionMessage<TRecipient>, perform some action and re-dispatch it via Send(). By default routing is done by Id, but missing a direct receiver we can now insert additional pattern matching to determine a suitable recipient. For this I've created an interface to let actors expose their accept criteria:

public interface IPatternMatchingActor {
    Expression<Func<MessageMeta, bool>> AcceptCriteria { get; }
}

While this could have been done by convention, I couldn't think of a reason other than current fashion to not use an interface contract to expose this actor capability, so an interface it was. The reason it's an Expression and operates on the MessageMeta rather than the expression is to facilitate serialization of the criteria for delivery across the process boundary so messages can be qualified for acceptance before attempting to cross that boundary.

Better plumbing

I now have everything I need to switch the notify.me bots to Calculon. Once that is done, and working reliably, it'll be time to improve the dispatch plumbing to improve speed and reduce concurrency bottlenecks in dispatch.