2

I'm creating a sample with Akka.Cluster with three nodes A, B and C where A is the lighthouse. So far, from the logs, the cluster works fine when there are no actors or when actors are local (created with spawn and spawnOpt). I want to create an actor from B and access it from C.

With

let _ = spawne system "r-actor" <@ actorOf (fun msg -> printfn "Received: %s" msg) @> []

I get

2016-08-31 01:59:00.1185|INFO|Akka.Actor.EmptyLocalActorRef|Message String from akka://calculatorSystem/deadLetters to akka://calculatorSystem/user/r-actor was not delivered. 1 dead letters encountered.

Using

let r = FromConfig.Instance :> RouterConfig |> SpawnOption.Router
let _ = spawne system "r-actor" <@ actorOf (fun msg -> printfn "Received: %s" msg) @> [r]

throws the exception

An unhandled exception of type Akka.Configuration.ConfigurationException occurred in Akka.dll
Additional information: Configuration problem while creating [akka://calculatorSystem/user/r-actor] with router dispatcher [akka.actor.default-dispatcher] and mailbox and routee dispatcher [akka.actor.default-dispatcher] and mailbox [].

The test function on Node C is

let rec calculate content =
  printfn "Processing %s" content
  let actor = select "/user/r-actor" system
  actor <! content
  let text = Console.ReadLine()
  if text <> "quit" then
    calculate text
calculate "sample1"

HOCON (Node B)

    akka {
      actor {
        provider = "Akka.Cluster.ClusterActorRefProvider, Akka.Cluster"
        serializers {
          wire = "Akka.Serialization.WireSerializer, Akka.Serialization.Wire"
        }
        serialization-bindings {
          "System.Object" = wire
        }
        deployment {
          /user/add {
            router = round-robin-pool
            nr-of-instances = 10
            cluster {
              enabled = on
              max-nr-of-instances-per-node = 10
              allow-local-routees = off
            }
          }
          /user/r-actor {
            router = round-robin-pool
            nr-of-instances = 10
            cluster {
              enabled = on
              max-nr-of-instances-per-node = 10
              allow-local-routees = off
            }
          }
        }
      }
      remote {
        log-remote-lifecycle-events = DEBUG
        log-received-messages = on
        helios.tcp {
          transport-class = "Akka.Remote.Transport.Helios.HeliosTcpTransport, Akka.Remote"
          applied-adapters = []
          transport-protocol = tcp
          hostname = "127.0.0.1"
          port = 0
        }
      }
      loggers = ["Akka.Logger.NLog.NLogLogger,Akka.Logger.NLog"]
      cluster {
        seed-nodes = [
          "akka.tcp://calculatorSystem@127.0.0.1:7001"
        ]
        roles = ["add-service"]
        auto-down-unreachable-after = 10s
      }
    }

How do I create an actor that can be called by another node in the cluster?

ildjarn
  • 62,044
  • 9
  • 127
  • 211
ritcoder
  • 3,274
  • 10
  • 42
  • 62

1 Answers1

2

When defining router configuration, don't use /user prefix - it's added automatically.

Also if you want to select actor that resides on another node, you need to use full actor path (with node address) - it's due to fact, that there may be different actors living on different nodes with the same path. You must be precise.

Usually address of the node won't be known to you at compile time. There are two ways to extract it:

1. Get addresses from the cluster state

It's easier but you're loosing the ability to react on joining/leaving nodes. You're forced to check it every time, which is slow. This example uses ActorSelection.ResolveOne to retrieve actual IActorRef instance.

async {
    let members = Cluster.Get(system).State.Members
    let actorRefs =
        members 
        |> Seq.filter (fun m -> m.Roles.Contains("expected")) // use roles to filter out nodes that shouldn't be checked
        |> Seq.map (fun m -> 
            let selection = select (m.Address.ToString() + "user/r-actor") system
            let actorRef = selection.ResolveOne(timeout) |> Async.AwaitTask)
        |> Async.Parallel }

2. Subscribe to cluster events and react on joining/leaving nodes

Here you can react on nodes as they join/leave. It also uses Identify/ActorIdentity to receive actual IActorRef, which is faster option.

let aref =  
    spawn system "listener"
    <| fun mailbox ->
        let cluster = Cluster.Get (mailbox.Context.System)
        cluster.Subscribe (mailbox.Self, [| typeof<ClusterEvent.IMemberEvent> |])
        mailbox.Defer <| fun () -> cluster.Unsubscribe (mailbox.Self)
        let rec loop () = 
            actor {
                let! (msg: obj) = mailbox.Receive ()
                match msg with
                | :? ClusterEvent.MemberUp as up -> 
                    // new node joined the cluster
                    let selection = select (up.Member.Address.ToString() + "user/r-actor") mailbox
                    selection <! Identify(null) // request actor under selection to identify itself
                | :? ActorIdentity as id when id.Subject <> null ->
                    // actor has identified itself
                    id.Subject <! "hello"
                | :? ClusterEvent.MemberRemoved as rem -> 
                    //  node leaved the cluster, invalidate all actors from that node
                | _ -> ()
                return! loop () }
        loop ()

In case of doubts, I've written a blog post about creating Akka.NET clusters from F#. Maybe you'll find it useful.

Bartosz Sypytkowski
  • 7,463
  • 19
  • 36
  • Tried the `listener` code above (2) and also from the blog post. I'm receiving the events but `ActorIdentity.Subject` is always null so the second case does not fire. - This was because the address wasn't right. It should have been `/user/r-actor` – ritcoder Aug 31 '16 at 10:56
  • From the message above, to make the call, we'll need to know the exact location of the actor. What happens to location transparency? It appears I do not fully understand what the cluster is to achieve. If I have roles [add, substract, divide, calculate], how to I structure it, such that `calculate` can access all three and I can have multiple instances in the cluster? – ritcoder Aug 31 '16 at 11:12
  • Location transparency means that you may send messages to actor, given its `IActorRef`, the same way no matter on which machine it resides - you can serialize actor ref as part of message too, and it still will be working, when deserialized. Your problem however was how to get that actor ref on the first place. Regarding calculate example: 1. You can create, add/sub/div actors as children of calculate - it's the fastest option. 2. You can use [Akka.Cluster.Sharding](http://getakka.net/docs/clustering/cluster-sharding). 3. There are also cluster routers, but won't fit this case. – Bartosz Sypytkowski Aug 31 '16 at 11:37
  • I've having a scenario where add/sub/div are individual applications. How do I create them as children of calculate? – ritcoder Aug 31 '16 at 13:13
  • What do you mean by applications? You can create actors as children of the current actor by using either `spawn mailbox ...` or `spawne mailbox ...`. Second option allows you to deploy them on the node of your choice. – Bartosz Sypytkowski Aug 31 '16 at 17:36
  • Applications as in individual processes. Can you use `spawn ...` for that? A similar implementation will help understand what is possible and what is not. For now, I'm exploring the Akkling examples. – ritcoder Aug 31 '16 at 17:56