Friday, June 26, 2015

Failover Trigger: Strictly ordered concurrency

I've stumbled on this concurrency pattern a few times now, and figured it's worth a quick write-up. The basic setup is a number of tasks that must not run simultaneously, and later tasks are only triggered if/when an earlier task fails. With fully-nonblocking code it can be tricky to synchronize everything correctly. Enter the Failover Trigger.

void inputSucceeded(Input input) { /* cleanup... */ } void noInputSucceeded() { /* cleanup... */ } void runTask(Input input, SettableFuture<Void> failover) { // Calls either failover.set(null) or inputSucceeded(input). } final AtomicReference<ListenableFuture<Void>> failovers = new AtomicReference<>(immediateFuture(null)); void addInput(final Input input) { final SettableFuture<Void> failover = SettableFuture.create(); failovers.getAndSet(failover) .addListener(() -> runTask(input, failover)); } void noMoreInputs() { failovers.getAndSet(null) .addListener(() -> noInputSucceeded()); }
This now allows any number of addInputs to be run concurrently, but ensures that everything in runTask (up to the failover.set call, at least) is fully synchronized. Inputs are handled on a first-come-first-served basis. If noMoreInputs is called after all inputs are added, then exactly one of inputSucceeded or noInputSucceeded will run to perform whatever cleanup is necessary.

I call this the "failover trigger" pattern because it provides each task with a trigger it can use to optionally start the next task.