Asynchronous Apex
These are technical notes I compiled while studying using Trailhead, Salesforce's free self-learning portal.
Asynchronous Processing Basics
Explain the difference between synchronous and asynchronous processing. Choose which kind of asynchronous Apex to use in various scenarios.- Asynchronous Apex
- Asynchronous processes are processes or functions that execute a task “in the background” without the user having to wait for the task to finish
- Processes are run in a separate thread, at a later time
- Typically asynchronous apex is used for callouts to external systems, operations that require higher limits, and code that needs to run at a certain time
- Key Benefits:
- User Efficiency: saves users from needing to wait for a long running process to complete
- Scalability: allowing features of the platform to execute at some point in the future when there are more resources available allows the platform to handle more jobs using parallel processing
- Higher Limits: asynchronous processes are started in a new thread, with higher governor and execution limits
- Four types of asynchronous apex:
- Future Methods: run in their own thread, do not start until resources are available. Commonly used for web service callouts.
- Batch Apex: run large jobs that would exceed normal processing limits. Commonly used for data cleansing or archiving of records.
- Queueable Apex: similar to future methods, but provide additional job chaining and allow more complex data types. Commonly used to perform sequential processing operations with external Web services.
- Scheduled Apex: schedule Apex to run at a specified time. Commonly used for daily or weekly tasks.
- Different types of asynchronous operations are not mutually exclusive - common pattern is to kick off Batch Apex job from a Scheduled Apex job
- Asynchronous processes are processes or functions that execute a task “in the background” without the user having to wait for the task to finish
- Increased Governor and Execution Limits
- SOQL queries limit is doubled from 100 to 200 queries
- Heap size and max CPU time are similarly larger
- Governor limits are independent of the limits in the synchronous request that queued the async request initially
- IE, two separate Apex invocations and more than double the processing capability
- How Asynchronous Processing Works
- A few challenges with asynchronous processing in a multitenant environment:
- Ensure fairness of processing: make sure every customer gets a fair share
- Ensure fault tolerance: ensure no asynchronous requests are lost due to equipment or software failures
- Platform uses a queue-based asynchronous processing framework - three parts to the request lifecycle:
- Enqueue: request is placed into the queue along with the appropriate data to process it
- Persistance: requests are stored in persistent storage for failure recovery and provide transactional capabilities
- Dequeue: request is removed from the queue and processed - if it fails, transaction control ensures requests are not lost
- Requests are processed by a handler: handler is code that performs functions for a specific request type
- Handlers are executed by a finite number of worker threads on each of the application servers that make up an instance
- A few challenges with asynchronous processing in a multitenant environment:
- Resource Conservation
- Asynchronous processing has lower priority than real-time interaction
- Queueing framework monitors system resources like server memory and CPU usage and reduces asynchronous processing when thresholds are exceeded
- So, there’s no guarantee on processing time, but it’ll all work out in the end
Use Future Methods
When to use future methods. The limitations of using future methods. How to use future methods for callouts. Future method best practices.- Future Apex
- Future Apex is used to run processes in a separate thread at a later time when system resources become available
- Use
@future
annotation to identify methods that run asynchronously, called “future methods” - Future methods are commonly used for:
- Callouts to external Web services
- Operations you want to run in their own thread, such as some sort of resource-intensive calculation or processing of records
- Isolating DML operations on different sObject types to prevent the “mixed DML error”
- Future Method Syntax
- Future methods must be static methods, can only return void, can’t take standard or custom objects as arguments
- Common pattern is to pass a list of record IDs you want to process asynchronously
- Remember that future methods are not guaranteed to execute in the same order as they are called
- Also possible that two future methods could run concurrently, which could result in record locking and a runtime error if two methods were updating the same record
- Future methods must be static methods, can only return void, can’t take standard or custom objects as arguments
public class SomeClass {
@future
public static void someFutureMethod(List<Id> recordIds) {
List<Account> accounts = [Select Id, Name from Account Where Id IN :recordIds];
// process account records to do awesome stuff
}
}
- Sample Callout Code
- Class below has methods for making the callout both synchronously and asynchronously where callouts are not permitted
public class SMSUtils {
// Call async from triggers, etc, where callouts are not permitted.
@future(callout=true)
public static void sendSMSAsync(String fromNbr, String toNbr, String m) {
String results = sendSMS(fromNbr, toNbr, m);
System.debug(results);
}
// Call from controllers, etc, for immediate processing
public static String sendSMS(String fromNbr, String toNbr, String m) {
// Calling 'send' will result in a callout
String results = SmsMessage.send(fromNbr, toNbr, m);
insert new SMS_Log__c(to__c=toNbr, from__c=fromNbr, msg__c=results);
return results;
}
}
- Test Classes
- Testing future methods is different than typical Apex testing
- Enclose test code between
startTest()
andstopTest()
test methods to test future methods - the system will collect all asynchronous calls made after thestartTest()
, then run them synchronously whenstopTest()
is executed. - Note the first test class uses a “mock” responses instead of making an actual callout to the REST API endpoint
- The second test class contains a test method that tests both the asynchronous and synchronous methods as the former calls the latter
@isTest
public class SMSCalloutMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
// Create a fake response
HttpResponse res = new HttpResponse();
res.setHeader('Content-Type', 'application/json');
res.setBody('{"status":"success"}');
res.setStatusCode(200);
return res;
}
}
@IsTest
private class Test_SMSUtils {
@IsTest
private static void testSendSms() {
Test.setMock(HttpCalloutMock.class, new SMSCalloutMock());
Test.startTest();
SMSUtils.sendSMSAsync('111', '222', 'Greetings!');
Test.stopTest();
// runs callout and check results
List<SMS_Log__c> logs = [select msg__c from SMS_Log__c];
System.assertEquals(1, logs.size());
System.assertEquals('success', logs[0].msg__c);
}
}
- Best Practices
- Avoid design patterns that add large numbers of future requests over a short period of time since every future method invocation adds one request to the asynchronous queue
- If design could add 2000 or more requests at a time, requests could get delayed due to flow control
- Other best practices:
- Ensure that future methods execute as fast as possible
- Bundle webb service callouts together from the same future method instead of using separate future methods for each callout
- Conduct thorough testing at scale - ex: test that a trigger enqueueing the
@future
calls is able to handle a trigger collection of 200 records - Consider using Batch AApex instead of future methods to process large numbers of records asynchronously - this is more efficient than creating a future request for each record
- Avoid design patterns that add large numbers of future requests over a short period of time since every future method invocation adds one request to the asynchronous queue
- Other things to Remember
- Future methods can’t be used in Visualforce controllers in
getMethodName()
,setMethodName()
, nor in the constructor - Can’t call a future method from a future method, or invoke a trigger that calls a future method while running a future method
- Can’t use the
getContent()
andgetContentAsPDF()
methods in future methods - Limited to 50 future calls per Apex invocation, and there’s a limit on future calls per 24-hour period as well
- Future methods can’t be used in Visualforce controllers in
public without sharing class AccountProcessor {
@future
public static void countContacts(List<Id> accountIds) {
List<Account> accounts = [SELECT Id,
(SELECT Id FROM Contacts)
FROM Account
WHERE Id IN :accountIds];
for (Account acc: accounts) {
acc.Number_of_Contacts__c = acc.Contacts.size();
}
update accounts;
}
}
@isTest
private class AccountProcessorTest {
@isTest
private static void countContactsTest() {
// Load test data
List<Account> accounts = new List<Account>();
for (Integer i=0; i<300; i++) {
accounts.add(new Account(Name='Test Account' + i));
}
insert accounts;
List<Contact> contacts = new List<Contact>();
List<Id> accountIds = new List<Id>();
for (Account acc: accounts) {
contacts.add(new Contact(FirstName=acc.Name,
LastName='TestContact',
AccountId=acc.Id));
accountIds.add(acc.Id);
}
insert contacts;
// Do the test
Test.startTest();
AccountProcessor.countContacts(accountIds);
Test.stopTest();
// Check the result
List<Account> accs = [SELECT Id,
Number_of_Contacts__c FROM Account];
for (Account acc : accs) {
System.assertEquals(1,
acc.Number_of_Contacts__c,
'Error: At least 1 Account with incorrect Number of Contacts');
}
}
}
Use Batch Apex
Where to use Batch Apex. The higher Apex limits when using batch. Batch Apex syntax. Batch Apex best practices.- Batch Apex
- Batch Apex is used to run large jobs (thousands/millions of records) that would normally exceed processing limits
- If you have a lot of records to process (data cleansing or archiving, for example), Batch Apex is probably your best solution
- Under the hood:
- Execution logic of the batch class is called once for each batch
- Each batch is placed on the Apex job queue and is executed as a discrete transaction. This means:
- Every transaction starts with a new set of governor limits, so it is easier to ensure code stays within execution limits
- If one batch fails, all other successful batches aren’t rolled back
- Batch Apex Syntax
- Batch Apex classes must implement the
Database.Batchable
interface and include the following methods:start
: used to collect records or objects to be passed to the interface methodexecute
for processing- Returns either a
Database.QueryLocator
object or anIterable
containing the records or bojects passed to the job- Generally
Database.QueryLocator
does the trick, useIterable
for more advanced scenarios
- Generally
- Returns either a
execute
: performs the actual processing for each chunk of “batch” of data passed to the method- Default batch size is 200 records
- Not guaranteed to execute in the order they are received from the start method
- Takes a reference to the
Database.BatchableContext
object and a list ofsObjects
such asList<sObject>
or a list of parameterized types
finish
: used to execute post-processing operations (such as sending an email) called once after all batches are processed
- Skeleton of a Batch Apex class:
- Batch Apex classes must implement the
public class MyBatchClass implements Database.Batchable<sObject> {
public (Database.QueryLocator | Iterable<sObject>) start(Database.BatchableContext bc) {
// collect the batches of records or objects to be passed to execute
}
public void execute(Database.BatchableContext bc, List<P> records){
// process each batch of records
}
public void finish(Database.BatchableContext bc){
// execute any post-processing operations
}
}
- Invoking a Batch Class
- To invoke, instantiate it and then call
Database.executeBatch
with the instance - Pass a second scope parameter to specify number of records
- Check a batch job’s progress with
AsyncApexJob
records or manage the job in the Apex Job Queue
- To invoke, instantiate it and then call
// To invoke
MyBatchClass myBatchObject = new MyBatchClass();
Id batchId = Database.executeBatch(myBatchObject);
// To pass a second parameter specifying number of records
Id batchId = Database.executeBatch(myBatchObject, 100);
// To check progress after invocation
AsyncApexJob job = [SELECT Id, Status, JobItemsProcessed,
TotalJobItems, NumberOfErrors
FROM AsyncApexJob
WHERE ID = :batchId ];
- Using State in Batch Apex
- Batch Apex is typically stateless - each execution is considered a discrete transaction
- But, if you specify
Database.Stateful
in the class definition, you can maintain state across all transactions- When using, only instance member variables retain their values between transactions
- This is useful for counting or summarizing records as they’re processed
- Sample Batch Apex Code
- Following is practical example - Consider a business requirement that states all contacts for companies in the US must have their parent company’s billing address as their mailing address
- Following finds all account records and updates the asocated contacts with their account’s mailing address, then sends an email with the results of the bulk job and the number of records updated
public class UpdateContactAddresses implements
Database.Batchable<sObject>, Database.Stateful {
// instance member to retain state across transactions
public Integer recordsProcessed = 0;
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(
'SELECT ID, BillingStreet, BillingCity, BillingState, ' +
'BillingPostalCode, (SELECT ID, MailingStreet, MailingCity, ' +
'MailingState, MailingPostalCode FROM Contacts) FROM Account ' +
'Where BillingCountry = \'USA\''
);
}
public void execute(Database.BatchableContext bc, List<Account> scope){
// process each batch of records
List<Contact> contacts = new List<Contact>();
for (Account account : scope) {
for (Contact contact : account.contacts) {
contact.MailingStreet = account.BillingStreet;
contact.MailingCity = account.BillingCity;
contact.MailingState = account.BillingState;
contact.MailingPostalCode = account.BillingPostalCode;
// add contact to list to be updated
contacts.add(contact);
// increment the instance member counter
recordsProcessed = recordsProcessed + 1;
}
}
update contacts;
}
public void finish(Database.BatchableContext bc){
System.debug(recordsProcessed + ' records processed. Shazam!');
AsyncApexJob job = [SELECT Id, Status, NumberOfErrors,
JobItemsProcessed,
TotalJobItems, CreatedBy.Email
FROM AsyncApexJob
WHERE Id = :bc.getJobId()];
// call some utility to send email
EmailUtils.sendMessage(job, recordsProcessed);
}
}
- Testing Batch Apex
- Following demonstrate how to test the above batch class - inserts some records, calls the Batch Apex class, then asserts the records were updated properly
- Note that test methods can execute only one batch, and you should also ensure the
Iterable
returned by thestart()
method matches the batch size
@isTest
private class UpdateContactAddressesTest {
@testSetup
static void setup() {
List<Account> accounts = new List<Account>();
List<Contact> contacts = new List<Contact>();
// insert 10 accounts
for (Integer i=0;i<10;i++) {
accounts.add(new Account(name='Account '+i,
billingcity='New York', billingcountry='USA'));
}
insert accounts;
// find the account just inserted. add contact for each
for (Account account : [select id from account]) {
contacts.add(new Contact(firstname='first',
lastname='last', accountId=account.id));
}
insert contacts;
}
@isTest static void test() {
Test.startTest();
UpdateContactAddresses uca = new UpdateContactAddresses();
Id batchId = Database.executeBatch(uca);
Test.stopTest();
// after the testing stops, assert records were updated properly
System.assertEquals(10, [select count() from contact where MailingCity = 'New York']);
}
}
- Best Practices
- Only use Batch Apex if you have more than one batch of records. Otherwise, use Queueable Apex
- Tune any SOQL query to gather records to execute as quickly as possible
- minimize number of asynchronous requests created to minimize chance of delays
- Use extreme care if planning to invoke a batch job from a trigger - you need to guarantee that the trigger won’t add more batch jobs than the limit
@isTest
public class LeadProcessorTest {
@isTest
private static void testBatchClass() {
// Load test data
List<Lead> leads = new List<Lead>();
for (Integer i=0; i<200; i++) {
leads.add(new Lead(LastName='Connock', Company='Salesforce'));
}
insert leads;
// Perform the test
Test.startTest();
LeadProcessor lp = new LeadProcessor();
Id batchId = Database.executeBatch(lp, 200);
Test.stopTest();
// Check the result
List<Lead> updatedLeads = [SELECT Id
FROM Lead
WHERE LeadSource = 'Dreamforce'];
System.assertEquals(200,
updatedLeads.size(),
'Error: At least 1 Lead record not updated correctly');
}
}
@isTest
public class LeadProcessorTest {
@isTest
private static void testBatchClass() {
// Load test data
List<Lead> leads = new List<Lead>();
for (Integer i=0; i<200; i++) {
leads.add(new Lead(LastName='Connock', Company='Salesforce'));
}
insert leads;
// Perform the test
Test.startTest();
LeadProcessor lp = new LeadProcessor();
Id batchId = Database.executeBatch(lp, 200);
Test.stopTest();
// Check the result
List<Lead> updatedLeads = [SELECT Id
FROM Lead
WHERE LeadSource = 'Dreamforce'];
System.assertEquals(200,
updatedLeads.size(),
'Error: At least 1 Lead record not updated correctly');
}
}
Control Processes with Queueable Apex
When to use the `Queueable` interface. The differences between queueable and future methods. Queueable Apex syntax. Queueable method best practices.- Queueable Apex
- Queueable Apex combines the simplicity of future methods and the power of Batch Apex
- Queueable Apex provides a class structure that the platform serializes for you
- Called by
System.enqueueJob()
method that returns a job ID - Queueable Apex lets you submit jobs for asynchronous processing similar to future methods, but with added benefits:
- Non-primitive types: member variables can include non-primitive types like sObjects or custom Apex types
- Monitoring:
System.engueueJob()
returns the ID of theAsyncApexJob
record - Chaining jobs: chain one job to another job by starting a second job from a running job. Useful if you need to do sequential processing.
- Queueable Versus Future
- Queueable methods are functionally equivalent to future methods, so most of the time you’ll probably want to use queueable instead
- Use future methods instead of queueable when functionality is sometimes executed synchronously and sometimes asynchronously
- Easier to refactor a method that way than it is to convert to a queueable class
- Just create a similar future method that wraps your synchronous method like this:
@future
static void myFutureMethod(List<String> params) {
// call synchronous method
mySyncMethod(params);
}
- Queueable Syntax
- To use Queueable Apex, implement
Queueable
interface
- To use Queueable Apex, implement
public class SomeClass implements Queueable {
public void execute(QueueableContext context) {
// awesome code here
}
}
- Sample Code
- Common use for Queueable Apex is to take some set of sObject records, execute some processing such as making a callout to an external REST endpoint or perform some calculations, then update them in the database asynchronously
- Following code takes a collection of Account records, sets
parentId
for each record, then updates the records in the database - Add the class as a job on the queue, with the second code block below
- Monitor progress through the Apex Jobs page or by querying
AsyncApexJob
as shown in the third code block
public class UpdateParentAccount implements Queueable {
private List<Account> accounts;
private ID parent;
public UpdateParentAccount(List<Account> records, ID id) {
this.accounts = records;
this.parent = id;
}
public void execute(QueueableContext context) {
for (Account account : accounts) {
account.parentId = parent;
// perform other processing or callout
}
update accounts;
}
}
// find all accounts in ‘NY’
List<Account> accounts = [select id from account where billingstate = ‘NY’];
// find a specific parent account for all records
Id parentId = [select id from account where name = 'ACME Corp'][0].Id;
// instantiate a new instance of the Queueable class
UpdateParentAccount updateJob = new UpdateParentAccount(accounts, parentId);
// enqueue the job for processing
ID jobID = System.enqueueJob(updateJob);
SELECT Id, Status, NumberOfErrors FROM AsyncApexJob WHERE Id = :jobID
- Testing Queueable Apex
- To ensure that the queueable process runs within the test method, the job is submitted to the queue between the
Test.startTest
andTest.stopTest
block - very similar to Batch Apex testing
- To ensure that the queueable process runs within the test method, the job is submitted to the queue between the
@isTest
public class UpdateParentAccountTest {
@testSetup
static void setup() {
List<Account> accounts = new List<Account>();
// add a parent account
accounts.add(new Account(name='Parent'));
// add 100 child accounts
for (Integer i = 0; i < 100; i++) {
accounts.add(new Account(
name='Test Account'+i
));
}
insert accounts;
}
static testmethod void testQueueable() {
// query for test data to pass to queueable class
Id parentId = [SELECT id
FROM account
WHERE name = 'Parent'][0].Id;
List<Account> accounts = [SELECT id, name
FROM account
WHERE name LIKE 'Test Account%'];
// Create our Queueable instance
UpdateParentAccount updater = new UpdateParentAccount(accounts, parentId);
// startTest/stopTest block to force async processes to run
Test.startTest();
System.enqueueJob(updater);
Test.stopTest();
// Validate the job ran. Check if record have correct parentId now
System.assertEquals(100, [SELECT count()
FROM account
WHERE parentId = :parentId]);
}
}
- Chaining Jobs
- One of the best features in Queueable Apex is job chaining - allows you to run jobs sequentially
- To chain jobs, submit the second job from the
execute()
method of your queueable class - You can only add one job from an executing job, which means only one child job can exist for each parent job
- Ex: if you have a second class called
SecondJob
that implementsQueueable
, add this class to the queue in theexecute()
method as follows:- Note you can’t chain queueable jobs in an Apex test - check if Apex is running in test context by calling
Test.isRunningTest()
before chaining jobs
- Note you can’t chain queueable jobs in an Apex test - check if Apex is running in test context by calling
public class FirstJob implements Queueable {
public void execute(QueueableContext context) {
// Awesome processing logic here
// Chain this job to next job by submitting the next job
System.enqueueJob(new SecondJob());
}
}
- Other things to remember
- Execution of a queued job counts against the shared limit for asynchronous Apex method executions
- You can add up to 50 jobs to the queue with
System.enqueueJob
- You can only add one job from an executing job with
System.enqueueJob
, which means that only one child job can exist for each parent queueable job - No limit is enforced on the depth of chained jobs (except in Developer Edition and Trial orgs - limit: 5)
public without sharing class AddPrimaryContact implements Queueable {
private Contact contact;
private String state;
public AddPrimaryContact (Contact inputContact, String inputState) {
this.Contact = inputContact;
this.State = inputState;
}
public void execute(QueueableContext context) {
// Retrieve 200 Acccount records
List<Account> accounts = [SELECT Id
FROM Account
WHERE BillingState = :state
LIMIT 200];
// Create empty list of Contact records
List<Contact> contacts = new List<Contact>();
// Iteraate through the Account records
for (Account acc : accounts) {
// Clone (copy) the Contact record, make the clone a
// child of the specific Account record and add to the
// list of Contacts
Contact contactClone = contact.clone();
contactClone.AccountId = acc.Id;
contacts.add(contactClone);
}
insert contacts;
}
}
@isTest
public class AddPrimaryContactTest {
@isTest
private static void testQueueableClass() {
// Load test data
List<Account> accounts = new List<Account>();
for (Integer i=0; i<500; i++) {
Account acc = new Account(Name='Test Account');
if (i<250) {
acc.BillingState = 'NY';
} else {
acc.BillingState = 'CA';
}
accounts.add(acc);
}
insert accounts;
Contact contact = new Contact(FirstName='Simon',
LastName='Connock');
insert contact;
// Perform the test
Test.startTest();
Id jobId = System.enqueueJob(New AddPrimaryContact(contact, 'CA'));
Test.stopTest();
// Check the result
List<Contact> contacts = [SELECT Id
FROM Contact
WHERE Contact.Account.BillingState = 'CA'];
System.assertEquals(200, contacts.size(),
'ERROR: Incorrect number of Contact records found');
}
}
Schedule Jobs Using the Apex Scheduler
When to use scheduled Apex. How to monitor scheduled jobs. Scheduled Apex syntax. Scheduled method best practices.- Scheduled Apex
- Apex Scheduler lets you delay execution so you can run Apex classes at a specified time
- Ideal for daily or weekly maintenance tasks using Batch Apex
- To use the scheduler, write an Apex class that implements the
Scheduleable
interface
- Scheduled Apex Syntax
- To invoke Apex classes to run at specific times, implement the
Schedulable
interface for the class, then schedule an instance of the class to run at a specific time usingSystem.schedule()
method - Class implements the
Schedulable
interface must implementexecute()
method - Parameter of this method is a
SchedulableContext
object - After a class has been scheduled, a
CronTrigger
object is created that represents the scheduled job - it provides agetTriggerId()
method that returns the ID of aCronTrigger
API object
- To invoke Apex classes to run at specific times, implement the
public class SomeClass implements Schedulable {
public void execute(SchedulableContext ctx) {
// awesome code here
}
}
- Sample Code
- This class queries for open opportunities that should have closed by the current date, and creates a task on each one to remind the owner to update the opportunity
- You can schedule your class to run either programmatically or from the Apex Scheduler UI
public class RemindOpptyOwners implements Schedulable {
public void execute(SchedulableContext ctx) {
List<Opportunity> opptys = [SELECT Id, Name, OwnerId, CloseDate
FROM Opportunity
WHERE IsClosed = False AND
CloseDate < TODAY];
// Create a task for each opportunity in the list
TaskUtils.remindOwners(opptys);
}
}
- Using the System.Schedule Method
- After you implement a class with
Schedulable
, use theSystem.schedule()
method to execute it - Use care if you schedule a class from a trigger - need to be able to guarantee the trigger won’t add more scheduled job classes than the limit
System.schedule()
method takes three arguments: name for the job, CRON expression used to represent the time and date the job will run, and an instance of a class that implements theSchedulable
interface
- After you implement a class with
RemindOpptyOwners reminder = new RemindOpptyOwners();
// Seconds Minutes Hours Day_of_month Month Day_of_week optional_year
String sch = '20 30 8 10 2 ?';
String jobID = System.schedule('Remind Opp Owners', sch, reminder);
- Scheduling a Job from the UI
- Also possible to schedule a class using the user interface:
- Setup > Quick Find > Apex > Apex Classes
- Click Schedule Apex
- Name the job something like
Daily Oppty Reminder
- Click the lookup button to get a list of all classes that can be scheduled
- Select
Weekly
orMonthly
for the frequency and set the frequency desired - Select start and end dates and a preferred start time
- Also possible to schedule a class using the user interface:
- Testing Scheduled Apex
- With Scheduled Apex you must also ensure that the scheduled job is finished bbefore testing against results - use
startTest()
andstopTest()
again around theSystem.schedule()
method to ensure processing finishes before continuing
- With Scheduled Apex you must also ensure that the scheduled job is finished bbefore testing against results - use
@isTest
private class RemindOppyOwnersTest {
// Dummy CRON expression: midnight on March 15.
// Because this is a test, job executes
// immediately after Test.stopTest().
public static String CRON_EXP = '0 0 0 15 3 ? 2022';
static testmethod void testScheduledJob() {
// Create some out of date Opportunity records
List<Opportunity> opptys = new List<Opportunity>();
Date closeDate = Date.today().addDays(-7);
for (Integer i=0; i<10; i++) {
Opportunity o = new Opportunity(
Name = 'Opportunity ' + i,
CloseDate = closeDate,
StageName = 'Prospecting'
);
opptys.add(o);
}
insert opptys;
// Get the IDs of the opportunities we just inserted
Map<Id, Opportunity> opptyMap = new Map<Id, Opportunity>(opptys);
List<Id> opptyIds = new List<Id>(opptyMap.keySet());
Test.startTest();
// Schedule the test job
String jobId = System.schedule('ScheduledApexTest',
CRON_EXP,
new RemindOpptyOwners());
// Verify the scheduled job has not run yet.
List<Task> lt = [SELECT Id
FROM Task
WHERE WhatId IN :opptyIds];
System.assertEquals(0, lt.size(), 'Tasks exist before job has run');
// Stopping the test will run the job synchronously
Test.stopTest();
// Now that the scheduled job has executed,
// check that our tasks were created
lt = [SELECT Id
FROM Task
WHERE WhatId IN :opptyIds];
System.assertEquals(opptyIds.size(),
lt.size(),
'Tasks were not created');
}
}
- Things to Remember
- You can only have 100 scheduled Apex jobs at one time and there are a maximum number of scheduled Apex executions per 24-hour period
- Use care if you’re planning to schedule a class from a trigger - need to guarantee the trigger won’t add more scheduled jobs than the limit
- Synchronous Web services callouts are not supported from scheduled Apex - to make callouts, make an aasynchronous callout by placing the callout in a method annotated with
@future(callout=true)
and call the method from the scheduled Apex
public without sharing class DailyLeadProcessor implements Schedulable {
public void execute(SchedulableContext ctx) {
// Returns the ID of the CronTrigger schedule
// System.debug('Context ' + ctx.getTriggerId());
// Get 200 Lead records and modify the LeadSource field
List<Lead> leads = [SELECT Id, LeadSource
FROM Lead
WHERE LeadSource = null
LIMIT 200];
for (Lead l : leads) {
l.LeadSource = 'Dreamforce';
}
// Update the modified records
update leads;
}
}
@isTest
public class DailyLeadProcessorTest {
// Midnight every day
private static string CRON_EXP = '0 0 0 ? * * *';
@isTest
private static void testSchedulableClass() {
// Load test data
List<Lead> leads = new List<Lead>();
for (Integer i=0; i<500; i++) {
if (i<250) {
leads.add(new Lead(LastName='Connock',
Company='Salesforce'));
} else {
leads.add(new Lead(LastName='Connock',
Company='Salesforce',
LeadSource='Other'));
}
}
insert leads;
// Perform the test
Test.startTest();
String jobId = System.schedule('Process Leads',
CRON_EXP,
new DailyLeadProcessor());
Test.stopTest();
// Check the result
List<Lead> updatedLeads = [SELECT Id, LeadSource
FROM Lead
WHERE LeadSource = 'Dreamforce'];
System.assertEquals(200,
updatedLeads.size(),
'ERROR: At least 1 record not updated correctly');
// Check the scheduled time
List<CronTrigger> cts = [SELECT Id, TimesTriggered,NextFireTime
FROM CronTrigger
WHERE Id = :jobId];
System.debug('Next Fire Time ' + cts[0].NextFireTime);
}
}
Monitor Asynchronous Apex
How to monitor the different types of jobs. How to use the flex queue.- Monitoring Asynchronous Jobs
- Async Jobs work silently in the background, but there are a few ways to monitor jobs:
- Setup > Quick Find > “Jobs” > Apex Jobs
- Shows status of all asynchronous Apex jobs with info about each job’s execution as well as their type
- The Apex Flex Queue also allows you to monitor the status of Apex jobs and reorder them to control which jobs are processed first
- Async Jobs work silently in the background, but there are a few ways to monitor jobs:
- Monitoring Future Jobs
- Future jobs show up on the Apex Jobs page like any other jobs
- Future jobs are not part of the flex queue currently, though
- You can query
AsyncApexJob
to find your future job, but you need to filter on some other field likeMethodName
orJobType
to find your job - Reference this Stack Exchange post for more info
- You can query
- Monitoring Queued Jobs with SOQL
- Perform a SOQL query on
AsyncApexJob
by filtering on the Job ID thatSystem.euqueueJob()
method returns
- Perform a SOQL query on
AsyncApexJob jobInfo = [SELECT Status, NumberOfErrors
FROM AsyncApexJob
WHERE Id = :jobID];
- Monitoring Queue Jobs with the Flex Queue
- Apex Flex Queue enables you to submit up to 100 batch jobs for execution
- Any jobs that are submitted for execution are in holding status and placed in the Flex queue
- Jobs are processed first-in, first-out
- You can shuffle the order
- As system resources become available, the system takes the next job from the top of the Apex Flex queue and moves it to the batch job queue
- System can process up to five queued or active jobs simultaneously per organization
- Status of those moved jobs changes from Holding to Queued
- Monitoring Scheduled Jobs
- After an Apex job has been scheduled, you can find more info about it by querying CronTrigger
- Following includes number of times the job has run, and the date and time when the job will run again
- Uses a
jobID
variable which is returned fromSystem.schedule()
method
CronTrigger ct = [SELECT TimesTriggered, NextFireTime
FROM CronTrigger
WHERE Id = :jobID];
- If you’re performing the query inside the execute method of your schedulable class, you can obtain the ID of the current job by calling
getTriggerId()
on theSchedulableContext
argument variable
public class DoAwesomeStuff implements Schedulable {
public void execute(SchedulableContext sc) {
// some awesome code
CronTrigger ct = [SELECT TimesTriggered, NextFireTime
FROM CronTrigger
WHERE Id = :sc.getTriggerId()];
}
}
- You can also get the job’s name and type from the
CronJobDetail
record associated with theCronTrigger
record- Use the
CronJobDetail
relationship when performing the query.
- Use the
CronTrigger job = [SELECT Id, CronJobDetail.Id, CronJobDetail.Name,
CronJobDetail.JobType
FROM CronTrigger
ORDER BY CreatedDate DESC
LIMIT 1];
- Alternatively, query
CronJobDetail
directly to get the job’s name and type
CronJobDetail ctd = [SELECT Id, Name, JobType
FROM CronJobDetail
WHERE Id = :job.CronJobDetail.Id];
- Lastly, get the total count of all Apex scheduled jobs excluding all other scheduled job types, perform the following
- Value
7
is specified for the job type, which corresponds to the scheduled Apex job type - See CronJobDetail for more detail
- Value
- Future Method jobs do not show up in the Apex Flex queue
- The Flex Queue is processed first-in first-out