Salesforce Hacks: System.LimitException: Too many queueable jobs added to the queue: 2

Queueable classes are great to use and provides a wide range of advantages when compared to future methods. But there are some undocumented limitations which you will come across if you are using Queueable classes extensively inside your batch classes.

Lets take an example of badly written trigger on Account. 

trigger AccountTrigger on Account (before insert) {
    for(Account acc: Trigger.new){
        Database.executeBatch(new CreateContactBatch(), 2);
    }
} 

The above trigger calls a batch class for every account record that gets inserted in database. So if you insert N accounts in a single transaction via dataloader or anonymous block or through any other means, the trigger will execute the batch class N number of times. Below is the code for batch class:

global class CreateContactBatch implements Database.Batchable<sObject> {
    
    global Database.QueryLocator start(Database.BatchableContext BC) {
        String query = 'SELECT Id FROM Account';
        return Database.getQueryLocator(query);
    }
    
    global void execute(Database.BatchableContext BC, List<Account> scope) {
        List<Account> accList = (List<Account>)scope;
        for(Account acc : accList){
            System.enqueueJob(new CreateContactQueueable());
        }
    }   
    
    global void finish(Database.BatchableContext BC) {
    }
}

And below is the queueable class:

public class CreateContactQueueable implements Queueable{
    public void execute(QueueableContext context){
        // logic goes here
    }
}

The above badly designed batch class enqueues a Queueable class for every single record which comes in the execute method. Lets assume that there are 2 records fetched in the start method which are passed to execute method since the batch size is 2. Below are the limitations with Queueable apex as per Salesforce documentation:

Queueable Apex Limits

  • The execution of a queued job counts once against the shared limit for asynchronous Apex method executions.
  • You can add up to 50 jobs to the queue with System.enqueueJob in a single transaction. To check how many queueable jobs have been added in one transaction, call Limits.getQueueableJobs().
  • No limit is enforced on the depth of chained jobs, which means that you can chain one job to another job and repeat this process with each new child job to link it to a new child job. For Developer Edition and Trial organizations, the maximum stack depth for chained jobs is 5, which means that you can chain jobs four times and the maximum number of jobs in the chain is 5, including the initial parent queueable job.
  • When chaining jobs, you can add only one job from an executing job with System.enqueueJob, which means that only one child job can exist for each parent queueable job. Starting multiple child jobs from the same queueable job isn’t supported.

In the documentation, Salesforce has not specified any limits for the batch class. If you execute the above code, you will see the below exception in debug logs:

13:39:29:010 EXCEPTION_THROWN [11]|System.LimitException: Too many queueable jobs added to the queue: 2

But Salesforce says that you can add 50 jobs to the queue with System.enqueueJob in a single transaction. This does not applies to batch classes as we can see. Now how to bypass this undocumented limit?

The approach is simple. We need to first check how many queueable jobs are added in the queue in the current transaction by making use of Limits class. If the number comes as 1, then call a schedulable class and enqueue the queueable class from the execute method of schedulable class. Below is the code sample for the same:

global class CreateContactBatch implements Database.Batchable<sObject> {
    
    global Database.QueryLocator start(Database.BatchableContext BC) {
        String query = 'SELECT Id FROM Account';
        return Database.getQueryLocator(query);
    }
    
    global void execute(Database.BatchableContext BC, List<Account> scope) {
        List<Account> accList = (List<Account>)scope;
        for(Account acc : accList){
            if(Limits.getQueueableJobs() == 1){
                String hour = String.valueOf(Datetime.now().hour());
                String min = String.valueOf(Datetime.now().minute()); 
                String ss = String.valueOf(Datetime.now().second() + 5);
                //parse to cron expression
                String nextFireTime = ss + ' ' + min + ' ' + hour + ' * * ?';
                System.schedule('ScheduledJob ' + String.valueOf(Math.random()), nextFireTime, new CreateContactSchedulable());
            }else{
                System.enqueueJob(new CreateContactQueueable());
            }
        }
    }   
    
    global void finish(Database.BatchableContext BC) {
    }
}​

Below is the queueable class:

public class CreateContactSchedulable implements Schedulable {
    public void execute(SchedulableContext sc) {
        System.enqueueJob(new CreateContactQueueable());
        // Abort the job once the job is queued
        System.abortJob(sc.getTriggerId());
    }
}

Comments

  1. great example.We could also limit batch size to 1 on case to case basis.But this solution is more versatile.

    ReplyDelete
  2. great code...
    really helped

    just remember that

    String ss = String.valueOf(Datetime.now().second() + 5);

    will throw an exception when second > 55

    ReplyDelete

Post a Comment

Popular posts from this blog

Salesforce Lightning: Countdown timer

Building an Org Role Hierarchy component in LWC