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 action 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 IdeaExchange.

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.

To attach actions to your queueable jobs, first define a class that implements the System.Finalizer interface. Then, attach a finalizer within a queueable job’s execute method. Attach the finalizer by invoking the System.attachFinalizer method, using as argument the instantiated class that implements the System.Finalizer interface. 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.

An execute (FinalizerContext ctx) method is called on the provided Finalizer 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. The System.FinalizerContext class is created and injected by the Apex runtime engine as an argument to the execute method. 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.

Example

Identify how far a queueable job proceeds before an unforeseen, uncatchable error occurs. This example also obtains details of the error.
1public class AccountUpdateLoggingFinalizer implements Finalizer {
2    // Used to maintain progress
3    List<String> acctNames;
4    
5    public AccountUpdateLoggingFinalizer() {
6        acctNames = new List<String>();
7    }
8    
9    public void execute(FinalizerContext ctx) {        
10        Id parentQueueableJobId = ctx.getAsyncApexJobId();
11        System.Debug('Executing Finalizer that was attached to Queueable Job ID: ' + parentQueueableJobId);
12        if (ctx.getAsyncApexJobResult() == FinalizerParentJobResult.SUCCESS) {
13            // Queueable executed successfully
14            System.Debug('Parent Queueable (Job ID: ' + parentQueueableJobId + '): completed successfully!');
15        } else {
16            // Queueable failed
17            // Log some additional information.
18            System.Debug('Parent Queueable (Job ID: ' + parentQueueableJobId + '): FAILED!');
19            System.Debug('Parent Queueable Exception: ' + ctx.getAsyncApexJobException().getMessage());
20
21            // Show the accounts that were processed before Queueable Job encountered the exception            
22            System.Debug('Parent Queueable processed following accounts:');
23            for (String acctName : acctNames) {
24                System.Debug(acctName);
25            }
26        }        
27    }
28    
29    public void reportProgress(Account acct) {
30        acctNames.add(acct.Name);
31    }
32}
1public class FollowupActionQueueable implements Queueable {
2    public void execute(QueueableContext ctx) {
3       System.Debug('FollowupActionQueueable is executing');
4    }
5}
1public class AccountUpdateQueueable implements Queueable {
2
3    public void execute(QueueableContext ctx) {
4    
5        // Create a transaction finalizer
6        AccountUpdateLoggingFinalizer finalizer = new AccountUpdateLoggingFinalizer();
7
8        // Attach the transaction finalizer to this queueable
9        System.attachFinalizer(finalizer);
10 
11        // Do some (partial) work
12        Account acct = new Account();
13        acct.Name = '1st Account';
14        insert(acct);
15        
16        // Send some status update to the finalizer
17        finalizer.reportProgress(acct);
18        
19        // do some work that results in an unforeseen, uncatchable exception
20        someWork();
21       
22        // Attempt to do some more work
23        Account acct2 = new Account();
24        acct2.Name = '2nd Account';
25        insert(acct2);
26        
27        // Report more progress
28        finalizer.reportProgress(acct2);
29    }
30    
31    private void someWork() {
32        // regular implementation that could result in an un-catchable
33        // exception e.g. System.LimitException due to CPU usage over limits
34        
35        // for demonstration, try to enqueue 2 jobs so this method results in
36        // System.LimitException because more than one job cannot be enqueued
37        // from a Queueable
38        System.enqueueJob(new FollowupActionQueueable());
39        System.enqueueJob(new FollowupActionQueueable());
40    }
41}

Example

Flush the full log to the database when the job is completed, rather than writing to the database every time the log is updated.

The full log is written to the database irrespective of whether the job succeeded or failed, because the finalizer is executed in both cases.
1public class LoggingFinalizer implements Finalizer {
2   private List<LogMessage__c> logRecords = new List<LogMessage__c>();
3
4   public void execute(FinalizerContext ctx){
5      Database.insert(logRecords, false);
6   }
7
8   public void addLog(String message){
9      logRecords.add(new LogMessage__c(
10                           Message__c = message,
11                           Request__c = FinalizerContext.getAsyncApexJobId(),
12                           Source__c = 'LoggingFinalizer'
13         ));
14   }
15}
1public class SomeQueueuableJob implements Queueuable, Database.AllowsCallouts {
2   public void execute(QueueuableContext ctx) {
3      LoggingFinalizer f = new LoggingFinalizer();
4      System.attachFinalizer(f);
5      DateTime start = DateTime.now();
6      f.addLog('About to callout to external system...');
7      /* do callout here */
8      f.addLog('Callout completed in, ' + DateTime.now().getTime() - start.getTime() + 'ms');
9   }
10}

Example

Enqueue different kinds of jobs depending on whether the current job succeeded or failed.

1public class PathControlFinalizer implements Finalizer {
2   private List<LogMessage__c> logRecords = new List<LogMessage__c>();
3
4   public void execute(FinalizerContext ctx){
5      Database.insert(logRecords, false);
6        Id parentQueueableJobId = ctx.getAsyncApexJobId();
7        System.Debug('Executing Finalizer that was attached to Queueable Job ID: ' + parentQueueableJobId);
8        if (ctx.getAsyncApexJobResult() == FinalizerParentJobResult.SUCCESS) {
9            // Queueable executed successfully
10            System.Debug('Parent Queueable (Job ID: ' + parentQueueableJobId + '): completed successfully!');
11            // Upon successful completion of parent/previous job, enqueue type B job
12            System.enqueueJob(new QueueableTypeB()); 
13        } else {
14            // Queueable failed
15            // Log some additional information.
16            System.Debug('Parent Queueable (Job ID: ' + parentQueueableJobId + '): FAILED!');
17            System.Debug('Parent Queueable Exception: ' + ctx.getAsyncApexJobException().getMessage());
18            // Due to failure in parent/previous job, enqueue type C job
19            System.enqueueJob(new QueueableTypeC());
20        }
21   }
22
23   public void addLog(String message){
24      logRecords.add(new LogMessage__c(
25                           Message__c = message,
26                           Request__c = FinalizerContext.getRequestId(),
27                           Source__c = 'LoggingFinalizer'
28         ));
29   }
30}
1public class ParentQueueableJobA implements Queueuable, Database.AllowsCallouts {
2   public void execute(QueueuableContext ctx) {
3      PathControlFinalizer f = new PathControlFinalizer();
4      System.attachFinalizer(f);
5      DateTime start = DateTime.now();
6      f.addLog('About to callout to external system...');
7      /* do callout here */
8      f.addLog('Callout completed in, ' + DateTime.now().getTime() - start.getTime() + 'ms');
9   }
10}

Example

Identify how far a queueable job proceeds before a catchable error occurs. This example also obtains details of the error.

1public class AccountUpdateLoggingFinalizer implements Finalizer {
2
3    // Used to maintain progress
4    List<String> acctNames;
5    
6    public AccountUpdateLoggingFinalizer() {
7        acctNames = new List<String>();
8    }
9    
10    public void execute(FinalizerContext ctx) {        
11        Id parentQueueableJobId = ctx.getAsyncApexJobId();
12        System.Debug('Executing Finalizer that was attached to Queueable Job ID: ' + parentQueueableJobId);
13        if (ctx.getAsyncApexJobResult() == FinalizerParentJobResult.SUCCESS) {
14            // Queueable executed successfully
15            System.Debug('Parent Queueable (Job ID: ' + parentQueueableJobId + '): completed successfully!');
16        } else {
17            // Queueable failed
18            // Log some additional information.
19            System.Debug('Parent Queueable (Job ID: ' + parentQueueableJobId + '): FAILED!');
20            System.Debug('Parent Queueable Exception: ' + ctx.getAsyncApexJobException().getMessage());
21
22            // Show the accounts that were processed before Queueable Job encountered the exception            
23            System.Debug('Parent Queueable processed following accounts:');
24            for (String acctName : acctNames) {
25                System.Debug(acctName);
26            }
27        }        
28    }
29    
30    public void reportProgress(Account acct) {
31        acctNames.add(acct.Name);
32    }
33}
1public class AccountUpdateQueueable implements Queueable {
2
3    public void execute(QueueableContext ctx) {
4    
5        // Create a transaction finalizer
6        AccountUpdateLoggingFinalizer finalizer = new AccountUpdateLoggingFinalizer();
7
8        // Attach the transaction finalizer to this queueable
9        System.attachFinalizer(finalizer);
10 
11        // Do some (partial) work
12        Account acct = new Account();
13        acct.Name = '1st Account';
14        insert(acct);
15        
16        // Send some status update to the finalizer
17        finalizer.reportProgress(acct);
18        
19        // do some work & conditionally throw a catchable/user-defined exception
20        boolean status = doSomeWork();
21        if (!status) {
22           throw new TestException('Unhandled test exception');
23        }
24        
25        // Attempt to do some more work
26        Account acct2 = new Account();
27        acct2.Name = '2nd Account';
28        insert(acct2);
29        
30        // Report more progress
31        finalizer.reportProgress(acct2);
32    }
33    
34    private class TestException extends Exception { }
35}