Using Erlang-style message passing from F# to coordinate asynchronous tasks in AutoCAD

This is something I've been meaning to attempt for a while, and have finally been spurred to do it by next week's AU Virtual session on F#. Not that I expect to have time to present this during the session (60 minutes is already feeling way too short for the material I want to cover), but I at least wanted to have this working so I could present with a touch more authority. 🙂

In last year's session we used Asynchronous Workflows to improve the performance of IO-bound operations, such as retrieving multiple RSS feeds and displaying the results in AutoCAD. Asynchronous Workflows works very well for coordinating such tasks, but the mechanism also has limitations when coordinating activities that require more complex networks of communication. This is where agent-based message passing – a technique that has gained a great deal of press lately thanks to Erlang, another functional programming language, and that Microsoft has also adopted in another of its object-oriented programming languages called Axum – comes into its own.

The idea is that you set up a number of agents (also known as actors) that perform different roles, and they sit there waiting for messages telling them when to perform the task(s) they've been designed for. If needed, these agents can send their own messages to other agents to get further tasks done. The results of these tasks can be reported back to whoever needs to know what has happened (or is happening). I'm sure this is a gross over-simplification of the technique, but there you go.

In the below code, which solves the same "problem" of turning AutoCAD into an RSS reader, defines a single agent which collects hyperlink information by downloading and parsing RSS feeds before sending a response with this data back to the caller. The collection is done asynchronously, but the code that fires off the "jobs" to collect the data is actually synchronous. In the next post we'll look at a completely asynchronous version to see whether it's worth the effort of making it so.

Here's the F# code, defining our MRSS command:

// Declare a specific namespace and module name

 

module AgentSyncRssReader.Commands

 

// Import managed namespaces

 

open Autodesk.AutoCAD.Runtime

open Autodesk.AutoCAD.ApplicationServices

open Autodesk.AutoCAD.DatabaseServices

open Autodesk.AutoCAD.Geometry

open System.Xml

open System.IO

open System.Net

open Microsoft.FSharp.Control.WebExtensions

 

// The RSS feeds we wish to get. The first two values are

// only used if our code is not able to parse the feed's XML

 

let feeds =

  [ ("Through the Interface",

    "http://blogs.autodesk.com/through-the-interface",

    "http://through-the-interface.typepad.com/" +

      "through_the_interface/rss.xml");

 

    ("Don Syme's F# blog",

    "http://blogs.msdn.com/dsyme/",

    "http://blogs.msdn.com/dsyme/rss.xml");

 

    ("Shaan Hurley's Between the Lines",

    "http://autodesk.blogs.com/between_the_lines",

    "http://autodesk.blogs.com/between_the_lines/rss.xml");

 

    ("Scott Sheppard's It's Alive in the Lab",

    "http://blogs.autodesk.com/labs",

    "http://labs.blogs.com/its_alive_in_the_lab/rss.xml");

 

    ("Volker Joseph's Beyond the Paper"
;
,

    "http://blogs.autodesk.com/beyond_the_paper",

    "http://dwf.blogs.com/beyond_the_paper/rss.xml") ]

 

// Fetch the contents of a web page, asynchronously

 

let httpAsync(url:string) =

  async { let req = WebRequest.Create(url)

          use! resp = req.AsyncGetResponse()

          use stream = resp.GetResponseStream()

          use reader = new StreamReader(stream)

          return reader.ReadToEnd() }

 

// Load an RSS feed's contents into an XML document object

// and use it to extract the titles and their links

// Hopefully these always match (this could be coded more

// defensively)

 

let titlesAndLinks (name, url, xml) =

  try

    let xdoc = new XmlDocument()

    xdoc.LoadXml(xml)

 

    let titles =

      [ for n in xdoc.SelectNodes("//*[name()='title']")

          -> n.InnerText ]

    let links =

      [ for n in xdoc.SelectNodes("//*[name()='link']") ->

          let inn = n.InnerText

          if  inn.Length > 0 then

            inn

          else

            let href = n.Attributes.GetNamedItem("href").Value

            let rel = n.Attributes.GetNamedItem("rel").Value

            if List.exists

                (fun x -> href.Contains(x))

                ["feedburner";"feedproxy";"hubbub"] then

              ""

          &
#160;
else

              href ]

 

    let descs =

      [ for n in xdoc.SelectNodes

          ("//*[name()='description' or name()='subtitle'" +

          " or name()='summary']")

            -> n.InnerText ]

 

    // A local function to filter out duplicate entries in

    // a list, maintaining their current order.

    // Another way would be to use:

    //    Set.of_list lst |> Set.to_list

    // but that results in a sorted (probably reordered) list.

 

    let rec nub lst =

      match lst with

      | a::[] -> [a]

      | a::b ->

        if a = List.head b then

          nub b

        else

          a::nub b

      | [] -> []

 

    // Filter the links to get (hopefully) the same number

    // and order as the titles and descriptions

 

    let real = List.filter (fun (x:string) -> x.Length > 0) 

    let lnks = real links |> nub

 

    // Return a link to the overall blog, if we don't have

    // the same numbers of titles, links and descriptions

 

    let lnum = List.length lnks

    let tnum = List.length titles

    let dnum = List.length descs

 

    if tnum = 0 || lnum = 0 || lnum <> tnum ||

      dnum <> tnum then

      [(name,url,url)]

    else

      List.zip3 titles lnks descs

  with _ -> []

 

// For a particular (name,url) pair,

// create an AutoCAD HyperLink object

 

let hyperlink (name,url,desc) =

  let hl = new HyperLink()

  hl.Name <- url

  hl.Description <- desc

  (name, hl)

 

// Use asynchronous workflows in F# to download

// an RSS feed and return AutoCAD HyperLinks

// corresponding to its posts

 

let hyperlinksAsync (name, url, feed) =

  async { let! xml = httpAsync feed

          let tl = titlesAndLinks (name, url, xml)

          return List.map hyperlink tl }

 

// Now we declare our command

 

[<CommandMethod("mrss")>]

let createHyperlinksFromRssAsyncViaMailbox() =

 

  let starttime = System.DateTime.Now

 

  // Let's get the usual helpful AutoCAD objects

 

  let doc =

    Application.DocumentManager.MdiActiveDocument

  let ed = doc.Editor

  let db = doc.Database

 

  // "use" has the same effect as "using" in C#

 

  use tr =

    db.TransactionManager.StartTransaction()

 

  // Get appropriately-typed BlockTable and BTRs

 

  let bt =

    tr.GetObject

      (db.BlockTableId,OpenMode.ForRead)

      :?> BlockTable

  let ms =

    tr.GetObject

      (bt.[BlockTableRecord.ModelSpace],

      OpenMode.ForWrite)

      :?> BlockTableRecord

 

  // Add text objects linking to the provided list of

  // HyperLinks, starting at the specified location

 

  // Note the valid use of tr and ms, as they are in scope

 

  let addTextObjects (pt : Point3d) lst =

    // Use a for loop, as we care about the index to

    // position the various text items

 

    let len = List.length lst

    for index = 0 to len - 1 do

      let txt = new DBText()

      let (name:string,hl:HyperLink) = List.nth lst index

      txt.TextString <- name

 
;    
let offset =

        if index = 0 then

          0.0

        else

          1.0

 

      // This is where you can adjust:

      //  the initial outdent (x value)

      //  and the line spacing (y value)

 

      let vec =

        new Vector3d

          (1.0 * offset,

          -0.5 * (float index),

          0.0)

      let pt2 = pt + vec

      txt.Position <- pt2

      ms.AppendEntity(txt) |> ignore

      tr.AddNewlyCreatedDBObject(txt,true)

      txt.Hyperlinks.Add(hl) |> ignore

 

  // Define our agent to process messages regarding

  // hyperlinks to gather and process

 

  let agent =

    MailboxProcessor.Start(fun inbox ->

      let rec loop() = async {     

 

        // An asynchronous operation to receive the message

 

        let! (i, tup, reply :

          AsyncReplyChannel<(string * HyperLink) list>) =

            inbox.Receive()

 

        // And another to collect the hyperlinks for a feed

 

        let! res = hyperlinksAsync tup

 

        // And then we reply with the results

        // (the list of hyperlinks)

 

        reply.Reply(res)

 

        // Recurse to process more messages

 

        return! loop()

      }

 

      // Start the loop

 

      loop()

    )

 

  // Iterate through the list of feeds, firing off messages

  // to our agent for each one

 

  List.iteri

    (fun i item ->

   &#
160; 
let res = agent.PostAndReply(fun rep -> (i, item, rep))

 

      // Once we have the response (synchronously), create

      // the corresponding AutoCAD text objects

 

      let pt =

        new Point3d

          (15.0 * (float i),

          30.0,

          0.0)

      addTextObjects pt res

    )

    feeds

 

  tr.Commit()

 

  let elapsed =

    System.DateTime.op_Subtraction

      (System.DateTime.Now, starttime)

 

  ed.WriteMessage("\nElapsed time: " + elapsed.ToString())

 

Here are the results of the MRSS command:

Results of the MRSS command

The code executes a little more slowly than the "pure" asynchronous workflows version (ARSS), but more quickly than the purely synchronous version (RSS). I think this can probably be explained by the additional effort needed under the covers to coordinate the various workers that are getting the data and processing it, before modifying the contents of the AutoCAD Database. The ARSS command did this by waiting to get the data on all the feeds before creating the text objects in one fell swoop, which – for this relatively simple example – is probably more efficient. I suspect the complexity of a message-passing architecture (at least I find it complex right now – I'm sure it grows on you) becomes worthwhile when you start tackling more complex tasks.

Leave a Reply

Your email address will not be published. Required fields are marked *