Hi Jakub,
We ran into the same problem when doing asynchronous work. As far as I can tell, there are three main things that need to be taken care of carefully.
1. Evict failures properly
2. Avoid races (which you spoke to)
3. Interruptions should not cancel work for other workloads
1. is pretty easy, you simply need to set up a handler so that when the future fails, it's evicted.
2. is also pretty easy–it simply requires that you cache the actual future, not the result. it can be done in other ways, but this is the simplest.
3. is a little tricky, and depends on the future implementation. In Twitter's, we have a special facility for "detachable" Promises, which can be interrupted efficiently without cancelling the underlying work, but can still cancel the work that would have been done on that future. As an example:
The first thread comes in, and tries to read a key, "FANCY_KEY" from the cache. It sees that it isn't cached, so it populates it asynchronously, and we get a handle on a Future. We add a handler to the Future, so that when it's returned, it logs the returned message.
The second thread comes in, and tries to read the same key, and gets a handle on a Future (the same one, so we don't duplicate work).
The first thread passes some timeout, and cancels the work. We want it to tear down the handler that it had registered on the Future, but we don't want it to cancel the underlying work–the second thread will need it, after all.
You can see the library
here, although it uses Twitter Futures.
The core idea is that we combine a few simple primitives, and it gives us everything that we need. Those primitives are:
C.
interruption (this is simple with Twitter Futures so it doesn't need its own class)
We have a wrapper for the
guava caches (their cache APIs are quite good), and when we move to jdk8, we will probably look into supporting
caffeine too.
Best,
Moses