Skip to content
Paul Louth edited this page Mar 25, 2017 · 3 revisions

Source

Eventually consistent isn't always the desired behaviour, often you want to just find a Process that does 'a thing' and you want it to do that thing now. Roles facilitate that behaviour. Each node in the cluster must have a role name. Roles use the Process.ClusterNodes property to work out which member nodes are actually available (it's at most 3 seconds out of date, if a node has died, otherwise it's updated every second).

If you had 10 mail-servers, you could find the least-busy SMTP process by doing something like this:

    ProcessId pid = Role.LeastBusy["mail-server"]["user"]["outbound"]["smtp"];

    tell(pid, email);

The first child mail-server is the role name (which you specify when you call Cluster.register(...) at the start of your app), the rest of it is a relative leaf /user/outbound/smtp that will refer to N processes in the mail-server role.

The problem with that ProcessId is that you need to know about the inner workings of the mail-server node to know that the smtp Process is on the leaf /user/outbound/smtp, and that means that the Process hierarchy for the mail-server can't ever change. However because pid is just a ProcessId the mail-server nodes themselves could register it instead:

    register("smtp", Role.LeastBusy["mail-server"]["user"]["outbound"]["smtp"]);

Note, even if you have 10 nodes in the mail-server role, and they all call register with the same role ProcessId, there will only be one value stored in the process registry.

Then any other node that wanted to send a message to the least-busy smtp Process could call:

    tell("@smtp", msg);

You'll notice also that the mail-server nodes themselves have the control over how to route messages, whether it's least-busy, round-robin, etc. They can change their strategy without it affecting the sender applications.

Although this SMTP example isn't a great one, it should indicate how you can use registered names to represent a dynamically changing set of nodes and processes in the cluster.

Registered processes

Built in roles

Name Description
Role.Broadcast Dispatches to all nodes in the role
Role.LeastBusy Dispatches to the least busy node in the role - This is done by interrogating the queues of each Process that matches. Therefore you should use this with caution if you have a large number of cluster-nodes. This is most useful when you have a few Processes that do long running jobs.
Role.Random Dispatches to a random node in the role
Role.RoundRobin Dispatches to each node in the role in a round-robin fashion
Role.First Dispatches to the first node in the role. This is done by sorting the nodes by their node name.
Role.Second Dispatches to the second node in the role. This is done by sorting the nodes by their node name.
Role.Third Dispatches to the third node in the role. This is done by sorting the nodes by their node name.
Role.Last Dispatches to the last node in the role. This is done by sorting the nodes by their node name and reversing the list.
Role.Next Dispatches to the next node in the role. Unlike other Roles, you do not specify the role-name as the first child - your node's cluster membership and node-name is used to work out which role and what the next node is.
Role.Prev Dispatches to the previous node in the role. Unlike other Roles, you do not specify the role-name as the first child - your node's cluster membership and node-name is used to work out which role and what the next node is. Note, this is much less efficient than Role.Next for large role memberships.

User defined roles

Roles are just dispatchers and work in exactly the same way. They just have bespoke behaviour for working with cluster nodes. You can register any dispatcher like so:

    // A dispatcher called 'dead' that dispatches everything to dead-letters
    var deadLetterDisp = Dispatch.register(
        "dead", 
        (ProcessId leaf) => new [] { DeadLetters }
    );

A dispatcher merely turns a leaf ProcessId into an enumerable of ProcessIds. Once you have the result of Dispatch.register you can then create relative ProcessIds:

    ProcessId pid = deadLetterDisp["user"]["proc"];

The resulting ProcessId will look like this:

    /disp/dead/user/proc

What gets passed to the dispatcher function is:

    /user/proc

Knowing this and the context of the dispatcher (that it's a dead-letter dispatcher) means you can use this information to build the resulting ProcessIds.

Roles use this exact mechanism. Here's the real definition for Role.First that dispatches to the first node in a role.

    Role.First = Dispatch.register("role-first", leaf => Role.NodeIds(leaf).Take(1));

The result is that Role.First is a ProcessId that looks like this:

    /disp/role-first

When used with a tell (for example) the first child is by convention the role name, followed by a relative path into the member-node Process hierarchy.

    var pid = Role.First["some-role"]["user"]["some-process"];
    tell(pid, "Hello");

The pid value would look like this:

    /disp/role-first/some-role/user/some-process

Going back to the definition of Role.First, the leaf value will be:

    /some-role/user/some-process

The Role.NodeIds(...) helper function will take that, extract the role name some-role, use Process.ClusterNodes to find all nodes in the role some-role and will build a sorted enumerable of ProcessIds using the remaining path /user/some-process.

    /some-node/user/some-process
    /some-other-node/user/some-process
    /yet-another-node/user/some-process
    ...

Hopefully you can see that by calling Take(1) on Role.Nodes(...) it will implement the 'first' behaviour needed for Role.First.

It is also trivial to implement most of the other built-in role types. Here's Role.Broadcast and Role.LeastBusy

    // Broadcast
    Role.Broadcast = Dispatch.register("role-broadcast", Role.NodeIds);

    // Least busy
    Role.LeastBusy = Dispatch.register("role-least-busy", leaf =>
                         Role.NodeIds(leaf)
                            .Map(pid => Tuple(inboxCount(pid), pid))
                            .OrderBy(tup => tup.Item1)
                            .Map(tup => tup.Item2)
                            .Take(1));

Source

Clone this wiki locally