Yesterday, I was baffled by the following observed behavior:
- Insert six Contacts using
Database.insert(cList,false)
, five of which had some validation error, one which succeeded - The successful Contact, in the before insert trigger, derives the value of accountId, if null in the inserted record
- The system.assert to verify the defaulting of accountId failed, even though the debug log clearly showed that it was set
Further investigation of the debug log showed the before insert trigger for the successful Contact was executing twice, and in the second iteration, the defaulting of the accountId was not occurring.
Before insert trigger executed twice? Huh?
After some reductionist testing, I verified that this only happened when using optAllOrNothing = false in Database.insert(cList,false)
. That is, allow for partial successes. According to the documentation (in a section I had never read or paid attention to).
When errors occur because of a bulk DML call that originates from the SOAP API with default settings, or if the allOrNone parameter of a Database DML method was specified as false, the runtime engine attempts at least a partial save:
(1) During the first attempt, the runtime engine processes all records. Any record that generates an error due to issues such as validation rules or unique index violations is set aside.
(2) If there were errors during the first attempt, the runtime engine makes a second attempt that includes only those records that did not generate errors. All records that didn’t generate an error during the first attempt are processed, and if any record generates an error (perhaps because of race conditions) it is also set aside.
(3) If there were additional errors during the second attempt, the runtime engine makes a third and final attempt which includes only those records that didn’t generate errors during the first and second attempts. If any record generates an error, the entire operation fails with the error message, “Too many batch retries in the presence of Apex triggers and partial failures.”
It is the second point that is interesting – “the runtime engine makes a second attempt that includes only those records that did not generate errors” – ahhh, that is why the debug log showed two executions of before insert on the same record.
Now, why did the second attempt not default the Contact’s accountId using my Apex logic?
Answer: At the end of my before insert trigger handler, I set a static variable to prevent the trigger handler from re-executing (sort of a reflex action to avoid unnecessary SOQLs). Hence, when the second attempt at before insert was made, the static variable prevented the defaulting and the record saved successfully, except without a non-null AccountId. Hence the assertion failed.
A different portion of the SFDC Apex doc states:
When a DML call is made with partial success allowed, more than one attempt can be made to save the successful records if the initial attempt results in errors for some records. For example, an error can occur for a record when a user-validation rule fails. Triggers are fired during the first attempt and are fired again during subsequent attempts. Because these trigger invocations are part of the same transaction, static class variables that are accessed by the trigger aren’t reset. DML calls allow partial success when you set the allOrNone parameter of a Database DML method to false or when you call the SOAP API with default settings. For more details, see Bulk DML Exception Handling.
Note the sentence: “Because these trigger invocations are part of the same transaction, static class variables that are accessed by the trigger aren’t reset”.
So, while governor limits are not affected by the retry, the static variables remain persistent and hence the trigger handler derivation logic remained switched off as a result of the conclusion of the initial before insert trigger handler.
Removing the setting of the static variable solved the issue.