@future method called inside a loop
ID |
apex.correctness.future_called_in_loop |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
medium |
Remediation Effort |
medium |
Resource |
Resource Management |
Language |
Apex |
Description
Reports a call inside a for, while or do loop to a @future method declared in the same class, whether called plainly or through a qualified reference. Only same-class calls are reported: OpenGrep does not resolve a callee across files, so a loop calling a @future method declared in another class is not detected.
Rationale
A transaction may queue 50 asynchronous calls, and each @future invocation spends one. A loop over a trigger’s 200 records therefore throws LimitException on the fifty-first record, and because triggers run in batches of 200 the failure appears only once the data is large enough - typically in production, on an import, and never in the test that ran with three records. Every queued call is also a separate transaction with its own governor limits and its own start-up cost, so even below the ceiling the work is far slower than one call would be.
The following code illustrates the pattern detected by this rule:
public class AccountSyncService {
@future
public static void syncOne(Id accountId) {
System.debug(LoggingLevel.INFO, accountId);
}
@future(callout=true)
public static void pushOne(Id accountId) {
System.debug(LoggingLevel.INFO, accountId);
}
@Future
public static void auditOne(Id accountId) {
System.debug(LoggingLevel.INFO, accountId);
}
public static void syncAll(List<Account> accounts) {
// FLAGGED: @future method called inside a loop
for (Account a : accounts) {
syncOne(a.Id);
}
}
public static void pushAll(List<Id> accountIds) {
Integer n = 0;
// FLAGGED: @future method called inside a loop
while (n < accountIds.size()) {
pushOne(accountIds[n]);
n++;
}
}
public static void auditAll(List<Id> accountIds) {
// FLAGGED: @future method called inside a loop
for (Integer i = 0; i < accountIds.size(); i++) {
AccountSyncService.auditOne(accountIds[i]);
}
}
public static void syncAllBulk(List<Account> accounts) {
Set<Id> ids = new Set<Id>();
for (Account a : accounts) {
ids.add(a.Id);
}
syncMany(ids);
}
@future
public static void syncMany(Set<Id> accountIds) {
System.debug(LoggingLevel.INFO, accountIds.size());
}
public static void stampAll(List<Account> accounts) {
for (Account a : accounts) {
stampOne(a);
}
update accounts;
}
public static void stampOne(Account a) {
a.Description = 'stamped';
}
}
Remediation
Collect the ids in the loop and call the @future method once, after it, with the whole collection: a @future method may take a Set<Id> or a List<Id> and iterate inside the asynchronous transaction. Where the work needs more than one asynchronous transaction, a Queueable chain or a Database.Batchable class gives you a supported way to span several.