1

I just want to confirm my understanding is correct of whether or not Queueable Apex will alleviate an issue I am having with @future methods.

Right now, in my org's account trigger handler class, we do various separate complex queries/calculations/DML operations. Everything is bulkified properly, but we have 3 hefty methods that all have been written with the @future annotation. They all do very different things and not necessarily on the same List of accounts so it wouldn't make sense for them to all operate in one method.

When these triggers fire on large accounts, and the 3 future methods get fired, I am often getting Apex errors in my org saying ": System.QueryException: Record Currently Unavailable: The record you are attempting to edit, or one of its related records, is currently being modified by another user. Please try again." I assume this is because the async code for these 3 occasions is running at the same moment.

If I made all three of these methods into separate queueable classes, and invoked them from my trigger once I got the records I needed to call it on, would this, in theory, remove any deadlocking issues if implemented correctly? Or is there another issue I should be addressing?

Thanks

1
  • 3
    No, it wouldn't. You need an explicit solution in your architecture that considers the possibility and/or prevents the deadlock This could be custom code, existing code that strings components and/or calls together in some way, automation that is not code, data model changes, etc.
    – identigral
    Commented Mar 6, 2023 at 16:57

3 Answers 3

5

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.

2
  • Thanks for all that - question about FOR UPDATE though. I noticed another dev did put a FOR UPDATE in two of the 3 future methods in question. Though I have seen exceptions thrown both on the FOR UPDATE methods and the non-FOR UPDATE method. When one of the FOR UPDATE ones fails, I see the "record modified by another user" exception. Occasionally, the one that does NOT have a FOR UPDATE fails, and it says "UNABLE_TO_LOCK_ROW, unable to obtain exclusive access to this record: []". Is it just that the inconsistency is causing the errors? If I add FOR UPDATE to the 3rd will that help?
    – Chris
    Commented Mar 6, 2023 at 19:16
  • @Chris Adding FOR UPDATE will help, certainly, but if you're consistently getting other locking exceptions, using a chained batchable instead of future methods can alleviate the situation. I also generally recommend queueable anyways, because you can attach Finalizers and also control the chain/order of execution by having each queueable call the next in line, as my last example demonstrates.
    – sfdcfox
    Commented Mar 6, 2023 at 19:45
3

It would, but only if you implemented the queueable methods in a way that they run in a queue instead of immediately after the main transaction. In that regard, you might be better off writing queueable methods that call each other passing the accounts in from the main account trigger context (the list of accounts, that is).

That would be akin to writing a faster batch.

You can even find libraries for that, like Apex Chainable.

2
  • 1
    While the other answers below can and do work, upvoting this answer. The correct solution-space abstraction is a single-consumer Queue which yields sequential, one-at-a-time message processing. In our opinion the "least bad" implementation of this abstraction in SF is chaining of async "jobs".
    – identigral
    Commented Mar 6, 2023 at 18:25
  • Just beware of two possible problems: 1. Use of async limits, but more importantly 2. Problems with the number of queueables you can enqueue per transaction. The latter can get it the way in a synchronous context, but can be a disaster in an already async context.
    – Phil W
    Commented Mar 6, 2023 at 20:18
1

This sounds very much like the sort of problem I was looking to solve with my Governor friendly asynchronous processing from Triggers article.

This, and the related github demo, show how you can write code executed out-of-transaction and in a single threaded manner. The latter avoids deadlocks, while the former is done without actually using any of your async apex limits via the use of platform events.

The fact that my article and demo use flagging on records isn't essential, especially if you have a "request" object that represents a request to process a record. What is important though is to avoid having platform events contain valuable information not found anywhere else. The reason: because platform event delivery is not guaranteed, whereas database transactions are (once committed successfully).

My demo has a single apex trigger based platform event handler that processes the entire queue of platform events. This is how you get single threaded processing, and therefore avoid row lock contention.

5
  • the use case in that article your wrote is one I have faced several times, and I really like your solution. One question though - is it documented somewhere that PW are single threaded? I ran a simple POC, and that definitely seems to be the case, but I couldn't find confirmation\explanation anywhere? Thanks Commented Jan 20 at 15:57
  • 1
    I discussed with the PM and they confirmed this - it is implied by the way you can guarantee receiving events in order and can retry from a given ID.
    – Phil W
    Commented Jan 20 at 17:11
  • 1
    @BritishBoyinDC BTW, see this example too - variant on the pattern.
    – Phil W
    Commented Jan 20 at 19:02
  • I have often referred back to this answer, but feel your approach is actually the better solution now? salesforce.stackexchange.com/questions/216432/… Commented Jan 21 at 15:57
  • Thanks @BritishBoyinDC. I have added an answer there too.
    – Phil W
    Commented Jan 21 at 16:49

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .