Before discussing continuations we first need to discuss how callouts work. A typical callout sends a request via an API to a service, waits for a response, & then returns the requested information (or an error).
Using the analogy of a restaurant this can be illustrated by a typical transaction. A customer (the client) places an order with a waiter (the API). The waiter then delivers the request to the kitchen (a web service) & then the food’s ready they deliver it to the client (or maybe they inform the client that they’re all out of what they ordered). One thing to note about these callouts is that there’s an open thread awaiting reply. What this means for our analogy is that the waiter delivers the order to the kitchen & waits there idle until the food is ready to be brought to the table.
Generally speaking, callouts are made and responses are given quite fast. However, in some instances, the information you’re looking for may take some time for the process. For example, say your waiter brings the request to the kitchen for a bowl of soup. The soup is already prepared & heated. A chef scoops some soup into a bowl & hands it to your waiter who brings it to your table. It didn’t really matter that the waiter was idle while the chef was putting the soup into the bowl. Now imagine you order something more complex that the chef needs time to prepare. It doesn’t make sense to have the waiter attending your order while it’s being prepared. If this were the case the restaurant would have to hire many more waiters.
Salesforce is a multi-tenant environment meaning that it operates in a similar way to a restaurant. Your Salesforce org is your table in the restaurant & you share communal assets (the waiters & the kitchen) with other customers. Waiters cost money to hire & it doesn’t make sense for Salesforce to have all their waiters standing in the kitchen while the chefs prepare food for a certain number of people.
Salesforce is, of course, aware of this which is why they’ve implemented concurrent request limits. Each organisation’s concurrent request limit is 10. Once a synchronous Apex request runs longer than 5 seconds, it begins to count against this limit. What they’re effectively saying is that your table (your Salesforce org) can’t have more than 10 waiters standing in the kitchen while your food (your Apex request) is being prepared (unless it’s going to take less than 5 seconds for the waiter to leave your table, get the food from the kitchen, & bring it to your table).
This is where Continuations come into the equation. In Apex a Continuation refers to an asynchronous external callout (a callout that runs in the background). What this means is that the thread you opened when you made a callout becomes dormant while it’s awaiting a response. Reverting to our restaurant analogy this means that the waiter can deliver your order to the kitchen & not stand waiting for it to be prepared. They can go take other orders & bring other food to other clients. When your food has been prepared the kitchen will notify an available waiter who then delivers it to your table.
In Apex we implement this functionality by using the Continuation class.
In many cases, there’s no issue in using a normal callout to consume an external service. However, in some cases, your org may have many users interacting with the org. You may also have a scenario whereby your Apex request connects with an external service. If this external service takes a number of seconds to respond and if many users are making this request at the same time you run the risk of exceeding the concurrent request limit of 10 (remember if your entire Apex request takes more than 5 seconds it will be counted against this limit).
For example, let's say you’re a financial institution with a number of Visualforce pages that act as custom-built dashboards for a number of different funds. Each page makes a call out to an external REST service to obtain the performance metrics of the fund over a certain timespan. The metrics you’re obtaining from the service are very detailed and extensive, as a result the entire Apex request often exceeds 5 seconds.
Now imagine you have a few hundred users in an office who are constantly clicking between these Visualforce pages and making these requests. The result of this will be error messages in place of data resulting in poor user experience.
This is an instance where we should be utilising the continuation class. Our Apex request will be made and our callout initiated. Our thread will then lie dormant until our callout receives its reply. We set a callback method within our continuation class meaning that we can define what occurs once the external reply is received.
In the below example we’re making a callout to a fictional web service & displaying it on our page. For the sake of the example, the data being returned only contains two columns – Timestamp & Value. We’re also making a callout to a single web service.
Please note however that you can add up to three callouts to a single continuation. To do so simply add a new http request to your continuation instance using the addHttpRequest method. This is already showcased in the below example.
<apex:page controller="MyContinuationController"> <apex:form id="result"> <apex:pageBlock> <apex:pageBlockSection columns="1"> <apex:pageBlockSectionItem> <apex:outputPanel rendered="{!FundData != null}"> <apex:pageBlockTable value="{!FundData}" var="fund_row"> <Apex:column value="{!fund_row.Timestamp}" headervalue="Timestamp"/> <Apex:column value="{!fund_row.Value}" headervalue="Fund Value"/> </apex:pageBlockTable> </apex:outputPanel> </apex:pageBlockSectionItem> </apex:pageBlockSection> </apex:pageBlock> </apex:form> </apex:page>
public with sharing class MyContinuationController { public List<FundDataWrapperClass> FundData{get;set;} public String MyRequest; // This is the URL of the service we wish to consume private static final String my_service_endpoint = 'https://www.mysampleendpoint.com'; // The constructor calls the service on load public MyContinuationController() { makeCallout(); } public void makeCallout() { // The number being passed in the Continuation constructor represents the timeout in seconds for our callout. //The maximum timeout we can set is 120. Continuation my_continuation_instance = new Continuation(40); // This is where we define what method is to run once a response is received my_continuation_instance.continuationMethod='displayCalloutResults'; // Create callout request HttpRequest my_http_request = new HttpRequest(); my_http_request.setMethod('GET'); my_http_request.setEndpoint(my_service_endpoint); this.MyRequest = my_continuation_instance.addHttpRequest(my_http_request); } private void displayCalloutResults() { HttpResponse my_response = Continuation.getResponse(this.MyRequest); if(my_response.getBody() != null && my_response.getStatusCode() == 200){ FundData = (List<FundDataWrapperClass>)json.deserialize(my_response.getBody(),List<FundDataWrapperClass>.class); } } // This class is used to describe the return format of the HTTP response body private class FundDataWrapperClass { public Datetime Timestamp{get;set;} public String Value{get;set;} } }