-
Notifications
You must be signed in to change notification settings - Fork 18
Roles
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.
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. |
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));