2
votes

I need to geocode an Address object, and then store the updated Address in a search engine. This can be simplified to taking an object, performing one long-running operation on the object, and then persisting the object. This means there is an order of operations requirement that the first operation be complete before persistence occurs.

I would like to use Akka to move this off the main thread of execution.

My initial thought was to use a pair of Futures to accomplish this, but the Futures documentation is not entirely clear on which behavior (fold, map, etc) guarantees one Future to be executed before another.

I started out by creating two functions, defferedGeocode and deferredWriteToSearchEngine which return Futures for the respective operations. I chain them together using Future<>.andThen(new OnComplete...), but this gets clunky very quickly:

Future<Address> geocodeFuture = defferedGeocode(ec, address);

geocodeFuture.andThen(new OnComplete<Address>() {
    public void onComplete(Throwable failure, Address geocodedAddress) {
        if (geocodedAddress != null) {
            Future<Address> searchEngineFuture = deferredWriteToSearchEngine(ec, addressSearchService, geocodedAddress);

            searchEngineFuture.andThen(new OnComplete<Address>() {
                public void onComplete(Throwable failure, Address savedAddress) {
                    // process search engine results
                }
            });
        }
    }
}, ec);

And then deferredGeocode is implemented like this:

private Future<Address> defferedGeocode(
        final ExecutionContext ec, 
        final Address address) {

    return Futures.future(new Callable<Address>() {
        public Address call() throws Exception {
            log.debug("Geocoding Address...");
            return address;
        }
    }, ec);

};

deferredWriteToSearchEngine is pretty similar to deferredGeocode, except it takes the search engine service as an additional final parameter.

My understand is that Futures are supposed to be used to perform calculations and should not have side effects. In this case, geocoding the address is calculation, so I think using a Future is reasonable, but writing to the search engine is definitely a side effect.

What is the best practice here for Akka? How can I avoid all the nested calls, but ensure that both the geocoding and the search engine write are done off the main thread?

Is there a more appropriate tool?

Update:

Based on Viktor's comments below, I am trying this code out now:

ExecutionContext ec;
private Future<Address> addressBackgroundProcess(Address address) {
    Future<Address> geocodeFuture = addressGeocodeFutureFactory.defferedGeocode(address);

    return geocodeFuture.flatMap(new Mapper<Address, Future<Address>>() {
        @Override
        public Future<Address> apply(Address geoAddress) {
            return addressSearchEngineFutureFactory.deferredWriteToSearchEngine(geoAddress);
        }
    }, ec);
}

This seems to work ok except for one issue which I'm not thrilled with. We are working in a Spring IOC code base, and so I would like to inject the ExecutionContext into the FutureFactory objects, but it seems wrong for this function (in our DAO) to need to be aware of the ExecutionContext.

It seems odd to me that the flatMap() function needs an EC at all, since both futures provide one.

Is there a way to maintain the separation of concerns? Am I structuring the code badly, or is this just the way it needs to be?

I thought about creating an interface in the FutureFactory's that would allow chaining of FutureFactory's, so the flatMap() call would be encapsulated in a FutureFactory base class, but this seems like it would be deliberately subverting an intentional Akka design decision.

1

1 Answers

1
votes

Warning: Pseudocode ahead.

Future<Address> myFutureResult = deferredGeocode(ec, address).flatMap(
  new Mapper<Address, Future<Address>>() {
    public Future<Address> apply(Address geocodedAddress) {
      return deferredWriteToSearchEngine(ec, addressSearchService, geocodedAddress);
   }
  }, ec).map(
     new Mapper<Address, SomeResult>() {
       public SomeResult apply(Address savedAddress) {
         // Create SomeResult after deferredWriteToSearchEngine is done
       }
    }, ec);

See how it is not nested. flatMap and map is used for sequencing the operations. "andThen" is useful for when you want a side-effecting-only operation to run to full completion before passing the result on. Of course, if you map twice on the SAME future-instance then there is no ordering guaranteed, but since we are flatMapping and mapping on the returned futures (new ones according to the docs), there is a clear data-flow in our program.