Ping - Pong with Remoting

A full working example is available here

This example consists of two actors Ping and Pong that exchange a set of messages. When the Ping actor is created a counter is initialized in this case to 100000. Once this counter reaches zero the messages stop flowing and the actors shutdown. The message cascade is started by the Ping actor which sends a Ping message to the Pong actor, which then returns a Pong message back to the Ping actor, which then decreaments its count.

As with any actor system, we need to define the message types. With remoting these types need to be put in a seperate assembly when using binary serialization, so they can be shared between the processes. If you are using JSON or XML this need not be the case. In the PingPong sample we create a seperate assembly called PingPongDomain and enter the following types definition.

type PingPong =
    | Ping
    | Pong
    | Stop

To configure the a process that contains actors for remoting, we need to configure the transports that are available to the host. In this case we are using the built in TCP transport.

Once the actor host has started we need to create a system to hold the actor and enable remoting on that system, by calling the EnableRemoting method. This method takes two parameters. The first parameter defines how actor systems will communicate with each other when looking up actors. The second defines how the actor systems discover each other.

let system = ActorHost.Start()
                      .SubscribeEvents(fun (evnt:ActorEvent) -> printfn "%A" evnt)
                            [new TCPTransport(TcpConfig.Default(IPEndPoint.Create(12002)))],
                            new BinarySerializer(),
                            new TcpActorRegistryTransport(TcpConfig.Default(IPEndPoint.Create(12003))),
                            new UdpActorRegistryDiscovery(UdpConfig.Default(), 1000)

Once the hosts and systems are setup with the transports configured. The implementation of the actors stays identical to the in-process implementation.

let ping count =
    actor {
        name "ping"
        body (
                let pong = !~"pong"
                let rec loop count = 
                    messageHandler {
                        let! msg = Message.receive()
                        match msg with
                        | Pong when count > 0 ->
                              if count % 1000 = 0 then printfn "Ping: ping %d" count
                              do! pong.Value Ping
                              return! loop (count - 1)
                        | Ping -> failwithf "Ping: received a ping message, panic..."
                        | _ -> pong.Value <-- Stop
                loop count        

let pong = 
    actor {
        name "pong"
        body (
            let rec loop count = messageHandler {
                let! msg = Message.receive()
                match msg with
                | Ping -> 
                      if count % 1000 = 0 then printfn "Pong: ping %d" count
                      do! Message.reply Pong
                      return! loop (count + 1)
                | Pong _ -> failwithf "Pong: received a pong message, panic..."
                | _ -> ()
            loop 0        

One thing to consider when remoting is enabled on a actor system, is the latency that maybe introduced when actors are resolved. Depending on how qualified the actor path is, the lookup may have to wait for a reply from all of the peers that the actor system has discovered. For more information about actor lookups see here

namespace Cricket
namespace System
namespace System.Net
type PingPong =
  | Ping
  | Pong
  | Stop

Full name: Remoting.PingPong
union case PingPong.Ping: PingPong
union case PingPong.Pong: PingPong
union case PingPong.Stop: PingPong
val system : ActorHost

Full name: Remoting.system
type ActorHost =
  interface IDisposable
  private new : configuration:ActorHostConfiguration -> ActorHost
  member private Configure : f:(ActorHostConfiguration -> ActorHostConfiguration) -> unit
  member private RegisterActor : ref:ActorRef -> unit
  member private ResolveActor : name:ActorPath -> ActorRef list
  member SubscribeEvents : eventF:('a0 -> unit) -> ActorHost
  member private CancelToken : CancellationToken
  member private EventStream : IEventStream
  member private Name : string
  static member Dispose : unit -> unit

Full name: Cricket.ActorHost
static member ActorHost.Start : ?name:string * ?loggers:ILogWriter list * ?serializer:'a0 * ?registry:ActorRegistry * ?metrics:Diagnostics.MetricsConfiguration * ?tracing:Diagnostics.TracingConfiguration * ?cancellationToken:System.Threading.CancellationToken -> ActorHost
val evnt : ActorEvent
type ActorEvent =
  | ActorStarted of ActorRef
  | ActorShutdown of ActorRef
  | ActorRestart of ActorRef
  | ActorErrored of ActorRef * exn
  | ActorLinked of ActorRef * ActorRef
  | ActorUnLinked of ActorRef * ActorRef

Full name: Cricket.ActorEvent
val printfn : format:Printf.TextWriterFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
Multiple items
type TCPTransport =
  interface ITransport
  new : config:TcpConfig -> TCPTransport

Full name: Cricket.TCPTransport

new : config:TcpConfig -> TCPTransport
type TcpConfig =
  {ListenerEndpoint: IPEndPoint;
   Backlog: int;
   PoolSize: int;
   BufferSize: int;}
  static member Default : listenerEndpoint:IPEndPoint * ?backlog:int * ?poolSize:int * ?bufferSize:int -> TcpConfig

Full name: Cricket.TcpConfig
static member TcpConfig.Default : listenerEndpoint:IPEndPoint * ?backlog:int * ?poolSize:int * ?bufferSize:int -> TcpConfig
Multiple items
type IPEndPoint =
  inherit EndPoint
  new : address:int64 * port:int -> IPEndPoint + 1 overload
  member Address : IPAddress with get, set
  member AddressFamily : AddressFamily
  member Create : socketAddress:SocketAddress -> EndPoint
  member Equals : comparand:obj -> bool
  member GetHashCode : unit -> int
  member Port : int with get, set
  member Serialize : unit -> SocketAddress
  member ToString : unit -> string
  static val MinPort : int

Full name: System.Net.IPEndPoint

IPEndPoint(address: int64, port: int) : unit
IPEndPoint(address: IPAddress, port: int) : unit
static member IPEndPoint.Create : ?port:int -> IPEndPoint
Multiple items
type BinarySerializer =
  interface ISerializer
  new : unit -> BinarySerializer

Full name: Cricket.BinarySerializer

new : unit -> BinarySerializer
Multiple items
type TcpActorRegistryTransport =
  interface IActorRegistryTransport
  new : config:TcpConfig -> TcpActorRegistryTransport

Full name: Cricket.TcpActorRegistryTransport

new : config:TcpConfig -> TcpActorRegistryTransport
Multiple items
type UdpActorRegistryDiscovery =
  interface IActorRegistryDiscovery
  new : udpConfig:UdpConfig * ?broadcastInterval:int -> UdpActorRegistryDiscovery

Full name: Cricket.UdpActorRegistryDiscovery

new : udpConfig:UdpConfig * ?broadcastInterval:int -> UdpActorRegistryDiscovery
type UdpConfig =
  {Id: Guid;
   Port: int;
   ConnectMethod: UdpConnectMethod;}
  member RemoteEndpoint : IPEndPoint
  static member Broadcast : ?id:Guid * ?port:int * ?address:IPAddress -> UdpConfig
  static member Default : ?id:Guid * ?port:int * ?connectMethod:UdpConnectMethod -> UdpConfig
  static member Direct : address:IPAddress * ?id:Guid * ?port:int -> UdpConfig
  static member Multicast : ?id:Guid * ?port:int * ?group:IPAddress -> UdpConfig

Full name: Cricket.UdpConfig
static member UdpConfig.Default : ?id:System.Guid * ?port:int * ?connectMethod:UdpConnectMethod -> UdpConfig
val ping : count:int -> ActorConfiguration<PingPong>

Full name:
val count : int
val actor : ActorConfigurationBuilder

Full name:
custom operation: name (string)

Calls ActorConfigurationBuilder.Name
custom operation: body (MessageHandler<ActorCell<'a>,unit>)

Calls ActorConfigurationBuilder.Body
val pong : Lazy<ActorSelection>
val loop : (int -> MessageHandler<ActorCell<PingPong>,unit>)
val messageHandler : Message.MessageHandlerBuilder

Full name: Cricket.ActorConfiguration.messageHandler
val msg : PingPong
Multiple items
module Message

from Cricket

type Message<'a> =
  {Id: uint64 option;
   Sender: ActorRef;
   Message: 'a;}

Full name: Cricket.Message<_>
val receive : unit -> MessageHandler<ActorCell<'a>,'a>

Full name: Cricket.Message.receive
val post : targets:'a -> msg:'b -> MessageHandler<ActorCell<'c>,unit>

Full name:
property System.Lazy.Value: ActorSelection
val failwithf : format:Printf.StringFormat<'T,'Result> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.failwithf
val pong : ActorConfiguration<PingPong>

Full name: Remoting.pong
val reply : msg:'a -> MessageHandler<ActorCell<'b>,unit>

Full name: Cricket.Message.reply
Fork me on GitHub