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
https://help.salesforce.com/articleView?id=000232321&type=1&mode=1
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);
}
}