Akka.NET with F#
Getting functional with Reactive systems
Russ Cam
@forloop
About me
- Independent Software Engineer
-
Interested in
- Distributed systems
- Cloud computing
- Message-driven architectures
-
Software Design,
Patterns and Practices
-
Holla!
My First Computer
The Acorn Electron
-
Budget BBC Micro
- Released in 1983 (31 years ago)
- Audio cassette tapes!
- 32 kB RAM
Classic titles
Arguably the Grand Theft Auto of its generation, albeit with a hippy twist.
Agent-based Programming
a.k.a. The Actor model
...is a mathematical model of concurrent computation that treats
"actors" as the universal primitives of concurrent computation:
in response to a message that it receives, an actor can make local decisions,
create more actors, send more messages, and determine how to respond to the next message received.
First proposed in 1973!
- Actor architectures are not a new idea
- 1973 - Carl Hewitt, Peter Bishop and Richard Steiger
- paper "A universal modular ACTOR formalism for artificial intelligence"
Sequential flow
- Single control flow
- One entry point
- Other components called
- Control proceeds until result is returned
Parallel flow
-
Work that can be split up is done so using multiple threads/tasks
to perform it in parallel
- Improved throughput over sequential model
- Coordination model not indifferent from sequential
- Work can be concurrent, but proceeds until result is returned
Agent-based (Actor) flow
- Communication through asynchronous message passing
- Non-blocking
- Reactive system - may be less well defined entry and exit points
Ericsson AXD 301
- Erlang
- 99.9999999% uptime - "9 Nines"
- over 20 years!
- ~0.631 secs downtime per year
- well known example of an Actor architecture is the Ericsson AXD 301
- Asynchronous Transfer Mode (ATM) switching system to unify telecommunication and computer networks
- ERLANG
- 9 nines = service uptime, NOT uptime of computers running the system
akka.net
- Port of the JVM Akka Toolkit to the .NET Framework
- Heavily influenced by the Actor model implementation in Erlang
- Licensed under Apache 2
- Let it crash philosophy
- Named after Swedish mountain in region of Laponia.
Who's using akka?
Investment and Merchant Banking
Retail
Social Media
Simulation
Gaming and Betting
Automobile and Traffic Systems
Data Analytics
Why do we need a Reactive Manifesto?
- Akka adheres to the Reactive Manifesto
- The requirements for applications today have changed
- Users expect millsecond response times
- The system may be distributed over multiple cores and machines
Message Passing
- Asynchronous message passing to establish a boundary between components
- Loose Coupling
- Isolation
- Location Transparency
- Delegate Errors as messages
Resilient
- Stay Responsive in face of failure
- Contain failures to specific portions of system
Elastic
- React to the workload exerted on the system
- Scalable up and out
Responsive
- Respond in timely manner, if at all possible
- Builds user confidence in the system
Anatomy of an actor
Objects that encapsulate state and behaviour, communicating exclusively through message passing
Can do 4 things
- Receive and respond to incoming messages
- Send messages to other actors
- Create new actors
- Change behaviour in response to an incoming message
In Akka, pretty much everything is an actor
- Loggers
- Routing
- Persistence
- Eventbus
- Default Lifecycle monitoring
Who creates the first Actor?
Actor System
- Create top level actors
- Heavyweight structure, so create one per logical applicaytion
- Use System.create as it sets up serialization support for Remote Deployment
- EXAMPLE 0_ActorSystem.fsx
- Error Kernel Pattern
-
Fault Tolerance by pushing the most error prone operations
to the edges of the actor hierarchy
Creating Actors
types
type EchoActor() =
inherit UntypedActor()
override this.OnReceive (msg:obj) =
printfn "Received message %A" msg
let system = System.create "system" <| Configuration.defaultConfig()
// Use Props() to create actor from type definition
let echoActor = system.ActorOf(Props(typedefof<EchoActor>), "echo")
// tell a message
echoActor <! "Hello World!"
How very C# like!
actorOf
Receives next message from mailbox
let echo (msg:obj) =
printfn "Received message %A" msg
let system = System.create "system" <| Configuration.defaultConfig()
// use spawn in conjunction with actorOf.
// Wraps provided function to give actor behaviour
let echoActor = spawn system "echo" (actorOf echo)
// tell a message
echoActor <! "Hello World!"
actorOf2
Receives "self" actor and next message from mailbox
let echo (mailbox:Actor<'a>) msg =
printfn "Received message %A" msg
let system = System.create "system" <| Configuration.defaultConfig()
// use spawn in conjunction with actorOf2.
// Wraps provided function to give actor behaviour
let echoActor = spawn system "echo" (actorOf2 echo)
// tell a message
echoActor <! "Hello World!"
actor computation expression
Receives "self" actor and returns a Continuation
let echo (mailbox:Actor<'a>) =
let rec loop () = actor {
let! msg = mailbox.Receive ()
printfn "Received message %A" msg
return! loop ()
}
loop ()
let system = System.create "system" <| Configuration.defaultConfig()
// use spawn in conjunction with actor computation expression
let echoActor = spawn system "echo" echo
// tell a message
echoActor <! "Hello World!"
- self-invoking recursive function
-
type Cont<'In, 'Out> =
| Func of ('In -> Cont<'In, 'Out>)
| Return of 'Out
- ActorType
- ActorOf
- ActorOf2
- Props
- EXAMPLE 1_ActorType.fsx
- EXAMPLE 2_ActorOf.fsx
- EXAMPLE 2_ActorOf2.fsx
- EXAMPLE 3_ActorComputationExpression.fsx
I can create actors, now what?
Message passing
ActorRef
// ActorRef
let echoActorRef = spawn system "echo"
(actorOf (fun m -> printfn "received %A" m))
// tell a message to actor ref
echoActorRef <! "Hello World!"
- ActorSystem actually returns an ActorRef to the actor
- ActorRef is essentially an address to a particular actor
- Location transparency
ActorSelection
// ActorSelection, using wildcard actor path
let echoActorSelection = select "/user/echo*" system
// tell a message to actor selection
echoActorSelection <! "Hello World!"
- Location Transparency
- ActorSelection could be more than one ActorRef
- Prefer ActorRef where possible. Object to communicate with an actor.
- ActorRef can be passed around in messages, etc.
- ActorRef examples:
Tell
One way, Fire & Forget
actor <! message
- Send a message, no reply expected
- "Fire and Forget"
- No blocking waiting for reply
- Passing messages via ActorRefs
- Passing messages via ActorSelection
Ask
Request / Response
async {
let! response = actor <? message
// do something with response
} |> Async.RunSynchronously
- Send a message, expects a reply back
- Does not block as it's an async method, but can timeout (basically a task)
- Tell method is preferred because it offers better scaling and concurrency
- Overhead to Ask involves the ActorSystem creating temporary actors to handle asynchonicity/li>
-
Ask should be preferred largely if the constraints of the system prevent the use of tell e.g.
May be working with a web framework that does not allow Tell
Forward
actorToForwardTo.Forward message
-
Forward is similar to Tell with the primary difference being that the original sender
address and reference are preserved.
-
Useful for when working with routers, load balancers, replicators and supervisors
PipeTo
async {
// some async workflow
} |!> myActor
or
myActor <!| async {
// some async workflow
}
-
When running an async workflow/task, can pipe the response to an actor
-
Right now, there is a limitation in the F# API whereby the sender of the message piped with PipeTo is NoSender rather than the
actor that encapsulated the async workflow. This is going to be addressed in future.
What about that hierarchy of actors?
Supervision
type CustomException() =
inherit Exception()
type Message =
| Echo of string
| Crash
let system = System.create "system" <| Configuration.defaultConfig()
let child (childMailbox:Actor<Message>) =
let rec childLoop() = actor {
let! msg = childMailbox.Receive()
match msg with
| Echo info ->
let response =
sprintf "Child %s received: %s"
(childMailbox.Self.Path.ToStringWithAddress()) info
childMailbox.Sender() <! response
| Crash ->
printfn "Child %A received crash order"
(childMailbox.Self.Path)
raise (CustomException())
return! childLoop()
}
childLoop()
let parent (parentMailbox:Actor<Message>) =
// parent actor spawns child - is the supervisor of the child
let child = spawn parentMailbox "child" child
let rec parentLoop() = actor {
let! (msg: Message) = parentMailbox.Receive()
child.Forward msg
return! parentLoop()
}
parentLoop()
// spawn with options to define a Supervisor Strategy
let parentChild =
spawnOpt system "parent" parent <|
[ SpawnOption.SupervisorStrategy (
Strategy.OneForOne(fun e ->
match e with
| :? CustomException -> Directive.Restart
| _ -> SupervisorStrategy.DefaultDecider(e))); ]
...and one for all
The AllForOneStrategy is applicable in cases where the ensemble of children has such
tight dependencies among them, that a failure of one child affects the function of the others,
i.e. they are inextricably linked. Since a restart does not clear out the mailbox,
it often is best to terminate the children upon failure and re-create them explicitly
from the supervisor (by watching the children’s lifecycle); otherwise you have to make
sure that it is no problem for any of the actors to receive a message which was queued
before the restart but processed afterwards.
- Escalate (default supervisor directive) - thereby failing itself
- Resume - keeping its accumulated internal state
- Restart - clearing out its accumulated internal state
- Stop - permanently
Lifecycle Monitoring
a.k.a. Deathwatch
- Monitor the lifecycle of another actor
- Know when it is terminated
monitor (targetActor:IActorRef, watcher:ICanWatch)
and
demonitor (targetActor:IActorRef, watcher:ICanWatch)
Creating groups of similar actors?
Routers
Pools and Groups
- RoundRobin
- Broadcast
- Consistent Hashing
- TailChopping
- ScatterGatherFirstCompleted
- SmallestMailbox
- Random
- Broadcast - send the same message to all routees
- Consistent Hashing - send messages with same "hash key" to same routee
- Tail Chopping - send message to one, wait a little, send to another, return first, discard rest
Akka Remote
- Needs to be enabled on both systems
- HOCON (Human-Optimized Config Object Notation)
Shared assemblies
let config = Configuration.parse """
akka {
actor {
provider =
"Akka.Remote.RemoteActorRefProvider, Akka.Remote"
deployment {
/my-actor {
remote = "akka.tcp://system2@localhost:8080"
}
}
}
remote {
helios.tcp {
transport-class =
"Akka.Remote.Transport.Helios.HeliosTcpTransport, Akka.Remote"
applied-adapters = []
transport-protocol = tcp
port = 8090
hostname = localhost
}
}
}"""
let system = System.create "system" <| config
let myActor = system.ActorOf<SomeActor>("my-actor")
- Build an ActorSystem across several processes
F# Quotations
let system = System.create "system" <| Configuration.defaultConfig()
let myActor = spawne system "my-actor"
<| (<@ fun mailbox ->
printfn "Actor pre start"
mailbox.Defer (fun () -> printfn "Actor post stop")
let rec loop () =
actor {
let! msg = mailbox.Receive ()
// msg handling here
return! loop ()
}
loop () @>)
[SpawnOption.Deploy(
Deploy(RemoteScope (Address.Parse "akka.tcp://system2@localhost:8080")))]
- Serialized using FsPickler and sent across the wire (System.create adds FsPickler support)
- No need to stop your remote nodes to reload shared actor assemblies when updated
-
DISADVANTAGE: Code embedded inside quotation must use only functions, types
and variables known to both endpoints. There are limited ways to define functions
inside quotation expression (and no way to define types), but generally speaking
in most cases it’s better to define them in separate library and share between nodes
Changing Actor Behaviour?
FSM
Finite State Machine
let actor = spawn system "MyActor" <|
fun mailbox ->
let rec loop1() = actor {
let! message = mailbox.Receive()
// do something with message
return! loop2()
}
and loop2() = actor {
let! message = mailbox.Receive()
return! loop3()
}
and loop3() = actor {
let! message = mailbox.Receive()
return! loop1()
}
loop1()
- Way to change the behaviour of actors at runtime
- In response to messages
No time for...
Dispatchers
Mailboxes
Event stream/bus
Scheduler
Clustering
Testing
Persistence/Snapshots
What about MailboxProcessor<`a>?
Messages and Actors - F# for fun and profit
#nowarn "40"
let printerAgent = MailboxProcessor.Start(fun inbox->
// the message processing function
let rec messageLoop = async {
// read a message
let! msg = inbox.Receive()
// process a message
printfn "message is: %s" msg
// loop to top
return! messageLoop
}
// start the loop
messageLoop
)
- Need to directly reference mailboxes
- Built on top an unbounded queue
- Not distributed
- Cannot be remote deployed
- Strongly typed mailbox - Pro and Cons
-
Some challenges in being consumed from C# e.g.
Example 1
Example 2
Event Stream/Bus
- Provides a pub/sub model for events
- Useful for side effects to main control flow i.e. no tight coupling
- Originally conceived as a way to send messages to a group of Actors
- Does not preserve the original sender of the message
- EXAMPLE 11_EventStream
Scheduling
- Need to make things happen in the future
- Provided by the Actor System
- Exact timing inexact
- Cancellable via CancellationToken
Akka.NET with F#
Getting functional with Reactive systems
Russ Cam
@forloop