Channels


Disclaimer:  The Maestro project has been renamed.  While referred to in this article as ‘Maestro’, moving forward the project is referred to with codename ‘Axum’.

Given a programming model such as Maestro, where components are strictly isolated from each other, we have to ask how components communicate with each other. Clearly, without some form of communication, the components are not merely isolated from each other, they are unaware of each other.

For components to form an application, they need to cooperate and to be aware of each other in some way. In any typical object-oriented model, classes are so aware of each other that they know each others’ names, or can easily discover them through reflection. Even if you get a reference to some abstract base class or interface, you can always ask the runtime system to reveal the true type of what you are interacting with.

Also, most objects are created via explicit constructor calls, rather than through abstract factories. This is true in Maestro, too – while not strictly an object-oriented language, it still has to live within the conceptual framework of .NET and therefore it lives and dies by creating objects.

However, in Maestro there is one kind of object that is not created via constructor calls, and for which we cannot discover (at least not very easily and often not at all) the type through reflection: agent instances. Recall from my earlier post that agents are used to access domain state instead of methods and properties.

Unlike methods, agents are objects with their own thread of control and control logic. Agents are created by asking some abstract factory to create a channel to the agent. The system for establishing such factories is worth a post of its own, so we’ll skip the details here.

Here’s what it looks like to create a channel to a new agent:

Channel1 chan = channelFactory.Connect<Channel1>(addr);

the argument for the ‘Connect’ method is called the “address” of the agent, similar to the address of a web site or service. How we get the address of an agent is irrelevant right now; it belongs in that separate post on factories I mentioned.

Once we have the channel, we use its members to communicate indirectly with the agent.

Channels have three kinds of members:

  • Ports
  • States
  • Functions

First, and most important, are the ports – you can think of them as queues into which data can be placed, only the queues may be distributed across processes or machines. Each port is used either to send data to the agent, or for the agent to send data to the other side.

Ports are therefore directional, and declared as either ‘input’ or ‘output’ ports. Each port is used as a source and a target, depending on which side of the channel you are. To communicate with an agent, you send to the channel’s input ports and receive data from its output ports, while the agent does the opposite.

We call this the perspective on the port, with the agent having the implementing perspective and the other side having the using perspective.

So, we now have all the definitions we need to look at a channel definition:

public channel Channel1
{
    input string FirstMessage;

    output int Result;
}

This is all we need for the examples from my previous post – a single input port and a single output port. This channel definition is complete and correct and may be used by any agent for which it makes sense to send a string as a first message and receive an integer as a result. In fact, the channel is even more flexible than we think, because it doesn’t define how many messages should be sent via ‘FirstMessage’ before we should expect a response. Thus, it can be used for many different implementations and agents, without much in common.

However, in a world where we have prevented you from knowing whom you’re really communicating with, it’s kind of scary not to know more about the expected behavior of whomever it is that you are dealing with.

To handle this, Maestro gives you the option of adding a protocol definition to the channel. The protocol is written as a state machine. Two states always exist: Start and End. The latter is defined in the runtime’s base channel and cannot be redefined, while the former is overridden by any channel that provides its own protocol – it is by adding state members to the channel that its protocol is defined:

public channel Channel1
{
    input string FirstMessage;

    output int Result;

    Start: { FirstMessage –> State_1; }

    State_1: { Result –> End; }
}

What this protocol tells us is that a single use of ‘FirstMessage’ followed by a single use of the ‘Result’ port leads to the entire conversation between client and agent being over. The state consists of a set of ‘transitions’ each taking an expression of the port names of the channel and pointing at a new state.

What does this provide us, really? Well, even if we don’t ever know which agent implementation we’re dealing with, we can know how it will behave vis-a-vis the messages that it expects and produces, and the runtime can enforce the protocol at each end of the channel, thus moving the detection of protocol errors closer to their sources.

There’s more to channels than this, but I think this is plenty for right now. Channels, domains and agents form the core of the Maestro isolation model and by grasping those three concepts, you have a majority of the higher-level ideas that make up the Maestro programming model.

As Artur pointed out in his post earlier, there are other aspects of the language than isolation, mostly having to do with overcoming the challenges of asynchronous programming. Maybe in the next couple of days or weeks, I’ll find something to say about those, too.

Niklas Gustafsson
Mastro Team


Comments (9)

  1. Steve Bjorg says:

    I’m surprised that you care about stateful protocols.  Didn’t REST and now web-oriented architecture (WOA) make a compelling case for stateless protocols?  They have been shown to scale and work well for the vast majority of applications with some exceptions of course, but there are always exceptions.

    Rather than focusing on message order, why not focus on data longevity instead?  Good downstream cache-management and "data revalidation hints" are a pain to deal with, yet essential to scalable deployments.  Having support for such concepts at the Maestro level would, imho, provide a lot more benefit than worrying about message order.

    Cheers,

    – Steve

  2. Jeffry Borror says:

    Thanks for an interesting set of posts. I feel like I’ve walked into the middle of the movie.

    How does Maestro compare to process calculi such as the pi calculus?  It seems that there is a relation to the join calculus.

    Have you considered a version of Maestro based on F#? Not only does it have objects and mutable data, but pipelining is the metaphor for function composition.  Maestro provides a natural generalization to the concurrent world.   (Maybe the F# team would give you way to mark a function as pure.)  It might be more natural to express your operations as combinators.

  3. Garry Trinder says:

    Regarding caring about message order:

    While stateless patterns are usually good for scalability, there are good uses for stateful actors, particularly for in-process uses. Open a book on Erlang and you’ll find lots of very stateful actors.

    Thus, Maestro doesn’t take a stand — use stateless protocols if they serve your purposes. If you find the need for stateful contracts, the language is there to help.

    Regarding pi and F#:

    I would say that Maestro’s model is closer to CSP than it is to pi-calculus. However, it’s not quite CSP, either — Maestro communication is asynchronous and buffered.

    F# — interesting that you would bring it up. It would be fascinating to explore what such a langiage would look like, wouldn’t it?

  4. Rajesh Karmani says:

    Channel contracts seem similar to interface contracts in OO programming: they separate the behavior from the implementation. But interfaces are enforced at compile time. As I understand, channel contracts are enforced at run-time in Maestro. Can they be enforced at compile time?

  5. Garry Trinder says:

    Rajesh,

    What do you mean with "interfaces are enforced at compile time"?

  6. Rajesh Karmani says:

    Sorry for being vague. I meant that correct usage (calling methods) of an interface type is checked at compile time.

  7. Garry Trinder says:

    Interfaces and channels are checked at compile-time in that respect, i.e. that ports are used correctly from a type perspective.

    What is different about channels is that they have protocols, which interfaces obviously don’t (although adding them doesn’t take much imagination). We currently check them at runtime, but it is also possible to do static verification of contracts through model checking, which can be quite expensive to do.

    On the positive side, model checking seems more feasible to do in finite time when you have bilateral chcannels rather then many-to-one or many-to-many mailboxes that are sometimes used in actor-based models.

    Hopefully, this addresses your question.

  8. Note: a variant of this text also appears in the Axum Programmer’s Guide, which will be distributed with