Skip To Content
Back to Blog

Using Asynchronous Apex Automations To Bypass CPU Time Limits

By 04.11.23
Reading time: 5 minutes

Apex is great for building out complex automations that would be difficult or impossible to do with declarative automations such as Flows or Process Builders. Among these are complex asynchronous automations including Queueable, Batch, and Schedulable Apex. Using asynchronous automations is increasingly important as the business processes and automations that power them become more complex. Here I’ll explore the limitations of Queueable, Batch, and Schedulable Apex, how the open-source Apex Async Chainable library solves these limitations, and some use cases to get you started.

Why asynchronous?

The evolution of the Salesforce Platform has enabled more complex automations to power more complex and evolving business processes. However, the governor limits that are in place to support Salesforce’s multi-tenanted nature have largely stayed the same. Some of the common limits include:

  • CPU Timeout: 10 seconds
  • SOQL Limit: 100 queries
  • DML Limit: 150 dmls
  • Callout Limit: 100 callouts

Each automation introduced in an already complex environment runs the risk of causing a worse user experience with longer save times and/or hitting one of the governor limits, which will prevent the save from happening. 

You don’t want your user to get an “Apex CPU time limit exceeded error,” so how do you support more complex business processes when the limits stay the same?

One answer is moving automations from running synchronously to asynchronously. The main distinction between synchronous and asynchronous is:

  • Synchronous automations run in the same transaction as the record being saved; any uncaught exception will propagate to the user and prevent the record from saving
  • Asynchronous automations are queued up and run in a different transaction than the record being saved; any uncaught exception will cause the job to fail in the background and will not prevent the record from saving

While there is support for asynchronous automations in declarative (Flow/Workflow Rule) development, this blog will focus on the imperative (Apex) development options, namely:

  • Queueable: Run a standalone asynchronous job
  • Batch: Process a large set of records
  • Schedulable: Schedule Apex to run at a configured interval

In some cases, chaining asynchronous jobs together — that is, running one asynchronous job after another completes — is needed. With the out-of-the-box Queueable, Schedulable, and Batch interfaces provided by Salesforce, this may look something like:

// SchedulableImplementation.cls
class SchedulableImplementation implements Schedulable {
    public void execute(SchedulableContext context) {
        // Custom business logic
        ...
        // Run next asynchronous process
        Database.executeBatch(new BatchImplementation());
    }
}

// BatchImplementation.cls
class BatchImplementation implements Database.Batchable {
    public Database.QueryLocator start(BatchableContext context) { ... }

    public void execute(BatchableContext context, List scope) {
        // Custom business logic
        ... 
    }

    public void finish(BatchableContext context) {
        // Run next asynchronous process
        System.enqueueJob(new QueueableImplementation());
    }
}

// QueueableImplementation.cls
class QueueableImplementation implements Queueable {
    public void execute(QueueableContext context) {
        // Custom business Logic
        ...
        // Run next asynchronous process
        ... and so forth
    }
}

And would be started by running the schedulable class:

// Anonymous Apex for scheduling

System.schedule(jobName, cronExp, new SchedulableImplementation());

One limitation with this approach is that the business logic of each asynchronous job is tightly coupled with the logic to chain the next asynchronous job, meaning any adjustments to the order of execution could require adjustments to all downstream asynchronous jobs. So how can we make this process better?

Using the Apex Async Chainable Library

The Async Apex Chainable library was created to decouple the business logic and chaining logic of asynchronous automations. The library aims to solve this limitation (and others) with the following functionality:

  • Allows Queueable, Batch, or Schedulable to be chained
  • Exposes extensible classes that have the chaining logic embedded and surfaces abstract methods for custom business logic
  • Utilizes Transaction Finalizers for Queueables to ensure the chain can continue even if an uncaught exception is surfaced in the Queueable (including uncatchable limit exceptions)
  • Kill Switch to stop the Chainables globally or stop the execution of a specific Chainable
  • Lightweight (<150 lines of testable code)

With that being said, let’s look at how the above example would be implemented with the Apex Async Chainable library:   

// SchedulableImplementation.cls
class SchedulableImplementation extends ChainableSchedulable {
    public SchedulableImplementation() {
        // Scheduling Info
        super(jobName, cronExp);
    }
}

// BatchImplementation.cls
class BatchImplementation extends ChainableBatchQueryLocator {
    protected override Database.QueryLocator start() { ... }

    protected override void execute(List scope) {
        // Custom business logic
        ... 
    }

    protected override Boolean finish() {}
}

// QueueableImplementation.cls
class QueueableImplementation extends ChainableQueueable {
    protected override Boolean execute(QueueableContext context) {
        // Custom business Logic
        ...
    }
}

And would be started by chaining the jobs in a promise-like approach…

// Anonymous Apex for scheduling
new SchedulableImplementation()
    .then(new BatchImplementation())
    .then(new QueueableImplementation())
    ...
    .run();

Now that we’ve seen how the library works, let’s dive into some use cases.

Running dependent rollups

Let’s assume we want to run a series of rollups in a certain order. In this case, a daily job that…

  • Rolls up Open Opportunity Amounts to Accounts
  • Rolls up Account Opportunity Amounts to Household Accounts

This could be achieved with Async Apex Chainable with the following code:

// OpportunityRollupScheduler.cls
public class OpportunityRollupScheduler extends ChainableSchedulable {
    public RollupScheduler() {
        // Run daily at 9 PM
        super('OpportunityRollup', '0 0 21 1/1 * ? *');
    }
}

// OpportunityToAccountRollup.cls
public class OpportunityToAccountRollup extends ChainableBatchQueryLocator {
    protected override Database.QueryLocator start() {
        String query = 'SELECT Id, (SELECT Amount FROM Opportunities WHERE IsClosed = FALSE) FROM Account WHERE RecordType.DeveloperName != \'Household\'';
        return Database.getQueryLocator(query);
    }

    protected override void execute(List scope) {
        for (Account account : (List<Account) scope) {
             account.Open_Opportunity_Amount__c = 0;
             for (Opportunity opportunity : account.Opportunities) {
                 account.Open_Opportunity_Amount__c += opportunity.Amount;
             }
        }

        update scope;
    }

    protected override Boolean finish() {
        return true;
    }
}

// AccountToHouseholdAccountRollup.cls
public class AccountToHouseholdAccountRollup extends ChainableBatchQueryLocator {
    protected override Database.QueryLocator start() {
        String query = 'SELECT Id, (SELECT Open_Opportunity_Amount__c FROM ChildAccounts) FROM Account WHERE RecordType.DeveloperName = \'Household\'';
        return Database.getQueryLocator(query);
    }

    protected override void execute(List scope) {
        for (Account householdAccount : (List<Account) scope) {
             householdAccount.Open_Opportunity_Amount__c = 0;
             for (Account account : householdAccount.ChildAccounts) {
                 householdAccount.Open_Opportunity_Amount__c += account.Open_Opportunity_Amount__c;
             }
        }

        update scope;
    }

    protected override Boolean finish() {
        return true;
    }
}

// Anonymous Apex for scheduling
new OpportunityRollupScheduler()
    .then(new OpportunityToAccountRollup())
    .then(new AccountToHouseholdAccountRollup())
    .run();

Launch another asynchronous job when approaching governor limits

Another use case could be proactively running another asynchronous job when a limit is about to be reached. In this case, let’s assume Apex is responsible for getting all Financial Account Transactions for a given Financial Account and that the API is paginated with an unknown number of pages. Because the limit is 100 callouts per apex transaction, if we get close to that number we can kick off one or more jobs to finish the request.

// GetFinancialAccountTransactions.cls
public class GetFinancialAccountTransactions extends ChainableQueueable {
    private Id financialAccountId;
    private Integer nextPage;
    
    public GetFinancialAccountTransactions(Id financialAccountId) {
        this(financialAccountId, 0);
    }
    
    public GetFinancialAccountTransactions(Id financialAccountId, Integer nextPage) {
        this.financialAccountId = financialAccountId;
        this.nextPage = nextPage;
    }
    
    protected override Boolean execute() {
        // Continue getting the next page until there are no more pages or the limit is reached for callouts
        while (this.nextPage != null && Limits.getCallouts() < Limits.getLimitCallouts()) {
            // Build request
            Http h = new Http();
            HttpRequest request = new HttpRequest();
            request.setEndpoint('SomeEndpoint/' + this.financialAccountId + '?page=' + this.nextPage);

            // Get response
            HttpResponse response = h.send(request);
            ResponseWrapper responseWrapper = (ResponseWrapper) JSON.deserializeUntyped(response.getBody());
            
            // Do processing of response
            
            // Set next page (assume if it's null there are no more pages)
            this.nextPage = responseWrapper.nextPage;
        }
        if (this.nextPage != null) {
            // If nextPage is not empty we've reached the callout limit and need to kick off a new job
            this.then(new GetFinancialAccountTransactions(this.financialAccountId, this.nextPage));
        }
        
        return true;
    }
    
    private class ResponseWrapper {
        List financialAccountTransactions;
        Integer nextPage;
    }
    
    private class FinancialAccountTransactionWrapper {
        String externalId;
        Decimal amount;
    }
}

// Run GetFinancialAccountTransactions from any other apex
new GetFinancialAccountTransactions(financialAccountId).run();

Logging uncaught Queueable exceptions

Another advantage of the Apex Async Chainable library is that it uses a Transaction Finalizer for Queueables. This can be leveraged to do logging for any uncaught exceptions (including uncatchable limit exceptions) that may happen in ChainableQueueable implementations.

// LoggingFinalizer.cls
public class LoggingFinalizer extends ChainableFinalizer {
    protected override Boolean execute() {
        switch on this.context.getResult() {
            when SUCCESS {
                // Do any operations you want on success
            }
            when UNHANDLED_EXCEPTION {
                // Do any logging required when an unhandled exception occurs (including uncatchable limit exceptions)
            }
        }
        // Whether to run the next Chainable
        return true|false|this.defaultRunNext();
    }
}

// QueueableImplementation.cls
public class QueueableImplementation extends QueueableImplementation {
    public CustomQueueable() {
        super(new LoggingFinalizer());
    }
    
    protected override Boolean execute() {
        // Execute logic here
        ...
    }
}

// Run QueueableImplementation from any other apex
new QueueableImplementation().run();

Start taking advantage of asynchronous Apex automation

If you’d like to read more about the library or deploy it to your Salesforce org, you can visit the Github page here: Apex Async Chainable. Our team of expert Salesforce developers is here to help you achieve whatever your organization needs from the platform, and we have deep familiarity with the Apex programming language. Find out how we can assist your team. 

 

We don't support Internet Explorer

Please use Chrome, Safari, Firefox, or Edge to view this site.