1

I have a class that's used to prioritize leads in a lead queue based on rules that our business users can create and manage. One method in particular is used to find the first record in the prioritized queue and assign ownership to a given user ID (I'm not including the code for getNextId(), but I can confirm that it's properly returning a valid lead ID, or null if the lead queue is currently empty):

public with sharing class LeadPriorityQueue extends SObjectPriorityQueue {

    ....

    public override Id transferNextRecord(Id userId) {
        Id nextId = this.getNextId();
        if (nextId != null) {
            Lead nextLead = [SELECT Id FROM Lead WHERE Id = :nextId FOR UPDATE];

            try {
                nextLead.OwnerId = userId;
                update nextLead;
            }
            catch (DmlException e) {
                this.transferNextRecord(userId);
            }
        }
        return nextId;
    }

    ....

}

As a note, the query contained in getNextId() has a mandatory ORDER BY clause, so I'm forced to lock the record in a separate query after obtaining the ID.

Anyway, this works well. Parallel requests occasionally receive the same lead ID from getNextId(), but locking the record and catching the DmlException works nicely.

I now have the requirement to call transferNextRecord(Id userId) from a custom button, and as a result was forced to convert it to a web service method:

global with sharing class LeadPriorityQueue extends SObjectPriorityQueue {

    ....

    webService static Id transferNextRecord(Id userId) {
        LeadPriorityQueue leadQueue = new LeadPriorityQueue();
        Id nextId = leadQueue.getNextId();
        if (nextId != null) {
            Lead nextLead = [SELECT Id FROM Lead WHERE Id = :nextId FOR UPDATE];

            try {
                nextLead.OwnerId = userId;
                update nextLead;
            }
            catch (DmlException e) {
                LeadPriorityQueue.transferNextRecord(userId);
            }
        }
        return nextId;
    }

    ....

}

This time around, the lock isn't being respected. If two users click the custom button within ~1-2 seconds of one another, both users get the same lead ID back and the user to click the button last wins ownership. I understand that SF docs state that a second request to lock a record will wait up to 10 seconds for the first lock to be released (in practice, the second user has to wait ~3-5 seconds for the first transaction to complete). My questions are these:

  1. With the locking process as it is, why would the non-static method throw a DmlException upon the attempted update of the locked record, while the static method waits for the prior lock to be released?

  2. The behavior of the static method is not desirable. How can I ensure that a second attempt to lock the record fails so a new lead ID can be requested?

Thanks!!

  • Off the top of my head, after the FOR UPDATE SOQL query completes the web service version should recheck that the nextId it has is still valid from the LeadPriorityQueue. I.e. Get records Id to process, Get lock to record to record, verify that the record hasn't been processed while waiting for the lock, do the actual work. – Daniel Ballinger Jun 23 '15 at 3:29
0

From Locking Statements:

Locking Considerations

  • If you attempt to lock a record currently locked by another client, your process waits for the lock to be released before acquiring a new lock. If the lock isn’t released within 10 seconds, you will get a QueryException. Similarly, if you attempt to update a record currently locked by another client and the lock isn’t released within 10 seconds, you will get a DmlException.

I believe this is a timing issue. The code is relying on the DmlException being thrown to prevent the subsequent lead query from updating the OwnerId.

However, this isn't guaranteed. It's possible for the second call to transferNextRecord to wait within the 10 second window for the lock to be released. If this does occur, the second call will proceed to change the OwnerId that was just set.

The timing in the web service context is likely different from the UI context.

I'd suggest checking if the lead id is still in the lead queue after you successfully get the lock on the lead. If it is proceed as usual. If not, you will want to drop your lock on the current lead before looking for the next lead.

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.