Newer Version Available

This content describes an older version of this product. View Latest

Transaction Finalizers (Beta)

The Transaction Finalizers feature enables you to attach actions, using the System.Finalizer interface, to asynchronous Apex jobs that use the Queueable framework. A specific use case is to design recovery actions when a Queueable job fails.

As a beta feature, Transaction Finalizers is a preview and isn’t part of the “Services” under your master subscription agreement with Salesforce. Use this feature at your sole discretion, and make your purchase decisions only on the basis of generally available products and features. Salesforce doesn’t guarantee general availability of this feature within any particular time frame or at all, and we can discontinue it at any time. This feature is for evaluation purposes only, not for production use. It’s offered as is and isn’t supported, and Salesforce has no liability for any harm or damage arising out of or in connection with it. All restrictions, Salesforce reservation of rights, obligations concerning the Services, and terms for related Non-Salesforce Applications and Content apply equally to your use of this feature. You can provide feedback and suggestions for this feature in the TransactionFinalizers group in the Trailblazer Community.

Note

To use this beta feature, it isn’t necessary to enable the 'Transaction Finalizers' feature in scratch orgs. The Transaction Finalizers feature isn’t restricted to scratch orgs, can be used in production orgs and sandboxes, and can be included in managed packages.

Note

Before Transaction Finalizers, there was no direct way for you to specify actions to be taken when asynchronous jobs succeeded or failed. You could only poll the status of AsyncApexJob using a SOQL query, and re-enqueue the job if it failed. With transaction finalizers, you can attach a post-action sequence to a Queueable job and take relevant actions based on the job execution result.

A Queueable job that failed due to an unhandled exception can be successively re-enqueued five times by a Transaction Finalizer. This limit applies to a series of consecutive Queueable job failures. The counter is reset when the Queueable job completes without an unhandled exception.

Finalizers can be implemented as an inner class. Also, you can implement both Queueable and Finalizer interfaces with the same class.

System.Finalizer Interface

The System.Finalizer interface includes the execute method:
1global void execute(System.FinalizerContext ctx) {}
This method is called on the provided FinalizerContext instance 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. An instance of System.FinalizerContext injected by the Apex runtime engine as an argument to the execute method.

System.FinalizerContext Interface

The System.FinalizerContext interface contains four methods.
  • getAsyncApexJobId method:
    1global Id getAsyncApexJobId {}
    Returns the ID of the Queueable job for which this finalizer is defined.
  • getRequestId method:
    1global String getRequestId {}
    Returns the request ID, a string that uniquely identifies the request, and can be correlated with Event Monitoring logs. To correlate with the AsyncApexJob table, use the getAsyncApexJobId method instead. The Queueable job and the Finalizer execution both share the (same) request ID.
  • getResult method:
    1global System.ParentJobResult getResult {}
    Returns the System.ParentJobResult enum, which represents the result of the parent asynchronous Apex Queueable job to which the finalizer is attached. The enum takes these values: SUCCESS, UNHANDLED_EXCEPTION.
  • getException method:
    1global System.Exception getException {}
    Returns the exception with which the Queueable job failed when getResult is UNHANDLED_EXCEPTION, null otherwise.
To attach actions to your Queueable jobs, implement the FinalizerContext interface as follows.
  1. Define a class that implements the System.FinalizerContext interface.
  2. Attach a finalizer within a Queueable job’s execute method. To attach the finalizer, invoke the System.attachFinalizer method, using as argument the instantiated class that implements the System.FinalizerContext interface.
    1global void attachFinalizer(Finalizer finalizer) {}

Only one finalizer instance can be attached to any Queueable job. You can enqueue a single asynchronous Apex job (Queueable, Future, or Batch) in the finalizer’s implementation of the execute method. Callouts are allowed in finalizer implementations.

Note

Logging Finalizer Example

This example demonstrates the use of Transaction Finalizers in logging messages from a Queueable job, regardless of whether the job succeeds or fails. The LoggingFinalizer class here implements both Queueable and Finalizer interfaces. The Queueable implementation instantiates the finalizer, attaches it, and then invokes the addLog() method to buffer log messages. The Finalizer implementation of LoggingFinalizer includes the addLog(message, source) method that allows buffering log messages from the Queueable job into finalizer's state. When the Queueable job completes, the finalizer instance commits the buffered log. The finalizer state is preserved even if the Queueable job fails, and can be accessed for use in DML in finalizer implementation or execution.

1public class LoggingFinalizer implements Finalizer, Queueable {
2
3  // Queueable implementation
4  // A queueable job that uses LoggingFinalizer to buffer the log
5  // and commit upon exit, even if the queueable execution fails
6
7    public void execute(QueueableContext ctx) {
8        String jobId = '' + ctx.getJobId();
9        System.debug('Begin: executing queueable job: ' + jobId);
10        try {
11            // Create an instance of LoggingFinalizer and attach it
12            LoggingFinalizer f = new LoggingFinalizer();
13            System.attachFinalizer(f);
14
15            // While executing the job, log using LoggingFinalizer.addLog()
16            DateTime start = DateTime.now();
17            f.addLog('About to do some work...', jobId);
18
19            while (true) {
20              // Results in limit error
21            }
22        } catch (Exception e) {
23            System.debug('Error executing the job [' + jobId + ']: ' + e.getMessage());
24        } finally {
25            System.debug('Completed: execution of queueable job: ' + jobId);
26        }
27    }
28
29  // Finalizer implementation
30  // Logging finalizer provides a public method addLog(message,source) that allows buffering log lines from the Queueable job.
31  // When the Queueable job completes, regardless of success or failure, the LoggingFinalizer instance commits this buffered log.
32  // Custom object LogMessage__c has four custom fields-see addLog() method.
33
34    // internal log buffer
35    private List<LogMessage__c> logRecords = new List<LogMessage__c>();
36
37    public void execute(FinalizerContext ctx) {
38        String parentJobId = '' + ctx.getAsyncApexJobId();
39        System.debug('Begin: executing finalizer attached to queueable job: ' + parentJobId);
40
41        // Update the log records with the parent queueable job id
42        System.Debug('Updating job id on ' + logRecords.size() + ' log records');
43        for (LogMessage__c log : logRecords) {
44            log.Request__c = parentJobId; // or could be ctx.getRequestId()
45        }
46        // Commit the buffer
47        System.Debug('committing log records to database');
48        Database.insert(logRecords, false);
49
50        if (ctx.getResult() == ParentJobResult.SUCCESS) {
51            System.debug('Parent queueable job [' + parentJobId + '] completed successfully.');
52        } else {
53            System.debug('Parent queueable job [' + parentJobId + '] failed due to unhandled exception: ' + ctx.getException().getMessage());
54            System.debug('Enqueueing another instance of the queueable...');
55        }
56        System.debug('Completed: execution of finalizer attached to queueable job: ' + parentJobId);
57    }
58
59    public void addLog(String message, String source) {
60        // append the log message to the buffer
61        logRecords.add(new LogMessage__c(
62            DateTime__c = DateTime.now(),
63            Message__c = message,
64            Request__c = 'setbeforecommit',
65            Source__c = source
66        ));
67    }
68}

Retry Queueable Example

This example demonstrates how to re-enqueue a failed Queueable job in its finalizer. It also shows that jobs can be re-enqueued up to a queueable chaining limit of 5 retries.

1public class RetryLimitDemo implements Finalizer, Queueable {
2
3  // Queueable implementation
4  public void execute(QueueableContext ctx) {
5    String jobId = '' + ctx.getJobId();
6    System.debug('Begin: executing queueable job: ' + jobId);
7    try {
8        Finalizer finalizer = new RetryLimitDemo();
9        System.attachFinalizer(finalizer);
10        System.debug('Attached finalizer');
11        Integer accountNumber = 1;
12        while (true) { // results in limit error
13          Account a = new Account();
14          a.Name = 'Account-Number-' + accountNumber;
15          insert a;
16          accountNumber++;
17        }
18    } catch (Exception e) {
19        System.debug('Error executing the job [' + jobId + ']: ' + e.getMessage());
20    } finally {
21        System.debug('Completed: execution of queueable job: ' + jobId);
22    }
23  }
24
25  // Finalizer implementation
26  public void execute(FinalizerContext ctx) {
27    String parentJobId = '' + ctx.getAsyncApexJobId();
28    System.debug('Begin: executing finalizer attached to queueable job: ' + parentJobId);
29    if (ctx.getResult() == ParentJobResult.SUCCESS) {
30        System.debug('Parent queueable job [' + parentJobId + '] completed successfully.');
31    } else {
32        System.debug('Parent queueable job [' + parentJobId + '] failed due to unhandled exception: ' + ctx.getException().getMessage());
33        System.debug('Enqueueing another instance of the queueable...');
34        String newJobId = '' + System.enqueueJob(new RetryLimitDemo()); // This call fails after 5 times when it hits the chaining limit
35        System.debug('Enqueued new job: ' + newJobId);
36    }
37    System.debug('Completed: execution of finalizer attached to queueable job: ' + parentJobId);
38  }
39}