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
Ben MeyersBen Meyers 

Scheduled apex runs out of time

I have the following three classes that are used to schedule an apex job.  Yesterday I noticed the job had 13 batches complete successfully yet only 12 actions occurred insdie KeysBatch.  Is there a good way to debug what happened or a way to alert me when a batch fails to complete or reschedule? 
public class ScheduleHandler implements ScheduledDispatcher.IScheduleDispatched 
{
	private string query = 'SELECT Quote__c, Name FROM c2g__codaInvoice__c WHERE Small_Platform_Key__c = true';
    
	public void execute(SchedulableContext sc)
    {
        system.abortJob(sc.getTriggerID());	// Always abort the job on completion
        
        system.debug('Here execute');
        
        //Run key batch
        KeysBatch batch = new KeysBatch(query);
        Database.executeBatch(batch, 1);
    } 

    public static String GetSchedulerExpression(Datetime dt) {
        return ('' + dt.second() + ' ' + dt.minute() + ' ' + dt.hour() + ' ' + dt.day() + ' ' + dt.month() + ' ? ' + dt.year());
    }

	private static Boolean ScheduledInContext = false;

    public static void StartScheduler()
    {
    	if(ScheduledInContext) return;

		ScheduledInContext = true;
    	
        List<CronTrigger> jobs = [SELECT Id, CronJobDetail.Name, State, NextFireTime
                                  FROM CronTrigger where CronJobDetail.Name='Small Platform Schedule Job'];
    	if(jobs.size()>0 && jobs[0].state!='COMPLETED' && 
           jobs[0].state!='ERROR' && jobs[0].state!='DELETED') return;	// Already running

        Set<String> activejobstates = new Set<String>{'Queued','Processing','Preparing'};
		List<AsyncApexJob> apexjobs = [Select ID, ApexClass.Name from AsyncApexJob 
                                           where ApexClass.Name = 'KeysBatch' And Status in :activejobstates];
		if(apexjobs.size()>0) return;  // The batch is running
    	
		System.schedule('Small Platform Schedule Job', 
	          GetSchedulerExpression(DateTime.Now().addMinutes(10)), 
	          new ScheduledDispatcher());
    }

}

global class KeysBatch implements Database.Batchable<sObject>, Database.AllowsCallouts
{
    private string query = '';
    
    public KeysBatch(string query)
    {
        this.query = query;
    }
    
	global Database.QueryLocator start(Database.BatchableContext BC)
    {
        return Database.getQueryLocator(query);
    }
    
    global void execute(Database.BatchableContext BC, List<c2g__codaInvoice__c> invoices)
    {
       for (c2g__codaInvoice__c i : invoices)
        {
                try
                {
                  }  
                catch(Exception e)
                {
                    
                }
            }
         }
    }   
    
    global void finish(Database.BatchableContext BC)
    {
        if (!Test.isRunningTest())
            ScheduleHandler.StartScheduler();
    }
    
    
}
global class ScheduledDispatcher Implements Schedulable {
	public Interface IScheduleDispatched 
    { 
        void execute(SchedulableContext sc); 
    } 
    
    global void execute(SchedulableContext sc) 
    {
        Type targettype = Type.forName('ScheduleHandler');   
        if(targettype!=null) {
            system.debug('Inside Scheduler');
            IScheduleDispatched obj = (IScheduleDispatched)targettype.NewInstance();
            obj.execute(sc);   
        } 
    } 
}

 
Best Answer chosen by Ben Meyers
Neetu_BansalNeetu_Bansal
Hi Ben,

You can send the mail in the finish method to get the results, like how many records processed, total count, error count. For this you ned to implement Database.Stateful in your batch class. Below is a sample code:
/**
global with sharing class TestBatch implements Database.Batchable<sObject>, Database.Stateful
{
    // To store total number of records to be processed
    private Integer totalRecords = 0;
    //To hold count of error records
    private Integer errorRecords = 0;
    // To create table string
	String tableFormat = '';
    
    /**
     * Name: start
     * @param: BC - Database.BatchableContext
     * @return: Database.QueryLocator - Return the list of data
     * Desc: Batch Start method
    **/
    global Database.QueryLocator start( Database.BatchableContext BC )
    {
    	// Create query 
        String query = 'Select Id, Name from Account';
        
        return Database.getQueryLocator(query);
    }
    
    /**
     * Name: execute
     * @param: BC - Database.BatchableContext
     * @return:
     * Desc: Batch execution method, to process results
    **/
    global void execute( Database.BatchableContext BC, List<Account> accounts )
    {
        List<Contact> contacts = new List<Contact>();
        
        for( Account acc : accounts )
        {
            Contact con = new Contact( LastName = 'Test', AccountId = acc.Id );
            
            contacts.add( con );
            
            // Increase the count of total records
            totalRecords++;
        }
        
        if( contacts.size() > 0 )
        {
        	// To store success or failure results of contact insertion
        	List<Database.Saveresult> results = Database.insert( contacts, false );
        	
        	// Iterate over all the results
        	for( integer i = 0; i < results.size(); i++ )
        	{
        		// Check if insertion is successful or not
        		if( !results[i].isSuccess() )
        		{
        			// Increase the count of error records
        			errorRecords++;
        			
        			// To create table row
        			tableFormat += '<tr><td>'+contacts[i].AccountId+'</td>';
        			
        			// Itearte over errors to store error in table string
        			for( Database.Error err : results[i].getErrors() )
        			{
        				tableFormat += '<td>'+err.getStatusCode()+'</td><td>'+err.getMessage()+'</td><td>'+err.getFields()+'</td></tr>';
        			}
        		}
        	}
        }
    }
    
    /**
     * Name: finish
     * @param: BC - Database.BatchableContext
     * @return:
     * Desc: Batch finish method
    **/
    global void finish( Database.BatchableContext BC )
    {
		//Query to get the user
    	User user = [ Select Id, email from User where Id =: UserInfo.getUserId() ];
    	
    	Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();

		//Strings to hold the email addresses to which you are sending the email. 
		String[] toAddresses = new String[] {user.email}; 
		
		//Assign the addresses for the To and CC lists to the mail object. 
		mail.setToAddresses(toAddresses);
		
		//Specify the subject line for your email address. 
		mail.setSubject( 'Batch Executed Completely' );
		
		//Success message:
        String message = 'Total Records Processed: '  + totalRecords +
                         '<br /><br />' + 'Success Records: ' + (totalRecords - errorRecords) +
                         '<br />' + 'Error Records: ' + errorRecords
                         
        // If error record exist, create error table                
        if( errorRecords > 0 )
        {
        	String tableMessage = '<html><head><table border="1"><tr><th>Account Id</th><th>Status Code</th><th>Error Message</th><th>Error Fields</th></tr>'+tableFormat+'</table></head></html>';
        	message += '<br /><br /><br />' + tableMessage;
        }
        
        // To store mail body                 
		String mailBody = ( totalRecords > 0 ) ? message : 'No Records to process' ;
		
		// Specify the text content of the email. 
		mail.setHTMLBody(mailBody);
		
		// Check if Organization email limit exceeded
		try
		{
			// Send the email you have created.
			Messaging.reserveSingleEmailCapacity( 1 );
			Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
		}
		catch( Exception e )	
		{
			// To debug results
			system.debug( 'Email cannnot be send.' );
		}
	}
}
Here in to Address you can add as many email addresses as you want. Let me know, if you need any other help.

Thanks,
Neetu