More than one API? Decisions, decisions…

This article is an installment to F# Advent Calendar 2017.

If you develop .NET applications based on actor model, you must be familiar with Akka.NET - a port of JVM Akka framework that cleverly combines design and even major implementation details of the original library written in Scala with .NET coding practices. Scala is a functional language and actor model is a great fit for functional programming, and to expose Akka to functional programming Akka.NET developers provided an idiomatic F# API for it almost from the beginning of the project when it was in its early beta. Even though usage of Akka.NET F# API will never match the popularity of its C# counterpart (about 1.5 millions downloads of the main Akka.NET NuGet package by the end of 2017), the F# API NuGet packages download statistics (more than 20K total, about 5.5K for the version 1.2.3, the latest in 2017) shows stable increase of developers programming Akka in F#.

Then there is Akkling - according to its documentation, an experimental Akka.NET F# library, currently at the version 0.8 with 3.6K total package downloads at the time of the article writing. Experiments pave a road to innovations, but if you currently writing in F# a system for production, why would you want to use anything else than the official Akka.NET F# API, especially since it is used to teach Akka programming at a popular Petabridge Akka.NET bootcamp (where you can choose to view code examples in either C# or F#)?

Well, don’t let version numbers and statuses fool you. First of all, Akkling is written and maintained by the major contributor to the official F# API Bartosz Sypytkowsky, so by inspecting Akkling you can get an idea of how Akka.NET F# would have been written today. The new API is not compatible with the original one, and this is the main reason why the changes haven't been merged. So think about it as Akka.NET F# API 2.0.

Next, the work on Akkling began when Akka.NET still didn't reach RTM status, and the library has 117 commits since 05.06.2015, only in 2017 it had 38 commits, some of them added new features like operators for Akka Streams. The library is in active development phase, unlike the official F# API which covers only basic actor management functionality, with its main file FsApi.fs gone through 107 commits (less than Akkling), and the 11 commits in 2017 it had was mostly about bumping version number and updating dependencies.

The bottom line: the Akka.NET F# API that follows main Akka.NET library is in its maintenance phase. Bugs reported are fixed, and that's about it. Don't expect it to support cluster sharding or Akka Streams. But even if your usage of actors is restricted to general cases, there are several important reasons why you should look into Akkling, and I will present them in the next sections.

What does Akkling contain?

Akkling consists of the following libraries, and it's easy to judge from library names what they cover:

  • Akkling.Cluster.Sharding
  • Akkling.DistributedData
  • Akkling.Persistence
  • Akkling.Streams

Akkling also complements Akka.NET TestKit with Akkling.TestKit that can be used to write in-memory unit tests using either of standard test frameworks (including FsCheck). If you are familiar with official Akka.NET F# API you probably remember that it only consists of Akka.FSharp and Akka.Persistence.FSharp libraries.

But Akkling does not only extend F# API to cover more Akka features. There are three core actor usage scenarios where Akkling makes a big difference:

  • Typed actors
  • Actor lifecycle events
  • Better persistent actors

Each of the these points may alone become a reason to switch to Akkling, and in our project last two were sufficient reasons. So let me guide you through these points and explain what they are in essence.

Typed actors

Even though typed actors represent the biggest difference between Akkling and the original API - and the main reason for incompatibility, they don't bring extra functionality. They just enforce compile-time type check of the messages sent to actors so you won't be able to send 42 to an actor expecting a string. Unfortunately actors often participate in scenarios more complex than processing of domain commands and events. They may need to react on infrastructure messages (more on this in the next section) when their typed nature can be an obstacle. But if an actor is defined purely for a purpose of handling domain messages, it will certainly benefit from being defined as typed.

The actor definition for both original F# API and Akkling looks almost identical. Assuming we have a message type:

type ActorMessages =
    | Greet of string
    | Hi

… then we can define an actor for such messages using the original F# API:

let actor = 
    spawn system "MyActor"
    <| fun mailbox ->
        let loop () =
            actor {
                let! message = mailbox.Receive()
                match message with
                | Greet(name) -> 
                    printfn "Hello %s" name
                    return! loop ()
                | Hi ->
                    printfn "Hello from F#!"
                    return! loop () 
        loop ()

… and Akkling:

let actor = 
    spawnAnonymous system 
    <| props(fun mailbox ->
        let rec loop () = 
            actor {
                let! message = mailbox.Receive()
                match message with
                | Greet(name) -> 
                    printfn "Hello %s" name
                    return! loop ()
                | Hi ->
                    printfn "Hello from F#!"
                    return! loop () } 
        loop ())

The only syntactic difference is use of props function in Akkling. Once an actor is defined, its usage is the same using both libraries:

actor <! Hi
actor <! Greet "Figaro"

Both the original API and Akkling infer a mailbox type from messages received by its mailbox, so if it expects a discriminated union of a certain type (and this is how domain commands and events are typically defined in F#), then expanding it to process messages of a different type won't pass compiler check. However, current F# API restricts type enforcement to the actor mailbox definition, while Akkling goes further and enforces the same type on actor usage, so the following code won't compile in Akkling but will be accepted by the original F# API causing a message to be published to Unhandled event stream:

actor <! 2017

If an actor needs to process messages of different types, you will need to define it as a typed actor of obj. Akkling also defines typed and untyped operators for interop with actors defined using Akka.NET C# or original F# API (yes, you can mix Akkling and current F# API, and this is what we did in the transition period in our project). It even has retype operator if an actor is a kind of chameleon and handles messages of various types. Use of retype pretty much defeats the purpose of having typed actors. In fact, when it comes to actor typing, Akka.NET C# API provides more flexibility than the F# ones (both the original and Akkling). In C# you define separate message handlers for messages of different types:

public class MyActor: ReceiveActor
    public MyActor()
        Receive<string>(message => {...});
        Receive<int>(message => {...});

In F# your actor's mailbox will have to be of a type obj, and you will need to write code that performs message type checking at runtime:

match message with
| :? string as m -> (...)  
| :? int as m -> (...)
| _ -> failwith "unknown message"

Needless to say, typed actor implementation in Akkling doesn't provide the same level of type inference and enforcement that F# is known for. This has been discussed in various channels with developers using and contributing to Akka.NET, but improvement possibilities are not obvious and they will require another major revision of Akka.NET F# API.

Lifecycle events

If you need one single reason to switch to Akkling, then this is the one. The original Akka.NET F# API lacks a few features comparing to its C# counterpart, and ability to respond to actor lifecycle events is one of the most significant. Consider the following scenario:

  • An actor fails processing a message, raising an exception
  • An actor's supervisor invokes Restart supervision strategy and a new instance is created
  • A newly created actor goes through initial lifecycle stages (sending preStart and preRestart events)
  • A part of preRestart event details is the message that caused the failure in first place, it may be retried at this stage

Current F# API doesn't expose actor lifecycle events, so there is no way to respond to preRestart event and retry the message (C# API has no problems with that), so the offending message will be lost. You will have to complement the actor system with some external mechanism that will take care of failed messages so they can be retried. Or use Akkling.

Here's sample code for a simple actor that retries failed messages after actor restart.

let rec loop () = actor {
    let! (msg: obj) = m.Receive ()
    match msg with
    | LifecycleEvent e ->
        match e with
        | PreStart -> printfn "Actor %A has started" m.Self
        | PreRestart (exn, msg) -> printfn "Actor %A has restarted due to an error: %s" m.Self exn.Message
        | _ -> return Unhandled
    | x -> printfn "%A" x
    return! loop ()

Ironically, support for lifecycle events introduced by Akkling is a main obstacle to efficient use of its other major addition: typed actors. In order for an actor to respond to lifecycle events, it needs to accept messages of different types, so its mailbox must accept messages of type obj. End of strong typing. 

Persistent actors

No Akka API would be useful without support for actor persistence, so Akka.NET F# API had persistent actors from its earliest days. They performed their main function (persisted incoming messages) but made it hard to do anything else expected from an actor. And they also lacked lifecycle events - a more critical omission for a piece of code that has additional lifecycle stages during restoration of its state from an external database and often greater chances to fail due to tight coupling to an external system.

When using the original F# API you had to define handlers to set initial persistent actor state, recover its state from a persistent store and to process commands. Assuming these function are called empty, apply and exec, you could then define the actual actor:

let myActor = 
    spawnPersist system "unique_id" {
        state = empty
        apply = apply
        exec = exec
    } []

The command handler exec can contain commands that cause persistent state to be updated as well as commands that don’t affect the actor state:

let exec (mailbox: Eventsourced<_,_,_>) state cmd = 
    match cmd with
    | "print" -> printf "State is: %A\n" state
    | s       -> mailbox.PersistEvent (update state) [s]

As you can see from the implementation above, persistent actor definition is very different from non-persistent once and leaves you little flexibility (for example, you can’t have multiple command handlers for different actor states). I already mentioned lack of support for lifecycle events.

With Akkling you get more or less a “normal” actor with fine-grained control of all its lifecycle aspects. You use the same spawn function to create a persistent actor, the only difference in is that you use it with propsPersist instead of props:

let counter =
    spawn system "counter-1" <| propsPersist(fun mailbox ->
        let rec loop state =
            actor {
                let! msg = mailbox.Receive()
                match msg with
                | Event(changed) -> return! loop (state + changed.Delta)
                | Command(cmd) ->
                    match cmd with
                    | GetState ->
                        mailbox.Sender() <! state
                        return! loop state
                    | Inc -> return Persist (Event { Delta = 1 })
                    | Dec -> return Persist (Event { Delta = -1 })
        loop 0)

(this code example is taken from Akkling source code repository).

You can also specify handlers for Akkling’s PersistentLifecycleEvent (ReplaySucceeded and ReplayFailed), and you will most likely do this to invoke your own recovery strategy in case your persistent store is unavailable or failing to respond in time.

Routers - when less is more

Soon after we started porting our code to use Akkling we discovered something that at first glance looked like a showstopper - lack of ConsistentHashing routers. Our system managed files and it was crucial that same file wasn’t processed simultaneously by two sibling actors supervised by a router. Therefore we couldn’t configure such router as RoundRobin or SmallestMailbox strategy, we need a strategy that could be configured to provide file affinity, sending messages related to a given file to the same actor. ConsistentHashing strategy made it possible - we just had to define hashing function based on file path.

Unlike other strategies, ConsistentHashing strategy can not be configured from Akka HOCON configuration file. It requires a hashing function that must be declared in code. And unlike Akka.NET C# and F# (original) API, Akkling only supports setting routing strategy from configuration (using FromConfig method). So no consistent hashing in Akkling.

I submitted an issue in GitHub, and to my surprise, Bartosz - always very positive to feedback about bugs or missing functionality - was reluctant this time to implement missing strategy. Instead he explained me that he couldn’t see good use cases for such strategy and therefore wasn’t really keen on implementing it in Akkling, preferring instead that developers solve such scenarios differently. I was confused at first because I didn’t see an alternative to consistent hashing to support file affinity when using routers, but upon giving it more thought I realized that we can simply create an actor per file. Developers sometimes forget that actors are cheap to create and have very low memory footprint, so an actor system shouldn’t hesitate to create an actor per file, per bank account, per person etc., and such simple architecture will guarantee that all messages related to a given entity (file, bank account, person) will be processed in order and by the same actor. The next day we started revising our system to get rid of consistent hashing strategy, something that didn’t take long time and ended with cleaner code. So this is a good example of how restricting number of choices (while keeping a set of essential ones) guides developers to avoid design mistakes.

Beyond core functionality

As I wrote in the beginning of the article, no new features are added to current Akka.NET F# API, they are added to Akkling. Support for cluster sharding and Akka Streams are among the biggest news. With Akka systems in production growing from singleton services to full featured clusters, developers will appreciate idiomatic F# API for cluster sharding. To create an instance of a sharded actor you should call spawnSharded function instead of spawn and pass it an additional argument for message extractor function that extract the entry identifier and the shard identifier from incoming messages, e.g.:

let actor = spawnSharded extractor system "MyActor" actorProps

Here’s an example of extractor function computing for each message its shardId and entityId:

let extractor shardPrefix numberOfShards (entityExtractor : 'payload -> string) (message:obj) =
    let entity = getEntityId message
    let hash = entityId.GetHashCode()
    let shardId = sprintf "%s_%d" shardPrefix ((abs hash) % numberOfShards)
    shardId, entityId, message

When it comes to Akka Streams, the elegance of Scala GraphDSL sets the bar on a fairly high level which will not be easy to reach. If you follow Akkling development you may have noticed recent updates in Akling.Streams, and GitHub discussions on a subject of syntax simplicity where it was suggested to introduce Akka Streams computational expressions to make the GraphDSL builder implicit, like in Scala. So even though today Akkling has decent support for composing Akka streams, expect more (breaking) changes until the library offers an optimal syntax to express stages of stream composition. But I hope that example ported to Akkling by Vasily Kirichenko shows that even today’s Akkling syntax for Akka Streams is simple enough to compose streams of different complexity. Here’s the code:

let system = System.create "test" (Configuration.defaultConfig())
let mat = system.Materializer()

let balancer worker count =
    Graph.create (fun b ->
        let balancer = b.Add(Balance(count, waitForAllDownstreams = true))
        let merge = b.Add(Merge<_> count)
        for _ in 1..count do
            b.From(balancer) => worker => merge |> ignore
        FlowShape(balancer.In, merge.Out)
    ) |> Flow.FromGraph

let worker = |> (fun x -> x + 100)

let results = 
    |> Source.ofList  
    |> Source.via (balancer worker 3)
    |> Source.runForEach mat (printfn "%d") 
    |> Async.RunSynchronously

What else? F# helpers for distributed data, action schedulers, event streaming, supervision strategy and more - you will find usage examples for most of them in Akkling examples folder. Small pieces of code arranged as F# script files, you don’t even need to build a solution to try them. So this is the Akka.NET F# API you should use. Hopefully it will become the official one soon. It deserves it.

Publisert 27.12.2017 av

Vagif Abilov