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?