Avoid Concurrency
Learning Objectives
- Explain why we have concurrency limits.
- Identify processes that create concurrency problems.
- Implement techniques to avoid concurrency limits.
How Concurrency Works
Concurrency is when multiple processes are running at the same time. When too many processes are trying to do something simultaneously, that can slow the system down.
If your app has a lot of users doing something that calls out to another system, you end up with many pages simultaneously waiting for a response.
It’s like riding in an elevator in the morning with dozens of other people. It can take time to get them all to their floors. You can optimize by grouping floors in certain elevator banks, programming elevator banks to be more efficient about which floors a given car goes to.
Concurrency Limits
The Salesforce system is the same. We have a certain number of processes (threads) that can be used at any given time. Normally the processes run fast, and we do not experience slowdowns. However, the longer a process runs, the less available processing capacity we have. To accommodate this, we allow only a certain number of long-running processes to function per org at a time. When you reach your limit, you cannot start a new long-running transaction until a previous one has completed.
Identifying long-running processes can take a bit of time. But the more you practice, the better you become. Generally, these processes fall into a few buckets:
- API Requests. A long-running API request is one that takes over 20 seconds. Salesforce allows only 25 to run at a time in an org.
- Callouts. When we make calls to another system, Salesforce waits for a response. If the responding system is slow, that can cause issues. You can have only a certain number of callouts at a time.
- Concurrency between Salesforce systems. Though Salesforce is a large ecosystem, it does have multiple subsystems in place. The core clouds (Sales Cloud, Service Cloud, Platform) are all built on our core application. Other applications, though, are built in their own stacks, including Marketing Cloud Engagement, BigObjects, and Retail Cloud. When calling to these systems, we also need to limit long-running callouts.
Tune Your Long-Running Transactions
If your calls are taking a long time, you need to tune your code. This can mean:
- Reducing the number of Apex trigger, workflow, and validation rules on your objects. All of these take time, and the more you have, the longer Salesforce takes to save a record.
- Reducing the complexity of your query. Salesforce allows queries to run for up to 2 minutes, but generally we want them to run as fast as possible. Adding additional filters, custom indexes, and ID filters to your query statements are good starts.
- Switching to asynchronous processes. When making a call, you can do it asynchronously (send a request to be made when there are resources free, and periodically check to see if the request is done). Moving from synchronous to asynchronous where possible affords you higher limits, because Salesforce can do the processing as resources become available.
- Reducing the amount of data you are passing. Creating 200 records per call takes a long time. Try creating 100 per call, or 50. Tuning your request size can be a bit of an art form and requires a deep understanding of your data model.
Let’s look at some tuning examples.
Reduce the Number of Apex Triggers
A common development pitfall is the assumption that trigger invocations never include more than one record. Apex triggers are optimized to operate in bulk, which, by definition, requires developers to write logic that supports bulk operations.
trigger MileageTrigger on Mileage__c (before insert, before update) { User c = [SELECT Id FROM User WHERE mileageid__c = Trigger.new[0].id]; }
trigger MileageTrigger on Mileage__c (before insert, before update) { for(mileage__c m : Trigger.new){ User c = [SELECT Id FROM user WHERE mileageid__c = m.Id]; } }
Trigger MileageTrigger on Mileage__c (before insert, before update) { Set<ID> ids = Trigger.newMap.keySet(); List<User> c = [SELECT Id FROM user WHERE mileageid__c in :ids]; }
This pattern respects the bulk nature of the trigger by passing the Trigger.new collection to a set, then using the set in a single SOQL query. This pattern captures all incoming records within the request while limiting the number of SOQL queries.
Best Practices for Designing Bulk Programs
- Minimize the number of data manipulation language (DML) operations by adding records to collections and performing DML operations against these collections.
- Minimize the number of SOQL statements by preprocessing records and generating sets, which can be placed in single SOQL statement used with the IN clause.
Reduce the Complexity of Queries
You can take several approaches to reduce the complexity of your queries. Sometimes you can only go so far squeezing efficiency out of your queries, but the more effective they are, the better. Salesforce provides the Query resource so you can get information about how Salesforce executes your query. Using this resource, you can see which approaches are the most effective ones. Use the ones you can. The more efficient your queries, the more likely they can avoid limits and run fast. Here are some examples.
- Avoid Negative Operators like '!=' or NOT CONTAINS.
- Avoid Inefficient Operators like leading wildcards.
- Sort optimization, which is used with date and number fields. Use an ORDER BY with a LIMIT clause. In cases where the selectivity threshold can’t be met, sort optimization might be better than a table scan.
Go Asynchronous
Use asynchronous callouts to make long-running requests from a Visualforce page to an external Web service and process responses in callback methods. As a result, you can make more long-running callouts and you can integrate your Visualforce pages with complex back-end assets.
This diagram shows the execution path of an asynchronous callout, starting from a Visualforce page. A user invokes an action on a Visualforce page that requests information from a Web service (step 1). The app server hands the callout request to the Continuation server before returning to the Visualforce page (steps 2–3). The Continuation server sends the request to the Web service and receives the response (steps 4–7), then hands the response back to the app server (step 8). Finally, the response is returned to the Visualforce page (step 9).
A typical Salesforce application that benefits from asynchronous callouts contains a Visualforce page with a button. Users click that button to get data from an external Web service. For example, a Visualforce page that gets warranty information for a certain product from a Web service. Thousands of agents in the organization can use this page. Therefore, a hundred of those agents can click the same button to process warranty information for products at the same time.
Example: Asynchronous Callout
You have a Visualforce page with a button that performs a callout to a web service. When users click the button, it calls a startRequest() method in the CalloutController Apex class. This method waits for a response from the web service. As you can imagine, as you wait for the response, other web service requests could pile up.
You need to add a continuation and callback method, which waits for the response. The following code shows a synchronous callout, which could hit the concurrent request limit. The code also shows how to write a continuation and callback method to avoid concurrency, so you can compare the two approaches.
public class CalloutController { // Unique label corresponding to the continuation public String requestLabel; // Result of callout public String result {get;set;} public Object startRequest() { //this part up here is common to both the callout //and continuations approaches String sessionId = UserInfo.getSessionId(); HttpRequest req = new HttpRequest(); req.setHeader('Authorization', 'Bearer ' + sessionID); req.setHeader('Content-Type', 'application/json'); req.setHeader('Accept', 'application/json'); req.setEndpoint('https://mycompany.my.salesforce.com/services/data/v40.0/tooling/' + 'completions?type=apex'); req.setMethod('GET'); //Here's how it works with a callout Http h = new Http(); HttpResponse res = h.send(req); processResponse(res); //and that is all. This works fine until the service is too slow and lots of people use it. //Here is what it needs to change to Continuation con = new Continuation(40); con.continuationMethod='handleCallback'; this.requestLabel = con.addHttpRequest(req); return con; //that's the difference for continuations. } //Here is the callback that waits for a response before continuing public Object handleCallback() { HttpResponse response = Continuation.getResponse(this.requestLabel); return processResponse(response); } // Callback method is only needed for continuations. public Object processResponse(HttpResponse response) { result = response.getBody(); // Return null to re-render the original Visualforce page return null; } }
Then the Visualforce page wakes up when the callout returns.
Look at you! You’re already identifying concurrent requests and learning how to avoid them. This does take some knowledge and practice, but you’re on your way. In the next unit, we take a deep dive into another way to shape up your code.