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
KruviKruvi 

Callout limits Apex and batch

Hello

 

I'm looking for advice on how to approach this case I have:

 

I need to process thousands of objects when a user clicks on a button and for each object I have to do one HTTP callout.

 

I know there is a limit of 10 callouts per request and 1 callout per batch execution.

Also there is a Maximum timeout for all callouts request.

 

I thought of two options:

 

  1. split my objects to groups of 10 using javascript and call my apex class for each group. It this case, in the extreme case there might be hundreds of such groups.
  2. use batch apex (I haven't done it before but I know there such an option) when each batch iteration will process 1 object. In this case thousands of iterations will be needed.

I'm not sure what is the best solution for my case and which will not hit the Limits.

 

Thanks

 

Kruvi

 

Best Answer chosen by Admin (Salesforce Developers) 
Shashikant SharmaShashikant Sharma


Can the batch be triggered from the button on my page?

Yes, add a method in controller class like and use that asa ction of your button

 

public void runBatchJOB()
{
BatchClassName batchCls = new BatchClassName();
// 1 is size of batch
database.executebatch(batchCls , 1); 
}

Yes Thats correect that only one callOut is allowed in one batch execution. SO your batch size will be 1  

 

Is there any way I can estimate how much time will the batch operation will take based on the number of records?

No batch calls are future calls and they get executed when the resource are availbale. Mostly it should not take more than 5 minutes.

All Answers

Shashikant SharmaShashikant Sharma

You can use batch and just set batch size to 10 so it will get 10 items in a chunk and use full 10 call out limit in one time then next chunk will come. My suggestion would be a batch class.

OdedHarnivOdedHarniv
Thanks mate

I thought the batch has a limit of 1 callout per iteration.
Am I wrong?

Thanks
KruviKruvi

Thanks

 

I have two questions:

 

Is there any way I can estimate how much time will the batch operation will take based on the number of records?

Can the batch be triggered from the button on my page?

 

Many thanks

 

 

Shashikant SharmaShashikant Sharma


Can the batch be triggered from the button on my page?

Yes, add a method in controller class like and use that asa ction of your button

 

public void runBatchJOB()
{
BatchClassName batchCls = new BatchClassName();
// 1 is size of batch
database.executebatch(batchCls , 1); 
}

Yes Thats correect that only one callOut is allowed in one batch execution. SO your batch size will be 1  

 

Is there any way I can estimate how much time will the batch operation will take based on the number of records?

No batch calls are future calls and they get executed when the resource are availbale. Mostly it should not take more than 5 minutes.

This was selected as the best answer
KruviKruvi

Hi

 

I'm running into trouble here, maybe someone can help.

 

Since in my case I process 1 record at a time and as a result I want to insert 1 record.

I want to collect all the records that need to be inserted to the database and insert them in one operation when the batch finish method is executed.

 

But I see that the List that should collect all the records to insert is not persistent between the calles to execute() .

This is my code:

 

global public with sharing class MicroCampaignBatchPublish implements Database.Batchable<sObject>{

	private String campaignId;
	private String templateId; 
	private String campaignNum;
	private String URL;
	
	private Blob data;		
	private String b64Data;
	private String encodedId;
	
	private SMS__c sms;
	private String SMSBody;
	private	String myGreeting = '';
	
	private SMSSender.Sender sender;

	global String query { get; set; }
	
	global final List<SMS__c> sentSMSs = new List<SMS__c>();
		 
	public MicroCampaignBatchPublish(String gatewayName, String fromNumber, String greeting, String SMSText,
									 String campaignId, String campaignNum, String templateId){
									 	
		sender = new SMSSender.Sender(gatewayName, fromNumber);
		
		if(greeting != null)
			myGreeting = greeting;
		
		SMSBody = SMSText;
		this.campaignId = campaignId;
		this.campaignNum = campaignNum;
		this.templateId = templateId;
		this.URL = MicroCampaignAPI.SiteUrl;	
	} 
	
	global Database.QueryLocator start(Database.BatchableContext BC){
    	return Database.getQueryLocator(query);
   	}
   	
    global void execute(Database.BatchableContext BC, List<Micro_Campaign_Member__c> scope){
		
		if(scope == null)
			system.debug(Logginglevel.DEBUG, 'scope is null');			   				
			
		for(Micro_Campaign_Member__c member :scope){
			if(member.Contact__c == null && 
			   member.Contact__r.MobilePhone == null &&
			   member.Contact__r.FirstName == null){
			   	
		   		system.debug(Logginglevel.DEBUG, 'One of the Contact field in member is null: ' + member);			   				
				continue;
			}		
	
	        // Generate the data to be encrypted.
	        data = Blob.valueOf(member.Member_Num__c + campaignNum);
	        // Generate an encrypted form of the data using base64 encoding
	        b64Data = EncodingUtil.base64Encode(data);			
			encodedId = EncodingUtil.urlEncode(b64Data, 'UTF-8');
/*		
	   		system.debug(Logginglevel.DEBUG, 'encodedMember: ' + member.Member_Num__c + campaignNum);			   	
	   		system.debug(Logginglevel.DEBUG, 'encodedMember: ' + encodedId);			
*/		
			SMSBody = SMSBody + ' ' + URL + '?cid=' + encodedId;		
			SMSBody = myGreeting + ' ' + member.Contact__r.FirstName +', ' + SMSBody;
/*	
	   		system.debug(Logginglevel.DEBUG, 'SMSBody: ' + SMSBody);			   	
*/				
			sms = sender.sendASingleSMS(member.Contact__r.MobilePhone, member.Contact__r.FirstName, SMSBody);
				
			if(sms != null){
				sms.Micro_Campaign__c = campaignId;
				sms.Campaign_Member__c = member.Id;
				sms.To_Contact__c = member.Contact__c;
				sms.SMS_Template__c = templateId;
				sentSMSs.add(sms); 
			}
			else
		   		system.debug(Logginglevel.DEBUG, 'SMS is null from sender');			   	
			
	   		system.debug(Logginglevel.DEBUG, sentSMSs.size() + ' SMSs to insert');			   	
			
			if(sentSMSs.size() > 249){
		   		system.debug(Logginglevel.DEBUG, 'Inserting SMS: ' + sentSMSs.size());			   	
				MicroCampaignAPI.insertSMSs(sentSMSs);
				sentSMSs.clear();			
			}				
		}			
    }

    global void finish(Database.BatchableContext BC){
		
		if(sentSMSs.size() > 0){
	   		system.debug(Logginglevel.DEBUG, 'Inserting SMS: ' + sentSMSs.size());			   	
			MicroCampaignAPI.insertSMSs(sentSMSs);	
		}
	   // Get the ID of the AsyncApexJob representing this batch job  	    
	   // from Database.BatchableContext.  	    
	   // Query the AsyncApexJob object to retrieve the current job's information. 	    
	   AsyncApexJob a = [Select Id, Status, NumberOfErrors, JobItemsProcessed,
	      				 TotalJobItems, CreatedBy.Email
	      				 from AsyncApexJob 
	      				 where Id =: BC.getJobId()];
	      
	   // Send an email to the Apex job's submitter notifying of job completion. 	    
	   Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
	   String[] toAddresses = new String[] {a.CreatedBy.Email};
	   mail.setToAddresses(toAddresses);
	   mail.setSubject('Publish Campaign process ' + a.Status);
	   mail.setPlainTextBody ('The batch job Publish Campaign processed ' + a.TotalJobItems +
	   						  ' SMSs with '+ a.NumberOfErrors + ' failures.');
	   						  
	   Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });		   	
    }
}

 

 

 

What is the right way to achieve this?

 

Many thanks

chubsubchubsub

Shashikant, I applied your suggestion to my Batch Apex controller and it worked well, but when I went to test it, these were the only lines I could not cover.  How can these lines be tested?  I've tried to move it into it's own class so I could test it idependetly from the batch class.  Is this the correct route?  I'm only getting 75% coverage still, below is some of my code:

 

Batch Class:

 


public String contactQuery = 'Select Id from Contact';

global Database.QueryLocator start(Database.BatchableContext ctx) {
//Query the one contact we're working with
// Set the queryLocator to locate single contact
return Database.getQueryLocator(contactQuery);
}
global void execute(Database.BatchableContext ctx, List<sOBject> scope) {

//custom logic......

global void finish(Database.BatchableContext ctx){ 

//custom logic...

}

 

 

TestBatchApex Class: - This covers the code above 100%, but withOUT the call method you offered

 

Test.startTest();
String squery = 'Select Id, LastName from Contact where Id IN (\'' + c1.Id + '\',\'' + c2.Id + '\') ';
system.debug('jeremy squery' + squery);

List<Contact> testd = database.query(squery);
system.debug('jeremy testd' + testd);

batchReferrals batch = new batchReferrals();
batch.contactquery = squery;
ID jobId = Database.executeBatch(batch, 2);

Test.stopTest();

 

 

runBatch Class:  This is the class I included in the call method where I am trying to test independently from the BatchClass:


//class to run batch from VS page


public static void runBatchJOB()
{
batchReferrals batchCls = new batchReferrals();
// 2 is size of batch
if (!test.isRunningTest())
database.executebatch(batchCls , 2); This is the only line I am unable to get any coverage on, the rest is covered.
}

static testmethod void testrunBatchJob()
{

// do stuff to cover custom logic......

String squery = 'Select Id, LastName from Contact where Id IN (\'' + c1.Id + '\',\'' + c2.Id + '\') ';
List<Contact> testd = database.query(squery);
runBatchReferrals rbr = new runBatchReferrals();
runBatchReferrals.runBatchJob();
batchReferrals batchCls = new batchReferrals();
ID jobId = Database.executeBatch(batchCls, 2);

}



 

Starz26Starz26

remove the !test.isrunningtest() if statement

 

Then in the test class for the controller, call runBatchJob().

 

That will then cover the line.

 

 

Starz26Starz26

Is there a specific reason why you do not wish to insert the records 1 at a time?

 

You can use database.stateful to maintain the list through the execute method but that will not carry over to the finally method. It may carry over if you use the static keyword for the list but have not tried it.....

chubsubchubsub

Thanks Starz, your suggestion on removing that line if (!test.isRunningTest()) worked, I thought I needed that in there so it would not blow governer limits.  

 

My batch size is 2 as I only wanted to process 2 contacts at a time, just to ensure I won't hit any limits when running that batch.  I am using the Database.Stateful and it seems to working, althought, I will be doing the QA here soon to really ensure it's working as it should be. 

 

Thanks again for your help.