Attach Actions to Asynchronous Apex Jobs Using Transaction Finalizers (Pilot)

With Spring ’20, we introduce a way to attach actions to queueable, asynchronous jobs using a new System.Finalizer interface. A specific use case is to design recovery action when a queueable job fails.

Where: This change applies to Lightning Experience and Salesforce Classic only in scratch orgs that have enabled the feature during org creation. Because finalizers are currently in pilot and are available only in scratch orgs, do not attempt to package finalizers.

Note

Note

The TransactionFinalizers feature is available as a pilot program only in scratch orgs that have enabled the feature during org creation. The functionality of this feature is subject to change, and is not available for production organizations while in pilot. Pilot programs are subject to change. This feature isn’t generally available unless or until Salesforce announces its general availability in documentation or in press releases or public statements. We can’t guarantee general availability within any particular time frame or at all. Make your purchase decisions only on the basis of generally available products and features. You can provide feedback and suggestions for this feature in the TransactionFinalizers group in the IdeaExchange.

Why: Currently, there is no direct way for you to specify actions to be taken when asynchronous jobs succeed or fail. You can only poll the status of asyncapexjob using a SOQL query, and re-enqueue the job if it fails. With transaction finalizers, you can attach a post-action sequence to an asynchronous job and take relevant actions based on the job execution result.

How: First, implement a class that implements the System.Finalizer interface. Then, attach a finalizer within a queueable job’s execute method by invoking the System.attachFinalizer method with an argument of the instantiated class that implements the finalizer interface. Only one finalizer can be attached to any queueable job. You can enqueue a single asynchronous Apex job (queueable, future, or batch) in your finalizer. You can also make callouts in the finalizer.

An execute (FinalizerContext ctx) method is called for every enqueued job with a finalizer attached. Within the execute method, you can define the actions to be taken at the end of the queueable job. The System.Finalizer class is created and injected by the Apex runtime engine as an argument to the execute method. You must not create or derive an instance of this class. The System.FinalizerParentJobResult enum represents the  result of the parent asynchronous Apex queueable job to which the finalizer is attached. The enum takes these values: SUCCESS, UNHANDLED_EXCEPTION.

This example implements the Finalizer interface to identify how far a queueable job proceeds before an unforeseen, uncatchable error occurs, and also obtains details of the error.
public class AccountUpdateLoggingFinalizer implements Finalizer {
    // Used to maintain progress
    List<String> acctNames;
    
    public AccountUpdateLoggingFinalizer() {
        acctNames = new List<String>();
    }
    
    public void execute(FinalizerContext ctx) {        
        Id parentQueueableJobId = ctx.getAsyncApexJobId();
        System.Debug('Executing Finalizer that was attached to Queueable Job ID: ' + parentQueueableJobId);
        if (ctx.getAsyncApexJobResult() == FinalizerParentJobResult.SUCCESS) {
            // Queueable executed successfully
            System.Debug('Parent Queueable (Job ID: ' + parentQueueableJobId + '): completed successfully!');
        } else {
            // Queueable failed
            // Log some additional information.
            System.Debug('Parent Queueable (Job ID: ' + parentQueueableJobId + '): FAILED!');
            System.Debug('Parent Queueable Exception: ' + ctx.getAsyncApexJobException().getMessage());

            // Show the accounts that were processed before Queueable Job encountered the exception            
            System.Debug('Parent Queueable processed following accounts:');
            for (String acctName : acctNames) {
                System.Debug(acctName);
            }
        }        
    }
    
    public void reportProgress(Account acct) {
        acctNames.add(acct.Name);
    }
}
public class FollowupActionQueueable implements Queueable {
    public void execute(QueueableContext ctx) {
       System.Debug('FollowupActionQueueable is executing');
    }
}
public class AccountUpdateQueueable implements Queueable {

    public void execute(QueueableContext ctx) {
    
        // Create a transaction finalizer
        AccountUpdateLoggingFinalizer finalizer = new AccountUpdateLoggingFinalizer();

        // Attach the transaction finalizer to this queueable
        System.attachFinalizer(finalizer);
 
        // Do some (partial) work
        Account acct = new Account();
        acct.Name = '1st Account';
        insert(acct);
        
        // Send some status update to the finalizer
        finalizer.reportProgress(acct);
        
        // do some work that results in an unforeseen, uncatchable exception
        someWork();
       
        // Attempt to do some more work
        Account acct2 = new Account();
        acct2.Name = '2nd Account';
        insert(acct2);
        
        // Report more progress
        finalizer.reportProgress(acct2);
    }
    
    private void someWork() {
        // regular implementation that could result in an un-catchable
        // exception e.g. System.LimitException due to CPU usage over limits
        
        // for demonstration, try to enqueue 2 jobs so this method results in
        // System.LimitException because more than one job cannot be enqueued
        // from a Queueable
        System.enqueueJob(new FollowupActionQueueable());
        System.enqueueJob(new FollowupActionQueueable());
    }
}