Transaction Finalizers

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.
The Transaction Finalizers feature provides a direct way for you to specify actions to be taken when asynchronous jobs succeed or fail. Before Transaction Finalizers, you could only take these two actions for asynchronous job failures:
  • Poll the status of AsyncApexJob using a SOQL query and re-enqueue the job if it fails
  • Fire BatchApexErrorEvents when a batch Apex method encounters an unhandled exception
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.

The Queueable job and the Finalizer run in separate Apex and Database transactions. For example, the Queueable can include DML, and the Finalizer can include REST callouts. Using a finalizer doesn’t count as an extra execution against your daily Async Apex limit. Synchronous governor limits apply for the Finalizer transaction, except in these cases where asynchronous limits apply:
  • Total heap size
  • Maximum number of Apex jobs added to the queue with System.enqueueJob
  • Maximum number of methods with the future annotation allowed per Apex invocation
For more information on governor limits, see Execution Governors and Limits.

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 is 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.
Attach the finalizer to your Queueable jobs using the System.attachFinalizer method.
  1. Define a class that implements the System.Finalizer 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.Finalizer interface.
    1global void attachFinalizer(Finalizer finalizer) {}

Implementation Details

  • 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.
  • The Finalizer framework uses the state of the Finalizer object (if attached) at the end of Queueable execution. Mutation of the Finalizer state, after it’s attached, is therefore supported.
  • Variables that are declared transient are ignored by serialization and deserialization, and therefore don’t persist in the Transaction Finalizer.

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

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}

Considerations

If a job request is terminated unexpectedly, such as a database shutdown during system upgrade, the transaction finalizer can fail to execute.

Best Practices

We urge ISVs to exercise caution in using global Finalizers with state-mutating methods in packages. If a subscriber org’s implementation invokes such methods in the global Finalizer, it can result in unexpected behavior. Examine all state-mutating methods to see how they affect the finalizer state and overall behavior.