Wednesday, 02 September 2015
Functional Map API: Working with single entries
In this blog post we’ll continue with the introduction of the experimental Functional Map API, which was released as part of Infinispan 8.0.0.Final, focusing on how to manipulate data using single-key operations.
As mentioned in the Functional Map API introduction, there are three types of operations that can be executed against a functional map: read-only operations (executed via ReadOnlyMap), write-only operations (executed via WriteOnlyMap), and read-write operations (executed via ReadWriteMap) and .
Firstly, we need construct instances of ReadOnlyMap, WriteOnlyMap and ReadWriteMap to be able to work with them:
Next, let’s see all three types of operations in action, chaining them to store a single key/value pair along with some metadata, then read it and finally delete a returning the previously stored data:
This example demonstrates some of the key aspects of working with single entries using the Functional Map API:
-
Single entry methods are asynchronous returning CompletableFuture instances which provide methods to compose and chain operations so that it can feel is they’re being executed sequentially. Unfortunately Java does not have Haskell’s do notation or Scala’s for comprehensions to make it more palatable, but it’s great news that Java finally offers mechanisms to work with CompletableFutures in a non-blocking way, even if they’re a bit more verbose than what’s proposed in other languages.
-
All data-handling methods for WriteOnlyMap return CompletableFuture<Void>, meaning that the user can find out when the operation has completed but nothing else, because there’s nothing the function can provide that could not be computed in advance or outside the function.
-
The return type for most of the data handling methods in ReadOnlyMap (and ReadWriteMap) are quite flexible. So, a function can decide to return value information, or metadata, or for convenience, it can also return the ReadEntryView it receives as parameter. This can be useful for users wanting to return both value and metadata parameter information.
-
The read-write operation demonstrated above showed how to remove an entry and return the previously associated value. In this particular case, we know there’s a value associated with the entry and hence we called ReadEntryView.get() directly, but if we were not sure if the value is present or not, ReadEntryView.find() should be called and return the Optional instance instead.
-
In the example, Lifespan metadata parameter is constructed using the new Java Time API available in Java 8, but it could have been done equally with java.util.concurrent.TimeUnit as long as the conversion was done to number of milliseconds during which the entry should be accessible.
-
Lifespan-based expiration works just as it does with other Infinispan APIs, so you can easily modify the example to lower the lifespan, wait for duration to pass and then verify that the value is not present any more.
If storing a constant value, WriteOnlyMap.eval(K, Consumer) could be used instead of WriteOnlyMap.eval(K, V, Consumer), making the code clearer, but if the value is variable, WriteOnlyMap.eval(K, V, Consumer) should be used to avoid, as much as possible, functions capturing external variables. Clearly, operations exposed by functional map can’t cover all scenarios and there might be situations where external variables are captured by functions, but these should in general, should be a minority. Here is as example showing how to implement ConcurrentMap.replace(K, V, V) where external variable capturing is required:
The reason we didn’t add a WriteOnly.eval(K, V, V, Consumer) to the API is because value-equality-based replace comparisons are just one type of replace operations that could be executed. In other cases, metadata parameter based comparison might be more suitable, e.g. Hot Rod replace operation where version (a type of metadata parameter) equality is the deciding factor to determine whether the replace should happen or not.
In the next blog post, we’ll be looking at how to work with multiple entries using the Functional Map API.
Cheers,
Galder
Tags: functional introduction API lambda
Friday, 21 August 2015
New Functional Map API in Infinispan 8 - Introduction
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 'https://docs.oracle.com/javase/8/docs/api/java/util/Map.html#put-K-V-[V put(K, V)]', 'https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ConcurrentMap.html#putIfAbsent-K-V-[V putIfAbsent(K, V)]', 'https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ConcurrentMap.html#replace-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 'https://docs.oracle.com/javase/8/docs/api/java/util/Map.html#put-K-V-[V put(K, V)]' into two operations: 'https://github.com/jsr107/jsr107spec/blob/v1.0.0/src/main/java/javax/cache/Cache.java#L194[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 'https://github.com/jsr107/jsr107spec/blob/v1.0.0/src/main/java/javax/cache/Cache.java#L599[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 https://github.com/infinispan/infinispan/blob/master/commons/src/main/java/org/infinispan/commons/api/functional/Traversable.java[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 'https://github.com/jsr107/jsr107spec/blob/v1.0.0/src/main/java/javax/cache/Cache.java#L505[void removeAll()]' and `void put(K, V)' or ConcurrentMap’s 'https://docs.oracle.com/javase/8/docs/api/java/util/Map.html#putAll-java.util.Map-[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 'https://github.com/infinispan/infinispan/blob/master/commons/src/main/java/org/infinispan/commons/api/functional/FunctionalMap.java#L281[eval()]', 'https://github.com/infinispan/infinispan/blob/master/commons/src/main/java/org/infinispan/commons/api/functional/FunctionalMap.java#L351[evalMany()]', and 'https://github.com/infinispan/infinispan/blob/master/commons/src/main/java/org/infinispan/commons/api/functional/FunctionalMap.java#L414[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)', 'https://github.com/jsr107/jsr107spec/blob/v1.0.0/src/main/java/javax/cache/Cache.java#L283[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. 'https://docs.jboss.org/infinispan/8.0/apidocs/org/infinispan/AdvancedCache.html#getCacheEntry-java.lang.Object-[getCacheEntry()]', 'https://docs.jboss.org/infinispan/8.0/apidocs/org/infinispan/commons/api/AsyncCache.html#getAsync-K-[getAsync()]', 'https://docs.jboss.org/infinispan/8.0/apidocs/org/infinispan/commons/api/AsyncCache.html#putAsync-K-V-[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
Tags: functional introduction API lambda