6

I'm taking a look at running 300+ test classes in parallel again today. The main error - in about 5% of the test classes - was:

UNABLE_TO_LOCK_ROW, unable to obtain exclusive access to this record

which makes sense given the parallel execution. But the question is how to fix these. There seem to be 2 root causes...

Contact locks

The tests have Contact record inserts that had no Account specified and there is some underlying Account defaulting trigger logic. I am assuming that the lock is on the parent Account so that specifying a separate Account every time will fix. Does that seem right? It appears to work.

Custom setting locks (and User locks)

Some tests modify hierarchical custom settings to exercise some of our configuration options and so update the org-level custom settings at the same time, hence the locking problem. (The current User is also modified in a smll number off cases.) No easy solution comes to mind for these: is there a good pattern for this case? There is this known issue on the subject Document known issue with UNABLE TO LOCK ROW when testing Custom Settings.

Presently trying this ugly code for the custom setting upserts in the tests:

/**
 * When tests are run in parallel, UNABLE_TO_LOCK_ROW errors occur where tests update the same custom setting.
 * This class aims to get around that by retrying over a longish time.
 */
public class Retry {

    public static void upsertOnUnableToLockRow(SObject sob) {

        // Only ever seen one retry needed this should be lots
        Integer n = 20;
        Integer sleepMs = 100;

        Exception lastException;
        for (Integer i = 0; i < n; i++) {
            try {
                upsert sob;
                return;
            } catch (DmlException e) {
                if (!e.getMessage().contains('UNABLE_TO_LOCK_ROW')) {
                    throw e;
                }
                lastException = e;
                sleep(sleepMs);
            }
        }

        throw lastException;
    }

    private static void sleep(Integer ms) {

        Long start = System.currentTimeMillis();
        while (System.currentTimeMillis() < start + ms) {
            // Throw away CPU cycles
        }
    }
}
  • Are you using SeeAllData=true? Have you tried row-locking statements (e.g. SELECT ... FROM CustomSetting__c FOR UPDATE)? – sfdcfox May 13 at 13:10
  • Hi @sfdcfox, Not using SeeAllData=true. Also seems as if the custom setting rows already have an implicit for update; – Keith C May 13 at 13:12
  • Keith - for the custom settings, why not use a selector pattern for these and then mock the selector results rather than directly trying to update via DML the custom settings? – cropredy May 13 at 17:09
  • Hi @cropredy, Yeah with 20-20 hindsight a good idea, but lots of code touching the objects and fields directly so significant work/risk to do such a refactor. – Keith C May 13 at 17:16
  • So sorry -- all of our org's custom metadata and custom settings are behind selector layers so they can be mocked; the issue you stated in OP happens to us with creating User records to test runAs (and mocking through selector layers doesn't help here) – cropredy May 13 at 17:41
0

With the caveat that a mocking solution is the better way to go, below is the class I ended up using to workaround the custom setting UNABLE_TO_LOCK_ROW errors. A bit more info here too https://force201.wordpress.com/2019/05/14/embracing-apex-parallel-testing/.

/**
 * When tests are run in parallel, UNABLE_TO_LOCK_ROW errors occur where tests update the same custom setting.
 * This class aims to get around that by retrying.
 * Can also be applied to ordinary SObjects.
 */
public class Retry {

    // Typically zero or one retry so this should be plenty unless there is some kind of deadlock
    private static final Integer TRIES = 50;
    private static final Integer SLEEP_MS = 100;

    public class RetryException extends Exception {
    }

    public static void upsertOnUnableToLockRow(SObject sob) {

        upsertOnUnableToLockRow(new SObject[] {sob});
    }

    public static void upsertOnUnableToLockRow(SObject[] sobs) {

        if (sobs.size() == 0) return;

        Long start = System.currentTimeMillis();
        Exception lastException;

        for (Integer i = 0; i < TRIES; i++) {
            try {
                SObject[] inserts = new SObject[] {};
                SObject[] updates = new SObject[] {};
                for (SObject sob : sobs) {
                    if (sob.Id != null) updates.add(sob);
                    else inserts.add(sob);
                }
                insert inserts;
                update updates;
                return;
            } catch (DmlException e) {
                // Immediately throw if an unrelated problem
                if (!e.getMessage().contains('UNABLE_TO_LOCK_ROW')) throw e;
                lastException = e;
                sleep(SLEEP_MS);
            }
        }

        Long finish = System.currentTimeMillis();
        throw new RetryException(''
            + 'Retry.upsertOnUnableToLockRow failed first id='
            + sobs[0].Id
            + ' of '
            + sobs.size()
            + ' records after '
            + TRIES
            + ' tries taking '
            + (finish - start)
            + ' ms with exception '
            + lastException
        );
    }

    private static void sleep(Integer ms) {

        Long start = System.currentTimeMillis();
        while (System.currentTimeMillis() < start + ms) {
            // Throw away CPU cycles
        }
    }
}

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.