How to create an actor in a clustered configuratio

2019-07-12 22:37发布

问题:

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?

回答1:

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.