In Infinispan 8.0.0.Beta3, we have a introduced a new experimental API for interacting with your data which takes advantage of the functional programming additions and improved asynchronous programming capabilities available in Java 8.
Over the next few weeks we'll be introducing different aspects of the API. In this first blog post, we'll focus on why we felt there's a need for a new approach, answering a few key questions.
ConcurrentMap and JCache
Map-like key/value pair APIs have often been used for distributed caching and in-memory data grids. Initially,
ConcurrentMap became popular but this was designed to be run within a single JVM, and hence some of the operations suffered in distributed environments or when persistence stores were attached. For example, methods such as '
V put(K, V)', '
V putIfAbsent(K, V)',
'V replace(K, V)' would force implementations to return the previous value, but often this value is not needed yet this could be expensive to transfer.
JSR-107 set out to improve on this and came up with the
JCache specification which solved this particular problem separating operations such ConcurrentMap's '
V put(K, V)' into two operations: '
void put(K, V)' and '
V getAndPut(K, V)', and it applied the same logic to other operations such as '
replace' by providing an alternative '
getAndReplace(K, V)'... etc.
However, even though JCache was designed with distributed caching in mind, it still failed to provide an API to execute operations asynchronously and hence avoid resource underutilization by having threads waiting for remote operations to complete. '
loadAll' is probably the only exception, and it would have been the perfect candidate to return a
Future or similar construct, but having to pass in a completion listener feels a bit clunky and cannot be chained easily.
In my opinion, the best parts of JCache are '
invoke' and '
invokeAll' methods. When you
look at them, you see a lot of potential to reimplement
get,
put,
getAndPut,
getAndReplace,
putAll,
getAll, and many others using these methods. In other words, as an implementer, all you should need to implement is those two functions, and the rest would be syntactic sugar for the user. Unfortunately, the way '
invoke' and '
invokeAll' handle arguments is a bit clunky, and really, it's just screaming for lambdas to be passed in and
CompletableFuture instances to be returned (Java 8!).
So, when Infinispan moved to Java 8, we decided to revisit these concepts and see if we could come up with a better, distilled map-like interface to be used for as either a caching or data grid API.
New Functional Map API
Infinispan's
Functional Map API is a distilled maplike asynchronous API which uses lambdas to interact with data.
Asynchronous and Lazy
Being an asynchronous API, all methods that return a single result, return a
CompletableFuture which wraps the result, so you can use the resources of your system more efficiently by having the possibility to receive callbacks when the
CompletableFuture has completed, or you can chain or compose them with other
CompletableFuture. If you do want to block the thread and wait for the result, just as it happens with a
ConcurrentMap or
JCache method call, you can simply call `
CompletableFuture.get()` (for such situations, we are working on finding ways to avoid unnecessary thread creation when the caller will block on the
CompletableFuture).
For those operations that return multiple results, the API returns instances of a
Traversable interface which offers a lazy pull-style API for working with multiple results. Although push-style interfaces for handling multiple results, such as RxJava, are fully asynchronous, they're harder to use from a user’s perspective.
Traversable, being a lazy pull-style API, can still be asynchronous underneath since the user can decide to work on the traversable at a later stage, and the
Traversable implementation itself can decide when to compute those results.
Lambda transparency
Since the content of the lambdas is transparent to Infinispan, the API has been split into 3 interfaces for read-only (
ReadOnlyMap), read-write (
ReadWriteMap) and write-only (
WriteOnlyMap) operations respectively, in order to provide hints to the Infinispan internals on the type of work needed to support lambdas.
For example, Infinispan has been designed in such way that our '
ConcurrentMap.get()' and '
JCache.getAll()' implementations do not require locks to be acquired. These
get()/
getAll() operations are read-only operations, and hence if you call our functional map
ReadOnlyMap's '
eval()' or '
evalMany()' operations, you get the same benefit. A key advantage of
ReadOnlyMap's '
eval()' and '
evalMany()' operations is that they take lambdas as parameters which means the returned types are more flexible, so we can return a value associated with the key, or we can return a boolean if a value has the expected contents, or we can return some metadata parameters from it, e.g. last accessed time, last modified time, creation time, lifespan, version information...etc.
Another important hint that is required to make efficient use of the system is to know when a write-only operation is being executed. Write-only operations require locks to be acquired and as demonstrated by
JCache's '
void removeAll()' and `
void put(K, V)' or
ConcurrentMap's '
putAll()', they do not require the previous value to be queried or read, which as explained above is a very important optimization since reading the previous value might require the persistence layer or a remote node to be queried.
WriteOnlyMap's '
eval()', '
evalMany()', and '
evalAll()' follow this same pattern with the added flexibility for the lambda to decide what kind of write operation to execute.
The final type of operations we have are read-write operations, and within this category we find CAS-like (Compare-And-Swap) operations. This type of operations require previous value associated with the key to be read and for locks to be acquired before executing the lambda. Most of the operations in
ConcurrentMap and
JCache operations fall within this domain including: '
V put(K, V)', '
boolean putIfAbsent(K, V)', '
V replace(K, V)', '
boolean replace(K, V, V)'...etc.
ReadWriteMap's '
eval()', '
evalMany()' and '
evalAll()' provide a way to implement the vast majority of these operations thanks to the flexibility of the lambdas passed in. So you can make CAS-like comparisons not only based on value equality but based on metadata parameter equality such as version information, and you can send back previous value or boolean instances to signal whether the CAS-like comparison succeeded.
$DEITY, I need to learn a new API!!!
This new functional Map-like API is meant to complement existing Key/Value Infinispan API offerings, so you'll still be able to use
ConcurrentMap or
JCache standard APIs if that's what suits your use case best.
The target audience for this new API is either:
- Distributed or persistent caching/in-memory data-grid users that want to benefit from CompletableFuture and/or Traversable for async/lazy data grid or caching data manipulation. The clear advantage here is that threads do not need to be idle waiting for remote operations to complete, but instead these can be notified when remote operations complete and then chain them with other subsequent operations.
- Users wanting to go beyond the standard operations exposed by ConcurrentMap and JCache, for example, if you want to do a replace operation using metadata parameter equality instead of value equality, or if you want to retrieve metadata information from values...etc.
Internally, we feel that this new functional Map-like API distills the Map-like APIs that we currently offer (including
ConcurrentMap and
JCache) and gets rid of a lot of duplication in our
AdvancedCache API (e.g. '
getCacheEntry()', '
getAsync()', '
putAsync()', '
put(K, V, Metadata)'...etc), and hence down the line, we'd want all these APIs to be implemented using the new functional Maplike API. By doing that, we hope to reduce the number of commands that our internal architecture implements, hence reducing our code base.
This new API also offers a new approach for passing per-invocation parameters, and much more flexible Metadata handling compared to our current approach. As we dig into this new API in next blog posts, we'll explain the differences and advantages provided by these.
Functional Map API usage examples
To give you a little taste of what the API looks like, here is a write-only operation to associate a key with a value, whose
CompletableFuture has been chained so that when it completes, a read-only operation can be executed to read the stored value, and when that completes, print it to the system output:
You can find more examples of this new API in
FunctionalConcurrentMap and
FunctionalJCache classes, which are implementations of
ConcurrentMap and
JCache respectively using the new Functional Map API.
Tell me more!!
Over the next few weeks I'll be posting examples looking at the finer details of these new Functional Map APIs, but if you're eager to get started, check the classes in org.infinispan.functional package,
FunctionalConcurrentMap and
FunctionalJCache which are
ConcurrentMap and
JCache implementations based on these Functional Map APIs, and
FunctionalMapTest which demonstrates operations that go beyond what
ConcurrentMap and
JCache offer.
Happy (functional) hacking :)
Galder