4

Background: The zqu__Quote__c object in our org has two child objects from another managed package we use for eSignature and credit card capture. One of those two child objects collects the Credit Card GUID/Token, the other collects the Signature information (Sign Date,Signer Name, etc). We have a need to copy the Credit Card GUID and the Signer Information up to the Quote, but because this information lives on two child objects that are both updated nearly-simultaneously in separate transactions, we often hit a race condition and one of the two updates fails, which results in the quote not being processed.

Since these child records have a relationship to zqu__Quote__c, I never actually need to query for the Quotes when attempting to update them. I typically just do something like this:

quoteList.add(new zqu__Quote__c(Id = recId,Field_Name__c = :VAL);
update QuoteList;

I would like to ensure that in 99.9% of cases, both updates will be successful, to the point where I want to invoke another transaction to try again if the previous transaction couldn't get either a query lock or a DML lock.

I've created the below Queueable class to achieve this, and want to know:

  1. Is this a good, recursive model that will continue to spawn new transactions until it successfully updates the records (barring any unrelated/unexpected DML Errors)?
  2. Is it completely unnecessary to do the inner try{}catch{} for UNABLE_TO_LOCK_ROW DmlExceptions under the presumption that I should never hit that error since my SOQL query obtained an exclusive lock?
  3. Are there any additional checks or logic changes I should make to increase my success rate?

Code:

public class updateQuotes implements Queueable {
    private Map<Id,zqu__Quote__c> quotes;

    public updateQuotes(Map<Id,zqu__Quote__c> quotes) {
        this.quotes = quotes;
    }

    public void execute(QueueableContext context) {
        System.debug('Start ZQuoteUtil.updateQuotes');
        Boolean isLocked = false;
        Id recordId;
        List<zqu__Quote__c> quotesToUpdate = new List<zqu__Quote__c> ();
        List<Log__c> logs = new List<Log__c>();
        try{
            quotesToUpdate = [SELECT Id FROM zqu__Quote__c WHERE Id IN :quotes.keySet() 
                                 FOR UPDATE];
            for(zqu__Quote__c q : quotesToUpdate){
                recordId = q.Id;
                q = quotes.get(recordId);
            }
            try{
                update quotesToUpdate;
            }catch(DmlException e){
                String msg = e.getMessage();
                if(msg.containsIgnoreCase('UNABLE_TO_LOCK_ROW')){
                    isLocked = true;
                }else{
                    logs.add(LogUtil.createLog(LogUtil.ERROR_STR, msg,'ZQuoteUtil', 'updateQuotes'));
                }
            }
        }catch(QueryException e){
            isLocked = true;
        }

        if(isLocked){
            ZQuoteUtil.updateQuotes tryAgain = new ZQuoteUtil.updateQuotes(quotes);
            if (!Test.isRunningTest()) {
                System.enqueueJob(tryAgain);
            }
        }

        System.debug('End ZQuoteUtil.updateQuotes');
    }
}
  • 2
    N.B. (1) queueable constructor should just be Ids, not entire object (2) the retry by re-enqueuing will fail if the initial queueable was initiated by an async transaction (like a batchable as queue chain depth can't be > 1; (3) I would not use the Test.isRunningtest as you can't assert in test methods that your retry logic works - use a different gating mechanism – cropredy Jan 9 '20 at 22:13
  • 1
    My concern would be running out of async executions in your 24hr limit reset period. I guess it depends how often you update these values. (I guess this is mitigated if you first try the DML operation in the normal flow and only resort to the queueable on lock failure.) – Phil W Jan 9 '20 at 22:14
  • 1
    To get around the queueable initiation issue, mentioned by @cropredy, you can use a combination of future and queueable (alternating as needed). This does, of course, mean you really do have to only use IDs since futures can only accept simple parameters. – Phil W Jan 9 '20 at 22:19
  • @cropredy Queuable methods can accept non-primitives (developer.salesforce.com/docs/atlas.en-us.apexcode.meta/…). In this case I prefer that it accepts objects because my intent is to pass the records with updated values to the queueable method for DML.The Test.IsRunningTest() is there based on this suggestion from sfdcfox: (salesforce.stackexchange.com/questions/149541/…) - has this changed since 2016? – Morgan Marchese Jan 9 '20 at 22:20
  • 1
    You'll find the 250,000 per 24hr limit documented here. Search for "maximum number of asynchronous Apex method executions" in that page. – Phil W Jan 9 '20 at 22:30

Your Answer

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

Browse other questions tagged or ask your own question.