Database Actors in Lift

We’ve been slowly migrating our Lift web app to more of an event-driven architecture. This approach offloads non-essential processing out of the HTTP request/response cycle into actors, and makes things a lot more flexible on the back-end. However, as we’ve discovered during this process, there are several things to be aware of by doing database processing in actors. In this post we’ll examine those problems and present our current solution, which is also demonstrated in an example Lift app and was recently discussed on the Lift mailing list.

Background

The app is very simple: users say things and these quotes show up on the home page. Other users can then “like” the quotes. The most-liked quotes are also shown on the home page.

Let’s take a look at the Like link in more detail. When you click it, a function in a snippet runs on the server-side.

A quote

This function toggles the like status between a Quote and a User. So after you like a quote, the link changes to Unlike so you can take back your like.

    def likeLink(q: Quote): NodeSeq = a(likeLinkText(q), "id" -> likeLinkId(q)) { 
      for (u <- User.currentUser)
        u toggle q
      Replace(likeLinkId(q), likeLink(q))
    }

This toggling makes its way to the QuoteLike meta mapper, to either the like or unlike method. In both cases, after the action is processed we send a message to the QuotePopularity actor.

object QuoteLike extends QuoteLike with LongKeyedMetaMapper[QuoteLike] with Logger {
...
  def like(u: User, q: Quote) = 
    if (!exists_?(u, q)) {
      val ql = QuoteLike.create.user(u).quote(q).saveMe
      debug("User " + u.id.is + " liked Quote " + q.id.is)
      QuotePopularity !<> q
      Full(ql)
    } else 
      Empty

  def unlike(u: User, q: Quote) = 
    for (ql <- find(u, q)) {
      ql.delete_!
      debug("User " + u.id.is + " unliked Quote " + q.id.is)
      QuotePopularity !<> q
    }

  def toggle(u: User, q: Quote) = if (exists_?(u, q)) unlike(u, q) else like(u, q)
}

This actor updates that quote’s popularity. The Popular Quotes section on the home page renders the top 10 quotes by descending popularity.

object QuotePopularity extends DbActor with Logger {
  protected def messageHandler = {
    case q: Quote => 
      val p = q.likeCount
      q.popularity(p).save
      debug("Quote " + q.id.is + " popularity = " + p)
  }
}

While we could easily have done this update in the QuoteLike like and unlike methods, we chose to offload this processing to an actor, since it is not essential to the AJAX response after the user clicks the Like/Unlike link. It’s a simple calculation in this example app, but imagine a more complex app where a lot of number-crunching must take place to determine trending items (*cough*Pongr*ahem*). We don’t want the AJAX response delayed, so we let an actor update the popularity sometime later. And the popularity of quotes is then updated & cached for the next home page load.

Problem

While this is a very common use case for actors (asynchronous “later” processing), what’s not immediately obvious is the dreaded database transaction. It’s standard in Lift to wrap every HTTP request inside a transaction. This is configured in Boot:

S.addAround(DB.buildLoanWrapper)

So at the end of our AJAX HTTP req/resp cycle due to the user clicking the Like/Unlike link, the new (or deleted) QuoteLike object is committed to the database, and can be read by other parts of our app. So far, so good.

However, by default, Lift actors are not wrapped in database transactions. So as soon as you send that message to the QuotePopularity actor, it may start updating the quote’s popularity. We have no guarantees as to when that actor will execute; it may be immediately in which case it won’t see the new/deleted QuoteLike, or it may happen to be after the QuoteLike is committed.

Another problem occurs if this actor makes changes to the database itself. Since it’s executing outside of a transaction, these changes are committed immediately, leaving some partially updated entities open to discovery by other parts of the app. Definitely not something we want to happen.

Solution

Our approach to solving this problem is the following simple DbActor trait:

trait DbActor extends LiftActor {
  override protected def aroundLoans = List(DB.buildLoanWrapper)

  def !<>(msg: Any): Unit = DB.performPostCommit { this ! msg }
}

We now follow these two best practices for actors that use the database:

  1. Extend DbActor instead of LiftActor
  2. Only send messages to the actor using the !<> method

So #1 ensures that this actor’s messageHandler method is executed inside a transaction. LiftActor has this awesome aroundLoans method, where we can simply wrap the actor in a DB.buildLoanWrapper (just like HTTP requests). Database changes made by the actor will now all be committed when the actor is finished. Our QuotePopularity actor above extends DbActor. Actors executing inside database transactions: check.

The !<> method in #2 ensures that this actor will only execute after the current database transaction commits. Again, Lift comes to the rescue with the DB.performPostCommit method, which does exactly what we want. So the QuotePopularity actor above will only execute after the AJAX HTTP request that creates/deletes a QuoteLike has been committed to the database. This way, we know for sure that the QuotePopularity actor will see the QuoteLike changes. Actors executing after the current transaction commits: check.

Conclusion

So just remember: if your Lift actor does anything with the database, follow the two best practices above. Wrap actor execution in a transaction, and send messages to the actor so that the actor executes after the current transaction commits.

It’s worth pointing out that we’re hard-coding the notification of the QuotePopularity actor above in the like and unlike methods. This is OK, but a better solution would be a generic pub-sub event system, where those methods would publish “quote liked/unliked” events, and QuotePopularity would just subscribe to those events. Similarly, QuotePopularity could publish a “quote popularity updated” event when it’s done, and something else like a comet actor could receive that event and update the Popular Quotes section of the home page. But that’s a topic for another blog post…

Advertisements

2 thoughts on “Database Actors in Lift

  1. Pingback: Tweets that mention Database Actors in Lift « Zach's Blog -- Topsy.com

  2. The “default” of wrapping all HTTP requests in a transaction is an attractive but poor practice. It doesn’t scale well, particularly if you have a lot of read-mostly requests, as it keeps the a DB connection open unnecessarily. It is better to open and use TXs only when required. This takes a little more thought and manual book-keeping but is definitely worth it in the long run.

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