0

Working with a consultant on a batch class that performs operations on the user object. We are working on implementing error handling. We must use platform events to handle errors to avoid mixed DML errors, since we are on the user object.

We are using allOrNone=false in database.update

The consultant has wired up a custom platform event for error logging, instead of using the BatchApexErrorEvent.

It seems to me like using the standard BatchApexErrorEvent would be preferable, but looking for input on whether I am missing something.

The database.update is not in a try catch block. I believe BatchApexErrorEvent only fires for uncaught errors, so it should fire.

Iterating over the saveResult objects appears to be working, but as we are at an early stage I would like to ensure we are going down the correct path.

Pasting the full code below, warts and all...

global class License_Downgrade implements Database.Batchable<sObject>,Database.RaisesPlatformEvents {
    
    global Database.QueryLocator start(Database.BatchableContext BC){
        // Query all active users with Profile : 'Base Programs'
        
        List<AuthSession> AuthSesuserLst =[Select UsersId,Users.ProfileId,Users.UserPermissionsMarketingUser,Users.Name, LastModifiedDate,LoginType,createddate,SessionType From AuthSession Where parentId = null Order by lastmodifieddate];
        Set<Id> excludeIds = new Set<Id>();
        for(AuthSession us :AuthSesuserLst){
            excludeIds.add(us.usersId);  
        }
        String query = 'SELECT UserPermissionsMarketingUser,Previous_Permission_Sets__c,Previous_Permission_Sets_inActive__c, Username, Id, Name, ProfileId,Profile.Name FROM User WHERE Profile.Name =\'Base Programs\' AND isActive=True AND Id NOT IN : excludeIds LIMIT 20';
        return Database.getQueryLocator(query);
    }
    
    global void execute(Database.BatchableContext BC, List<User> userLst){
        System.debug('User list ' + userLst );
        Set<Id> userIds = new Set<Id>();
        List<PermissionSetAssignment> permissionSetAssingmentList = new List<PermissionSetAssignment>(); // to assign prior permissions
         String permissionSetToAssign;
        Map<String,User> userMapList = new Map<String,User>();
        List<UserLicense> usLiceLst = [SELECT Id, TotalLicenses, UsedLicenses, Name, MasterLabel, Status FROM UserLicense WHERE MasterLabel = 'Salesforce Platform'];
        Integer totalLic = usLiceLst[0].TotalLicenses;  // Total Licenses
        Integer usedLic = usLiceLst[0].UsedLicenses;    // Used Licenses
        Integer remLic =  totalLic - usedLic;           // Remaining Licenses
        system.debug('Total licenses ' + usLiceLst );
        system.debug('remaining licenses ' + remLic );
        // check for remaining licenses to update user profiles
        List<User> finalUserList = new List<user>();
        String priorPermList;
        // Query profile Id 
        Profile  p = [SELECT Id, Name FROM Profile WHERE Name= 'Ashoka Platform User' LIMIT 1];
        Integer count = 0;
        for(User uRec : userLst){  
            uRec.ProfileId = p.Id;                        // new profileId
            uRec.UserPermissionsMarketingUser = false; 
            if(uRec.Previous_Permission_Sets__c != ''){
                uRec.Previous_Permission_Sets__c = '';
            }
            count +=1;
            if(count <= remLic){                         // check for remaining licenses
                finalUserList.add(uRec);                 // user lists to update
                userIds.add(uRec.Id);
                userMapList.put(uRec.Id,uRec);
                // assign prior permission set when switched to platform User
                if(uRec.Previous_Permission_Sets_inActive__c != null){
                    List<String> permissionSetIdsList = uRec.Previous_Permission_Sets_inActive__c.split(','); // getting all Ids into list.
                    for(String str : permissionSetIdsList){
                        system.debug('str ' + str);
                        if(str != ''){
                            permissionSetAssingmentList.add( new PermissionSetAssignment(AssigneeId = uRec.Id,PermissionSetId = str ) );  // permission set Assingment
                        }
                    }  
                }
            }
        }
        
        // taking backup of existing Permission sets assigned to user before update 
        for(PermissionSetAssignment psa : [SELECT Id, PermissionSetId, PermissionSetGroupId, AssigneeId FROM PermissionSetAssignment WHERE AssigneeId IN : userIds AND PermissionSet.ProfileId = null ]){
            User uRec = userMapList.get(psa.AssigneeId);
            if(uRec.Previous_Permission_Sets__c == null){
                uRec.Previous_Permission_Sets__c = '';
            }
            uRec.Previous_Permission_Sets__c =  uRec.Previous_Permission_Sets__c  + ',' + psa.PermissionSetId ;
            //system.debug('uRec.Previous_Permission_Sets__c ' + uRec.Previous_Permission_Sets__c);
        }       
        List<Error_Log_Event__e> errLst = new List<Error_Log_Event__e>();
        try{
            if(finalUserList.size() > 0){
                System.debug('update users ' + finalUserList );
                Database.SaveResult[] srList = Database.update(finalUserList, false);//Update finalUserList;                            // Update User records
                
                // Iterate through each returned result
                for(Integer i = 0; i < finalUserList.size(); i++){
                  Database.SaveResult sr = srList[i]; 
                    if (!sr.isSuccess()) {
                        // Operation failed, so get all errors                
                        for(Database.Error err : sr.getErrors()) {
                            errLst.add(new Error_Log_Event__e(Error_Type__c = 'License Downgrade',Error__c = err.getMessage(),UserId__c = finalUserList[i].Id ) );
                  }
                    }
                    
                }
                if(permissionSetAssingmentList.size() > 0){
                    
                    // Assign all Permission 
                   // AssignPermissionsClass.assignPermissionSets(permissionSetAssingmentList);
                    insert permissionSetAssingmentList;
                }
            }else{
                system.debug('All remaining licenses are used');
            }
        }
        catch(dmlException e){
            System.debug('error ' + e);
            //List<Error_Log_Event__e> errLst = new List<Error_Log_Event__e>();
            errLst.add(new Error_Log_Event__e(Error_Type__c = 'License Downgrade',Error__c = String.valueof(e) ) );
            //List<Database.SaveResult> results = EventBus.publish(errLst);
            //utils_logging.writeToLog(e);
        }
        finally{
            if(errLst.size() > 0){
            List<Database.SaveResult> results = EventBus.publish(errLst);
          } 
        }
    }
    
    global void finish(Database.BatchableContext BC){
        try{
            CronTrigger schClass = [SELECT Id,CronJobDetail.Name,CronJobDetail.Id,State FROM CronTrigger where CronJobDetail.Name='downgrade - every 10 minutes'AND State !='COMPLETE'];
            System.debug('schClass ' + schClass);
            system.abortJob(schClass.Id);
            DateTime scheduleTime = Datetime.now().addMinutes(10) ;
            system.debug('scheduleTime ' + scheduleTime);
            String hour = String.valueOf(scheduleTime.hour() );
            String min = String.valueOf(scheduleTime.minute() ); 
            String ss = String.valueOf(scheduleTime.second());
            system.debug('Datetime.now() ' + Datetime.now());
            //parse to cron expression
            String nextFireTime = ss + ' ' + min + ' ' + hour + ' * * ?';
            system.debug('nextFireTime ' + nextFireTime);
            Schedule_License_Downgrade sch = new Schedule_License_Downgrade();
            System.schedule('downgrade - every 10 minutes', nextFireTime, sch);
        }catch(exception e){
            utils_logging.writeToLog(e, 'License Downgrade', 'Schedule Downgrade');
        }
    }
    
    
}
  • It seems to me like using the standard BatchApexErrorEvent would be preferable - what are your decision criteria to rank preference? – identigral Aug 12 at 18:24
  • generally prefer to use standard unless there is any advantage to custom. i suppose you could end up with lots of triggers on the standard event. but i'm assuming one can get more info out of the standard event, that isnt possible with custom- there is a lot in the docs. i am not building this myself so cant test it, but my hunch is that standard is better. – gorav Aug 12 at 20:38
  • How do you decide on whether a custom platform event has "any advantage" over standard? – identigral Aug 12 at 20:44
  • If the behavior is identical, I would say use standard. If there is more that I can do with either, then I would evaluate what functionality each offers. I am not sure if the standard event will be triggered when using allOrNone=false. It seems like the standard event is meant to be used without try/catch or partial save, but it is not clear to me. So i am looking for clarity. – gorav Aug 13 at 10:42
1

You have some competing tools at play here:

  • Database partial success (allOrNothing = false)

  • BatchApexErrorEvent

    • Supports uncaught custom exceptions
    • Supports uncaught system exceptions
    • Supports uncatchable Limits exceptions

For partial successes, you need some place to save the failure information without rolling back the execute() transaction. Platform Events would be one way to do this or the batchable could just do DML to indicate what needs to be retried.

But for other more serious exceptions, if you want to record what to replay, you need BatchApexErrorEvent.

You should take a look at Andrew Fawcett's Batch retry framework. This suggests a common method that can be used by both the BatchApexErrorEvent trigger subscriber and your partial success logic within the batchable execute().

| improve this answer | |
  • thanks that clears things up. i must use platform events as i'm working on user object and get mixed dml error if i log to an object. seems like using custom events makes sense for dealing w partial success, and batchApexErrorEvent to deal with any uncaught execptions. – gorav Aug 14 at 20:38
  • @gorav -- wouldn't queueable also work in lieu of platform events? Not saying that is better, just an option – cropredy Aug 14 at 22:37
  • Yes true I believe future or queueable should work also to get around mixed dml. No sense of cost benefits between those options, I guess any of them will work. – gorav Aug 15 at 1:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.