10

I need to Geocode addresses on Account using an HTTP callout; the webservice will only service one address per callout. For this use case it is preferable to geocode the accounts immediately, rather than periodically via a scheduled batch. To do so, I have a class that implements Database.Batchable and Database.AllowsCallouts, which takes a list of account ids in the constructor. start() returns a QueryLocator for the needed account fields, and execute() makes the callouts. My trigger (on Account after insert, after update) calls Database.executeBatch() with a batch size of 10, to stay under the callout limit.

This works perfectly for data loads up to 200 records. Once I get over that size of course, the system splits the records into multiple chunks and invokes the trigger for each chunk (of up to 200 records), so I end up with concurrent executions of my Batchable class (as each trigger invocation is calling Database.executeBatch()), and then I get Rate Limit Exceeded errors from my geocoding service provider.

I'm considering changing my trigger logic a bit to query for running/queued batches; if any are found, to use system.scheduleBatch() instead of Database.executeBatch(), scaling the minutesFromNow param based on the number of batches found. Given that scheduled batch delays are guidelines only, this isn't a guarantee that two batches won't run concurrently, but it's possible this could be a 'good enough' solution for my particular case with the right delay param. If not, from there my next option is a separate SObject to track to-be-run batches, and logic in finish() to run the next batch (perhaps similar to this question, though perhaps without the controlling job).

Before I start complicating things, is there a simple way to prevent multiple instances of a given Batchable class from running concurrently? Or a simpler way of serializing the batches than writing my own scheduler?

  • How are you inserting/updating your Accounts? Is the 200 thing caused by the way in which you're inserting/updating records. Sounds like your trigger is firing more than once for > 200 records if you're ending up with concurrent batch jobs. – Phil Hawthorn Oct 23 '13 at 17:27
  • In this case, I'm testing with DataLoader using the Bulk API, but in any case, triggers always process records in chunks of 200 records at a time. Hmm, will clarify in my question; I used "batch" when I meant "chunk" (salesforce term, see e.g. salesforce.com/us/developer/docs/apexcode/Content/…) when discussing the number of records processed. – Jason Clark Oct 23 '13 at 19:09
  • 1
    If you do use System.scheduleBatch() beware this issue salesforce.stackexchange.com/questions/12794/… – Phil Hawthorn Oct 24 '13 at 21:01
9

I think using Batch Apex chaining is closer to what your aiming for here, though not as bullet proof as a scheduled job reviewing records that have yet to have a Geocode calculated. As while this is not as immediate, it does, as you've said give more predictable processing (may need to still consider schedule overlaps though) and has some built in error handling and retry semantics to it.

That said some notes on the chaining approach...

  • Ensuring there can be only one! You could query the AsyncApexJob by class Id to determine if a job is already running before starting another. Though there is still a small concurrency issue, as you cannot guarantee at that precise moment another parallel trigger invocation does not make the same query and arrive at the same answer (since there is no lock on the AsyncApexJob records). This could be quite a high likelihood if your hammering in a lot of Accounts. What you could do if you are concerned about this is use a custom object as a semaphore, this object has a unique constraint field on it, preventing multiple inserts if duplicates records are found. If you fail to insert into it, prior to attempting to star the job, you can take this as a sign the job is still running. When your job is completely done (see below) this record is removed.
  • Chainging the jobs. As you mentioned in the finish method you can start a new batch job if you determine new unprocessed Accounts have been inserted in the meantime. Be sure not to delete the semaphore record though, until you know you've not got any more work to do. As other trigger invocations maybe attempt to start a new job inbetween the transition between the current job ending and the new one starting. There is one other consideration here as well, only a maximum of 5 batch jobs can be queued/running in the org, so the chain could get broken. One solution to this is to query as per this answer and schedule a job in the future (using the new batch schedule feature) to try again, see this answer for more detail Cascading batch jobs.

Risk Management. As you can see there is some risk in the above, its a mater of judging the likelihood vs the overhead of the users in resolving the effects of the processing not occurring. For example you could have a button on the Account page as fall back to calculate as and when needed. Or have a scheduled job act as a secondary sweeper kicking off your job every day to check for records that have not been processed (again using the semaphore). Thus giving the users a near realtime update, with the added security in knowing the sweeper will pick up instances where the chaining fails due to fringe cases such as batch apex job governor and/or remaining concurrency issues in the solution.

When all is said and done... At the end of the day, triggers and batch jobs (or @future for that matter) don't mix that well. The advice in the Apex docs to think carefully about using them is a little to tame when you really start to think about all the considerations of starting jobs from a trigger context. That said, if you known and accept the potential points of failure and have some plan to observe and address them if they occur it can be acceptable.

Use extreme care if you are planning to invoke a batch job from a trigger. You must be able to guarantee that the trigger will not add more batch jobs than the five that are allowed. In particular, consider API bulk updates, import wizards, mass record changes through the user interface, and all cases where more than one record can be updated at a time.

Hope this helps!

|improve this answer|||||
4

Zach McElrath has build a framework to do exactly this: Relax. Not sure how well this will map to your particualr use case, though. From the GitHub page:

Relax

Taking all the pain out of Force.com Batch and Scheduled Job management.

What Relax lets you do:

  1. Run multiple batch/scheduled Apex jobs as often as every 1 MINUTE, every day
  2. Mass activate, unschedule, and change ALL of your Scheduled and Batch Apex jobs at once --- minimizing the hassle of code deployments involving Scheduled Apex
  3. Mix-and-match your Batch Apex routines into "chains" of jobs that run sequentially on a scheduled basis, without hard-coding the sequences into your code
  4. Define string/JSON parameters to pass into Relax jobs you create, allowing for massive reuse of your Batch/Scheduled Apex.
  5. Bonus: powerful 'MassUpdate' and 'MassDelete' Apex Classes that can be run as Relax Jobs are pre-included! Never write another line of Scheduled Apex just to do mass-update a million records! For an intro to Relax, check out this blog post:

Relax: Your Batch Scheduling Woes are Over

Note - Relax uses some techniques that are not considered best practices by the Apex product team, specifically:

  1. Suicidal Scheduled Apex. Scheduled Apex jobs that kill themselves and schedule another to run. A year ago, the only way to do a 1-minute cadence was suicidal Apex. Now, an org can have 100 scheduled jobs, so you could have 60 of them set to go and get a 1-minute cadence. It looks like Relax might still be using the old pattern. Regardless, there is unnecessary overhead when an org uses this and then doesn't have something to do every minute.

  2. Non-batch batch jobs. Using Batch Apex is intended to be for batch jobs (hence the name!), and there's a lot of overhead in setting up batches, enqueuing and running a separate start & execute message, etc. If you don't have a batch of records, @future is the preferred option.

Both of these techniques work now, but are kind of 'out there', and vulnerable to changes in the way that we prioritize and schedule jobs.

|improve this answer|||||
  • 2
    Can you enlighten us on what 'not considered best practices' is @metadaddy - would be interested to know since we may be using them too :) – Phil Hawthorn Oct 24 '13 at 13:06
  • 2
    Added detail above. – metadaddy Oct 24 '13 at 19:49
  • Thanks @metadaddy! Though sometimes suicide is the only way, what if there aren't enough batch slots free when the trigger fires...the only option is to check and throw an exception (not desirable in this case/dataloader) or schedule to run again in say 1 min time (and keep doing so until there is a slot). A batch apex queue is the real answer...now there's an idea ;) – Phil Hawthorn Oct 24 '13 at 20:57
  • I'll encourage JoshK to attend Zach's Dreamforce session - maybe we can have some creative discussion. – metadaddy Oct 25 '13 at 21:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.