Tuesday, 8 September 2015

Functional Map API: Working with multiple entries

We continue with the blog series on the experimental Functional Map API which was released as part of Infinispan 8.0.0.Final. In this blog post we'll be focusing on how to work with multiple entries at the same time. For reference, here are the previous entries in the series:
  1. Functional Map Introduction
  2. Working with single entries
The approach taken by the Functional Map API when working with multiple keys is to provide a lazy, pull-style API. All multi-key operations take a collection parameter which indicates the keys to work with (and sometimes contain value information too), and a function to execute for each key/value pair. Each function's ability depends on the entry view received as function parameter, which changes depending on the underlying map: ReadEntryView for ReadOnlyMap, WriteEntryView for WriteOnlyMap, or ReadWriteView for ReadWriteMap. The return type for all multi-key operations, except the ones from WriteOnlyMap, return an instance of Traversable which exposes methods for working with the returned data from each function execution. Let's see an example:

import org.infinispan.commons.api.functional.EntryVersion.NumericEntryVersion;
import org.infinispan.commons.api.functional.EntryView.*;
import org.infinispan.commons.api.functional.FunctionalMap.*;
import org.infinispan.commons.api.functional.MetaParam.MetaEntryVersion;
import org.infinispan.commons.api.functional.Traversable;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import static java.util.stream.Collectors.*;
ReadOnlyMap<String, String> readOnlyMap = ...;
WriteOnlyMap<String, String> writeOnlyMap = ...;
ReadWriteMap<String, String> readWriteMap = ...;
// Create some data to be stored
Map<String, String> entries = new HashMap<>();
entries.put("key1", "value1");
entries.put("key2", "value2");
entries.put("key3", "value3");
// Use multi-key write-only operation to store each key/value pair and
// generate version metadata (to keep it simple, version generated is either `1` or `2`)
CompletableFuture<Void> writeManyFuture = writeOnlyMap.evalMany(entries, (v, writeView) -> {
NumericEntryVersion version = new NumericEntryVersion((long) (Math.random() * 2 + 1));
writeView.set(v, new MetaEntryVersion<>(version));
});
Set<String> keys = entries.keySet();
// Once the write operation completes...
CompletableFuture<Void> readManyFuture = writeManyFuture.thenAccept(ignore -> {
// Use a multi-key read-only operation to read all values associated with given keys
Traversable<String> readViewTraverse = readOnlyMap.evalMany(keys, ReadEntryView::get);
readViewTraverse.forEach(view -> System.out.printf("Read value: %s%n", view));
});
// Once the read operation completes...
CompletableFuture<Void> removeManyFuture = readManyFuture.thenAccept(ignore -> {
// Use a multi-key read-write operation to remove each key/value pair
// and return content exposed as view
Traversable<ReadWriteEntryView<String, String>> prevTraverse = readWriteMap.evalMany(keys,
readWriteView -> {
readWriteView.remove();
return readWriteView;
});
// Collect previous entry views grouping them by metadata version
Map<MetaEntryVersion, List<ReadWriteEntryView<String, String>>> collected = prevTraverse
.collect(groupingBy(view -> view.findMetaParam(MetaEntryVersion.class).get()));
// Print a summary of the grouped set
System.out.printf("Values removed, grouped by version: %n");
collected.entrySet().forEach(entry -> {
String values = entry.getValue().stream().map(ReadEntryView::get).collect(joining(", "));
System.out.printf("Version=%s -> %s%n", entry.getKey(), values);
});
});
// Wait for the chain of operations to complete
removeManyFuture.get();

This example demonstrates some of the key aspects of working with multiple entries using the Functional Map API:
  • As explained in the previous blog post, all data-handling methods (including multi-key methods) for WriteOnlyMap return CompletableFuture<Void>, because there's nothing the function can provide that could not be computed in advance or outside the function.
  • Normally, the order of the Traversable matches the order of the input collection though this is not currently guaranteed.
There is a special type of multi-key operations which work on all keys/entries stored in Infinispan. The behaviour is very similar to the multi-key operations shown above, with the exception that they do not take a collection of keys (and/or values) as parameters:

import org.infinispan.commons.api.functional.EntryView;
import org.infinispan.commons.api.functional.EntryView.ReadWriteEntryView;
import org.infinispan.commons.api.functional.FunctionalMap.*;
import org.infinispan.commons.api.functional.Traversable;
import java.util.HashMap;
import java.util.Map;
import static java.util.stream.Collectors.joining;
ReadOnlyMap<String, String> readOnlyMap = ...;
WriteOnlyMap<String, String> writeOnlyMap = ...;
ReadWriteMap<String, String> readWriteMap = ...;
// Create some data to be stored
Map<String, String> entries = new HashMap<>();
entries.put("key1", "value1");
entries.put("key2", "value2");
entries.put("key3", "value3");
// Use multi-key write-only operation to store each key/value pair
writeOnlyMap.evalMany(entries, (v, writeView) -> writeView.set(v)).thenAccept(ignore -> {
// Use read-only entries traversable to locate a key/value pair with a particular value
boolean valueExists = readOnlyMap.entries().anyMatch(view -> view.get().equals("value2"));
System.out.printf("'value2' exists? %b%n", valueExists);
}).thenAccept(ignore -> {
// Use read-only keys traversable to count number of keys that have 'key' prefix
long numKeysPrefix = readOnlyMap.keys().filter(key -> key.startsWith("key")).count();
System.out.printf("Number of keys with 'key' as prefix? %d%n", numKeysPrefix);
}).thenAccept(ignore -> {
// Use read-write evalAll to replace all values and return latest entry view
Traversable<ReadWriteEntryView<String, String>> replaceAllTraverse = readWriteMap.evalAll(
readWriteView -> {
String prev = readWriteView.get();
readWriteView.set("new-" + prev);
return readWriteView;
});
// Print out entries after replacing
String pairs = replaceAllTraverse
.map(view -> view.key() + "=" + view.get())
.collect(joining(", "));
System.out.printf("After replacing, entries contain: %s%n", pairs);
}).thenCompose(ignore ->
// Use write-only evalAll to remove all entries, one by one
writeOnlyMap.evalAll(EntryView.WriteEntryView::remove)
).thenAccept(ignore -> {
// Use read-only keys traversable to verify that the map is empty
boolean isEmpty = !readOnlyMap.keys().findAny().isPresent();
System.out.printf("Is empty? %b", isEmpty);
}).get(); // Wait for the chain of operations to complete

There's a few interesting things to note about working with all entries using the Functional Map API:
  • When working with all entries, the order of the Traversable is not guaranteed.
  • Read-only's keys() and entries() offer the possibility to traverse all keys and entries present in the cache. When traversing entries, both keys and values including metadata are available. Contrary to Java's ConcurrentMap, there's no possibility to navigate only the values (and metadata) since there's little to be gained from such method and once a key's entry has been retrieved, there's no extra cost to provide the key as well.
It's worth noting that when we sat down to think about how to work with multiple entries, we considered having a push-style API where the user would receive callbacks pushed as the entries to work with were located. This is the approach that reactive APIs such as Rx follow, but we decided against using such APIs at this level for several reasons:
  1. We have huge interest in providing a Rx-style API for Infinispan, but we didn't want the core API to have a dependency on Rx or Reactive Streams
  2. We didn't want to reimplement a push-style async API since this is not trivial to do and requires careful thinking, specially around back-pressure and flow control.
  3. Push-style APIs require more work on the user side compared to pull-style APIs.
  4. Pull-style APIs can still be lazy and partly asynchronous since the user can decide to work with the Traversable at a later stage, and the separation between intermediate and terminating operations provides a good abstraction to avoid unnecessary computation.
In fact, it is this desire to keep a clear separation between intermediate and terminating operations at Traversable that has resulted in having no manual way to iterate over the Traversable. In other words, there is no iterator() nor spliterator() methods in Traversable since these are often associated with manual, user-end iteration, and we want to avoid such thing since in the majority of cases, Infinispan knows best how to exactly iterate over the data.

In the next blog post, we'll be looking at how to work with listeners using the Functional Map API.

Cheers,
Galder

No comments:

Post a Comment