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:
- Has it worked well for you?
- When batchables are chained (i.e. a second
Database.executeBatch
call is made in thefinish
method of the first batchable), I'm assuming that the first batchable will not be marked as complete before the second batchableAsyncApexJob
row has been inserted so there won't be moments where it looks like the batchables have completed. - 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. - 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)? - 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.