/* * Copyright 2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.atomix.collections; import io.atomix.catalyst.buffer.BufferInput; import io.atomix.catalyst.buffer.BufferOutput; import io.atomix.catalyst.concurrent.Listener; import io.atomix.catalyst.serializer.CatalystSerializable; import io.atomix.catalyst.serializer.Serializer; import io.atomix.collections.internal.MapCommands; import io.atomix.collections.internal.MapEntry; import io.atomix.collections.util.DistributedMapFactory; import io.atomix.copycat.client.CopycatClient; import io.atomix.resource.AbstractResource; import io.atomix.resource.ReadConsistency; import io.atomix.resource.Resource; import io.atomix.resource.ResourceTypeInfo; import java.time.Duration; import java.util.Collection; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.function.Consumer; /** * Stores a map of keys to values. * <p> * The distributed map stores a map of keys and values via an interface similar to that of {@link Map}. * Map entries are stored in memory on each stateful node and backed by disk, thus the size of the map is * limited to the available memory on the smallest node in the cluster. This map requires non-null keys but * supports {@code null} values. All keys and values must be serializable by a * {@link io.atomix.catalyst.serializer.Serializer}. Serializable types include implementations of * {@link java.io.Serializable} or {@link io.atomix.catalyst.serializer.CatalystSerializable}. See the * serialization API for more information. * <p> * A {@code DistributedMap} can be created either via the {@code Atomix} API or by wrapping a {@link CopycatClient} * directly. To create a map via the Atomix API, use the {@code getMap} factory method: * <pre> * {@code * DistributedMap<String, String> map = atomix.getMap("foo").get(); * } * </pre> * Maps are distributed and are referenced by the map name. If a value is {@link #put(Object, Object)} in * a map on one node, that value is immediately available for {@link #get(Object) reading} by any other node * in the cluster by operating on the same map. * <p> * In addition to supporting normal {@link java.util.Map} methods, this implementation supports values * with TTLs. When a key is set with a TTL, the value will expire and be automatically evicted from the map * some time after the TTL. * * @param <K> The map key type. * @param <V> The map entry type. * @author <a href="http://github.com/kuujo">Jordan Halterman</a> */ @ResourceTypeInfo(id = -11, factory = DistributedMapFactory.class) public class DistributedMap<K, V> extends AbstractResource<DistributedMap<K, V>> { /** * Distributed map options. */ public static class Options extends Resource.Options { public Options() { } public Options(Properties defaults) { super(defaults); } /** * Enables the local map cache. * <p> * When local caching is enabled, the {@link DistributedMap} will update a local in-memory map each * time a change event is received. The instance will hold the full map in memory and will service * all {@link ReadConsistency#LOCAL} reads from the local map. Thus, this feature should be used with * caution. <em>Do not enable caching on large maps.</em> * * @return The map options. */ public Options withLocalCache() { return withLocalCache(true); } /** * Sets whether to enable local caching. * <p> * When local caching is enabled, the {@link DistributedMap} will update a local in-memory map each * time a change event is received. The instance will hold the full map in memory and will service * all {@link ReadConsistency#LOCAL} reads from the local map. Thus, this feature should be used with * caution. <em>Do not enable caching on large maps.</em> * * @param enableCache Whether to enable local caching. * @return The map options. */ public Options withLocalCache(boolean enableCache) { setProperty("cache", String.valueOf(enableCache)); return this; } /** * Returns whether local caching is enabled. * <p> * When local caching is enabled, the {@link DistributedMap} will update a local in-memory map each * time a change event is received. * * @return Whether local caching is enabled. */ public boolean isLocalCache() { return Boolean.parseBoolean(getProperty("cache", "false")); } } private final Options options; private final Map<K, V> cache; private final Map<K, Map<Integer, Set<Consumer>>> eventListeners = new ConcurrentHashMap<>(); public DistributedMap(CopycatClient client) { this(client, new Options()); } public DistributedMap(CopycatClient client, Properties options) { super(client, options); this.options = new Options(options); if (this.options.isLocalCache()) { this.cache = new ConcurrentHashMap<>(); } else { this.cache = null; } } @Override public Options options() { return options; } /** * Returns {@code true} if the map is empty. * <p> * Note that depending on the configured {@link ReadConsistency} of the map instance, empty checks * may return stale results. To perform a fully consistent empty check, configure the map with * {@link ReadConsistency#ATOMIC} consistency (the default). * <pre> * {@code * map.with(Consistency.ATOMIC).isEmpty().thenAccept(isEmpty -> { * ... * }); * } * </pre> * For better performance with potentially stale results, use a lower consistency level. See the * {@link ReadConsistency} documentation for specific consistency guarantees. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} method to block the calling thread: * <pre> * {@code * if (map.isEmpty().get()) { * ... * } * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.isEmpty().thenAccept(isEmpty -> { * ... * }); * } * </pre> * * @return A completable future to be completed with a boolean value indicating whether the map is empty. */ public CompletableFuture<Boolean> isEmpty() { return client.submit(new MapCommands.IsEmpty()); } /** * Returns {@code true} if the map is empty. * <p> * Note that depending on the {@link ReadConsistency}, empty checks may return stale results. To perform a fully * consistent empty check, use {@link ReadConsistency#ATOMIC} consistency (the default). * <pre> * {@code * map.isEmpty(ReadConsistency.ATOMIC).thenAccept(isEmpty -> { * ... * }); * } * </pre> * For better performance with potentially stale results, use a lower consistency level. See the * {@link ReadConsistency} documentation for specific consistency guarantees. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} method to block the calling thread: * <pre> * {@code * if (map.isEmpty(ReadConsistency.ATOMIC).get()) { * ... * } * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.isEmpty(ReadConsistency.ATOMIC).thenAccept(isEmpty -> { * ... * }); * } * </pre> * * @param consistency The read consistency level. * @return A completable future to be completed with a boolean value indicating whether the map is empty. */ public CompletableFuture<Boolean> isEmpty(ReadConsistency consistency) { return client.submit(new MapCommands.IsEmpty(consistency.level())); } /** * Gets the number of key-value pairs in the map. * <p> * Note that depending on the configured {@link ReadConsistency} of the map instance, size checks * may return stale results. To perform a fully consistent size check, configure the map with * {@link ReadConsistency#ATOMIC} consistency (the default). * <pre> * {@code * map.with(Consistency.ATOMIC).size().thenAccept(size -> { * ... * }); * } * </pre> * For better performance with potentially stale results, use a lower consistency level. See the * {@link ReadConsistency} documentation for specific consistency guarantees. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} method to block the calling thread: * <pre> * {@code * int size = map.size().get(); * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.size().thenAccept(size -> { * ... * }); * } * </pre> * * @return A completable future to be completed with the number of entries in the map. */ public CompletableFuture<Integer> size() { return client.submit(new MapCommands.Size()); } /** * Gets the number of key-value pairs in the map. * <p> * Note that depending on the {@link ReadConsistency}, size checks may return stale results. To perform a * fully consistent size check, use {@link ReadConsistency#ATOMIC} consistency (the default). * <pre> * {@code * map.size(ReadConsistency.ATOMIC).thenAccept(size -> { * ... * }); * } * </pre> * For better performance with potentially stale results, use a lower consistency level. See the * {@link ReadConsistency} documentation for specific consistency guarantees. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} method to block the calling thread: * <pre> * {@code * int size = map.size(ReadConsistency.ATOMIC).get(); * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.size(ReadConsistency.ATOMIC).thenAccept(size -> { * ... * }); * } * </pre> * * @param consistency The read consistency level. * @return A completable future to be completed with the number of entries in the map. */ public CompletableFuture<Integer> size(ReadConsistency consistency) { return client.submit(new MapCommands.Size(consistency.level())); } /** * Returns {@code true} if the given key is present in the map. * <p> * Note that depending on the configured {@link ReadConsistency} of the map instance, checks * may return stale results. To perform a fully consistent check, configure the map with * {@link ReadConsistency#ATOMIC} consistency (the default). * <pre> * {@code * map.with(Consistency.ATOMIC).containsKey("foo").thenAccept(contains -> { * ... * }); * } * </pre> * For better performance with potentially stale results, use a lower consistency level. See the * {@link ReadConsistency} documentation for specific consistency guarantees. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} method to block the calling thread: * <pre> * {@code * if (map.containsKey("foo").get()) { * ... * } * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.containsKey("foo").thenAccept(contains -> { * ... * }); * } * </pre> * * @param key The key to check. * @return A completable future to be completed with a boolean indicating whether {@code key} is present in the map. * @throws NullPointerException if {@code key} is {@code null} */ public CompletableFuture<Boolean> containsKey(Object key) { return client.submit(new MapCommands.ContainsKey(key)); } /** * Returns {@code true} if the given key is present in the map. * <p> * Note that depending on the {@link ReadConsistency}, checks may return stale results. To perform a fully * consistent check, use {@link ReadConsistency#ATOMIC} consistency (the default). * <pre> * {@code * map.containsKey("foo", ReadConsistency.ATOMIC).thenAccept(contains -> { * ... * }); * } * </pre> * For better performance with potentially stale results, use a lower consistency level. See the * {@link ReadConsistency} documentation for specific consistency guarantees. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} method to block the calling thread: * <pre> * {@code * if (map.containsKey("foo", ReadConsistency.ATOMIC).get()) { * ... * } * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.containsKey("foo", ReadConsistency.ATOMIC).thenAccept(contains -> { * ... * }); * } * </pre> * * @param key The key to check. * @param consistency The read consistency level. * @return A completable future to be completed with a boolean indicating whether {@code key} is present in the map. * @throws NullPointerException if {@code key} is {@code null} */ public CompletableFuture<Boolean> containsKey(Object key, ReadConsistency consistency) { return client.submit(new MapCommands.ContainsKey(key, consistency.level())); } /** * Returns {@code true} if the map contains a key with the given value. * <p> * Note that depending on the configured {@link ReadConsistency} of the map instance, checks * may return stale results. To perform a fully consistent check, configure the map with * {@link ReadConsistency#ATOMIC} consistency (the default). * <pre> * {@code * map.with(Consistency.ATOMIC).containsValue("foo").thenAccept(contains -> { * ... * }); * } * </pre> * For better performance with potentially stale results, use a lower consistency level. See the * {@link ReadConsistency} documentation for specific consistency guarantees. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} method to block the calling thread: * <pre> * {@code * if (map.containsValue("foo").get()) { * ... * } * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.containsValue("foo").thenAccept(contains -> { * ... * }); * } * </pre> * * @param value The value for which to search keys. * @return A completable future to be completed with a boolean indicating whether {@code key} is present in the map. * @throws NullPointerException if {@code key} is {@code null} */ public CompletableFuture<Boolean> containsValue(Object value) { return client.submit(new MapCommands.ContainsValue(value)); } /** * Returns {@code true} if the map contains a key with the given value. * <p> * Note that depending on the {@link ReadConsistency}, checks may return stale results. To perform a fully * consistent check, use {@link ReadConsistency#ATOMIC} consistency (the default). * <pre> * {@code * map.containsValue("foo", ReadConsistency.ATOMIC).thenAccept(contains -> { * ... * }); * } * </pre> * For better performance with potentially stale results, use a lower consistency level. See the * {@link ReadConsistency} documentation for specific consistency guarantees. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} method to block the calling thread: * <pre> * {@code * if (map.containsValue("foo", ReadConsistency.ATOMIC).get()) { * ... * } * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.containsValue("foo", ReadConsistency.ATOMIC).thenAccept(contains -> { * ... * }); * } * </pre> * * @param value The value for which to search keys. * @param consistency The read consistency level. * @return A completable future to be completed with a boolean indicating whether {@code key} is present in the map. * @throws NullPointerException if {@code key} is {@code null} */ public CompletableFuture<Boolean> containsValue(Object value, ReadConsistency consistency) { return client.submit(new MapCommands.ContainsValue(value, consistency.level())); } /** * Gets a value from the map. * <p> * Note that depending on the configured {@link ReadConsistency} of the map instance, queries * may return stale results. To perform a fully consistent query, configure the map with * {@link ReadConsistency#ATOMIC} consistency (the default). * <pre> * {@code * map.with(Consistency.ATOMIC).get("key").thenAccept(value -> { * ... * }); * } * </pre> * For better performance with potentially stale results, use a lower consistency level. See the * {@link ReadConsistency} documentation for specific consistency guarantees. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} method to block the calling thread: * <pre> * {@code * String value = map.get("key").get(); * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.get("key").thenAccept(value -> { * ... * }); * } * </pre> * * @param key The key to get. * @return A completable future to be completed with the result once complete. * @throws NullPointerException if {@code key} is {@code null} */ @SuppressWarnings("unchecked") public CompletableFuture<V> get(Object key) { return client.submit(new MapCommands.Get(key)).thenApply(result -> (V) result); } /** * Gets a value from the map. * <p> * Note that depending on the {@link ReadConsistency}, queries may return stale results. To perform a fully * consistent query, use {@link ReadConsistency#ATOMIC} consistency (the default). * <pre> * {@code * map.get("key", ReadConsistency.ATOMIC).thenAccept(value -> { * ... * }); * } * </pre> * For better performance with potentially stale results, use a lower consistency level. See the * {@link ReadConsistency} documentation for specific consistency guarantees. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} method to block the calling thread: * <pre> * {@code * String value = map.get("key", ReadConsistency.ATOMIC).get(); * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.get("key", ReadConsistency.ATOMIC).thenAccept(value -> { * ... * }); * } * </pre> * * @param key The key to get. * @param consistency The read consistency level. * @return A completable future to be completed with the result once complete. * @throws NullPointerException if {@code key} is {@code null} */ @SuppressWarnings("unchecked") public CompletableFuture<V> get(Object key, ReadConsistency consistency) { if (consistency == ReadConsistency.LOCAL && cache != null) { return CompletableFuture.completedFuture(cache.get(key)); } return client.submit(new MapCommands.Get(key, consistency.level())).thenApply(result -> (V) result); } /** * Gets the value of {@code key} or returns the given default value if {@code key} does not exist. * <p> * If no value for the given {@code key} is present in the map, the returned {@link CompletableFuture} will * be completed {@code null}. If a value is present, the returned future will be completed with that value. * <p> * Note that depending on the configured {@link ReadConsistency} of the map instance, queries * may return stale results. To perform a fully consistent query, configure the map with * {@link ReadConsistency#ATOMIC} consistency (the default). * <pre> * {@code * map.with(Consistency.ATOMIC).getOrDefault("key", "Hello world!").thenAccept(value -> { * ... * }); * } * </pre> * For better performance with potentially stale results, use a lower consistency level. See the * {@link ReadConsistency} documentation for specific consistency guarantees. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} method to block the calling thread: * <pre> * {@code * String valueOrDefault = map.getOrDefault("key", "Hello world!").get(); * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.getOrDefault("key", "Hello world!").thenAccept(valueOrDefault -> { * ... * }); * } * </pre> * * @param key The key to get. * @param defaultValue The default value to return if the key does not exist. * @return A completable future to be completed with the result once complete. * @throws NullPointerException if {@code key} is {@code null} */ @SuppressWarnings("unchecked") public CompletableFuture<V> getOrDefault(Object key, V defaultValue) { return client.submit(new MapCommands.GetOrDefault(key, defaultValue)).thenApply(result -> (V) result); } /** * Gets the value of {@code key} or returns the given default value if {@code key} does not exist. * <p> * If no value for the given {@code key} is present in the map, the returned {@link CompletableFuture} will * be completed {@code null}. If a value is present, the returned future will be completed with that value. * <p> * Note that depending on the {@link ReadConsistency}, queries may return stale results. To perform a fully * consistent query, use {@link ReadConsistency#ATOMIC} consistency (the default). * <pre> * {@code * map.getOrDefault("key", "Hello world!", ReadConsistency.ATOMIC).thenAccept(value -> { * ... * }); * } * </pre> * For better performance with potentially stale results, use a lower consistency level. See the * {@link ReadConsistency} documentation for specific consistency guarantees. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} method to block the calling thread: * <pre> * {@code * String valueOrDefault = map.getOrDefault("key", "Hello world!", ReadConsistency.ATOMIC).get(); * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.getOrDefault("key", "Hello world!", ReadConsistency.ATOMIC).thenAccept(valueOrDefault -> { * ... * }); * } * </pre> * * @param key The key to get. * @param defaultValue The default value to return if the key does not exist. * @param consistency The read consistency level. * @return A completable future to be completed with the result once complete. * @throws NullPointerException if {@code key} is {@code null} */ @SuppressWarnings("unchecked") public CompletableFuture<V> getOrDefault(Object key, V defaultValue, ReadConsistency consistency) { if (consistency == ReadConsistency.LOCAL && cache != null) { return CompletableFuture.completedFuture(cache.getOrDefault(key, defaultValue)); } return client.submit(new MapCommands.GetOrDefault(key, defaultValue, consistency.level())).thenApply(result -> (V) result); } /** * Puts a value in the map for the given {@code key}. * <p> * Any previous value associated with the given {@code key} will be overridden. Additionally, if the previous value * was set with a TTL, the TTL will be cancelled and this value will be set with no TTL. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} or {@link CompletableFuture#join()} method to block the calling thread: * <pre> * {@code * String oldValue = map.put("key", "Hello world!").get(); * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.put("key", "Hello world!").thenAccept(oldValue -> { * ... * }); * } * </pre> * * @param key The key to set. * @param value The value to set. * @return A completable future to be completed with the result once complete. * @throws NullPointerException if {@code key} is {@code null} */ @SuppressWarnings("unchecked") public CompletableFuture<V> put(K key, V value) { return client.submit(new MapCommands.Put(key, value)).thenApply(result -> (V) result); } /** * Puts a value in the map with a time-to-live for the given {@code key}. * <p> * Any previous value associated with the given {@code key} will be overridden. Additionally, if the previous value * was set with a TTL, the TTL will be cancelled and this value's TTL will be set. * <p> * The {@code value} will remain in the map until the provided {@link Duration} of time has elapsed or it is overridden * by a more recent put operation. Note that the provided {@code ttl} should only be considered an estimate of time. * Values may be evicted at some arbitrary point after the provided duration has elapsed, but never before. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} or {@link CompletableFuture#join()} method to block the calling thread: * <pre> * {@code * String oldValue = map.put("key", "Hello world!", Duration.ofSeconds(10)).get(); * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.put("key", "Hello world!", Duration.ofSeconds(10)).thenAccept(oldValue -> { * ... * }); * } * </pre> * * @param key The key to set. * @param value The value to set. * @param ttl The duration after which to expire the key. * @return A completable future to be completed with the result once complete. * @throws NullPointerException if {@code key} is {@code null} */ @SuppressWarnings("unchecked") public CompletableFuture<V> put(K key, V value, Duration ttl) { return client.submit(new MapCommands.Put(key, value, ttl.toMillis())).thenApply(result -> (V) result); } /** * Puts a value in the map if the given {@code key} does not exist. * <p> * If the given {@code key} is not already present in the map, the key will be set to the given {@code value}. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} or {@link CompletableFuture#join()} method to block the calling thread: * <pre> * {@code * String oldValue = map.put("key", "Hello world!").get(); * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.put("key", "Hello world!").thenAccept(oldValue -> { * ... * }); * } * </pre> * * @param key The key to set. * @param value The value to set if the given key does not exist. * @return A completable future to be completed with the result once complete. * @throws NullPointerException if {@code key} is {@code null} */ @SuppressWarnings("unchecked") public CompletableFuture<V> putIfAbsent(K key, V value) { return client.submit(new MapCommands.PutIfAbsent(key, value)).thenApply(result -> (V) result); } /** * Puts a value with a time-to-live in the map if the given {@code key} does not exist. * <p> * If the given {@code key} is not already present in the map, the key will be set to the given {@code value}. * <p> * The {@code value} will remain in the map until the provided {@link Duration} of time has elapsed or it is overridden * by a more recent put operation. Note that the provided {@code ttl} should only be considered an estimate of time. * Values may be evicted at some arbitrary point after the provided duration has elapsed, but never before. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} or {@link CompletableFuture#join()} method to block the calling thread: * <pre> * {@code * String oldValue = map.put("key", "Hello world!", Duration.ofSeconds(10)).get(); * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.put("key", "Hello world!", Duration.ofSeconds(10)).thenAccept(oldValue -> { * ... * }); * } * </pre> * * @param key The key to set. * @param value The value to set if the given key does not exist. * @param ttl The time to live duration. * @return A completable future to be completed with the result once complete. * @throws NullPointerException if {@code key} is {@code null} */ @SuppressWarnings("unchecked") public CompletableFuture<V> putIfAbsent(K key, V value, Duration ttl) { return client.submit(new MapCommands.PutIfAbsent(key, value, ttl.toMillis())).thenApply(result -> (V) result); } /** * Removes a the value for the given {@code key} from the map. * <p> * If no value for the given {@code key} is present in the map, the returned {@link CompletableFuture} will * be completed {@code null}. If a value is present, that value will be removed from the distributed map and the * returned future will be completed with that value. If the previous value was set with a TTL, the TTL will * be cancelled. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} or {@link CompletableFuture#join()} method to block the calling thread: * <pre> * {@code * String oldValue = map.remove("key").get(); * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.remove("key").thenAccept(oldValue -> { * ... * }); * } * </pre> * * @param key The key to remove. * @return A completable future to be completed with the result once complete. * @throws NullPointerException if {@code key} is {@code null} */ @SuppressWarnings("unchecked") public CompletableFuture<V> remove(Object key) { return client.submit(new MapCommands.Remove(key)).thenApply(result -> (V) result); } /** * Removes the given {@code key} from the map if its value matches the given {@code value}. * <p> * If no value for the given {@code key} is present in the map or if the value doesn't match the provided {@code value}, * the returned {@link CompletableFuture} will be completed {@code false}. If a value is present and matches {@code value}, * that value will be removed from the distributed map and the returned future will be completed {@code true}. If the * previous value was set with a TTL, the TTL will be cancelled. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} or {@link CompletableFuture#join()} method to block the calling thread: * <pre> * {@code * if (map.remove("key", "Hello world!").get()) { * ... * } * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.remove("key", "Hello world!").thenAccept(removed -> { * ... * }); * } * </pre> * * @param key The key to remove. * @param value The value to remove. * @return A completable future to be completed with the result once complete. * @throws NullPointerException if {@code key} is {@code null} */ public CompletableFuture<Boolean> remove(K key, V value) { return client.submit(new MapCommands.RemoveIfPresent(key, value)); } /** * Replaces a value in the map if the {@code key} exists. * <p> * If the given {@code key} is not already present in the map, no change will be made and {@code null} will be returned. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} or {@link CompletableFuture#join()} method to block the calling thread: * <pre> * {@code * String oldValue = map.replace("key", "Hello world!").get(); * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.replace("key", "Hello world!").thenAccept(oldValue -> { * ... * }); * } * </pre> * * @param key The key to replace. * @param value The value with which to replace the key if it exists. * @return A completable future to be completed with the result once complete. * @throws NullPointerException if {@code key} is {@code null} */ @SuppressWarnings("unchecked") public CompletableFuture<V> replace(K key, V value) { return client.submit(new MapCommands.Replace(key, value)).thenApply(result -> (V) result); } /** * Replaces a value in the map if the {@code key} exist. * <p> * If the given {@code key} is not already present in the map, no change will be made and {@code null} will be returned. * <p> * If the value is successfully replaced, the {@code value} will remain in the map until the provided {@link Duration} * of time has elapsed or it is overridden by a more recent put operation. Note that the provided {@code ttl} should * only be considered an estimate of time. Values may be evicted at some arbitrary point after the provided duration * has elapsed, but never before. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} or {@link CompletableFuture#join()} method to block the calling thread: * <pre> * {@code * String oldValue = map.replace("key", "Hello world!", Duration.ofSeconds(10)).get(); * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.replace("key", "Hello world!", Duration.ofSeconds(10)).thenAccept(oldValue -> { * ... * }); * } * </pre> * * @param key The key to replace. * @param value The value with which to replace the key if it exists. * @param ttl The duration after which to expire the key/value. * @return A completable future to be completed with the result once complete. * @throws NullPointerException if {@code key} is {@code null} */ @SuppressWarnings("unchecked") public CompletableFuture<V> replace(K key, V value, Duration ttl) { return client.submit(new MapCommands.Replace(key, value, ttl.toMillis())).thenApply(result -> (V) result); } /** * Replaces a value in the map. * <p> * If the given {@code key} is not already present in the map, no change will be made and {@code null} will be returned. * If the key is present and its value matches {@code oldValue}, it will be updated with {@code newValue}. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} or {@link CompletableFuture#join()} method to block the calling thread: * <pre> * {@code * if (map.replace("key", "Hello world!", "Hello world again!").get()) { * ... * } * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.replace("key", "Hello world!", "Hello world again!").thenAccept(replaced -> { * ... * }); * } * </pre> * * @param key The key to replace. * @param oldValue The value to check. * @param newValue The value to replace. * @return A completable future to be completed with the result once complete. * @throws NullPointerException if {@code key} is {@code null} */ public CompletableFuture<Boolean> replace(K key, V oldValue, V newValue) { return client.submit(new MapCommands.ReplaceIfPresent(key, oldValue, newValue)); } /** * Replaces a value in the map with a time-to-live. * <p> * If the given {@code key} is not already present in the map, no change will be made and {@code null} will be returned. * If the key is present and its value matches {@code oldValue}, it will be updated with {@code newValue}. * <p> * If the value is successfully replaced, the {@code value} will remain in the map until the provided {@link Duration} * of time has elapsed or it is overridden by a more recent put operation. Note that the provided {@code ttl} should * only be considered an estimate of time. Values may be evicted at some arbitrary point after the provided duration * has elapsed, but never before. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} or {@link CompletableFuture#join()} method to block the calling thread: * <pre> * {@code * if (map.replace("key", "Hello world!", "Hello world again!", Duration.ofSeconds(10)).get()) { * ... * } * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.replace("key", "Hello world!", "Hello world again!", Duration.ofSeconds(10)).thenAccept(oldValue -> { * ... * }); * } * </pre> * * @param key The key to replace. * @param oldValue The value to check. * @param newValue The value to replace. * @param ttl The duration after which to expire the key/value. * @return A completable future to be completed with the result once complete. * @throws NullPointerException if {@code key} is {@code null} */ public CompletableFuture<Boolean> replace(K key, V oldValue, V newValue, Duration ttl) { return client.submit(new MapCommands.ReplaceIfPresent(key, oldValue, newValue, ttl.toMillis())); } /** * Reads the set of all keys in the map. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} or {@link CompletableFuture#join()} method to block the calling thread: * <pre> * {@code * Set<String> keys = map.keySet().get(); * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.keySet().thenAccept(keys -> { * keys.forEach(key -> ...); * }); * map.replace("key", "Hello world!", "Hello world again!", Duration.ofSeconds(10)).thenAccept(oldValue -> { * ... * }); * } * </pre> * * @return A completable future to be completed with the result once complete. */ @SuppressWarnings("unchecked") public CompletableFuture<Set<K>> keySet() { return client.submit(new MapCommands.KeySet()).thenApply(keys -> (Set<K>) keys); } /** * Reads the set of all keys in the map. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} or {@link CompletableFuture#join()} method to block the calling thread: * <pre> * {@code * Set<String> keys = map.keySet().get(); * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.keySet().thenAccept(keys -> { * keys.forEach(key -> ...); * }); * map.replace("key", "Hello world!", "Hello world again!", Duration.ofSeconds(10)).thenAccept(oldValue -> { * ... * }); * } * </pre> * <p> * If the provided {@link ReadConsistency} is {@link ReadConsistency#LOCAL} and local caching is enabled for * the map, reads will be serviced via the cache. Otherwise, reads will fall back to {@link ReadConsistency#SEQUENTIAL} * if the cache is not enabled. * * @param consistency The read consistency level. * @return A completable future to be completed with the result once complete. */ @SuppressWarnings("unchecked") public CompletableFuture<Set<K>> keySet(ReadConsistency consistency) { if (consistency == ReadConsistency.LOCAL && cache != null) { return CompletableFuture.completedFuture(cache.keySet()); } return client.submit(new MapCommands.KeySet(consistency.level())).thenApply(keys -> (Set<K>) keys); } /** * Reads the collection of all values in the map. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} or {@link CompletableFuture#join()} method to block the calling thread: * <pre> * {@code * Set<String> keys = map.keySet().get(); * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.keySet().thenAccept(keys -> { * keys.forEach(key -> ...); * }); * map.replace("key", "Hello world!", "Hello world again!", Duration.ofSeconds(10)).thenAccept(oldValue -> { * ... * }); * } * </pre> * * @return A completable future to be completed with the result once complete. */ @SuppressWarnings("unchecked") public CompletableFuture<Collection<V>> values() { return client.submit(new MapCommands.Values()).thenApply(values -> (Collection<V>) values); } /** * Reads the collection of all values in the map. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} or {@link CompletableFuture#join()} method to block the calling thread: * <pre> * {@code * Set<String> keys = map.keySet().get(); * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.keySet().thenAccept(keys -> { * keys.forEach(key -> ...); * }); * map.replace("key", "Hello world!", "Hello world again!", Duration.ofSeconds(10)).thenAccept(oldValue -> { * ... * }); * } * </pre> * <p> * If the provided {@link ReadConsistency} is {@link ReadConsistency#LOCAL} and local caching is enabled for * the map, reads will be serviced via the cache. Otherwise, reads will fall back to {@link ReadConsistency#SEQUENTIAL} * if the cache is not enabled. * * @param consistency The read consistency level. * @return A completable future to be completed with the result once complete. */ @SuppressWarnings("unchecked") public CompletableFuture<Collection<V>> values(ReadConsistency consistency) { if (consistency == ReadConsistency.LOCAL && cache != null) { return CompletableFuture.completedFuture(cache.values()); } return client.submit(new MapCommands.Values(consistency.level())).thenApply(values -> (Collection<V>) values); } /** * Reads the set of all entries in the map. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} or {@link CompletableFuture#join()} method to block the calling thread: * <pre> * {@code * Set<String> keys = map.keySet().get(); * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.keySet().thenAccept(keys -> { * keys.forEach(key -> ...); * }); * map.replace("key", "Hello world!", "Hello world again!", Duration.ofSeconds(10)).thenAccept(oldValue -> { * ... * }); * } * </pre> * * @return A completable future to be completed with the result once complete. */ @SuppressWarnings("unchecked") public CompletableFuture<Set<Map.Entry<K, V>>> entrySet() { return client.submit(new MapCommands.EntrySet()).thenApply(entries -> (Set<Map.Entry<K, V>>) entries); } /** * Reads the set of all entries in the map. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#get()} or {@link CompletableFuture#join()} method to block the calling thread: * <pre> * {@code * Set<String> keys = map.keySet().get(); * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.keySet().thenAccept(keys -> { * keys.forEach(key -> ...); * }); * map.replace("key", "Hello world!", "Hello world again!", Duration.ofSeconds(10)).thenAccept(oldValue -> { * ... * }); * } * </pre> * <p> * If the provided {@link ReadConsistency} is {@link ReadConsistency#LOCAL} and local caching is enabled for * the map, reads will be serviced via the cache. Otherwise, reads will fall back to {@link ReadConsistency#SEQUENTIAL} * if the cache is not enabled. * * @param consistency The read consistency level. * @return A completable future to be completed with the result once complete. */ @SuppressWarnings("unchecked") public CompletableFuture<Set<Map.Entry<K, V>>> entrySet(ReadConsistency consistency) { if (consistency == ReadConsistency.LOCAL && cache != null) { return CompletableFuture.completedFuture(cache.entrySet()); } return client.submit(new MapCommands.EntrySet(consistency.level())).thenApply(entries -> (Set<Map.Entry<K, V>>) entries); } /** * Removes all entries from the map. * <p> * This method returns a {@link CompletableFuture} which can be used to block until the operation completes * or to be notified in a separate thread once the operation completes. To block until the operation completes, * use the {@link CompletableFuture#join()} method to block the calling thread: * <pre> * {@code * map.clear().join(); * } * </pre> * Alternatively, to execute the operation asynchronous and be notified once the operation is complete in a different * thread, use one of the many completable future callbacks: * <pre> * {@code * map.clear().thenRun(() -> { * ... * }); * } * </pre> * * @return A completable future to be completed once the operation is complete. */ public CompletableFuture<Void> clear() { return client.submit(new MapCommands.Clear()); } /** * Registers a new event listener for the given event type. * <p> * Resource implementations should use this method to register event listeners for non-lifecycle * resource events. Calling this method will cause the local session to be automatically registered * with the state machine to listen for new events published in the state machine via the * {@code notify} method. * * @param type The event type for which to register the event listener. * @param callback The event listener callback. * @param <T> The event type. * @return A completable future to be completed once the event listener has been registered. */ protected synchronized <T extends Event> CompletableFuture<Listener<T>> onEvent(K key, EventType type, Consumer<T> callback) { Map<Integer, Set<Consumer>> keyListeners = this.eventListeners.computeIfAbsent(key, k -> new ConcurrentHashMap<>()); Set<Consumer> eventListeners = keyListeners.computeIfAbsent(type.id(), id -> new CopyOnWriteArraySet<>()); eventListeners.add(callback); return client.submit(new MapCommands.KeyListen(type.id(), key)).whenComplete((result, error) -> { if (error != null) { synchronized (this) { eventListeners.remove(callback); if (eventListeners.isEmpty()) { keyListeners.remove(type.id()); if (keyListeners.isEmpty()) { this.eventListeners.remove(key); } client.submit(new MapCommands.KeyUnlisten(type.id(), key)); } } } }).<Listener<T>>thenApply(v -> new Listener<T>() { @Override public void accept(T event) { callback.accept(event); } @Override public void close() { synchronized (this) { eventListeners.remove(callback); if (eventListeners.isEmpty()) { keyListeners.remove(type.id()); if (keyListeners.isEmpty()) { DistributedMap.this.eventListeners.remove(key); } client.submit(new MapCommands.KeyUnlisten(type.id(), key)); } } } }); } /** * Registers a {@link #put(Object, Object)} event listener. * * @param callback The put event callback. * @return The event listener context. */ public CompletableFuture<Listener<EntryEvent<K, V>>> onAdd(Consumer<EntryEvent<K, V>> callback) { return onEvent(Events.ADD, callback); } /** * Registers a {@link #put(Object, Object)} event listener. * * @param callback The put event callback. * @return The event listener context. */ public CompletableFuture<Listener<EntryEvent<K, V>>> onAdd(K key, Consumer<EntryEvent<K, V>> callback) { return onEvent(key, Events.ADD, callback); } /** * Registers a {@link #put(Object, Object)} event listener. * * @param callback The put event listener callback. * @return The event listener context. */ public CompletableFuture<Listener<EntryEvent<K, V>>> onUpdate(Consumer<EntryEvent<K, V>> callback) { return onEvent(Events.UPDATE, callback); } /** * Registers a {@link #put(Object, Object)} event listener. * * @param callback The put event listener callback. * @return The event listener context. */ public CompletableFuture<Listener<EntryEvent<K, V>>> onUpdate(K key, Consumer<EntryEvent<K, V>> callback) { return onEvent(key, Events.UPDATE, callback); } /** * Registers a {@link #remove(Object)} event listener. * * @param callback The remove event listener callback. * @return The event listener context. */ public CompletableFuture<Listener<EntryEvent<K, V>>> onRemove(Consumer<EntryEvent<K, V>> callback) { return onEvent(Events.REMOVE, callback); } /** * Registers a {@link #remove(Object)} event listener. * * @param callback The remove event listener callback. * @return The event listener context. */ public CompletableFuture<Listener<EntryEvent<K, V>>> onRemove(K key, Consumer<EntryEvent<K, V>> callback) { return onEvent(key, Events.REMOVE, callback); } @Override public CompletableFuture<DistributedMap<K, V>> open() { CompletableFuture<DistributedMap<K, V>> future = super.open().thenApply(m -> { client.<EntryEvent>onEvent("key", this::onEvent); return this; }); if (options.isLocalCache()) { return future.thenCompose(v -> onAdd(this::onAdd)) .thenCompose(v -> onUpdate(this::onUpdate)) .thenCompose(v -> onRemove(this::onRemove)) .thenApply(v -> this); } return future; } /** * Handles an event from the cluster. */ @SuppressWarnings("unchecked") private void onEvent(EntryEvent event) { Map<Integer, Set<Consumer>> keyListeners = eventListeners.get(event.entry.getKey()); if (keyListeners != null) { Set<Consumer> eventListeners = keyListeners.get(event.type.id()); if (eventListeners != null) { for (Consumer listener : eventListeners) { listener.accept(event); } } } } /** * Updates the cache when an entry is added to the map. */ private void onAdd(EntryEvent<K, V> event) { cache.put(event.entry.getKey(), event.entry.getValue()); } /** * Updates the cache when an entry is updated in the map. */ private void onUpdate(EntryEvent<K, V> event) { cache.put(event.entry.getKey(), event.entry.getValue()); } /** * Updates the cache when an entry is removed from the map. */ private void onRemove(EntryEvent<K, V> event) { cache.remove(event.entry.getKey()); } /** * Distributed queue events. */ public enum Events implements EventType { /** * Entry add event. */ ADD, /** * Entry update event. */ UPDATE, /** * Entry remove event. */ REMOVE; @Override public int id() { return ordinal(); } } /** * Map entry event. * * @param <K> The entry key type. * @param <V> The entry value type. */ public static class EntryEvent<K, V> implements Event, CatalystSerializable { private EventType type; private Map.Entry<K, V> entry; public EntryEvent() { } public EntryEvent(EventType type, Map.Entry<K, V> entry) { this.type = type; this.entry = entry; } @Override public EventType type() { return type; } /** * Returns the event entry. * * @return The event entry. */ public Map.Entry<K, V> entry() { return entry; } @Override public void writeObject(BufferOutput<?> buffer, Serializer serializer) { buffer.writeByte(type.id()); serializer.writeObject(entry.getKey(), buffer); serializer.writeObject(entry.getValue(), buffer); } @Override public void readObject(BufferInput<?> buffer, Serializer serializer) { type = Events.values()[buffer.readByte()]; K key = serializer.readObject(buffer); V value = serializer.readObject(buffer); entry = new MapEntry<>(key, value); } } }