Deploying .NET code instantly using Vagabond

 

This is post #30 of the English 2014 F# advent calendar. Thanks to Sergey Tihon for inviting me and suggesting the topic. Be sure to check out all the other awesome posts! In this post I will be describing Vagabond, a dependency management library of mine. I will try to walk through some of the subtleties in the .NET framework that drove the library implementation, in an attempt to make it more accessible. All code and examples presented can be found in this repository.

Prelude

It is often claimed in functional programming that functions are treated as values. This is a valid assumption in simple applications, permitting powerful patterns when writing code. But is this a general truth, or simply a linguistic abstraction? If functions really are values, one would expect that they exhibit all properties normally expected of values. For instance, it should be possible to serialise functions and transmit to a remote process for use. But wait, does serialising a function even make sense? In principle it should, we know for a fact that all code is essentially data. But the answer really depends on language/runtime support, serialization facilities and how lambdas are represented. Our goal here is to consider the .NET framework and F# in particular. How does F#/.NET represent lambdas? We’ll attempt to illustrate by example.

‘Thunk Server’

Our goal is to define a ‘thunk server’, an actor implementation that receives arbitrary thunks (lambdas of type unit -> 'T) which it executes replying with the result. For this exercise we will be using Thespian, a distributed actor framework for F#. The choice of actor framework is not particularly important; Thespian should be easy to grasp however, since its programming model closely follows that of the F# MailboxProcessor. We start off with the simplest imaginable conception of how a thunk server should be implemented:

open Nessos.Thespian

// Actor API: a thunk and reply channel in which to send the result
type ThunkMessage = (unit -> obj) * IReplyChannel<Choice<obj, exn>>

// actor body definition
let rec serverLoop (self : Actor<ThunkMessage>) : Async<unit> =
    async {
        // receive next message
        let! thunk, reply = self.Receive()
        // execute, catching any exception
        let result : Choice<obj, exn> =
            try thunk () |> Choice1Of2
            with e -> Choice2Of2 e
        // reply
        do! reply.Reply result
        return! serverLoop self
    }

// initialize an actor instance in the current process
let server : Actor<ThunkMessage> = Actor.Start "thunkServer" serverLoop

We can now interface with our thunk server like so:

/// submit a thunk for evaluation to target actor ref
let evaluate (server : ActorRef<ThunkMessage>) (thunk : unit -> 'T) : 'T =
    // post to actor and wait for reply
    let result = server <!= fun replyChannel -> (fun () -> thunk () :> obj), replyChannel
    // downcast if value, raise if exception
    match result with
    | Choice1Of2 o -> o :?> 'T
    | Choice2Of2 e -> raise e

evaluate server.Ref (fun () -> 1 + 1) // returns 2

But is this implementation correct? Remember, our goal is to submit lambdas to a remote process for execution. Using the companion project we can test precisely this scenario:

// spawn a local windowed process that runs the actor
let server : ActorRef<ThunkMessage> = spawnWindow ()

Let’s attempt to submit some code to the server:

// load a third party library and submit code for evaluation
#r "ThirdPartyLibrary.dll"
evaluate server ThirdPartyLibrary.thirdPartyLambda

Evaluation fails with the following error:

System.IO.FileNotFoundException: Could not load file or assembly 'ThirdPartyLibrary, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.
   at System.Reflection.RuntimeAssembly.GetType(RuntimeAssembly assembly, String name, Boolean throwOnError, Boolean ignoreCase, ObjectHandleOnStack type)
   at System.Reflection.RuntimeAssembly.GetType(String name, Boolean throwOnError, Boolean ignoreCase)
   at Nessos.FsPickler.ReflectionCache.loadMemberInfo(FSharpOption`1 tyConv, FSharpFunc`2 loadAssembly, FSharpFunc`2 getMethodSignature, CompositeMemberInfo mI)

Why does this happen? Because lambdas in F# are not exactly values. Rather, they are instances of compiler generated classes that inherit the abstract type FSharpFunc. Needless to say, a lambda cannot be successfully deserialised in a process unless its particular underlying class is loaded in the current application domain.

Fixing the problem

So how can we fix the problem? A popular approach is to distribute code using expression trees. Expression trees are clearly values, hence can be easily serialised and evaluated in the target machine. However, this is not a satisfactory solution: expression trees do not support type definitions; evaluation of expression trees is not always efficient; expression trees can still reference code in out-of-reach assemblies. We need to to better than this! Our experience illustrates that a proper serialisation format for lambdas must also include their dependent assemblies. So let’s try to do precisely that:

open System.IO
open System.Reflection

// Raw assembly container
type AssemblyPackage =
    {
        FullName : string
        AssemblyImage : byte []
    }

// transitively traverse assembly dependencies for given object
let gatherDependencies (object:obj) : AssemblyPackage list =
    let rec aux (gathered : Map<string, Assembly>) (remaining : Assembly list) =
        match remaining with
        // ignored assembly
        | a :: rest when gathered.ContainsKey a.FullName || a.GlobalAssemblyCache -> aux gathered rest
        // came across new assembly, add to state and include transitive dependencies to inputs
        | a :: rest ->
            let dependencies = a.GetReferencedAssemblies() |> Seq.map Assembly.Load |> Seq.toList
            aux (Map.add a.FullName a gathered) (dependencies @ rest)
        // traversal complete, create assembly packages
        | [] ->
            gathered
            |> Seq.map (fun (KeyValue(_,a)) ->
                                { FullName = a.FullName
                                  AssemblyImage = File.ReadAllBytes a.Location })
            |> Seq.toList

    aux Map.empty [object.GetType().Assembly]

// loads raw assemblies in current application domain
let loadRawAssemblies (pkgs : AssemblyPackage list) =
    pkgs |> List.iter (fun pkg -> Assembly.Load pkg.AssemblyImage |> ignore)

The code above uses reflection to traverse the assembly dependencies for a given object. The raw binary assembly images are then read from disk to be submitted and loaded by the remote recipient. We can now revise our thunk server implementation as follows:

// our actor API is augmented with an additional message
type ThunkMessage =
    | RunThunk of (unit -> obj) * IReplyChannel<Choice<obj, exn>>
    | LoadAssemblies of AssemblyPackage list

let rec serverLoop (self : Actor<ThunkMessage>) : Async<unit> =
    async {
        let! msg = self.Receive()
        match msg with
        | RunThunk(thunk, reply) ->
            let result : Choice<obj, exn> =
                try thunk () |> Choice1Of2
                with e -> Choice2Of2 e

            do! reply.Reply result
        | LoadAssemblies assemblies ->
            loadRawAssemblies assemblies

        return! serverLoop self
    }

/// submit a thunk for evaluation to target actor ref
let evaluate (server : ActorRef<ThunkMessage>) (thunk : unit -> 'T) =
    // traverse and upload dependencies
    server <-- LoadAssemblies (gatherDependencies thunk)
    // assembly upload complete, send thunk for execution
    let result = server <!= fun replyChannel -> RunThunk ((fun () -> thunk () :> obj), replyChannel)
    match result with
    | Choice1Of2 o -> o :?> 'T
    | Choice2Of2 e -> raise e

And we are done! We can use the companion project to verify that this indeed resolves the previously failing deployment scenario. Clearly, this should cover the case of assemblies missing from the remote process. But does it work with lambdas defined in F# interactive? Let’s try it out:

> evaluate localServer (fun () -> 1 + 1);;
System.NotSupportedException: The invoked member is not supported in a dynamic assembly.
   at System.Reflection.Emit.InternalAssemblyBuilder.get_Location()
   at ThunkServer.Naive2.aux@36-1.Invoke(KeyValuePair`2 _arg1)

Huh? What’s a dynamic assembly? And why does it appear here? Reading from MSDN:

(…) a set of managed types in the System.Reflection.Emit namespace that allow a compiler or tool to emit metadata and Microsoft intermediate language (MSIL) at run time (…)

Among such compilers is F# interactive, which uses a single dynamic assembly for emitting all code defined in a single session. This can be verified by writing

> (fun i -> i + 1).GetType().Assembly ;;
val it : System.Reflection.Assembly =
  FSI-ASSEMBLY, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
    {IsDynamic = true;
     Location = ?;}

We have reached a dead end, it seems. Clearly, dynamic assemblies cannot be distributed to remote processes. And even if they could, we would face more problems on account of dynamic assemblies expanding over time. What is there to do?

Enter Vagabond

Vagabond is an automated dependency management framework. It is capable of converting dynamic assemblies into standalone, distributable assemblies. This is achieved using Mono.Cecil and a modification of AssemblySaver, a parser for dynamic assemblies.

Basic usage

Having worked on the above examples, the Vagabond API should come off as being natural. We can begin using the library by initialising aVagabond state object:

open Nessos.Vagabond

let vagabond : Vagabond = Vagabond.Initialize(cacheDirectory = "/tmp/vagabond")

This will be our entry point for all interactions with the library. For starters, let us compute the dependencies for a lambda defined in F# interactive:

> let deps = vagabond.ComputeObjectDependencies((fun i -> i + 1), permitCompilation = true) ;;
val deps : System.Reflection.Assembly list =
  [FSI-ASSEMBLY_17a1c27a-5c8f-4acb-ae1a-5aea74027854_1, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]

This will return a single dependent assembly that is non-dynamic, generated by Vagabond. The numerical suffix indicates that this is the first slice produced of that particular dynamic assembly. Vagabond uses a slicing scheme to address the issue of dynamic assemblies that expand over time. To create an exportable assembly package, we simply call

let pkg : AssemblyPackage = vagabond.CreateAssemblyPackage(assembly, includeAssemblyImage = true)

Packages can be loaded to the current application domain using the same object:

let result : AssemblyLoadInfo = vagabond.LoadAssemblyPackage(pkg)

As soon as assembly dependencies have been uploaded to the recipient, communication can be established. Importantly, this must be done using the serialiser instance provided by the Vagabond object.

vagabond.Serializer : FsPicklerSerializer

This makes use of purpose-built FsPickler functionality to bridge between local dynamic assemblies and exported slices.

Putting it all together

Let’s now try to correctly implement our thunk server. We first define our updated actor body:

type ThunkMessage =
    // Evaluate thunk
    | RunThunk of (unit -> obj) * IReplyChannel<Choice<obj, exn>>
    // Query remote process on assembly load state
    | GetAssemblyLoadState of AssemblyId list * IReplyChannel<AssemblyLoadInfo list>
    // Submit assembly packages for loading at remote party
    | UploadAssemblies of AssemblyPackage list * IReplyChannel<AssemblyLoadInfo list>

let rec serverLoop (self : Actor<ThunkMessage>) : Async<unit> =
    async {
        let! msg = self.Receive()
        match msg with
        | RunThunk(thunk, reply) ->
            let result : Choice<obj, exn> =
                try thunk () |> Choice1Of2
                with e -> Choice2Of2 e

            do! reply.Reply result
        | GetAssemblyLoadState(assemblyIds, reply) ->
            // query local vagabond object on load state
            let info = vagabond.GetAssemblyLoadInfo assemblyIds
            do! reply.Reply info

        | UploadAssemblies(pkgs, reply) ->
            // load packages using local vagabond object
            let results = vagabond.LoadAssemblyPackages(pkgs)
            do! reply.Reply results

        return! serverLoop self
    }

/// create a local thunk server instance with given name
let createServer name = Actor.Start name serverLoop |> Actor.ref

It remains to define the client side of the assembly upload logic. To make sure that assemblies are not needlessly uploaded, we could either implement our own upload protocol using the previously described Vagabond API, or we could simply use the built-in protocol and only specify the communication implementation:

/// submit a thunk for evaluation to target actor ref
let evaluate (server : ActorRef<ThunkMessage>) (thunk : unit -> 'T) =
    // receiver implementation ; only specifies how to communicate with remote party
    let receiver =
        {
            new IRemoteAssemblyReceiver with
                member __.GetLoadedAssemblyInfo(ids: AssemblyId list) =
                    server.PostWithReply(fun reply -> GetAssemblyLoadState(ids, reply))

                member __.PushAssemblies (pkgs: AssemblyPackage list) =
                    server.PostWithReply(fun reply -> UploadAssemblies(pkgs, reply))
        }

    // submit assemblies using the receiver implementation and the built-in upload protocol
    vagabond.SubmitObjectDependencies(receiver, thunk, permitCompilation = true)
    |> Async.RunSynchronously
    |> ignore

    // dependency upload complete, send thunk for execution
    let result = server <!= fun replyChannel -> RunThunk ((fun () -> thunk () :> obj), replyChannel)
    match result with
    | Choice1Of2 o -> o :?> 'T
    | Choice2Of2 e -> raise e

Using the companion project we can now test our implementation. The examples below all work in F# interactive.

// spawns a windowed console application that hosts a single thunk server instance
let server : ActorRef<ThunkMessage> = ThunkServer.spawnWindow()

evaluate server (fun () -> 1 + 1)
evaluate server (fun () -> printfn "Remote side-effect")
evaluate server (fun () -> do failwith "boom!")

Deploying actors

An application of particular interest is the ability to remotely deploy actor definitions straight from F# interactive, just by using our simple thunk server:

// deploy an actor body remotely using thunk server
let deployActor name (body : Actor&amp;amp;lt;'T&amp;amp;gt; -&amp;amp;gt; Async&amp;amp;lt;unit&amp;amp;gt;) : ActorRef&amp;amp;lt;'T&amp;amp;gt; =
    evaluate server (fun () -&amp;amp;gt; let actor = Actor.Start name body in actor.Ref)

Let’s try this out by implementing a simple counter actor. All type definitions and logics can be declared straight from F# interactive.

type Counter =
    | IncrementBy of int
    | GetCount of IReplyChannel<int>

let rec body count (self : Actor<Counter>) = async {
    let! msg = self.Receive()
    match msg with
    | IncrementBy i -> return! body (count + i) self
    | GetCount rc ->
        do! rc.Reply count
        return! body count self
}

// deploy to thunk server, receive remote actor ref
let ref : ActorRef<Counter> = deployActor "counter" (body 0)

We can now test the deployment simply by interfacing with the actor ref we have received. Side effects can be added in the actor body to verify that code indeed runs in the remote window.

ref <-- IncrementBy 1
ref <-- IncrementBy 2
ref <-- IncrementBy 3

ref <!= GetCount // 6

Further Applications

Vagabond enables on-the-fly deployment of code, be it to a collocated process, your next-door server or an Azure-hosted cluster. It is applicable not only to F# and its shell, but should work with any .NET language/REPL such as the Roslyn-based scriptcs. The MBrace framework already makes use of the library, enabling instant deployment of distributed algorithms to the cloud. We plan to incorporate Vagabond functionality with Thespian as well. Vagabond is a powerful library from which most distributed frameworks running on .NET could benefit. I hope that today’s exposition will encourage a more widespread adoption.

Advertisements
Deploying .NET code instantly using Vagabond

10 thoughts on “Deploying .NET code instantly using Vagabond

  1. David Taylor-Fuller says:

    I like the idea. But how are the uploaded assemblies garbage collected? What happens If I upload the same code more than once?

    1. eirik says:

      Depends on what you mean by garbage collected. In .NET assemblies remain loaded for the entire lifetime of an application domain.

      The method of uploading assemblies is entirely up to you. You could implement a protocol that checks assembly load states in the remote party before uploading any missing image. In fact, Vagrant comes with a protocol that does just that. This is illustrated in the above post in the example that uses IRemoteAssemblyReceiver.

    1. eirik says:

      Vagabond is capable of distributing code defined in any .Net assembly, be it written in C#, F# or VB.net. In terms of the Vagabond API itself, it is probably best consumed using F#, so you would have to at least write a minimal wrapper in F# for your implementation.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s