4

We have several batchables that if run in parallel trample over each other's results. So I want to block a second invocation if there is already one running and just tell the user to wait.

My first thought was to hold a flag on a record - perhaps the job Id - and then clear the flag from the finish method. But as well as having to add data fields, there is the high risk of the flag never getting cleared under error conditions.

My second and current favourite thought is to query the AsyncApexJob table matching my batchable class name and namespace prefix and check the "Status" and/or "Completion Date" fields.

I have a few concerns that I'd appreciate feedback on if you have taken this approach:

  1. Has it worked well for you?
  2. When batchables are chained (i.e. a second Database.executeBatch call is made in the finish method of the first batchable), I'm assuming that the first batchable will not be marked as complete before the second batchable AsyncApexJob row has been inserted so there won't be moments where it looks like the batchables have completed.
  3. It looks like given the ParentJobId field, tying the chained jobs back to the original job is possible so only the original class name and namespace prefix is needed.
  4. There doesn't seem to be a good field to reliably grab the most recent records through; is order by CreatedDate desc the nearest thing (1 second granularity)?
  5. Any open source to solve this problem using this approach or any other approach?

PS

I missed emphasising in the question that the goal is to limit an individual user's execution of a batchable being kicked off from a button. Assuming a User is only logged in once, I think the AsyncApexJob approach can be used by only looking at records with a CreatedById that matches the logged in User.

PPS

I worked on code using AsyncApexJob table but in the end have given up on that. One problem was that some fairly convoluted batch chaining needed to be handled requiring batchable class name sequences to be registered and in some cases a no-op ChainFinishedBatchable to be used. But for my case, the locking really should be on the record Id that drives the batchables, not the User's Id.

So current plan is to add a custom BatchApexStatusEvent (start/finish) platform event and use that - in combination with the platform's BatchApexErrorEvent - to populate a custom table. The BatchApexStatusEvent is fired in the first start and the last finish. And the custom table is checked in the LWC controller for the button and in the first start.

The final implementation makes Apex calls to populate a custom table in the first batchable start and the last batchable finish. (Note that using Platform Events for the start/finish case left gaps as while a Platform Event can be fired within a transaction, it will be received some time later.) The platform's BatchApexErrorEvent is also used to update that table (where possible) to handle failures.

1
  • 1
    I have to admit, I use your second thought and have had no problems - but our environment was (really) unlikely to run into this. But Phil W's points are more thought through than mine
    – cropredy
    Commented Feb 2, 2023 at 21:46

2 Answers 2

5

Strap in! It's a long haul...

1. Has it worked well for you?

On its own, testing AsyncApexJob before starting a batch is prone to race conditions and will fail you on any implementation of scale.

Imagine you don't have a batch running and before starting it you test to see if AsyncApexJob contains an active entry (one with status 'Holding', 'Queued', 'Preparing' or 'Processing') for your Batchable namespace+class. Finding none there you call Database.executeBatch.

OK so far. But remember there's not just one user on the system. Imagine now you have two users both trying to start the batch more-or-less at the same time. Both transactions query the table and find no active entries, so both actually submit the job via Database.executeBatch. Now you have two batches running in parallel.

Oh dear.

Not to worry; there's a handy facet of the start method on a Batchable as described in the documentation:

Only one batch Apex job's start method can run at a time in an org.

Thus, to ensure you don't get a race condition going you need to have the start method repeat the query you did before executing the batch, this time having it exclude the current job by its job ID.

If your batch instance detects another batch already queued or executing it:

  1. Sends an email to the initiator telling them the batch could not be run due to a conflict with an existing execution and for them to try again.
  2. Returns an empty iterator (e.g. [SELECT Id FROM MyRelevantObject WHERE Id = NULL] or a custom iterator that returns nothing). Do this instead of using System.abortJob so the email can send and because this API requires specific permission to run that would not be granted to normal users.

If there were none, it can safely return the real data set (locator or iterator) to be processed in the knowledge that there cannot be another instance ahead of it in the queue or executing. No race conditions here, because of that guarantee of one-at-a-time execution of start.

BTW, it's still worth doing the pre-execution check because:

  • You can avoid some async execution wastage.
  • This is synchronous with the initiating transaction and that means you can immediately tell the initiator (e.g. the user at the Salesforce UI) that the batch could not be executed if there's one already going.

2. When batchables are chained...

Personally I recommend against an instant chaining of a Batchable. Recall that you have async apex limits and you don't want to get into a position where your batch rapidly burns these (one for each call to start, execute, and finish) if something goes wrong. Your batch can go rogue.

To help with this, I use System.scheduleBatch with a delay of 1 minute when doing this chaining. Now, this has some nice aspects:

  1. When you try to schedule the batch, you give a name for the scheduled job. If there's already a job scheduled with that name your new one won't be scheduled and you'll get a catchable exception in your apex code. This stops you accidentally scheduling more than one and getting into scheduling lots of instances of your Batchable.
  2. Once the batch's scheduled time arrives the batch is executed and the scheduled job is automatically removed (freeing up the job name).
  3. You cannot burn all your daily async executions if your batch goes rogue since you have that delay (there's only 1440 minutes in a day so that's the most you can waste), and the admin has some time to find and abort the scheduled job in this scenario.

You can subsequently rely on the test in start to catch a potential race condition and prevent this instance from running if another instance of the batch has been started in the meantime. Of course, you can extend the pre-execute test to also check for a pending scheduled job for your batch too.

3. ParentJobId field

This isn't relevant if you follow the above pattern.

4. There doesn't seem to be a good field to reliably grab the most recent records

Indeed, but that's OK if you follow the above pattern.

5. Any open source to solve this problem using this approach or any other approach?

Funny you should ask. I happen to have created a single-threaded alternative to Batchables, based on Platform Events.

The original motivation was to find a way to address problems like not being able to do a callout from a trigger or perform expensive processing in a trigger, while ensuring I support async and/or bulk DML scenarios, but this solution has other usages such as the idea of ensuring that some processing is done in a single-threaded manner.

I have an article about this published through Apex Hours, and you'll find the sample code over here in github.

There are some disadvantages to this approach:

  1. You need to mark the records that need processing (to avoid maintaining any state in the Platform Events, so you can survive failure to have these delivered).
  2. Each execution is given synchronous limits.
  3. There's always at least a 1 second gap between executions so there's an ultimate limit to through-put.
  4. It does use Platform Event publication limits.

However, the advantages are:

  1. It doesn't use any async limits.
  2. It's guaranteed single threaded.
  3. It can pick up again and still process the required records even if a Platform Event goes missing.
  4. It's really easy to interleave multiple different types of processing but still keep each and every execution single threaded.
  5. Platform Event publication limits are way bigger than async apex limits (250000 per hour for Platform Events and 250000* per 24 hours for async apex).

Note, of course, that you could have the Platform Event subscriber actually spawn asynchronous processes, such as Queueables (here it makes sense to mark the records as being handled by a given Queueable by storing the job ID on the records given to the Queueable). That can massively increase your potential through-put by generating parallel executions. This is, however, a whole different story that I'm not going to cover here.

6
  • Wow Phil, plenty to digest here - thank you for providing so much detail. The "Only one batch Apex job's start method can run at a time in an org" is good to know. And I'll take a look at your GitHub code. Note that I'm not in search of perfection here (unless that is reasonably easy to achieve) but rather going from no protection to - people clicking the button 5 times in a row - to some protection where most of the time they get a "Hey it's already running" message.
    – Keith C
    Commented Feb 2, 2023 at 21:23
  • 1
    @KeithC Doing the pre-execution test is a no-brainer since it allows synchronous notification of contention, but it is essential to repeat in start if you need to avoid contention or race conditions in the processing.
    – Phil W
    Commented Feb 2, 2023 at 21:37
  • This single-threading of a batch is something we have done successfully for years. The PE-based solution is new - something I did in my own time and not something in our product at this stage (but I see some good usages for it).
    – Phil W
    Commented Feb 2, 2023 at 21:39
  • #1 is something I should implement to my framework - thanks! and #2 - PE - definitely a way to do single-threading
    – cropredy
    Commented Feb 2, 2023 at 21:44
  • Not fool proof but you probabably want to use platform cache to store ParentJobId? You can use it to query very quickly, determine if that job is done or not, Commented Feb 7, 2023 at 14:16
2
  1. Yes, this approach of checking the "Status" and "Completion Date" fields in the AsyncApexJob table has worked well for many tasks.
  2. Yes, you're correct. When batchables are chained, the first batchable may not be marked as complete before the second batchable AsyncApexJob row is inserted.
  3. Yes, the ParentJobId field makes it possible to tie the chained jobs back to the original job.
  4. Ordering by CreatedDate desc is one of the options to get the most recent records, however, it has a 1 second granularity. You can use other fields like SystemModstamp or LastModifiedDate for higher precision, but their values may not change frequently.
  5. You can easily write a custom solution by querying the AsyncApexJob table as described above.
1
  • Thanks VikMants - re-assuring.
    – Keith C
    Commented Feb 2, 2023 at 18:53

You must log in to answer this question.

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