function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
HL-DevHL-Dev 

System.AsyncException when using @future annotation in bulk triggers

I'm working on an Apex trigger and class that will geocode addresses on the contact object once a new contact record is created with an address (After Insert) and after a contact record's address has changed (After Update). I'm making and HTTP request to google to geocode the coordinates as in:

- send an address to Google

- if I get a response back from Google, check the status

- if successful, then parse through the response and extract the coordinates and write them to the contact record, so they can later be referenced by my Google Maps application.

 

Here's my problem: originally, I wrote the trigger and class so that it only sends one address per trigger execution. This code works as expected. Then I modified my code to send addresses in batches and now I'm getting the following error message:

 

System.AsyncException: Future method cannot be called from a future method: my method name goes here()

 

 

Here's what my code does:

Trigger

- when a new contact record with an address or when a existing contact record's address is updated, then add its id to an array

- call the apex class to geocode by passing this mentioned array of contact ids

 

Class

- for each contact that was passed by the trigger, query the address and pass it to Google for geocoding

- read the response back from Google and if it was successful, then parse through the response, extract the coordinates and write to the contact record (update call)

 

Why would I get the following error message? Are bulk triggers not allow when using the @future annotation or is there something wrong with my logic?

 

Thanks in advance.

 

 

DManelskiDManelski

Just a stab in the dark... I was getting a similar error because I had other code running that was trying to update the address on a record more than once in the same transaction (lead conversion was trying to update address on account, tripping my account geocoding trigger more than once in the same trasnaction).  See if there's any code you have that would update an address and then update it again in the same transaction.

 

If that doesn't help, let me know and I'll be glad to pass along (what I know to be) a working contact geocoding trigger and class.

TehNrdTehNrd

Yup, appears to be a infinite loop type of problem. Update trigger on Contact loads the async job, async job updates the Contact, update trigger on Contact loads the async job, repeat forever.

I'm not sure if this will work with async classes but it should. What you need to do is create a flowControl variable in a seperate class that will control if the contact trigger should execute.

Flowcontrol Class
public class flowControl {

//---------------------------Contact Trigger Control--------------------------
public static boolean runContactTrigger = true;

//Return the run trigger boolean
public static boolean runContactTrigger(){
return runContactTrigger;
}

//Set the run Trigger boolean
public static void setRunContact(boolean value){
if(value == true){
runContactTrigger = true;
}else{
runContactTrigger = false;
}
}
}

Trigger:
trigger geoCode on Contact(after update) {
if(flowControl.runContactTrigger() == true){
//do the trigger logic
}
}

Async class:
In your async class before you execute the DML statement on the contact set the run trigger boolean to false
flowControl.setRunContactTrigger(false);

 I think this should work as it works for recursive triggers but I have never tried it with an async job.

 

Message Edited by TehNrd on 01-23-2009 11:01 AM
HL-DevHL-Dev

DManelski and TehNrd, 

Both of you were correct. My trigger was executing on both after insertions and updates of contact records which was causing the exception. What I still don't understand is the reason my code was working for one record. If I understand this correctly, the issue is that the asynchronous method is executing twice (due to the after insert and after update trigger) when a record is being created, so I was expecting my code not to work even when creating a single record.

 

TehNrd,

I'll look into your suggestion into separating the trigger logic, so my code can handle for after insert and after update.

 

Have another question: for now, I've changed my trigger to only execute after the insertion of new records. My code now doesn't raise any exceptions but the contact records aren't being updated with the coordinates from Google. As far as the insertion method, I'm inserting 10 contact records via the data loader. I tried to write a test method to start troubleshooting, but I'm getting a message that test methods are not available on call outs. What's the best way to debug this issue?

 

SuperfellSuperfell
when you get the response from google, you update the contact, this re-fires the trigger, which tries to re-enqueue another @future to geocode it. You cannot enqueue more futures from a future to stop these kinds of infinite loops occurring.
HL-DevHL-Dev

Yes, I understand why I was getting the exception earlier, but why would it work when inserting one record? Since previously, my trigger was executing on both after inserts and after updates, I was expecting my code not to allow me to insert even a single record. But that wasn't the case. What I understood is that after the insertion of the contact record the future method is called and also after the update, which isn't allowed (calling future method from another). Please let me know if I'm missing something.

 

But like I mentioned in my previous post, I changed the trigger so that it only executes after inserts and it no longer gives me the callout exception. Again, for a single record (i.e. creating a new contact record with address from the UI), the code works as expected and the coordinates are updated on the contact object. But when I try to insert 10 contact records using the data loader, my trigger doesn't seem to be executing at all. I'm checking for the Apex jobs queue and my logs and none of my debug statements are showing. However, I do see the execution of my trigger when I insert one record via the UI. Any ideas what could be the problem or how I could debug this issue?

 

Again, thanks for your help.

SuperfellSuperfell
post your code.
HL-DevHL-Dev

Class:

global class ApexBatchGeoCoder { @future (callout=true) public static void getGeoCoordinates(String[] myContactArray) { Contact[] myContact = [SELECT Id, Primary_Physical_Address__c, Primary_Physical_City__c, Primary_Physical_State__c, Primary_Physical_Zip__c, LatitudeR__c, LongitudeR__c FROM Contact WHERE Id IN :myContactArray]; for (Integer i=0; i<myContact.size(); i++) { System.debug('$$$ Contents for trigger: ' + myContact[i].Primary_Physical_Address__c + ', ' + myContact[i].Primary_Physical_City__c + ', ' + myContact[i].Primary_Physical_State__c + ', ' + myContact[i].Primary_Physical_Zip__c); } Integer count = 0; System.debug('### Geocoding this # of contacts: ' + myContact.size() + ' ###'); for (Contact currContact : myContact) { if (currContact.Primary_Physical_Address__c == null || currContact.Primary_Physical_Address__c == '') { currContact.Primary_Physical_Address__c = ','; } if (currContact.Primary_Physical_City__c == null || currContact.Primary_Physical_City__c == '') { currContact.Primary_Physical_City__c = ','; } if (currContact.Primary_Physical_State__c == null || currContact.Primary_Physical_State__c == '') { currContact.Primary_Physical_State__c = ','; } if (currContact.Primary_Physical_Zip__c == null || currContact.Primary_Physical_Zip__c == '') { currContact.Primary_Physical_Zip__c = ','; } String myAddy = currContact.Primary_Physical_Address__c + ',' + currContact.Primary_Physical_City__c + ',' + currContact.Primary_Physical_State__c + ',' + currContact.Primary_Physical_Zip__c; System.debug('### Geocoding address ' + count + ': ' + myAddy + ' ###'); XmlStreamReader myReader = myXSReader(myAddy); //ApexBatchGeocoder myBatchAsyncGeo = new ApexBatchGeocoder(); // Process the response to extract the coordinates String[] myCoordinates = parseHttpResponse(myReader); currContact.LongitudeR__c = Double.valueOf(myCoordinates[0]); currContact.LatitudeR__c = Double.valueOf(myCoordinates[1]); } update myContact; System.debug('### Contacts geocoded ###'); } public static XmlStreamReader myXSReader(String myAddy) { System.debug('### Sent GET request ###'); HttpRequest req = new HttpRequest(); myAddy = EncodingUtil.urlEncode(myAddy, 'UTF-8'); req.setEndpoint('http://maps.google.com/maps/geo?q=' + myAddy + '&output=xml&key=my_api_key_goes_here'); req.setMethod('GET'); //send the request Http myHttp = new Http(); HttpResponse res = myHttp.send(req); XmlStreamReader reader = null; //check the response if (res.getStatusCode() == 200) { // Convert the response into an XmlStreamReader class, so it can be later processed reader = new XmlStreamReader(res.getBody()); System.debug('### Got 200 response from Google ###'); } else { reader = null; System.debug('Callout failed: ' + res); } return reader; }

 

Trigger:

trigger BatchGeocodeContact on Contact (after insert) { String[] contactIdArray = new String[trigger.new.size()]; // Call future method to update coordinates within contact record // with data from Google server. // This is a async calls, it returns right away, after // enqueuing the request. // For new records, make the call to Google to get coordinates if (trigger.isInsert) { for (Contact myContact : trigger.New) { if ((myContact.Primary_Physical_Address__c != null) && (myContact.Primary_Physical_Zip__c != null)) { contactIdArray.add((String)myContact.Id); } } } if (contactIdArray.size() <= 10) { ApexBatchGeoCoder.getGeoCoordinates(contactIdArray); } }

 

cheenathcheenath

In your contact trigger you are calling the future method, which update the contact, which calls the contact trigger, which calls the future method again, making it a loop.

 

In the trigger you should check if the filed is set and if set not call the future method.

 

HTHs,

 

 

 

DManelskiDManelski

Yes, cheenath is right, if you're using the same criteria in your if statement in the trigger.isUpdate set -- i.e., if the Physical address is not null, that's going to create a loop in your update trigger.  

 

You could reference the trigger map to determine whether or not the address has changed, rather than just determining if it's null.  Here's some code from a contact geocoding trigger that may be of help:

 

 

trigger ONEN_Contact_GoogleGeocode on Contact (after insert, after update) {

 

string[] todo = new string[]{};

 

if (Trigger.isInsert) {

for ( Contact newc: Trigger.new) {

if ( newc.otherstreet != null &&

newc.otherCity != null &&

newc.otherstate != null ) {

todo.add((string)newc.id);

}

}

}

 

if (Trigger.isUpdate) {

for ( Contact newc: Trigger.new) {

Contact oldc = Trigger.oldMap.get(newc.id);

if ( newc.otherstreet != oldc.otherStreet ||

newc.otherCity != oldc.otherCity ||

newc.otherstate != oldc.otherstate ||

newc.otherpostalcode != oldc.otherpostalCode

) {

todo.add((string)newc.id);

}

}

}

 

// should limit to 10...

if ( todo.size() > 0 && todo.size() <= 10) {

ONEN_GoogleGeocode.geocodeContact( todo );

}

 

 

 

Message Edited by DManelski on 01-28-2009 11:38 AM