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
""
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 ->
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:
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.