11

I'm using Tony Scott's Tidy Trigger pattern for triggers. Suddenly, I'm facing CPU limit exception on trying to update 2k accounts. On checking the debug log I see that validation rules & duplicate rules are being called. But I'm not sure why a working pattern has stopped working all of a sudden. The debug log registers the following -

Class.TriggerFactory.execute: line 94, column 1
Class.TriggerFactory.createAndExecuteHandler: line 27, column 1
Trigger.AccountTrigger: line 3, column 1

Here is the Account TriggerHandler, I don't see that the any logic is written on the update events, beforeUpdate & afterUpdate are empty methods.

public with sharing class Account_TriggerHandler implements ITrigger{

    private list<Account> accList = new list<Account>();
    private Map<Id, Account> MapIdAcc = new Map<Id, Account>();
    private string TriggerEventName;


    public void bulkBefore() {

        if(Trigger.isDelete)
        {
          MapIdAcc = (Map<Id, Account>)Trigger.OldMap;
        }

        if(Trigger.isInsert){
            TriggerEventName = 'Insert';
        }
        else{
            TriggerEventName = 'Update';
        }
    }

    public void bulkAfter() {

    if(Trigger.isInsert)
        {
         accList = Trigger.new;
        }
    }

    public void beforeInsert(SObject so) {       

    }

    public void beforeUpdate(SObject oldSo, SObject so) {       


    }

    public void beforeDelete(SObject so) {    

    }

    public void afterInsert(SObject so) {

    }

    public void afterUpdate(SObject oldSo, SObject so) {

    }

    public void afterDelete(SObject so) {

    }

    public void andFinally() {
        if(Trigger.isAfter && Trigger.isInsert)
        {
        Account_TriggerHelper.sendEmailToSharedPeople(accList);
        Account_TriggerHelper.sendEmailOnBlockingCustomer(accList); 


        }

        if(Trigger.isBefore && !Trigger.isDelete){
            Account_TriggerHelper.populateApprovalGroup(accList, MapIdAcc, TriggerEventName); 
        } 
    }


}

Account_TriggerHelper -

public  class Account_TriggerHelper {
    public static void sendEmailToSharedPeople(List<Account> accList) {
        Map<Id, Account> mapOfAccIdAndAcc = new Map<Id, Account> ();
        Map<Id, Set<Id>> mapOfAccAndSharedUsers = new Map<Id, Set<Id>> ();
        Map<Id, String> mapOfUserAndName = new Map<Id, String> ();
        Set<Id> setOfGroupIds = new Set<Id> ();
        Map<Id, Set<Id>> mapOfGroupIdAndMember = new Map<Id, Set<Id>> ();
        String userType = Schema.sObjectType.User.getKeyPrefix();
        String groupType = Schema.sObjectType.Group.getKeyPrefix();

        for(sObject accS : accList) {
            Account acc = (Account)accS;
            mapOfAccIdAndAcc.put(acc.Id, acc);
        }
        for(AccountShare accShare : [SELECT Id, AccountId, UserOrGroupId
                                    FROM AccountShare
                                    WHERE AccountId IN :accList]) {
            if(!mapOfAccAndSharedUsers.containsKey(accShare.AccountId)) {
                mapOfAccAndSharedUsers.put(accShare.AccountId, new Set<Id> ());
            }
            if(String.valueOf(accShare.UserOrGroupId).startsWith(userType) && (!UserUtil.getUser(accShare.UserOrGroupId).Profile.Name.contains('Administrator'))) {
                mapOfAccAndSharedUsers.get(accShare.AccountId).add(accShare.UserOrGroupId);
            } 
            if(String.valueOf(accShare.UserOrGroupId).startsWith(groupType)) {
                setOfGroupIds.add(accShare.UserOrGroupId);
            }
        }
        for(GroupMember gm : [SELECT Id, UserOrGroupId, GroupId
                                FROM GroupMember
                                WHERE GroupId IN :setOfGroupIds]) {
            if(String.valueOf(gm.UserOrGroupId).startsWith(userType)) {
                if(!mapOfGroupIdAndMember.containsKey(gm.GroupId)) {
                    mapOfGroupIdAndMember.put(gm.GroupId, new Set<Id>());
                }
                mapOfGroupIdAndMember.get(gm.GroupId).add(gm.UserOrGroupId);
            }
        }
        for(AccountShare accShare : [SELECT Id, AccountId, UserOrGroupId
                                    FROM AccountShare
                                    WHERE UserOrGroupId IN :setOfGroupIds
                                    AND AccountId IN :mapOfAccIdAndAcc.keySet()]) {
            if(!mapOfAccAndSharedUsers.containsKey(accShare.AccountId)) {
                mapOfAccAndSharedUsers.put(accShare.AccountId, new Set<Id> ());
            }
            if(mapOfGroupIdAndMember.get(accShare.UserOrGroupId) != null) {
                mapOfAccAndSharedUsers.get(accShare.AccountId).addAll(mapOfGroupIdAndMember.get(accShare.UserOrGroupId));
            }
        }
        Set<Id> setOfUserIds = new Set<Id> ();
        for(Set<Id> lstId : mapOfAccAndSharedUsers.values()) {
            for(Id idVal : lstId) {
                if(!setOfUserIds.contains(idVal)) {
                    setOfUserIds.add(idVal);
                }
            }
        }
        for(User u : [SELECT Id, Name
                    FROM User
                    WHERE Id IN :setOfUserIds]) {
            mapOfUserAndName.put(u.Id, u.Name);
        }
        List<Messaging.SingleEmailMessage> mails = new List<Messaging.SingleEmailMessage>();
        for(Id idOfAcc : mapOfAccAndSharedUsers.keySet()) {
            for(Id idOfUser : mapOfAccAndSharedUsers.get(idOfAcc)) {
                Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
                mail.setTargetObjectId(idOfUser);
                mail.saveAsActivity = false;
                mail.setSubject('New Customer:' + mapOfAccIdAndAcc.get(idOfAcc).Name + ' has been shared with you.');
                String body = 'Dear ' + mapOfUserAndName.get(idOfUser) + ',<br/>' + '<br/>';
                body += 'Please be informed that customer ' + '<b>' + mapOfAccIdAndAcc.get(idOfAcc).Name +'</b>'+ ' has been shared with you.' + '<br/>'+ '<br/>';
                body += 'Please find the customer link ' + '<a href=" ' + MapOfAccIdAndAcc.get(idOfAcc).ORG_URL__c + String.valueOf(mapOfAccIdAndAcc.get(idOfAcc).Id) + '">here.</a>' + '<br/>' + '<br/>';
                body += 'Thanks & Regards,'+ '<br/>'+ '<br/>';
                body += 'Sales Team';
                mail.setHtmlBody(body);
                mails.add(mail);
            }
        }
        Messaging.sendEmail(mails);
    }

    public static void sendEmailOnBlockingCustomer(List<Account> accList) {
        Map<Id, String> mapOfAccIdAndName = new Map<Id, String> ();
        Map<Id, Set<Id>> mapOfAccAndSharedUsers = new Map<Id, Set<Id>> ();
        Map<Id, String> mapOfUserAndName = new Map<Id, String> ();
        Set<Id> setOfGroupIds = new Set<Id> ();
        String userType = Schema.sObjectType.User.getKeyPrefix();
        Map<Id, Set<Id>> mapOfGroupIdAndMember = new Map<Id, Set<Id>> ();
        for(sObject accV : accList) {
            Account acc = (Account)accV;
            Account oldVal = new Account();
            if(Trigger.oldMap != null && Trigger.oldMap.containsKey(acc.Id)) {
                oldVal = (Account)Trigger.oldMap.get(acc.Id);
            }
            if((Trigger.oldMap == null && (acc.Block_Customer__c == true || acc.Blocked_Customer__c == true)) || (acc.Block_Customer__c == true && oldVal.Blocked_Customer__c == false) || (acc.Blocked_Customer__c == true && oldVal.Blocked_Customer__c == false)) {
                mapOfAccIdAndName.put(acc.Id, acc.Name);
            }
        }
        for(AccountShare accShare : [SELECT Id, AccountId, UserOrGroupId
                                    FROM AccountShare
                                    WHERE AccountId IN :mapOfAccIdAndName.keySet()]) {
            if(String.valueOf(accShare.UserOrGroupId).startsWith(userType)) {
                if(!mapOfAccAndSharedUsers.containsKey(accShare.AccountId)) {
                    mapOfAccAndSharedUsers.put(accShare.AccountId, new Set<Id> ());
                }
                mapOfAccAndSharedUsers.get(accShare.AccountId).add(accShare.UserOrGroupId);
            } else {
                if(!setOfGroupIds.contains(accShare.UserOrGroupId)) {
                    setOfGroupIds.add(accShare.UserOrGroupId);
                }
            }
        }
        for(GroupMember gm : [SELECT Id, UserOrGroupId,GroupId
                                FROM GroupMember
                                WHERE GroupId IN :setOfGroupIds]) {
            if(String.valueOf(gm.UserOrGroupId).startsWith(userType)) {
                if(!mapOfGroupIdAndMember.containsKey(gm.GroupId)) {
                    mapOfGroupIdAndMember.put(gm.GroupId, new Set<Id>());
                }
                mapOfGroupIdAndMember.get(gm.GroupId).add(gm.UserOrGroupId);
            }
        }
        for(AccountShare accShare : [SELECT Id, AccountId, UserOrGroupId
                                    FROM AccountShare
                                    WHERE UserOrGroupId IN :setOfGroupIds
                                    AND AccountId IN :mapOfAccIdAndName.keySet()]) {
            if(!mapOfAccAndSharedUsers.containsKey(accShare.AccountId)) {
                mapOfAccAndSharedUsers.put(accShare.AccountId, new Set<Id> ());
            }
                                        if(mapOfGroupIdAndMember != null && mapOfGroupIdAndMember.containsKey(accShare.UserOrGroupId)) {
                                                     for(Id IdVal : mapOfGroupIdAndMember.get(accShare.UserOrGroupId)) {
                mapOfAccAndSharedUsers.get(accShare.AccountId).add(idVal);
            }   
                                        }
        }
        Set<Id> setOfUserIds = new Set<Id> ();
        for(Set<Id> lstId : mapOfAccAndSharedUsers.values()) {
            for(Id idVal : lstId) {
                if(!setOfUserIds.contains(idVal)) {
                    setOfUserIds.add(idVal);
                }
            }
        }
        for(User u : [SELECT Id, Email , Name
                        FROM User
                        WHERE Id IN :setOfUserIds]) {
            mapOfUserAndName.put(u.Id, u.Name);
        }
        List<Messaging.SingleEmailMessage> mails = new List<Messaging.SingleEmailMessage>();
        for(Id idOfAcc : mapOfAccAndSharedUsers.keySet()) {
            for(Id idOfUser : mapOfAccAndSharedUsers.get(idOfAcc)) {
                Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
                mail.setTargetObjectId(idOfUser);
                mail.saveAsActivity = false;
                mail.setSubject('Customer: ' + mapOfAccIdAndName.get(idOfAcc) + ' has been blocked.');
                String body = 'Dear ' + mapOfUserAndName.get(idOfUser) + ',<br/>' + '<br/>';
                body += 'Please be informed that customer ' + '<b>' + mapOfAccIdAndName.get(idOfAcc) +'</b>'+ ' has been blocked.' + '<br/>'+ '<br/>';
                body += 'Please find the customer link ' + '<a href=" ' + String.valueOf(mapOfAccIdAndName.get(idOfAcc)) +'">here.</a>' + '<br/>' + '<br/>';
                body += 'Thanks & Regards,';
                body += 'Sales Team';                
                mail.setHtmlBody(body);
                mails.add(mail);                             
                }
        }
        Messaging.sendEmail(mails);        
    }

    public static void populateApprovalGroup(List<Account> lstAccounts, Map<id, Account> oldMapAccounts, String strEvent){
        //Variable Declaration
        set<String> setSalesOfficeName = new Set<String>();
        Set<String> setSalesOfficeIds = new Set<String>();
        Map<String, List<Account>> mapSalesOfficeLstAccounts = new Map<String, List<Account>>();
        Map<String, List<Account>> mapSalesOfficeIdLstAccounts = new Map<String, List<Account>>();
        List<Account> lstAccountsToBeUpdated = new List<Account>();
        Id idSAPcustomer_RT = Schema.SObjectType.Account.getRecordTypeInfosByName().get('SAP Customer').getRecordTypeId();
        Boolean boolEligibleRec;

        //Iterate Over Accounts to Collect records
        for(Account iteratorAcc : lstAccounts){
            boolEligibleRec = false;
            //Block for SAP Customer
            if(iteratorAcc.recordTypeID == idSAPcustomer_RT) {
                if(strEvent == 'Insert') {
                    boolEligibleRec = true;
                }
                else if(strEvent == 'Update') {
                    if(oldMapAccounts.get(iteratorAcc.id).Sales_office_via_interface__c != iteratorAcc.Sales_office_via_interface__c) {
                       boolEligibleRec = true;
                    }
                }
                if(boolEligibleRec) {
                    setSalesOfficeIds.add(iteratorAcc.Sales_office_via_interface__c);
                    if(mapSalesOfficeIdLstAccounts.containsKey(iteratorAcc.Sales_office_via_interface__c)) {
                        mapSalesOfficeIdLstAccounts.get(iteratorAcc.Sales_office_via_interface__c).add(iteratorAcc);
                    } else {
                        mapSalesOfficeIdLstAccounts.put(iteratorAcc.Sales_office_via_interface__c, new List<Account>{iteratorAcc});
                    }
                }
            }
        }
        //Modify Block for SalesOffice Ids
        if(setSalesOfficeIds.size()>0) {
            List<Sales_Office__c> lstSalesOffice = [select Id, Name from Sales_Office__c WHERE Id =: setSalesOfficeIds];
            for(Sales_Office__c iteratorSalesOffice: lstSalesOffice) {
                setSalesOfficeName.add(iteratorSalesOffice.Name);
                if(mapSalesOfficeLstAccounts.containsKey(iteratorSalesOffice.Name)) {
                    mapSalesOfficeLstAccounts.get(iteratorSalesOffice.Name).addAll(mapSalesOfficeIdLstAccounts.get(iteratorSalesOffice.id));
                } else {
                    mapSalesOfficeLstAccounts.put(iteratorSalesOffice.Name, mapSalesOfficeIdLstAccounts.get(iteratorSalesOffice.id));
                }
            }
        }
        List<Sales_Office_and_Group_Mapping__c> lstSalesAndGroupMapping = [select Name, Group__c from Sales_Office_and_Group_Mapping__c WHERE Name=: setSalesOfficeName];
        for(Sales_Office_and_Group_Mapping__c iteratorSalesAndGroupMapping: lstSalesAndGroupMapping) { 
            for(Account varIteratorAcc : mapSalesOfficeLstAccounts.get(iteratorSalesAndGroupMapping.Name)) {
                varIteratorAcc.Approval_Group__c = iteratorSalesAndGroupMapping.Group__c;
                lstAccountsToBeUpdated.add(varIteratorAcc);
            }
        }
    }
}

This is the trigger -

trigger AccountTrigger on Account (after delete, after insert, after update, before delete, before insert, before update)
    {
    TriggerFactory.createAndExecuteHandler(Account_TriggerHandler.class);

    }

& following is the TriggerFactory -

public with sharing class TriggerFactory
{
    /**
     * Public static method to create and execute a trigger handler
     *
     * Arguments:   Type t - Type of handler to instatiate
     *
     * Throws a TriggerException if no handler has been found.
     */
    public static void createAndExecuteHandler(Type t)
    {
        // Get a handler appropriate to the object being processed
        ITrigger handler = getHandler(t);

        // Make sure we have a handler registered, new handlers must be registered in the getHandler method.
        if (handler == null)
        {
            throw new TriggerException('No Trigger Handler found named: ' + t.getName());
        }

        // Execute the handler to fulfil the trigger
        execute(handler);
    }

    /**
     * private static method to control the execution of the handler
     *
     * Arguments:   ITrigger handler - A Trigger Handler to execute
     */
    private static void execute(ITrigger handler)
    {
        // Before Trigger
        if (Trigger.isBefore)
        {
            // Call the bulk before to handle any caching of data and enable bulkification
            handler.bulkBefore();

            // Iterate through the records to be deleted passing them to the handler.
            if (Trigger.isDelete)
            {
                for (SObject so : Trigger.old)
                {
                    handler.beforeDelete(so);
                }
            }
            // Iterate through the records to be inserted passing them to the handler.
            else if (Trigger.isInsert)
            {
                for (SObject so : Trigger.new)
                {
                    handler.beforeInsert(so);
                }
            }
            // Iterate through the records to be updated passing them to the handler.
            else if (Trigger.isUpdate)
            {
                for (SObject so : Trigger.old)
                {
                    handler.beforeUpdate(so, Trigger.newMap.get(so.Id));
                }
            }
        }
        else
        {
            // Call the bulk after to handle any caching of data and enable bulkification
            handler.bulkAfter();

            // Iterate through the records deleted passing them to the handler.
            if (Trigger.isDelete)
            {
                for (SObject so : Trigger.old)
                {
                    handler.afterDelete(so);
                }
            }
            // Iterate through the records inserted passing them to the handler.
            else if (Trigger.isInsert)
            {
                for (SObject so : Trigger.new)
                {
                    handler.afterInsert(so);
                }
            }
            // Iterate through the records updated passing them to the handler.
            else if (Trigger.isUpdate)
            {
              for (SObject so : Trigger.old)
                  {
                     handler.afterUpdate(so, Trigger.newMap.get(so.Id));
                  }
            }      
        }

        // Perform any post processing
        handler.andFinally();
    }

    /**
     * private static method to get the named handler.
     *
     * Arguments:   Type t - Class of handler to instatiate
     *
     * Returns:     ITrigger - A trigger handler if one exists or null.
     */
    private static ITrigger getHandler(Type t)
    {
        // Instantiate the type
        Object o = t.newInstance();

        // if its not an instance of ITrigger return null
        if (!(o instanceOf ITrigger))
        {
            return null;
        }

        return (ITrigger)o;
    }

    public class TriggerException extends Exception {}
}

Line 94 on the TriggerFactory is -

 handler.afterUpdate(so, Trigger.newMap.get(so.Id));

Line 27 on the TriggerFactory is -

execute(handler);

Line 3 on the Trigger is -

TriggerFactory.createAndExecuteHandler(Account_TriggerHandler.class);

The debug log reveals too less about the classes & I see validation & duplicate rules being loggedLink to debug log

9
  • 4
    Posting the Account_TriggerHandler might get you some suggestions - all depends what work you are doing in there as all the factory is doing is delegating to that class.
    – Keith C
    Jan 31, 2017 at 16:59
  • 2
    The stack trace tells you where the exception occurred, but isn't very useful in discerning why the exception occurred. The underlying problem is probably not with the code that you've provided (so far). The trigger handler for Account is where I'd start your search. The dev console's analysis perspective is also useful
    – Derek F
    Jan 31, 2017 at 16:59
  • Can you share the complete log? Maybe link to it somewhere off site if it is really large. Jan 31, 2017 at 17:48
  • 1
    @Bennie Host a file publicly? Github Gist, Dropbox, Onedrive. One of those would be my first port of call. I thought with the full log it might be possible to get a better sense of what is going on. Jan 31, 2017 at 18:22
  • 1
    @DanielBallinger I have provided the Link to Debug log
    – Jarvis
    Jan 31, 2017 at 19:37

1 Answer 1

12

I did a visualization of your debug log. It looks something like this:

Debug Log Visualization

Yes, it does look like some incomprehensible colored lines, but it is showing the repetition from the blocks of 200 records being processed through before update triggers (red), Validation (green), after update triggers (orange), workflow (purple).

If you look at it as a tree you can see where the time from each iteration is going.

enter image description here

Your before and after update triggers are contributing to the CPU usage, but the Workflow is using much more per iteration. In the tree above, you can see 31 milliseconds for the first before update AccountTrigger, 11 ms for the AfterUpdate AccountTrigger. Compare that to the 134 ms for the Account Workflow.

DuplicateDetector is also taking 40 ms per iteration.

Also, don't discount the validation steps even though each one only takes a small amount of time. 200 times that small amount of time can add up.

"Time spent evaluating formulas for validation rules or workflows are counted towards the limit." Source

The log you provided doesn't have the Workflow logging, but that is where I'd focus my attention. Often the thing that triggers a CPU limit is just the unfortunate bit of code that happened to be running at the time. See Dan Appleman's The Dark Art Of CPU Benchmarking from Dreamforce 2016.

8
  • You did a visualisation... How do you do that? That's pretty cool. Is this a new feature of API 39? Feb 1, 2017 at 0:23
  • 2
    @CasparHarmer It's something I'm working on that I'll make available to everyone. It's actually available now (for free) in the FuseIT SFDC Explorer. However, I'm still tweaking it. Getting the colours right is hard. There are also some weird bugs in the Apex logging. I want to have a similar, but slightly different, approach to the Developer Console. Plus it will be happy to parse any log from raw text. Feb 1, 2017 at 0:28
  • Very nice! Anything that makes those horrible logs look better is a boon! I'm going to take a look now. Feb 1, 2017 at 0:30
  • @CasparHarmer Feedback is welcome. Especially if someone has skills with colour coding the events. Feb 1, 2017 at 0:31
  • 1
    Thanks Dan! You were bang on, the issue was with Workflow, someone checked Re-evaluate Workflow Rules after Field Change to True with a poor logic.
    – Jarvis
    Feb 1, 2017 at 14:17

You must log in to answer this question.

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