No comments yet

Generic Approach to Salesforce Queueable

Salesforce has a good capability of asynchronous execution. For instance future, batch, schedulable and queueable methods in apex. Although it has good async methods, the challenge is a missing framework. Basically, it has nice tools to execute the async job but it requires an orchestration mechanism. As a developer, most of the time you need to develop your own set of classes/methods to handle this.

In this article, I would like to mention a generic queueable method. It is the upgraded version of the future method. It resolves significant restrictions of it (you can read an article here). Mainly you can chain jobs within queueable methods. But it has still some restrictions which let our lives not easier (list of the restrictions). Especially this:

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

This means that if we want to make multiple queueable method call in one transaction then we need to create a logic to handle the chaining in the queueable class itself. This brings complexity to the usage of this async method.

Therefore I have created a generic method that can handle thousands of jobs in one transition. Basically, I have created an AsyncJobService.csl and you can add the jobs in queueable context, then afterwords run them in a chained way.

Adding a new job:

AsyncJobService.addJob(jobName, className, methodName, parameters);

Running them:

AsyncJobService.runJobInQueues();

Technical Overview

This implementation simply consists of three parts.

  1. A generic job object, AsyncJob.cls
  2. A callable class for class/method configuration, GenericCallable.cls
  3. A service class for orchestration of them, AsyncJobService.cls
Generic Approach to Salesforce Queueable

The service class is preparing and adding the jobs to a list and at the end, it calls the generic callable class to process them. The generic callable class goes through the list of jobs and runs them one by one in the queueable context.

The code can be found under this link. Although there is room to improve it, it works pretty well. If you want to use a method with this generic queueable class, first you need to create a static method and then configure it in GenericCallable.cls.

Code View

First, we are defining the object type

/***********************************************************************************************************************
Apex Class Name     : AsyncJob
Version             : 1.0
Created Date        : 30.03.2021
Function            : AsyncJob object definition
Modification Log    :
* Developer                 Date                    Description
* ----------------------------------------------------------------------------------------------------------------------
* Aykut Parlak             30/03/2021              Initial version
***********************************************************************************************************************/
public without sharing class AsyncJob {
    public String jobName;
    public String jobClass;
    public String jobMethod;
    public String jobStatus;
    public String jobError;
    public List<Id> recordIds;

    public AsyncJob(String jobName, String jobClass, String jobMethod, List<Id> recordIds) {
        this.jobName = jobName;
        this.jobClass = jobClass;
        this.jobMethod = jobMethod;
        this.recordIds = recordIds;
    }
}

The data structure is designed for working with record ids. Then we are having a generic queueable method:

/***********************************************************************************************************************
Apex Class Name     : GenericQueueable
Version             : 1.0
Created Date        : 30.03.2021
Function            : GenericQueueable class
Modification Log    :
* Developer                 Date                    Description
* ----------------------------------------------------------------------------------------------------------------------
* Aykut Parlak             30/03/2021              Initial version
***********************************************************************************************************************/
public class GenericQueueable implements Queueable, Database.AllowsCallouts {
    private List<AsyncJob> jobs;
    private Integer currentJobNumber;

    public GenericQueueable(List<AsyncJob> jobs, Integer currentJobNumber) {
        this.jobs = jobs;
        this.currentJobNumber = currentJobNumber;
    }

    public GenericQueueable(List<AsyncJob> jobs) {
        this.jobs = jobs;
        this.currentJobNumber = 0;
    }

    public void execute(QueueableContext context) {
        GenericCallable gc = new GenericCallable();
        gc.call('apexQueue', new Map<String, Object>{ 'jobs' => this.jobs, 'currentJobNumber' => this.currentJobNumber });
    }
}

We need a service class to orchestrate the job processes:

/***********************************************************************************************************************
Apex Class Name     : AsyncJobService
Version             : 1.0
Created Date        : 30.03.2021
Function            : AsyncJob Service class
Modification Log    :
* Developer                 Date                    Description
* ----------------------------------------------------------------------------------------------------------------------
* Aykut Parlak             30/03/2021              Initial version
***********************************************************************************************************************/
public with sharing class AsyncJobService {
    public static AsyncJob[] jobs;

    public static void addJob(AsyncJob job) {
        if (jobs != null)
            jobs.add(job);
        else {
            jobs = new List<AsyncJob>();
            jobs.add(job);
        }
    }

    public static void addJob(String jobName, String jobClass, String jobMethod, List<Id> recordIds) {
        AsyncJob job = new AsyncJob(jobName, jobClass, jobMethod, recordIds);
        addJob(job);
    }

    public static void runJobInQueues() {
        if (jobs == null || jobs.isEmpty())
            return;
        if (Test.isRunningTest())
            jobs = new List<AsyncJob>{ jobs[0] };
        GenericQueueable gq = new GenericQueueable(jobs);
        System.enqueueJob(gq);
        jobs.clear();
    }
}

The last class is the class where we can register our methods:

/***********************************************************************************************************************
Apex Class Name     : GenericCallable
Version             : 1.0
Created Date        : 30.03.2021
Function            : GenericCallable class
Modification Log    :
* Developer                 Date                    Description
* ----------------------------------------------------------------------------------------------------------------------
* Aykut Parlak             30/03/2021              Initial version
***********************************************************************************************************************/
public without sharing class GenericCallable implements Callable {
    public Object call(String action, Map<String, Object> args) {
        switch on action {
            when 'apexQueue' {
                Integer currentJobNumber = (Integer) args.get('currentJobNumber');
                Integer nextJobNumber = currentJobNumber + 1;
                List<AsyncJob> jobs = (List<AsyncJob>) args.get('jobs');
                AsyncJob job = jobs[currentJobNumber];
                
                // here you can implement the new class/method
                try {
                    if (job.jobClass == 'System' && job.jobMethod == 'debug') {
                        System.debug(job);
                    } else {
                        throw new GenericCallableException('Method not implemented');
                    }
                    job.jobStatus = 'OK';
                } catch (Exception e) {
                    job.jobStatus = 'ERROR';
                    job.jobError = e.getMessage();
                }

                if (nextJobNumber < jobs.size()) {
                    GenericQueueable gq = new GenericQueueable(jobs, nextJobNumber);
                    System.enqueueJob(gq);
                }
            }
            when else {
                throw new GenericCallableException('Method not implemented');
            }
        }
        return null;
    }

    public class GenericCallableException extends Exception {}
}

When we have a class/method for the job logic we can implement it here.

Summary

Apex Queueable method is enhanced with a generic async approach and we can chain thousand of jobs with easy add and run methods. It is good but not perfect so it requires improvement and this will be the next version of this implementation.

Post a comment