Facebook Apps on Windows Azure
[UPDATE 9/30/2009] I’ve since deleted this application, so you won’t be able to try it out. Links to the app have been removed from this post.
I just built my first Facebook App, and I’m hosting it on Windows Azure.
The Application
It’s called “Points!”, and the tagline is “What’s the point of life if you don’t know who’s winning?”
The concept is really simple. You enter a friend’s name, how many points you’d like to give him/her, and a reason. Then a notification gets sent to the friend telling them about it, and an item shows up in your Facebook feed saying who you gave points to and why.
When you visit the Points! application, you can see the points you’ve recently given out and received. (It shows you the most recent ten “awards.”)
The Data
The application uses exactly one table to store each Points! award, which has this structure:
public class Award : TableStorageEntity { public int FromUid { get; set; } public int ToUid { get; set; } public int NumPoints { get; set; } public string Reason { get; set; } public DateTime Created { get; set; } public Award() { } public Award(int from, int to, int num, string reason, DateTime created) { FromUid = from; ToUid = to; NumPoints = num; Reason = reason; Created = created; } }
As always, once I understood the kind of data I needed to store, I needed to determine how best to partition the data for performance. In this case, there are two common operations on the data:
- When rendering the home page, I need to show all the points associated with (sent by or received by) the user.
- When a user submits points, I need to store the points in the table.
My belief is that (1) is the more important query to optimize for, since that’s the initial load query and also the most likely operation. (People will visit the page at least as much as they send points.)
To optimize for (1), I need the partition key to be the user who’s visiting the page. But that means that all awards sent by a user and received by a user need to reside in that partition. That consequently means each award lives in two places, so what I did was turn (2) into two inserts.
In the world of traditional desktop databases, this sounds like a bad idea, but in the cloud, denormalization to increase performance is a common best practice.
The Application Logic
This is a straightforward ASP.NET MVC app. I should probably have used something like the Facebook Developer Toolkit to interact with Facebook, but I wanted to see exactly how the API and protocols work, so I did it by hand instead. It turned out not to be that much work.
Aside from boring signature and API stuff (which is better taken care of by a framework), this is the core logic of the application, in my controller class:
[VerifySignature] [AcceptVerbs(HttpVerbs.Post)] public ActionResult Index(string fb_sig_in_canvas, string fb_sig_user, string fb_sig_canvas_user) { var user = fb_sig_canvas_user ?? fb_sig_user; if (user == null) { return Content(LOGIN_URL); } else { return View((from award in new Models.PointsContext().AwardTable where award.PartitionKey == user select award).Take(10)); } } [VerifySignature] [AcceptVerbs(HttpVerbs.Post)] public ActionResult Create(string fb_sig_in_canvas, int fb_sig_user, int friend_selector_id, int number, string reason, string fb_sig_session_key) { DateTime created = DateTime.UtcNow; var outgoingAward = new Models.Award(fb_sig_user, friend_selector_id, number, reason, created) { PartitionKey = fb_sig_user.ToString(), RowKey = string.Format("{0:d19}", DateTime.MaxValue.Ticks - created.Ticks) }; var incomingAward = new Models.Award(fb_sig_user, friend_selector_id, number, reason, created) { PartitionKey = friend_selector_id.ToString(), RowKey = string.Format("{0:d19}", DateTime.MaxValue.Ticks - created.Ticks) }; var svc = new Models.PointsContext(); svc.AddObject("AwardTable", outgoingAward); svc.AddObject("AwardTable", incomingAward); svc.SaveChanges(); SendNotification(friend_selector_id, string.Format( "gave you {0} <a href='http://apps.facebook.com/pointsapp'>points</a> for {1}.", number, reason), fb_sig_session_key); PublishFeedItem(friend_selector_id, number, reason, fb_sig_session_key); return Content("<fb:redirect url=\"http://apps.facebook.com/pointsapp/\" />"); }
Future Work
There are some obvious missing features, like the ability to view how many total points you have, or a graph of how your points have gone up and down (did I mention you could give negative points?). You might also want to see your ranking… among Steve’s friends, who did he give the most points to?
One thing all those features have in common is that they require precomputation to be efficient. There’s no need to query the table and aggregate the points each time. I could keep a running total instead. I could draw graphs in advance and store them as blobs, etc. This sort of loosely-coupled architecture improves performance (all that work is off the critical path) and improves reliability (if drawing the graph stops working for some reason, the app isn’t down, just backed up).
If there’s sufficient interest, I might tackle some of this in a future post by adding a worker role. Every time an award is created, work is queued up for the worker to replicate the award (remember I need to store it in two place), to add the points to the necessary totals, to adjust rankings, to draw graphs, etc.
Let me know if you’d like to see such enhancements, especially if you’d be interested in contributing to the effort.