Home » Better Grails… I promise

Better Grails… I promise

Grails logoWhile we were upgrading our Grails apps to use version 2.4.2, there was a pretty delightful addition to the framework that escaped our attention amongst the rest of our excitement. What’s that “pretty important addition”? The Grails Promises API.

Introduced in version 2.3, the Promises library represents the Grails team’s effort to bring the GPars family of asyncronous APIs into Grails as a first-class citizen. This is a fancy way of saying that they’re providing “Grailsy” syntactic sugar around the GPars constructs (groovyx.gpars.dataflow.Dataflow, to be specific). And while you could have used GPars all along, having them available as part of Grails gives us an idiomatic way of performing asynchronous operations.

In Practice

You may be asking: So what? What’s the practical up-side to this?

The benefits of using the async APIs became apparent when we put certain parts of our web applications under the microscope. For example, we have known for a while that the landing page for one particular application (call it configuration) was habitually its slowest. Observe:

95th Percentile for 5 Slowest URLs in cc-configuration (Past 6 Months)

We don’t have to go far to see why:


package com.dealer.apps.configuration

class DashboardController {

  def legacyService
  def securityService

  def index() {
    
    def accountId = securityService.getViewingOrActualPrincipal().accountId
    def userId = securityService.getViewingOrActualPrincipal().userId
    
    def userSummary = legacyService.executeLegacyRpc([
        query: [userId: userId],
        rpc: "LoadUser"
      ], accountId, userId)?.result?.userSummary
    def companyInfo = legacyService.executeLegacyRpc([
        rpc: "LoadDealershipInfo"
      ], accountId, userId)?.result
    def phoneInfo = legacyService.executeLegacyRpc([
        rpc: "LoadPhoneManager"
      ], accountId, userId)?.result

    [userSummary: userSummary, companyInfo: companyInfo, phoneInfo: phoneInfo]
  }
}

Do you see the problem? If you don’t, go back and look again.

We’re making 3 synchronous, blocking calls over the network to go fetch some data. Now the “over the network” part isn’t really the concern — sometimes that’s just what you need to do. But why would we make three calls serially like this? Our worst case scenario is the sum of the response time (and/or timeouts) of these three poorly performing calls. In other words:

(+ (get-timing request1) (get-timing request2) (get-timing request3))

(RE: “Why!?” — This one is pretty simple: grails.async.Promises didn’t exist in Grails 2.0.3 when these “legacy RPC” requests were first added.)

So how does grails.async.Promises help improve this? Well, let’s take a stab at re-writing our controller:


package com.dealer.apps.configuration

import static grails.async.Promises.task
import static grails.async.Promises.waitAll

class DashboardController {

  def legacyService
  def securityService

  def index() {

    def accountId = securityService.getViewingOrActualPrincipal().accountId
    def userId = securityService.getViewingOrActualPrincipal().userId

    def userSummary, companyInfo, phoneInfo

    waitAll([
      task {
        userSummary = legacyService.executeLegacyRpc([
          query: [userId: userId],
          rpc: "LoadUser"
        ], accountId, userId)?.result?.userSummary
      },
      task {
        companyInfo = legacyService.executeLegacyRpc([
          rpc: "LoadDealershipInfo"
        ], accountId, userId)?.result
      },
      task {
        phoneInfo = legacyService.executeLegacyRpc([
          rpc: "LoadPhoneManager"
        ], accountId, userId)?.result
      }
    ])

    [userSummary: userSummary, companyInfo: companyInfo, phoneInfo: phoneInfo]
  }
}

Do you see what makes it better? We only block once! Our worst case scenario is now that we’re only as slow as the slowest one of these remote calls. In other words:

(max (get-timing request1) (get-timing request2) (get-timing request3))

To validate this approach, we did a little experiment with four rounds of tests. We tested the code that we already had (i.e., with three blocking calls), and then the version that used promises. Then in the next scenario, we simulated slow responses by adding an artificial 2 second delay to the method that handles the remote calls; then we just re-ran the tests we ran before. Here’s what we saw:

non-blocking vs. blocking on /configuration/dashboard/index

(Note the differences in the y-axes.)

Put another way, while this does improve our “happy path” best case scenarios, it doesn’t improve it much (average of 1340 milliseconds vs. 1470); however, for those terribly slow worst case scenarios, we are significantly faster (average of 3370 milliseconds vs. 7490).

So that’s nice.

Now… Go forth and async!

Further Reading

“Async, but async responsibly.” Go and learn first: