1

We have a queueable framework where we have an object AsyncRequest__c
Fields Are : ApexClassName__c, Content__c, AttachmentAvailable__c, Status__c etc

So whenever someone wants to enqueue a job they create a record in AsycnRequest__c with their handlerName in ApexClassName.

The handler implements an interface.

public interface AsycnRequestHandler {

    void execute(AsyncRequest__c queueItem);

}

And this gets called from my Queueable class which also implements Finalizer.

public without sharing class QueueableasycnRequestDispatcher implements Finalizer, Queueable, Database.AllowsCallouts {

    private AsyncRequest__c  asycnRequest;

    public QueueableasycnRequestDispatcher(AsyncRequest__c  asycnRequest) {
        this.asycnRequest = asycnRequest;
    }

    public void execute(QueueableContext queueableContext) {
        QueueableasycnRequestDispatcher finalizer = new QueueableasycnRequestDispatcher(asycnRequest);
        System.attachFinalizer(finalizer);
        asycnRequestHandler handler = (asycnRequestHandler) Type.forName(asycnRequest.ApexClassName__c).newInstance();
        try {
            handler.execute(asycnRequest);
            asycnRequest.Status__c = asycnRequestConstants.QUEUE_STATUS_COMPLETE;
        } catch (Exception objException){
            asycnRequest.Status__c = asycnRequestConstants.QUEUE_STATUS_FAILED;
            asycnRequest.Error__c = 'Failed with exception => ' + objException.getMessage() + ' at line ' + objException.getLineNumber() + ' with stacktrace as => ' + objException.getStackTraceString();
        }

    }


    public void execute(FinalizerContext finalizerContext) {
        if(finalizerContext.getResult() == ParentJobResult.SUCCESS && String.isNotEmpty(asycnRequest.Status__c) && !asycnRequest.Status__c.equalsIgnoreCase(asycnRequestConstants.QUEUE_STATUS_FAILED)){
            asycnRequest.Status__c = asycnRequestConstants.QUEUE_STATUS_COMPLETE;
        } else if (finalizerContext.getResult() == ParentJobResult.UNHANDLED_EXCEPTION){
            Exception objException = finalizerContext.getException();
            asycnRequest.Status__c = asycnRequestConstants.QUEUE_STATUS_FAILED;
            asycnRequest.Error__c = 'Failed with exception => ' + objException.getMessage() + ' at line ' + objException.getLineNumber() + ' with stacktrace as => ' + objException.getStackTraceString();
        }
        Database.update(asycnRequest);
        asycnRequestExecutor.execute();
    }
}

Now if someone tries to update Setup object in their handler's execute method, the finalizer update is failing with MIXED DML, I ran a test class and saw the behaviour.

Isn't Finalizer suppose to separate the transaction? My framework was built under this assumption. For now the update Database.update(asycnRequest); needs to happen in future or queueable. But any other ideas or any way to detect if the transactions is already in setup object, then only execute this in Async mode.

I know I can add a try catch on the update and check for MIXED_DML text and then fire Async operation. But any other way apart from try catch?

Updating Test class and Sample handler class

@IsTest
static void testBehaviorWithSetupObject() {
    Profile p = [SELECT Id FROM Profile WHERE Name='System Administrator'];
    User u1 = new User(
            Alias = 'abc',
            Email='[email protected]',
            EmailEncodingKey='UTF-8',
            FirstName='SampleQueueItemHandler',
            LastName='Test',
            LanguageLocaleKey='en_US',
            LocaleSidKey='en_GB',
            ProfileId = p.Id,
            TimeZoneSidKey='Europe/London',
            Username='[email protected]',
            IsActive=true
    );
    insert u1;
    Test.startTest();
    QueueItemUtils.createQueueItem('SampleQueueItemHandler', 'SampleQueueItemHandler', JSON.serialize(u1), 6, 'Queueable', true);
    Test.stopTest();
}

public with sharing class SampleQueueItemHandler implements QueueItemHandler {

    public void execute(AsycnRequest__c queueItem) {
        if(queueItem.Priority__c == 6) {
            User deserializedUser = (User) JSON.deserialize(queueItem.Content__c, User.class);
            List<User> users = [SELECT Id,FirstName FROM User WHERE Id = :deserializedUser.Id];
            System.debug('users -> '+users);
            users.get(0).FirstName = '[email protected]';

            update users;
            List<User> usersAfterUpdate = [SELECT Id,FirstName FROM User WHERE Id = :deserializedUser.Id];
            System.debug('usersAfterUpdate -> '+usersAfterUpdate);
        }
    }
}

And the exception occurs

System.DmlException: Update failed. First exception on row 0 with id a1J0k000001IdlAEAS; first error: MIXED_DML_OPERATION, DML operation on setup object is not permitted after you have updated a non-setup object (or vice versa): AsyncRequest__c, original object: User: [] Class.QueueableasycnRequestDispatcher.execute: line 36, column 1

Which is Database.update(queueItem); in Finalizer.

1 Answer 1

2

You have two problems.

First, you must attach the Finalizer before doing anything that might crash; ideally, this means your Finalizer should be in the first few lines of the execute method:

public void execute(QueueableContext queueableContext) {
    System.attachFinalizer(this);
    asycnRequestHandler handler = (asycnRequestHandler) Type.forName(asycnRequest.ApexClassName__c).newInstance();
    try {
        // ...

See the examples in the documentation.

Second, in a unit test, you may need to use System.runAs to separate normal DML from Setup DML. While it's true that the Finalizer will run in a separate transaction, whatever you're doing wrong has nothing to do with this Finalizer.

Note that you already have a try-catch block, so theoretically, nothing short of governor limits should break your code anyways.


Edit: A try-catch is still needed to prevent the test from failing:

@IsTest
static void testBehaviorWithSetupObject() {
    Profile p = [SELECT Id FROM Profile WHERE Name='System Administrator'];
    User u1 = new User(
            Alias = 'abc',
            Email='[email protected]',
            EmailEncodingKey='UTF-8',
            FirstName='SampleQueueItemHandler',
            LastName='Test',
            LanguageLocaleKey='en_US',
            LocaleSidKey='en_GB',
            ProfileId = p.Id,
            TimeZoneSidKey='Europe/London',
            Username='[email protected]',
            IsActive=true
    );
    insert u1;
    Test.startTest();
  try {
    QueueItemUtils.createQueueItem('SampleQueueItemHandler', 'SampleQueueItemHandler', JSON.serialize(u1), 6, 'Queueable', true);
    Test.stopTest();
  } catch(Exception e) {
  // Check the results here
  }
}
10
  • I wanted the code to break in test. Because in real world scenario, a user can be updated in execute, so will that fail? or it it only test class which fails and in real world things will work fine? Commented Dec 23, 2021 at 3:45
  • That finalizer change was the first thing that jumped out at me as well.
    – Adrian Larson
    Commented Dec 23, 2021 at 3:48
  • @NagendraSingh We need to see your unit test code to provide specific guidance, but you should be able to get it to work correctly. I'll write up a quick demo for you.
    – sfdcfox
    Commented Dec 23, 2021 at 3:55
  • From Test the code does break System.DmlException: Update failed. First exception on row 0 with id a1J0k000001IdlAEAS; first error: MIXED_DML_OPERATION, DML operation on setup object is not permitted after you have updated a non-setup object (or vice versa): AsyncRequest__c, original object: User: [] Class.QueueableasycnRequestDispatcher.execute: line 36, column 1 when updating AsycnRequest__c Commented Dec 23, 2021 at 3:55
  • @NagendraSingh Did you fix the finalizer as I specified? If not, I'd expect this to be a problem.
    – sfdcfox
    Commented Dec 23, 2021 at 3:56

You must log in to answer this question.

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