package com.lambdaworks.redis.cluster.models.partitions; import java.util.*; import com.lambdaworks.redis.cluster.SlotHash; import com.lambdaworks.redis.internal.LettuceAssert; /** * Cluster topology view. An instance of {@link Partitions} provides access to the partitions of a Redis Cluster. A partition is * represented by a Redis Cluster node that has a {@link RedisClusterNode#getNodeId() nodeId} and * {@link RedisClusterNode#getUri() connection point details}. * <p> * Partitions can be looked up by {@code nodeId} or {@code slot} (masters only). A nodeId can be migrated to a different host. * Partitions are cached to ensure a cheap lookup by {@code slot}. Users of {@link Partitions} are required to call * {@link #updateCache()} after topology changes occur. * </p> * * Topology changes are: * * <ul> * <li>Changes in {@link com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode.NodeFlag#MASTER}/ * {@link com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode.NodeFlag#SLAVE} state</li> * <li>Newly added or removed nodes to/from the Redis Cluster</li> * <li>Changes in {@link RedisClusterNode#getSlots()} responsibility</li> * <li>Changes to the {@link RedisClusterNode#getSlaveOf() slave replication source} (the master of a slave)</li> * <li>Changes to the {@link RedisClusterNode#getUri()} () connection point}</li> * </ul> * * <p> * All query/read operations use the read-only view. Updates to Partitions are performed in an atomic way. Changes to the * read-only cache become visible after the partition update is completed. * </p> * * @author Mark Paluch * @since 3.0 */ public class Partitions implements Collection<RedisClusterNode> { private static final RedisClusterNode[] EMPTY = new RedisClusterNode[SlotHash.SLOT_COUNT]; private final List<RedisClusterNode> partitions = new ArrayList<>(); private volatile RedisClusterNode slotCache[] = EMPTY; private volatile Collection<RedisClusterNode> nodeReadView = Collections.emptyList(); /** * Retrieve a {@link RedisClusterNode} by its slot number. This method does not distinguish between masters and slaves. * * @param slot the slot * @return RedisClusterNode or {@literal null} */ public RedisClusterNode getPartitionBySlot(int slot) { return slotCache[slot]; } /** * Retrieve a {@link RedisClusterNode} by its node id. * * @param nodeId the nodeId * @return RedisClusterNode or {@literal null} */ public RedisClusterNode getPartitionByNodeId(String nodeId) { for (RedisClusterNode partition : nodeReadView) { if (partition.getNodeId().equals(nodeId)) { return partition; } } return null; } /** * Update the partition cache. Updates are necessary after the partition details have changed. */ public void updateCache() { synchronized (partitions) { if (partitions.isEmpty()) { this.slotCache = EMPTY; this.nodeReadView = Collections.emptyList(); return; } RedisClusterNode[] slotCache = new RedisClusterNode[SlotHash.SLOT_COUNT]; List<RedisClusterNode> readView = new ArrayList<>(partitions.size()); for (RedisClusterNode partition : partitions) { readView.add(partition); for (Integer integer : partition.getSlots()) { slotCache[integer.intValue()] = partition; } } this.slotCache = slotCache; this.nodeReadView = Collections.unmodifiableCollection(readView); } } /** * Returns an iterator over the {@link RedisClusterNode nodes} in this {@link Partitions} from the read-view. The * {@link Iterator} remains consistent during partition updates with the nodes that have been part of the {@link Partitions} * . {@link RedisClusterNode Nodes} added/removed during iteration/after obtaining the {@link Iterator} don't become visible * during iteration but upon the next call to {@link #iterator()}. * * @return an iterator over the {@link RedisClusterNode nodes} in this {@link Partitions} from the read-view. */ @Override public Iterator<RedisClusterNode> iterator() { return nodeReadView.iterator(); } /** * Returns the internal {@link List} of {@link RedisClusterNode} that holds the partition source. This {@link List} is used * to populate partition caches and should not be used directly and subject to change by refresh processes. Access * (read/write) requires synchronization on {@link #getPartitions()}. * * @return the internal partition source. */ public List<RedisClusterNode> getPartitions() { return partitions; } /** * Adds a partition <b>without</b> updating the read view/cache. * * @param partition the partition */ public void addPartition(RedisClusterNode partition) { LettuceAssert.notNull(partition, "Partition must not be null"); synchronized (this) { slotCache = EMPTY; partitions.add(partition); } } /** * @return the number of elements using the read-view. */ @Override public int size() { return nodeReadView.size(); } /** * Returns the {@link RedisClusterNode} at {@code index}. * * @param index the index * @return the requested element using the read-view. */ public RedisClusterNode getPartition(int index) { return partitions.get(index); } /** * Update partitions and rebuild slot cache. * * @param partitions list of new partitions */ public void reload(List<RedisClusterNode> partitions) { LettuceAssert.noNullElements(partitions, "Partitions must not contain null elements"); synchronized (partitions) { this.partitions.clear(); this.partitions.addAll(partitions); updateCache(); } } /** * Returns {@literal true} if this {@link Partitions} contains no elements using the read-view. * * @return {@literal true} if this {@link Partitions} contains no elements using the read-view. */ @Override public boolean isEmpty() { return nodeReadView.isEmpty(); } /** * Returns {@literal true} if this {@link Partitions} contains the specified element. * * @param o the element to check for * @return {@literal true} if this {@link Partitions} contains the specified element */ @Override public boolean contains(Object o) { return nodeReadView.contains(o); } /** * Add all {@link RedisClusterNode nodes} from the given collection and update the read-view/caches. * * @param c must not be {@literal null} * @return {@literal true} if this {@link Partitions} changed as a result of the call */ @Override public boolean addAll(Collection<? extends RedisClusterNode> c) { LettuceAssert.noNullElements(c, "Partitions must not contain null elements"); synchronized (partitions) { boolean b = partitions.addAll(c); updateCache(); return b; } } /** * Remove all {@link RedisClusterNode nodes} from the {@link Partitions} using elements from the given collection and update * the read-view/caches. * * @param c must not be {@literal null} * @return {@literal true} if this {@link Partitions} changed as a result of the call */ @Override public boolean removeAll(Collection<?> c) { synchronized (partitions) { boolean b = getPartitions().removeAll(c); updateCache(); return b; } } /** * Retains only the elements in this {@link Partitions} that are contained in the specified collection (optional * operation)and update the read-view/caches. In other words, removes from this collection all of its elements that are not * contained in the specified collection. * * @param c must not be {@literal null} * @return {@literal true} if this {@link Partitions} changed as a result of the call */ @Override public boolean retainAll(Collection<?> c) { synchronized (partitions) { boolean b = getPartitions().retainAll(c); updateCache(); return b; } } /** * Removes all {@link RedisClusterNode nodes} and update the read-view/caches. */ @Override public void clear() { synchronized (partitions) { getPartitions().clear(); updateCache(); } } /** * Returns an array containing all of the elements in this {@link Partitions} using the read-view. * * @return an array containing all of the elements in this {@link Partitions} using the read-view. */ @Override public Object[] toArray() { return nodeReadView.toArray(); } /** * Returns an array containing all of the elements in this {@link Partitions} using the read-view. * * @param a the array into which the elements of this collection are to be stored, if it is big enough; otherwise, a new * array of the same runtime type is allocated for this purpose. * @param <T> type of the array to contain the collection * @return an array containing all of the elements in this {@link Partitions} using the read-view. */ @Override public <T> T[] toArray(T[] a) { return nodeReadView.toArray(a); } /** * Adds the {@link RedisClusterNode} to this {@link Partitions}. * * @param redisClusterNode must not be {@literal null} * @return {@literal true} if this {@link Partitions} changed as a result of the call */ @Override public boolean add(RedisClusterNode redisClusterNode) { synchronized (partitions) { LettuceAssert.notNull(redisClusterNode, "RedisClusterNode must not be null"); boolean add = getPartitions().add(redisClusterNode); updateCache(); return add; } } /** * Remove the element from this {@link Partitions}. * * @param o must not be {@literal null} * @return {@literal true} if this {@link Partitions} changed as a result of the call */ @Override public boolean remove(Object o) { synchronized (partitions) { boolean remove = getPartitions().remove(o); updateCache(); return remove; } } /** * Returns {@literal true} if this collection contains all of the elements in the specified collection. * * @param c collection to be checked for containment in this collection, must not be {@literal null} * @return */ @Override public boolean containsAll(Collection<?> c) { return nodeReadView.containsAll(c); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(getClass().getSimpleName()); sb.append(" ").append(partitions); return sb.toString(); } }