Today I'm going to show you how to setup your own Akka.NET cluster using F# code. This is a kind of newbie guide of how to setup a cluster, directed to people unfamiliar with it. If you don't want to use Akka with F#, but you're understand the language syntaxt, don't worry. You won't miss a thing, since most of the concepts are common for .NET (and JVM) environment.

In this example I'm going to use the Akkling - it's my fork of the existing Akka.FSharp library. While it's API is mostly compatible with official F# Akka API, one of it's features is that actor refs are statically typed on handled message types. If you're using F#, you'll probably find it handy.

install-package Akka.Cluster -pre 
install-package Akkling -pre

or using Paket

paket add nuget Akka.Cluster
paket add nuget Akkling

Almost all of the cluster initialization can be done through configuration. In case if you're not yet familiar with the whole concept, here are some useful informations providing both high level and detailed overview:

Cluster setup

While I'll focus on differences bellow, basic configuration, common for all nodes may look like this:

akka {
  actor {
    provider = "Akka.Cluster.ClusterActorRefProvider, Akka.Cluster"
  }
  remote {
    log-remote-lifecycle-events = off
    helios.tcp {
      hostname = "127.0.0.1"
      port = 2551        
    }
  }
  cluster {
    roles = ["seed"]  # custom node roles
    seed-nodes = ["akka.tcp://cluster-system@127.0.0.1:2551"]
    # when node cannot be reached within 10 sec, mark is as down
    auto-down-unreachable-after = 10s
  }
}

With this configuration set up, our cluster will be able to configure itself as the new nodes will join/leave it. No additional code will be necessary.

Some explanations:

  • Since from the outside cluster looks like a single actor system, it's important, that all nodes being a part of it, must share the same actor system name.
  • Just like in Remote Actor Deployment example, we override default actor provider here. In this case we'll use ClusterActorRefProvider.
  • While cluster is able to self-manage joining nodes, each of them must know at least one node (access point) that is already part of the cluster. For this reason, we may define so called seed-nodes under akka.cluster.seed-nodes config key. While in this example there is only one defined, in practice you'd want to have at least 2-3 of them in case, when some will go down for any reason.

The basic difference between seed node and the joining ones, is that seed must listen on well-defined address to be easily accessed. Because we have already defined seed node address on port 2551, our seed node must have akka.remote.helios.tcp.port value set to that port.

What we are going to achieve is to create a fully operative cluster environment with an actor aware of it's presence inside it. What I mean by that, is that we want an actor to be able to react on incoming cluster events. For this let's use Cluster extension, which gives a many useful features, when working with cluster from within an actor.

let aref = 
    spawn system "listener"
    <| fun mailbox ->
        // subscribe for cluster events at actor start 
        // and usubscribe from them when actor stops
        let cluster = Cluster.Get (mailbox.Context.System)
        cluster.Subscribe (mailbox.Self, [| typeof<ClusterEvent.IMemberEvent> |])
        mailbox.Defer <| fun () -> cluster.Unsubscribe (mailbox.Self)
        printfn "Created an actor on node [%A] with roles [%s]" cluster.SelfAddress (String.Join(",", cluster.SelfRoles))
        let rec seed () = 
            actor {
                let! (msg: obj) = mailbox.Receive ()
                match msg with
                | :? ClusterEvent.IMemberEvent -> printfn "Cluster event %A" msg
                | _ -> printfn "Received: %A" msg
                return! seed () }
        seed ()

At this moment we will be able to track all of the control events going through the cluster.

Few more important properties, I think are worth explaining, are:

  • cluster.SelfAddress returns an Akka address of the current node (with host:port included). That allows to quickly recognize current node among the others.
  • cluster.ReadView contains cluster overview including things such as health check monitors or collection of all cluster member nodes. Members are useful in cases when you need to communicate between actors placed on different nodes, as they contain necessary localization addresses.
  • cluster.SelfRoles returns collection of so called roles attached to the current node. You may specify them using akka.cluster.roles configuration key. One of their purposes is to group the nodes inside a cluster and limit responsibilities of your system to a particular subset of nodes using common abstract discriminators. Example: if you want, you may run akka system on a web server or even mobile device as part of the cluster, but ensure that no resource-intensive work will ever run there.

Sending messages between nodes

The last thing, I want to show in this post, is how to pass message between two actors placed on the different nodes.

With first node already set up, configuration of a second node is very similar, except that in this case akka.remote.helios.tcp.port may be set to 0 (dynamic resolution) and akka.cluster.roles shouldn't contain a seed role.

Here's an actor code:

let aref = 
    spawn system "greeter"
    <| fun mailbox ->
        let cluster = Cluster.Get (mailbox.Context.System)
        cluster.Subscribe (mailbox.Self, [| typeof<ClusterEvent.MemberUp> |])
        mailbox.Defer <| fun () -> cluster.Unsubscribe (mailbox.Self)
        let rec loop () = 
            actor {
                let! (msg: obj) = mailbox.Receive ()
                match msg with
                // wait for member up message from seed
                | :? ClusterEvent.MemberUp as up when up.Member.HasRole "seed" -> 
                    let sref = select (up.Member.Address.ToString() + "/user/listener") mailbox
                    sref <! "Hello"
                | _ -> printfn "Received: %A" msg
                return! loop () }
        loop ()

Because in cluster environment node addresses are usually dynamic, we can't use static configuration to resolve them. For this sample we're waiting for a node to join the cluster, and receive MemberUp event. Then if the member passed with the event is marked with seed role, we'll send a message to it's listener actor.

After runing both nodes we should be able to see Hello message on the seed node received each time a new process with the joining node code is started - with dynamic port allocation config value (0), you should be able to run it as a separate process multiple times receiving message on the seed each time.