2

There are many ways of solving apex trigger recursion using Static Boolean flags, static set of ids, static map of operation to set of Ids.

The old official documentation from Salesforce.com to fix recursion using static Boolean flag is not right as it works only for total records of 200. If the total records were 300 it would fail as static boolean flag is spread across all the batches/all records.

Of all, I feel that static Map<TriggerOperation, Set> blockedObjectIdsByOperation from What is a good pattern for recursion blocking in generic trigger handlers? is the right pattern.

Even though it makes sense with the above reference, Im really worried if I want to update 50 million records through data loader on an object. Since storing each processed Id would take up around 26 bytes in map. For 50 million records it would be more than 1 GB of heap space. I m not sure If I am missing something. Please throw some light on this.

2
  • In addition to the above, I see some suggestion around emptying the Map<TriggerOperation, Set> for every 1000 records of bulk data load. It might sound interesting. But, Im not able to for-see any repercussions of using this approach.
    – pioneer
    Commented Mar 20, 2023 at 15:15
  • so you wanted to not run same id an event more than once in the same transaction. if you apply the emptying once per every 1000 records, why cant you do for every 200 records. heap size for synchnous call is 6 MB.
    – Pratap M
    Commented Mar 20, 2023 at 15:22

1 Answer 1

3

First of all, Data Loader transactions (and indeed, any API call) are limited to 200. When you load 50 million records, you're loading 250,000 calls of 200 records each. The limits reset between each transaction, as does the heap usage, so there's no concern there.

When possible, using a single flag is the more appropriate option. The trick, as I stated in my previous answer, is to set the flag before doing a potentially recursive operation, then resetting the flag afterwards. The reset step is important, because that allows partial retries to work correctly, as well as recursive updates that may occur from workflow rules, flows, etc.

My personal favorite is to simply check which records need to be updated (e.g. they differ in values), rather than blindly update all records and depend on a recursion blocking scheme. But if I absolutely need to block recursion, I'm going to use the set flag, do DML, reset flag pattern. It's the safest recursion blocker that will work 100% of the time if you are also already filtering for "only necessary updates."

Give it a try sometime, and see how efficient it can be to simply prefer not to update records, rather than to try and block recursion. It's much easier to avoid the infinite loop than to break the infinite loop with a flag.

Here's a trivial example. Let's say we want to update the account's description from the contact's description. We can do this efficiently with:

Map<Id, Account> accounts = new Map<Id, Account>([
  SELECT Description FROM Account WHERE Id IN (SELECT AccountId FROM Contact WHERE Id = :Trigger.new
]);
Map<Id, Account> oldValues = new Map<Id, Account>();
Map<Id, Account> newValues = new Map<Id, Account>();
for(Contact record: Trigger.new) {
  oldValues.put(
    record.AccountId, 
    new Account(
      Id=record.AccountId,
      Description=accounts.get(record.AccountId).Description
    )
  );
  newValues.put(
    record.AccountId,
    new Account(
      Id=record.AccountId,
      Description=record.Description
    )
  );
}
for(Id accountId: newValues.keySet().clone()) {
  if(oldValues.get(accountId) == newValues.get(accountId)) {
    newValues.remove(accountId);
  }
}
update newValues.values();

While this example is overly complicated, it scales well with 10, 20, or even 100 fields. As many as you need. You could even write this out as a generic method with an interface or class that handles most of the work for you.

Note that there's no error handling and null checks, this is more illustrative than prescriptive, but hopefully you'll get the main point that it's easier to deal with recursion when you're not updating records that don't need to be updated, rather than blocking the triggers with any kind of static variable.

If you consider the trigger above for Contacts, and a similar one for Accounts (to update all Contact descriptions to match), you'll notice that the record that triggered the original update won't recurse back to the original contact, because the description will already match the new value. By checking for changes before applying them, you'll break almost all types of recursion naturally without any flags at all.

19
  • Wait a minute, isn't it the batch size (with a maximum value of 10,000) the determiner of the transaction boundry here?
    – Derek F
    Commented Mar 20, 2023 at 15:30
  • Thanks @sfdcfox for the Detailed explanation. If static variables(as they share values across transactions in trigger of multiple batches) memory does not increase over each processed transaction, then that should work.
    – pioneer
    Commented Mar 20, 2023 at 15:47
  • 1
    @DerekF Batches are always in 200, though. Even the Bulk API's 10k batches are 200 each.
    – sfdcfox
    Commented Mar 20, 2023 at 16:50
  • @pioneer static variables do not share across transactions, they are always initially clear of any values.
    – sfdcfox
    Commented Mar 20, 2023 at 16:51
  • 1
    @pioneer It doesn't matter if you use a flag or a Set of Ids; it matters that you reset the recursion blocker at the end of each trigger operation. The Set of Ids was a workaround that people came up with when they realized the Boolean flags were breaking things like unit tests, and never realized it was the fact that the recursion blocking solution itself wasn't complete. Repetition is not recursion, which is why neither solution is typically the answer.
    – sfdcfox
    Commented Mar 23, 2023 at 15:53

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .