Raise your hand if you like to stare at a hour glass or spinning wheel when trying to use a web page. Didn’t think so. Making a page more responsive and snappy is usually one of the top priorities of any web developer and one of the best ways to do so is to change a synchronous user interaction into an asynchronous one. In cases where the response to a given user input can potentially take a long time, instead of having the user wait for a response (i.e. synchronous), it is often better to accept the user input and then update the page asynchronously at a later point once the processing is complete on the server. To see such an ‘asynchronous’ UI design pattern in action, check out the Apex test execution page (Setup–>Develop–>Apex Test Execution) and the UI for uploading new managed/unmanaged packages (Setup–>Create–>Packages–>Upload). Force.com veterans in the audience will remember that both these functions used to be synchronous in previous incarnations of the platform whereby the user gave their input (Apex classes to test or the package components to upload) and then had to wait for the page to respond with the results. Now, they simply give their input and the page is eventually refreshed asynchronously once the server-side processing is complete. In the meantime, they are free to navigate to other pages and then come back to the same page to check the results.
Lets see how we can implement a similar UI design in Visualforce using Asynchronous Apex and the Force.com Streaming API. To follow along, you can download and install the sample application using this unmanaged package and/or peruse the code on this GitHub repo.
Use Case
In order to see this design pattern in action, we need a use case where a user input on a Visualforce page can potentially result in a lengthy response time. Our use case involves presenting yearly summary data for all Opportunities in an Org. Specifically, the VF page lets users select a fiscal year from a picklist and then presents summary data for the year (total $ amount for all Opportunities, total closed won $ amount, total closed lost $ amount etc.) using a simple Visualforce Pie Chart. For the purposes of this example, lets assume that the standard Salesforce Reporting engine cannot be used to run this type of report (hence the need for a custom VF page).
Asynchronous Apex
At first blush, you might think that calculating this type of yearly summary data is simply a matter of running a SOQL Aggregate query in the Apex controller and then displaying the results on the VF page. In other words, not exactly the type of long-running processing requirement that we’re looking for. However, remember that all SOQL queries in Apex (including aggregate functions) are subject to a governor limit of 50,000 records. For an Org that has hundreds of thousands (or even millions) of Opportunity records, a single SOQL query will therefore not scale. Even if we were to use the @ReadOnly annotation on our Visualforce page to process upto 1 Million Opportunity records, the user would have to wait for the page to respond with the computed summary data after they hit the ‘Summarize’ button. We want to make that interaction asynchronous and more immediate.
Lets start by making the server-side code asynchronous. We’ll use Batch Apex to calculate the yearly summary data which will make the code more scalable (no longer subject to the 50,000 SOQL limit) and asynchronous (since Batch Apex jobs are queued and executed asynchronously from the invoking thread).
Here’s a snippet from the doSummarize method of the SummarizeOppDataController class. This method is invoked when the user picks a fiscal year and clicks the ‘Summarize’ button on the VF page.
The logic for iterating through all the Opportunity records in a given fiscal year and computing the summary data is encapsulated in the SummarizeYearlyOpportunityData class. That class implements the Database.Batchable interface and processes Opportunity records in batches of 2000 records to compute the yearly summary figures like total amount for all Opportunities, total closed won amount etc. We store the summary data for any particular fiscal year in the Opp_Summary_Data__c custom object. Line 7 shows how we queue up the asynchronous batch job via a call to the Database.executeBatch system call. We also create a new instance of the Opp_Summary_Data__c object with an initial status of ‘Processing’ (lines 8-12). Once the batch job completes, the SummarizeYearlyOpportunityData class will update that record (using the Batch Job Id as the primary key) with the respective summary data for the fiscal year and also flip the status to ‘Complete’.
So now that our server-side code is asynchronous, this is what the user sees when they select a fiscal year and click ‘Summarize’.
Instead of the user waiting for the summary results to be computed and displayed, a new Batch Apex job is simply queued up and the VF page is updated instantly with a new record in the ‘Summary Results’ table. The ‘Show’ link next to that record is not yet enabled because the batch job hasn’t finished computing the summary data. The user is now free to navigate to other areas of the application and come back to this VF page to see the summary results for FY 2009 once they’re ready.
Note: Though this sample uses Batch Apex as an example of implementing asynchronous Apex logic, the same design pattern applies to processing done in a @future transaction. For example, say your Visualforce page has to make a SOAP/REST callout to an external system when the user clicks a button/link. If the SLA for the external web service is unpredictable and/or long, you can make that invocation via a @future call and then update the Visualforce page asynchronously once the external callout returns a response.
Streaming API
Now that our server-side logic is asynchronous, lets improve the VF page so that the user is informed as soon as the server-side processing is complete. Instead of having the user constantly refresh the VF page to see if a particular fiscal year summary is ready for viewing, we’ll use the Streaming API to ‘push’ the summary data to the user. For the uninitiated, the Streaming API lets you expose a near real-time stream of data from the Force.com platform. Administrators can create topics, to which applications can subscribe, receiving asynchronous notifications of changes to data in Force.com. Streaming API uses the Bayeux protocol and CometD to implement this push technology.
To begin, lets create a new PushTopic for the Streaming API. The following snippet from the CreatePushTopic class shows us how.
The most important part of any PushTopic is the ‘query’ parameter. This is what gets evaluated by the platform to determine if a new notification needs to be broadcast to all subscribed clients. The combination of the query shown above and setting the NotifyForOperations to ‘Update’ means that clients will be notified every time that a Opp_Summary_Data__c record is updated to the status of ‘Complete’ (which is what the SummarizeYearlyOpportunityData Batch Apex class does in its ‘finish’ method once the summary data is ready).
Now, lets subscribe to the above PushTopic in our SummarizeOppData Visualforce page so that we get notified as soon as a yearly summary is ready for viewing. The quickest way to use the Streaming API in a Visualforce page, is to follow the Visualforce example in the Streaming API developer guide. Here is a small JavaScript snippet from the SummarizeOppData page that shows the Streaming API in action.
Line 11 shows how we use the Cometd JS library to subscribe to the ‘OppSummaryData’ PushTopic that we created earlier.
With that last piece of the puzzle in place, lets review the end-to-end lifecycle of our Visualforce page. When the user selects a fiscal year and hits ‘Summarize’, we create a new Opp_Summary_Data__c record with ‘Processing’ status and queue up a Batch Apex job to compute the yearly summary data. Once the summary is ready, the Batch Apex code updates the status of the Opp_Summary_Data__c record to ‘Complete’. Thanks to a PushTopic that we created earlier, the Streaming API then pushes the update to our VF page where we enable the ‘Show’ link next to the respective summary record. The user can then click on the ‘Show’ link and see the summary data as a simple Pie Chart.
Testing and packaging
Like it or not, a Force.com application isn’t complete till you have >= 75% test coverage. Using Batch Apex in this example allowed me to use one of the new Apex features in Winter ’13, the ability to create test data from static resources, and I wanted to highlight that test code briefly. Here’s a snippet from the TestUtil class that creates the large number of Opportunity test records that we need to properly test bulk functionality like Batch Apex.
I created a simple CSV file with hundreds of Opportunity test records and uploaded it as a static resource named ‘testOpps’. Using the new Test.loadData method, I can then instantiate all the test records from that static resource in a single line of code.
One final note that would be of interest to any ISVs in the audience. As mentioned earlier, I created a package for this application so that developers could test the code in their respective DE Orgs. However, in order for the Visualforce page to work properly, an Org has to have the ‘OppSummaryData’ PushTopic described earlier. In order to resolve this dependency, I used another relatively new feature of the platform that allows an Apex script to be executed every time that a managed package is installed, upgraded or uninstalled in a subscriber Org. The CreatePushTopic class implements the InstallHandler interface which means that the code in the onInstall method will be executed whenever someone installs or upgrades my package.
That is where I’ve added my logic to create the PushTopic necessary for this application to work. Unfortunately, the InstallHandler functionality only works with Managed Packages and since I created an Unmanaged Package (so that devs could look at all the code), you will need to manually execute that code after installing the package. Just run
from the Execute Anonymous Apex code window in your Developer Console and you should be able to use and test the Visualforce page.