Use Case
- After insert triggers start Queueable
- Queueable
execute()
contends for same resource, uses spin lock to wait for resource availability. Many queueables could be running at same time. - Exception thrown when number of retries exceeded in spin lock
Spin lock (hat tip to @sfdcfox for this)
public class LocksServiceImpl implements ILocksService {
/**
* acquire - Obtain a lock that prevents other transactions from executing
*/
public void acquire(String lockItem) {
Integer retryCount = 0;
Exception error;
while (retryCount < LocksService.MAX_RETRIES) {
try {
// FOR UPDATE will pause this Txn until lock is freed by other transaction
Mutex__c[] mutexes = MutexesSelector.newInstance().selectForUpdateByItem(new Set<String> {lockItem});
if (mutexes.isEmpty()) {
insert new Mutex__c (Lockable_Item__c = lockItem);
}
return; // this Txn now has the lock on lockItem
}
catch (Exception e) {
if (Util.isTransientException(e)) { // UNABLE_TO_LOCK_ROW
error = e;
retryCount++;
}
else {
error = e;
break;
}
}
}
throw new LocksService.MutexException('Unable to obtain a transaction lock on ' + lockItem +
' retries:' + retryCount + ' v. maxRetries:' + LocksService.MAX_RETRIES + ' exception:' + Util.showException(error));
}
}
Challenge
- This works OK most of the time but sometimes the spin exceeds MAX_RETRIES and throws an exception. I'd like to implement some form of backoff logic to delay the spin lock retry rather than immediately repeat the
SELECT ..FOR UPDATE
- In fact, the higher the concurrency of queueables, the more likely this occurs
- I looked at some promising backoff algos like exponential and jitter
But how to variable delay in the most effective way and still get best performance?
Options
- CPU sleep for n secs - not great as limit of 10 CPU secs per Txn and I don't want to use that limit up while spin locking
- SOQL querying
n
records (say, the oldestn
Contacts - we have 10E6+ of these) - doesn't use CPU but burns SOQL rows that the transaction might need - Spin lock more than 10 times (current max retry) - doesn't help if multiple queueables are concurrently running - they all spin lock at the same time and all run out of retries at same time.
- Delay (using #1 or #2) the execution of System.enqueueJob in the after insert trigger. Maybe; the trigger is lightweight but I have no control over when SFDC starts the queueables so contention between queueables can still happen.
- Single thread all the queueables through a custom object pattern like Dan Appleman's Async Pattern (this is my fallback as that pattern is implemented at our org).
How can I do something of variable expensiveness (in elapsed time) that consumes the fewest SFDC limits?