Newer Version Available

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

Transaction Finalizers (Pilot)

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 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 Trailblazer Community.

Note

Because finalizers are currently in pilot and are available only in scratch orgs that have enabled the feature, do not attempt to package finalizers.

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.

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

Example

1public class SampleQueueable999 implements Queueable, IRetryTracker {
2
3   // Tracks number of consecutive times this is re-enqueued
4   protected integer enqueueRetryCount;
5
6   public SampleQueueable999() {
7      enqueueRetryCount = 0;
8   }
9
10   public void setRetryCount(integer retryCount) {
11      enqueueRetryCount = retryCount;
12   }
13
14   public void execute(QueueableContext ctx) {
15      RequeueLoggingFinalizer f = new RequeueLoggingFinalizer(
16                                                'SampleQueueable999', // Pass (fully qualified) current type name
17                                                enqueueRetryCount
18                                             );
19      System.attachFinalizer(f);
20
21      // While executing the job, log using RequeueLoggingFinalizer.addLog(String)
22      DateTime start = DateTime.now();
23      f.addLog('About to do some work...');
24
25      // Do some work here 
26      doSomeWork();
27
28      Long msElapsedTime = (DateTime.now().getTime() - start.getTime());
29      f.addLog('work completed in, ' + String.valueOf(msElapsedTime)  + 'ms');
30
31      // Uncomment below line to see retry logic (in finalizer) in action
32      // Throw new SomeXQueueableException('Deliberate Exception Message');
33   }
34
35   private void doSomeWork() {
36      // Do a call out
37      // Do some data proccessing
38      Integer delay = 1 * 10 * 1000;
39      Long begintime = DateTime.now().getTime();
40      while(DateTime.now().getTime() - begintime < delay);
41   }
42
43   // Custom exception
44   public class SomeXQueueableException extends Exception { }
45}
1public interface IRetryTracker {
2   void setRetryCount(integer retryCount);
3}
1/**
2 * RequeueLoggingFinalizer is a finalizer that commits log buffer from its parent queueable job.
3 * If the parent queueable job failed, it will re-enqueue a new instance of parent queueable job.
4 * If the parent queueable job failed for 3 consecutive re-enqueues... then it will stop further enqueue.
5 *
6 * NOTE: The parent queueable job can be re-enqueued iff it implements IRetryTracker interface (in addition
7 *       to implementing System.Queueable interface)
8 */
9public class RequeueLoggingFinalizer implements Finalizer {
10
11    protected String sourcename = 'RequeueLoggingFinalizer'; // For log records
12
13    // Tracks number of consecutive times the parent queueable is requeued on failure
14    protected Integer enqueueRetryCount;
15
16    // Parent queueable class type name
17    protected String concreteQueueableTypeName;
18
19    // Internal log buffer
20    // LogMessage__c is a custom object with datetime, log message (long text area of max length 131,072), source (text 255), request (text 255) fields
21    private List<LogMessage__c> logRecords = new List<LogMessage__c>();
22
23    public RequeueLoggingFinalizer(String queueableTypeNameStr, Integer retryCount) {
24        enqueueRetryCount = retryCount;
25        concreteQueueableTypeName = queueableTypeNameStr;
26    }
27
28    /**
29     * Logs a message that will be committed to db even in conditions when Queueable's transaction is rolled back
30     */
31    public void addLog(String message) {
32      // Append the log message to the buffer
33      logRecords.add(new LogMessage__c(
34                           datetime__c = DateTime.now(),
35                           message__c = message,
36                           request__c = '', // set this before commit
37                           source__c = this.sourcename
38         ));
39    }
40
41    /**
42     * Commits the log buffer and re-enqueues failed queueable (iff type is IRetryTracker)
43     */
44    public void execute(FinalizerContext ctx) {
45
46        String reqId = ctx.getRequestId();
47        String jobId = Id.valueOf(ctx.getAsyncApexJobId());
48        System.debug('Executing Finalizer that was attached to Queueable job (job id: ' + jobId + ', request id: ' + reqId + ')');
49
50        // Fix the getRequestId id in the log records
51        System.debug('Updating request id on ' + logRecords.size() + ' log records');
52        for (LogMessage__c log : logRecords) {
53            log.Request__c = reqId;
54        }
55
56        // Commit the log buffer
57        System.debug('committing log records to database');
58        Database.insert(logRecords, false);
59
60        // Debug log the status of the parent queueable job and
61        // if it failed, and implementing IRetryTracker interface, retry (re-enqueue) up to 3 times
62
63        if (ctx.getResult() == ParentJobResult.SUCCESS) {
64            System.debug('Parent Queueable (job id: ' + jobId + '): completed successfully!');
65
66        } else { // Queueable failed
67            System.debug('Parent Queueable (job id: ' + jobId + '): FAILED!');
68            System.debug('Parent Queueable Exception: ' + ctx.getException().getMessage());
69
70            Type systemQueueableType = Type.forName('System.Queueable');
71            Type retryTrackerType = Type.forName('IRetryTracker');
72            Type concreteQueueableType = Type.forName(concreteQueueableTypeName);
73
74            if (systemQueueableType.isAssignableFrom(concreteQueueableType) &&
75                retryTrackerType.isAssignableFrom(concreteQueueableType)) {
76
77                enqueueRetryCount++;
78                if (enqueueRetryCount <= 3 /* Maximum retry limit */) {
79
80                    // Create a new queueable for retry
81                    Object obj = concreteQueueableType.newInstance();
82
83                    // Pass the current retry count to the queueable so it can
84                    // pass that information to the new instance of the
85                    // finalizer that it will attach
86                    IRetryTracker retryTracker = (IRetryTracker) obj;
87                    retryTracker.setRetryCount( enqueueRetryCount );
88
89                    System.Queueable queueableObj = (System.Queueable) obj;
90                    ID newJobId = System.enqueueJob( queueableObj );
91                    System.debug('Re-enqueued a new instance of queueable \'' +
92                      concreteQueueableTypeName + '\' with job ID: ' + newJobId);
93                }
94            }
95        }
96    }
97}