You can keep the future methods, just make sure you use FOR UPDATE
in your methods. If you have the spare governor limits, you can even implement a spin wait:
sObject[] results;
while(results == null) {
try {
results = [SELECT Id FROM sObject WHERE Id = :idValues FOR UPDATE];
} catch(QueryException e) {
}
}
When you use FOR UPDATE
, you get up about 10 seconds before an exception is thrown. This means you can use as little as 6 SOQL per minute towards the governor limit of 100 to wait for those records to free up. As a bonus, the FOR UPDATE
time doesn't count against the CPU 60,000ms limit, either.
Or, you can go with Queueable, giving you the safety net of a Transaction Finalizer plus the ability to chain indefinitely if your updates must absolutely pass. In practice, this is almost certainly more than enough safety for even the busiest databases.
That said, combining the three methods into one might be just the solution. You can immediately re-query the records you need to get the lock, then do whatever extra work you want to do with the available expanded governor limits. For example:
class AsyncWrapper {
Set<Id> doThing1 = new Set<Id>();
Set<Id> doThing2 = new Set<Id>();
Set<Id> doThing3 = new Set<Id>();
}
AsyncWrapper thingsToDo = new AsyncWrapper();
for(sObject record: records) {
if(conditionsMetForThing1(record)) {
thingsToDo.doThing1.add(record.Id);
}
if(conditionsMetForThing2(record)) {
thingsToDo.doThing2.add(record.Id);
}
if(conditionsMetForThing3(record)) {
thingsToDo.doThing3.add(record.Id);
}
}
if(thingsToDo.doThing1.size() > 0 ||
thingsToDo.doThing2.size() > 0 ||
thingsToDo.doThing3.size() > 0) {
System.enqueueJob(new AsyncThingDoer(thingsToDo));
}
At which point, you can do all the things in one transaction, and save on some repetition:
static sObject[] getRecordsFromMap(Map<Id, sObject> source, Set<Id> targetIds) {
Map<Id, sObject> dupMap = source.clone();
dupMap.keySet().retainAll(targetIds);
return dupMap.values();
}
public void execute(QueueableContext context) {
Map<Id, sObject> recordsToProcess = new Map<Id, sObject>([
SELECT Whatever
FROM Whichever
WHERE Id = :data.doThing1 OR Id = :data.doThing2 OR Id = :data.doThing3
FOR UPDATE
]);
sObject[] doThing1Records = getRecordsFromMap(recordsToProcess, data.doThing1);
sObject[] doThing2Records = getRecordsFromMap(recordsToProcess, data.doThing2);
sObject[] doThing3Records = getRecordsFromMap(recordsToProcess, data.doThing3);
doThing1(doThing1Records);
doThing2(doThing2Records);
doThing3(doThing3Records);
}
Of course, if you need to do callouts, then you may need to break up the functions and reorder, but this is possible. Or, you could have the queueable chain to another queueable, thus guaranteeing that the first queueable will be done before the next starts.
The specifics of what you need to do to properly get your updates working are dependent on the fine details, but hopefully there's enough options here to get you started.