Skip to content

Getting Started with Retryable Actions

Johannes Fischer edited this page Dec 31, 2022 · 2 revisions

What are Retryable Actions

Retryable Actions is a concept of building asynchronous, custom actions that have a chance of failing in their execution, but are crucial to the operations of the org and should therefore be re-executed. A common example for such a use case is a web service based integration where a callout is made from Salesforce to a 3rd party system, i.e. Order Management System.

Best practices for retries are that they should be time delayed with an increase interval between attempts. This is really hard to do on the Salesforce platform because of the transactional nature of requests.

How to Use Retryable Actions

Retryable Actions have multiple stages (transactions) during their execution:

  1. Identification of an Action event
  2. Action Execution
  3. Action Re-Execution (up to 8 times if required)

Each stage is consists of the following:

  1. An event occurs that triggers a Retryable Action. This could be the update to a record, i.e. the Status of an Order to go from "Pending" to "Approved". The handler for the event will create a Retryable_Action__e platform event record and submit it. The record will contain an Action name, Record ID and optionally some additional information. I have used the Retryable Action Platform Event to include an entire set of records serialized as JSON, so that than Update on those records can be performed asynchronously.

  2. When the Retryable_Action__e is fired, a trigger is invoked that will look at the Retryable Action Configuration Custom Metadata object for registered Handlers. Each handler is an Apex class that implements the rflib_RetryableActionHandler interface. Each registered handler for a given action will be invoked with the batch of records for the given event.

  3. Should there be an error with any record in the batch, the framework with catch the exception, log it, and then throw an EventBus.RetryableException that will terminate the entire batch and mark it for a retry. This process can repeat up to 8 times after which the batch is considered failed. RFLIB will start logging WARN messages at the 6th attempt, so that the operations team can be notified.

Consideration and Best Practices

Consider the following when designing your Retryable Actions and handlers:

  • Salesforce expects the entire batch of Platform Events to fail and be retried, not just a single record. This means that operations for one event record can succeed before failing for the second record. However, in this situation, both records would be sent back for a retry at a later point. This means that all processes attached to a Retryable Action MUST be idempotent. This is especially important for web services, where the idempotency should be defined in the contract of the endpoint.

  • It is also recommended that each Action should only have a single handler. If more activities are needed in response to an event, it is recommended to consider the creation of multiple actions and handlers, so that the functionality is as much decoupled as possible.

  • Consider using the Platform Event Subscriber Configuration to set a maximum batch size for Retryable Actions, or to assign a specific Salesforce user account for the execution. The user account can be important if there is business logic in your org looking for specific Permission Sets and/or Custom Permissions to determine its behaviour. For example, if the action updates a record and a Validation Rule should should not be fired, a check for a Custom Permission could be added. In this case, a non-system user account (aka Service Account) is needed that can have the needed permission assigned.

Samples

Here are some samples for the use of the framework. You can find all of these in the RFLIB Demo Project..

Sample Configuration

<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <label>rflib_ImportDataActionHandler</label>
    <protected>false</protected>
    <values>
        <field>Action_Name__c</field>
        <value xsi:type="xsd:string">AsyncDataImport</value>
    </values>
    <values>
        <field>Active__c</field>
        <value xsi:type="xsd:boolean">true</value>
    </values>
    <values>
        <field>Class_Name__c</field>
        <value xsi:type="xsd:string">rflib_ImportDataActionHandler</value>
    </values>
    <values>
        <field>Order__c</field>
        <value xsi:type="xsd:double">1.0</value>
    </values>
</CustomMetadata>

Sample Handler

public class rflib_ImportDataActionHandler implements rflib_RetryableActionHandler {

    private static final rflib_Logger LOGGER = rflib_LoggerUtil.getFactory().createLogger('rflib_ImportDataActionHandler');

    public void execute(List<rflib_Retryable_Action__e> actions) {
        LOGGER.info('Executing import action.');
        try {
            LOGGER.info('importSampleData()');

            LOGGER.info('Deleting Bot_Command__c records');
            delete [SELECT Id from Bot_Command__c];

            LOGGER.info('Deleting Property_Favorite__c records');
            delete [SELECT Id from Property_Favorite__c];
            
            LOGGER.info('Deleting Property__c records');
            delete [SELECT Id from Property__c];
            
            LOGGER.info('Deleting Broker__c records');
            delete [SELECT Id from Broker__c];
            
            LOGGER.info('Importing Bot commands');
            StaticResource botCommandsResource = [SELECT Id, Body from StaticResource WHERE Name = 'botCommands'];
            String botCommandsJSON = botCommandsResource.body.toString();
            List<Bot_Command__c> botCommands = (List<Bot_Command__c>) JSON.deserialize(botCommandsJSON, List<Bot_Command__c>.class);
            insert botCommands;
            
            LOGGER.info('Importing brokers');
            StaticResource brokersResource = [SELECT Id, Body from StaticResource WHERE Name = 'brokers'];
            String brokersJSON = brokersResource.body.toString();
            List<Broker__c> brokers = (List<Broker__c>) JSON.deserialize(brokersJSON, List<Broker__c>.class);
            insert brokers;
            
            LOGGER.info('Importing properties');
            StaticResource propertiesResource = [SELECT Id, Body from StaticResource WHERE Name = 'properties'];
            String propertiesJSON = propertiesResource.body.toString();
            List<Property__c> properties = (List<Property__c>) JSON.deserialize(propertiesJSON, List<Property__c>.class);
            insert properties;
        } catch (Exception ex) {
            LOGGER.fatal('Failed to import sample data: ', ex);
            throw ex;
        }        
    }
}
Clone this wiki locally