2

First question on here, so please forgive me any errors in how I've posed my question.

A little context

The instance I work on has a lot of integrations with external systems. We perform 100.000's of callouts a day, so some of them will inevitably fail. I have a retry mechanism in place, but I've been working on an 'improved' version which is set up to work fully dynamically. Not quite there yet, but I'm getting close. I'll share the code on github once it's done. The mechanism works by checking the source method and object failing, then generating the code to run a retry and posting it to the tooling API to run as Anonymous Apex.

The Problem

The problem I'm having is that when I retry code which performs a callout it will return the error 'Callout loop not allowed', because I'm calling my own salesforce Instance from the Apex code. Using future callouts doesn't fix the issue and I've been unable to find away around this.

If anyone has any ideas I would be very grateful for your help!

Details

This is the actual code that performs the callout, perhaps I'm missing something:

//Perform the actual callout to the tooling API
HttpRequest request = new HttpRequest();
String instanceUrl = URL.getOrgDomainUrl().toExternalForm();
request.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionID());
request.setHeader('Content-Type', 'application/json');
request.setMethod('GET');
request.setEndpoint(instanceUrl + '/services/data/v46.0/tooling/executeAnonymous/?anonymousBody=' + EncodingUtil.urlEncode(totalApiBody, 'UTF-8'));
Http http = new Http();
HttpResponse response = http.send(request);

Some of the resources I've checked:

Callout loop not allowed error

Callout Loop Not Allowed

https://help.salesforce.com/articleView?id=000232321&type=1&mode=1

https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/intro_rest_resource_examples.htm

TotalAPIBody example

List%3CObject%3E+parameters+%3D+new+List%3CObject%3E%28%29%3BMap%3CString%2C+Map%3CGGRetryMechanism.RetryObject%2C+Integer%3E%3E+idRetryObjectMap+%3D+%28Map%3CString%2C+Map%3CGGRetryMechanism.RetryObject%2C+Integer%3E%3E%29Cache.Org.get%28%27local.RetryCache.idRetryObjectMap%27%29%3Bfor+%28RetryMechanism.RetryObject+RetryObject+%3A+idRetryObjectMap.get%28%27aefc05e2-e05a-49d4-913c-53d3ef539c0d%27%29.keySet%28%29%29%7Bfor+%28GGRetryMechanism.retryParameter+retryParameter+%3A+retryObject.retryParameters%29+%7Bparameters.add%28retryParameter.retryObject%29%3B%7D%7DRefundFormApexController.searchOrder%28%28String%29parameters%5B0%5D%29%3B

So more background information for clarity

The reason I want to create a fully dynamic retry mechanism is that with my current system I need to add every method I want to be able to retry to the class below. What I am trying to create now is a mechanism that will automatically detects and saves the failed class, method and input parameters as objects and tries to run the code at a later moment. In order to do that I need to be able to generate code dynamically, something that (I believe) is currently not possible within Apex. So I've created a class which generates the code dynamically as a String which is then passed to the tooling API to run in Anonymous Apex.

This is the system I'm looking to improve on:

Global class SCCQueueableClassForRetry implements Queueable, Database.AllowsCallouts {

    private String retrySource {get; set;}
    private Object retryObject {get; set;}

    public SCCQueueableClassForRetry(String retrySource, Object retryObject) {
        this.retrySource = retrySource;
        this.retryObject = retryObject;
    }

    public void execute(QueueableContext context) {
        //Syntax is class+method without dots, because the syntax doesn't work otherwise
        //Additional methods can be added, be sure to declare the type of object you want to pass
        Switch on retrySource {
            when 'SCCAccountImplGGBatchPatchCustProfile' {
                SCCAccountImpl.GGBatchPatchCustProfile((Map<String, Map<String, Object> >)retryObject);
            }
            when 'SCCAccountImplGGbatchSyncAccount' {
                SCCAccountImpl.batchSyncAccount((List<String>)retryObject);
            }
            when 'SCCAccountImplGGBatchPatchCustProfileCallOut' {
                SCCAccountImpl.GGBatchPatchCustProfileCallOut((String)retryObject, new List<Log__c>());
            }
            when 'SCCAccountImplGGBatchPatchCustAddressesCallOut' {
                SCCAccountImpl.GGBatchPatchCustAddressesCallOut((String)retryObject, new List<Log__c>());
            }
            when else {
                Util.writeLog(
                    new Log__c(
                        Reference_Type__c = 'Object',
                        Automation__c     = 'Apex',
                        Log_Level__c      = 'Error',
                        Source__c         = 'SCCQueueableClassForRetry',
                        Source_detail__c  = 'execute',
                        Message__c        = 'Method is not defined for retry',
                        Stack_Trace__c    = retrySource
                        )
                    );
            }
        }
    }
}

Here is the full class for my new system:

/*******************************************************************************************
 * Name            : GGRetryMechanismQueueable
 * Created By      : RickForce
 * Created Date    : 03/10/2019
 * Description     : Queueable to perform callouts with objects that future does not support
 * Test Class      : GGRetryMechanismQueueableTest
 ********************************************************************************************/

public class GGRetryMechanismQueueable implements Queueable, Database.AllowsCallouts {

    private String retryObjectId;

    public GGRetryMechanismQueueable(String retryObjectId) {
        this.retryObjectId = retryObjectId;
    }

    public void execute(QueueableContext context) {
        Map<String, Map<GGRetryMechanism.RetryObject, Integer>> idRetryObjectMap = new Map<String, Map<GGRetryMechanism.RetryObject, Integer>>();
        Map<GGRetryMechanism.RetryObject, Integer> retryObjectCounterMap = new Map<GGRetryMechanism.RetryObject, Integer>();
        List<String> parameterTypes = new List<String>();
        String sourceName = '';
        String methodParameters = '';

        //Prepare the body that will be used in the callout
        idRetryObjectMap = (Map<String, Map<GGRetryMechanism.RetryObject, Integer>>)Cache.Org.get('local.GallCache.idRetryObjectMap');
        for (GGRetryMechanism.RetryObject retryObject : idRetryObjectMap.get(retryObjectId).keySet()) {
            sourceName = retryObject.retrySource;
            for (GGRetryMechanism.retryParameter retryParameter : retryObject.retryParameters) {
                parameterTypes.add(retryParameter.type);
            }
        }

        String initList = 'List<Object> parameters = new List<Object>();';
        String retrieveFromCache = 'Map<String, Map<GGRetryMechanism.RetryObject, Integer>> idRetryObjectMap = (Map<String, Map<GGRetryMechanism.RetryObject, Integer>>)Cache.Org.get(\'local.GallCache.idRetryObjectMap\');';
        String getObject = 'for (GGRetryMechanism.RetryObject RetryObject : idRetryObjectMap.get(\'' + retryObjectId + '\').keySet()){';
        String fillList = 'for (GGRetryMechanism.retryParameter retryParameter : retryObject.retryParameters) {parameters.add(retryParameter.retryObject);}}';

        for (Integer i = 0; i < parameterTypes.size(); i++) {
            if (i <= (parameterTypes.size() - 2)) {
                methodParameters += '(' + parameterTypes[i]  + ')parameters[' + i + '], ';
            }
            else{
                methodParameters += '(' + parameterTypes[i]  + ')parameters[' + i + ']';
            }
        }
        String methodCall = sourceName + '(' + methodParameters + ');';
        String totalApiBody = initList + retrieveFromCache + getObject + fillList + methodCall;

        //Perform the actual callout to the tooling API
        HttpRequest request = new HttpRequest();
        String instanceUrl = URL.getOrgDomainUrl().toExternalForm();
        request.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionID());
        request.setHeader('Content-Type', 'application/json');
        request.setMethod('GET');
        request.setEndpoint(instanceUrl + '/services/data/v46.0/tooling/executeAnonymous/?anonymousBody=' + EncodingUtil.urlEncode(totalApiBody, 'UTF-8'));
        Http http = new Http();
        HttpResponse response = http.send(request);
    }
}
  • can u paste totalApiBody as well – Ayub Oct 5 '19 at 13:09
  • Just added an example! Hopes this helps – RickForce Oct 5 '19 at 16:31
2

The issue isn't so much that you're trying to invoke a callout to the same org you're already in. Rather, the issue is that your retry mechanism is making a callout to a Salesforce org (any Salesforce org) and then the transaction started by that callout is trying to make another callout (to any Salesforce org) from there.

To work around this issue in the past, I've made use of classes that implement the Queueable interface.

The general flow of the system I'm working with is:

  • 1st party, external (external to Salesforce, that is) system
  • to developer sandbox (ApexRest endpoint)

    • +callout to 1st party system

    • to full copy sandbox (ApexRest endpoint)

      • +callout to 1st party system.

The callout in the developer sandbox isn't an issue (because the apex code started to run because of an external system), but rather the callout in the full copy sandbox. This is because the transaction in my full copy sandbox was initiated by another Salesforce org.

I'm not quite sure how @future is failing you here, but implementing both Queueable and Database.allowsCallouts is enough to break the link for me and allow my code to run.

| improve this answer | |
  • Hi Derek, thanks for thinking with me here. Although I believe you're technically right about the 'callout loop not allowed' error, it doesn't seem to be the case here. Let me draw out out the flow: 1. Retry Mechanism runs in Apex, calls Tooling API => 2. Tooling API start the code that has to be retried. => 3. Retried code performs callout and loop error is raised. Non Salesforce systems will also return the callout loop error. I could implement queueable, but my current system already works that way. So then I've lost ;) Thanks again for helping out! – RickForce Oct 6 '19 at 8:11
  • @RickForce Are you sure you're using queueable? In your question, you said "future callouts". @future and queueable are different things. – Derek F Oct 6 '19 at 11:10
  • Yes I'm sure. Sorry for the confusion, what I meant to say is: My current retry mechanism is based on a queueable interface. The new one I'm working on and to which this question relates does not use one. It directly tries to rerun the failed method by calling the tooling API. This causes a callout loop error, even if the called method is '@future' – RickForce Oct 6 '19 at 11:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.