com.datastax.driver.core.Metadata Maven / Gradle / Ivy
/*
* Copyright DataStax, Inc.
*
* 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.
*
* The following only applies to changes made to this file as part of YugaByte development.
*
* Portions Copyright (c) YugaByte, Inc.
*
* 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 com.datastax.driver.core;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.yugabyte.driver.core.QualifiedTableName;
import com.yugabyte.driver.core.TableSplitMetadata;
import io.netty.util.collection.IntObjectHashMap;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Keeps metadata on the connected cluster, including known nodes and schema definitions. */
public class Metadata {
private static final Logger logger = LoggerFactory.getLogger(Metadata.class);
final Cluster.Manager cluster;
volatile String clusterName;
volatile String partitioner;
// Holds the contact points until we have a connection to the cluster
private final List contactPoints = new CopyOnWriteArrayList();
// The hosts, keyed by their host_id
private final ConcurrentMap hosts = new ConcurrentHashMap();
final ConcurrentMap keyspaces =
new ConcurrentHashMap();
private volatile TokenMap tokenMap;
// Partition split metadata for each table.
private volatile Map tableSplits;
final ReentrantLock lock = new ReentrantLock();
// See https://github.com/apache/cassandra/blob/trunk/doc/cql3/CQL.textile#appendixA
private static final IntObjectHashMap> RESERVED_KEYWORDS =
indexByCaseInsensitiveHash(
"add",
"allow",
"alter",
"and",
"any",
"apply",
"asc",
"authorize",
"batch",
"begin",
"by",
"columnfamily",
"create",
"delete",
"desc",
"drop",
"each_quorum",
"from",
"grant",
"in",
"index",
"inet",
"infinity",
"insert",
"into",
"keyspace",
"keyspaces",
"limit",
"local_one",
"local_quorum",
"modify",
"nan",
"norecursive",
"of",
"on",
"one",
"order",
"password",
"primary",
"quorum",
"rename",
"revoke",
"schema",
"select",
"set",
"table",
"to",
"token",
"three",
"truncate",
"two",
"unlogged",
"update",
"use",
"using",
"where",
"with");
/**
* Set the partition splits metadata for each table. Typically called when refreshing the schema
* metadata or the known list of nodes.
*
* @param tableSplits the table splits map
*/
void setTableSplits(Map tableSplits) {
this.tableSplits = tableSplits;
}
/**
* Returns the partition split metadata for a given table.
*
* @param keyspaceName the keyspace name
* @param tableName the table name
* @return the table split metadata
*/
public TableSplitMetadata getTableSplitMetadata(String keyspaceName, String tableName) {
if (tableSplits == null) {
return null;
}
return tableSplits.get(new QualifiedTableName(keyspaceName, tableName));
}
Metadata(Cluster.Manager cluster) {
this.cluster = cluster;
}
// rebuilds the token map with the current hosts, typically when refreshing schema metadata
void rebuildTokenMap() {
lock.lock();
try {
if (tokenMap == null) return;
this.tokenMap =
TokenMap.build(
tokenMap.factory,
tokenMap.primaryToTokens,
keyspaces.values(),
tokenMap.ring,
tokenMap.tokenRanges,
tokenMap.tokenToPrimary);
} finally {
lock.unlock();
}
}
// rebuilds the token map for a new set of hosts, typically when refreshing nodes list
void rebuildTokenMap(Token.Factory factory, Map> allTokens) {
lock.lock();
try {
this.tokenMap = TokenMap.build(factory, allTokens, keyspaces.values());
} finally {
lock.unlock();
}
}
Host newHost(EndPoint endPoint) {
return new Host(endPoint, cluster.convictionPolicyFactory, cluster);
}
void addContactPoint(EndPoint contactPoint) {
contactPoints.add(newHost(contactPoint));
}
List getContactPoints() {
return contactPoints;
}
Host getContactPoint(EndPoint endPoint) {
for (Host host : contactPoints) {
if (host.getEndPoint().equals(endPoint)) {
return host;
}
}
return null;
}
/**
* @return the previous host associated with this id, or {@code null} if there was no such host.
*/
Host addIfAbsent(Host host) {
return hosts.putIfAbsent(host.getHostId(), host);
}
boolean remove(Host host) {
return hosts.remove(host.getHostId()) != null;
}
Host getHost(UUID hostId) {
return hosts.get(hostId);
}
/**
* @param broadcastRpcAddress the untranslated broadcast RPC address, as indicated in
* server events.
*/
Host getHost(InetSocketAddress broadcastRpcAddress) {
for (Host host : hosts.values()) {
if (broadcastRpcAddress.equals(host.getBroadcastRpcAddress())) {
return host;
}
}
return null;
}
Host getHost(EndPoint endPoint) {
for (Host host : hosts.values()) {
if (host.getEndPoint().equals(endPoint)) {
return host;
}
}
return null;
}
// For internal use only
Collection allHosts() {
return hosts.values();
}
/*
* Deal with case sensitivity for a given element id (keyspace, table, column, etc.)
*
* This method is used to convert identifiers provided by the client (through methods such as getKeyspace(String)),
* to the format used internally by the driver.
*
* We expect client-facing APIs to behave like cqlsh, that is:
* - identifiers that are mixed-case or contain special characters should be quoted.
* - unquoted identifiers will be lowercased: getKeyspace("Foo") will look for a keyspace named "foo"
*/
static String handleId(String id) {
// Shouldn't really happen for this method, but no reason to fail here
if (id == null) return null;
boolean isAlphanumericLowCase = true;
boolean isAlphanumeric = true;
for (int i = 0; i < id.length(); i++) {
char c = id.charAt(i);
if (c >= 65 && c <= 90) { // A-Z
isAlphanumericLowCase = false;
} else if (!((c >= 48 && c <= 57) // 0-9
|| (c == 95) // _ (underscore)
|| (c >= 97 && c <= 122) // a-z
)) {
isAlphanumeric = false;
isAlphanumericLowCase = false;
break;
}
}
if (isAlphanumericLowCase) {
return id;
}
if (isAlphanumeric) {
return id.toLowerCase();
}
// Check if it's enclosed in quotes. If it is, remove them and unescape internal double quotes
return ParseUtils.unDoubleQuote(id);
}
/**
* Quotes a CQL identifier if necessary.
*
* This is similar to {@link #quote(String)}, except that it won't quote the input string if it
* can safely be used as-is. For example:
*
*
* - {@code quoteIfNecessary("foo").equals("foo")} (no need to quote).
*
- {@code quoteIfNecessary("Foo").equals("\"Foo\"")} (identifier is mixed case so case
* sensitivity is required)
*
- {@code quoteIfNecessary("foo bar").equals("\"foo bar\"")} (identifier contains special
* characters)
*
- {@code quoteIfNecessary("table").equals("\"table\"")} (identifier is a reserved CQL
* keyword)
*
*
* @param id the "internal" form of the identifier. That is, the identifier as it would appear in
* Cassandra system tables (such as {@code system_schema.tables}, {@code
* system_schema.columns}, etc.)
* @return the identifier as it would appear in a CQL query string. This is also how you need to
* pass it to public driver methods, such as {@link #getKeyspace(String)}.
*/
public static String quoteIfNecessary(String id) {
return needsQuote(id) ? quote(id) : id;
}
/**
* We don't need to escape an identifier if it matches non-quoted CQL3 ids ([a-z][a-z0-9_]*), and
* if it's not a CQL reserved keyword.
*
* When 'Migrating from compact storage' after DROP COMPACT STORAGE on the table, it can have a
* column with an empty name. (See JAVA-2174 for the reference) For that case, we need to escape
* empty column name.
*/
private static boolean needsQuote(String s) {
// this method should only be called for C*-provided identifiers,
// so we expect it to be non-null
assert s != null;
if (s.isEmpty()) return true;
char c = s.charAt(0);
if (!(c >= 97 && c <= 122)) // a-z
return true;
for (int i = 1; i < s.length(); i++) {
c = s.charAt(i);
if (!((c >= 48 && c <= 57) // 0-9
|| (c == 95) // _
|| (c >= 97 && c <= 122) // a-z
)) {
return true;
}
}
return isReservedCqlKeyword(s);
}
/**
* Builds the internal name of a function/aggregate, which is similar, but not identical, to the
* function/aggregate signature. This is only used to generate keys for internal metadata maps
* (KeyspaceMetadata.functions and. KeyspaceMetadata.aggregates). Note that if simpleName comes
* from the user, the caller must call handleId on it before passing it to this method. Note that
* this method does not necessarily generates a valid CQL function signature. Note that
* argumentTypes can be either a list of strings (schema change events) or a list of DataTypes
* (function lookup from client code). This method must ensure that both cases produce the same
* identifier.
*/
static String fullFunctionName(String simpleName, Collection> argumentTypes) {
StringBuilder sb = new StringBuilder(simpleName);
sb.append('(');
boolean first = true;
for (Object argumentType : argumentTypes) {
if (first) first = false;
else sb.append(',');
// user types must be represented by their names only,
// without keyspace prefix, because that's how
// they appear in a schema change event (in targetSignature)
if (argumentType instanceof UserType) {
UserType userType = (UserType) argumentType;
String typeName = Metadata.quoteIfNecessary(userType.getTypeName());
sb.append(typeName);
} else {
sb.append(argumentType);
}
}
sb.append(')');
return sb.toString();
}
/**
* Quote a keyspace, table or column identifier to make it case sensitive.
*
*
CQL identifiers, including keyspace, table and column ones, are case insensitive by default.
* Case sensitive identifiers can however be provided by enclosing the identifier in double quotes
* (see the CQL
* documentation for details). If you are using case sensitive identifiers, this method can be
* used to enclose such identifiers in double quotes, making them case sensitive.
*
*
Note that reserved CQL
* keywords should also be quoted. You can check if a given identifier is a reserved keyword
* by calling {@link #isReservedCqlKeyword(String)}.
*
* @param id the keyspace or table identifier.
* @return {@code id} enclosed in double-quotes, for use in methods like {@link #getReplicas},
* {@link #getKeyspace}, {@link KeyspaceMetadata#getTable} or even {@link
* Cluster#connect(String)}.
*/
public static String quote(String id) {
return ParseUtils.doubleQuote(id);
}
/**
* Checks whether an identifier is a known reserved CQL keyword or not.
*
*
The check is case-insensitive, i.e., the word "{@code KeYsPaCe}" would be considered as a
* reserved CQL keyword just as "{@code keyspace}".
*
*
Note: The list of reserved CQL keywords is subject to change in future versions of
* Cassandra. As a consequence, this method is provided solely as a convenience utility and should
* not be considered as an authoritative source of truth for checking reserved CQL keywords.
*
* @param id the identifier to check; should not be {@code null}.
* @return {@code true} if the given identifier is a known reserved CQL keyword, {@code false}
* otherwise.
*/
public static boolean isReservedCqlKeyword(String id) {
if (id == null) {
return false;
}
int hash = caseInsensitiveHash(id);
List keywords = RESERVED_KEYWORDS.get(hash);
if (keywords == null) {
return false;
} else {
for (char[] keyword : keywords) {
if (equalsIgnoreCaseAscii(id, keyword)) {
return true;
}
}
return false;
}
}
private static int caseInsensitiveHash(String str) {
int hashCode = 17;
for (int i = 0; i < str.length(); i++) {
char c = toLowerCaseAscii(str.charAt(i));
hashCode = 31 * hashCode + c;
}
return hashCode;
}
// keyword is expected as a second argument always in low case
private static boolean equalsIgnoreCaseAscii(String str1, char[] str2LowCase) {
if (str1.length() != str2LowCase.length) return false;
for (int i = 0; i < str1.length(); i++) {
char c1 = str1.charAt(i);
char c2Low = str2LowCase[i];
if (c1 == c2Low) {
continue;
}
char low1 = toLowerCaseAscii(c1);
if (low1 == c2Low) {
continue;
}
return false;
}
return true;
}
private static char toLowerCaseAscii(char c) {
if (c >= 65 && c <= 90) { // A-Z
c ^= 0x20; // convert to low case
}
return c;
}
private static IntObjectHashMap> indexByCaseInsensitiveHash(String... words) {
IntObjectHashMap> result = new IntObjectHashMap>();
for (String word : words) {
char[] wordAsCharArray = word.toLowerCase().toCharArray();
int hash = caseInsensitiveHash(word);
List list = result.get(hash);
if (list == null) {
list = new ArrayList();
result.put(hash, list);
}
list.add(wordAsCharArray);
}
return result;
}
/**
* Returns the token ranges that define data distribution in the ring.
*
* Note that this information is refreshed asynchronously by the control connection, when
* schema or ring topology changes. It might occasionally be stale.
*
* @return the token ranges. Note that the result might be stale or empty if metadata was
* explicitly disabled with {@link QueryOptions#setMetadataEnabled(boolean)}.
*/
public Set getTokenRanges() {
TokenMap current = tokenMap;
return (current == null) ? Collections.emptySet() : current.tokenRanges;
}
/**
* Returns the token ranges that are replicated on the given host, for the given keyspace.
*
* Note that this information is refreshed asynchronously by the control connection, when
* schema or ring topology changes. It might occasionally be stale (or even empty).
*
* @param keyspace the name of the keyspace to get token ranges for.
* @param host the host.
* @return the (immutable) set of token ranges for {@code host} as known by the driver. Note that
* the result might be stale or empty if metadata was explicitly disabled with {@link
* QueryOptions#setMetadataEnabled(boolean)}.
*/
public Set getTokenRanges(String keyspace, Host host) {
keyspace = handleId(keyspace);
TokenMap current = tokenMap;
if (current == null) {
return Collections.emptySet();
} else {
Map> dcRanges = current.hostsToRangesByKeyspace.get(keyspace);
if (dcRanges == null) {
return Collections.emptySet();
} else {
Set ranges = dcRanges.get(host);
return (ranges == null) ? Collections.emptySet() : ranges;
}
}
}
/**
* Returns the set of hosts that are replica for a given partition key.
*
* Note that this information is refreshed asynchronously by the control connection, when
* schema or ring topology changes. It might occasionally be stale (or even empty).
*
* @param keyspace the name of the keyspace to get replicas for.
* @param partitionKey the partition key for which to find the set of replica.
* @return the (immutable) set of replicas for {@code partitionKey} as known by the driver. Note
* that the result might be stale or empty if metadata was explicitly disabled with {@link
* QueryOptions#setMetadataEnabled(boolean)}.
*/
public Set getReplicas(String keyspace, ByteBuffer partitionKey) {
keyspace = handleId(keyspace);
TokenMap current = tokenMap;
if (current == null) {
return Collections.emptySet();
} else {
Set hosts = current.getReplicas(keyspace, current.factory.hash(partitionKey));
return hosts == null ? Collections.emptySet() : hosts;
}
}
/**
* Returns the set of hosts that are replica for a given token range.
*
* Note that it is assumed that the input range does not overlap across multiple host ranges.
* If the range extends over multiple hosts, it only returns the replicas for those hosts that are
* replicas for the last token of the range. This behavior may change in a future release, see JAVA-1355.
*
*
Also note that this information is refreshed asynchronously by the control connection, when
* schema or ring topology changes. It might occasionally be stale (or even empty).
*
* @param keyspace the name of the keyspace to get replicas for.
* @param range the token range.
* @return the (immutable) set of replicas for {@code range} as known by the driver. Note that the
* result might be stale or empty if metadata was explicitly disabled with {@link
* QueryOptions#setMetadataEnabled(boolean)}.
*/
public Set getReplicas(String keyspace, TokenRange range) {
keyspace = handleId(keyspace);
TokenMap current = tokenMap;
if (current == null) {
return Collections.emptySet();
} else {
Set hosts = current.getReplicas(keyspace, range.getEnd());
return hosts == null ? Collections.emptySet() : hosts;
}
}
/**
* The Cassandra name for the cluster connect to.
*
* @return the Cassandra name for the cluster connect to.
*/
public String getClusterName() {
return clusterName;
}
/**
* The partitioner in use as reported by the Cassandra nodes.
*
* @return the partitioner in use as reported by the Cassandra nodes.
*/
public String getPartitioner() {
return partitioner;
}
/**
* Returns the known hosts of this cluster.
*
* @return A set will all the know host of this cluster.
*/
public Set getAllHosts() {
return new HashSet(allHosts());
}
/**
* Checks whether hosts that are currently up agree on the schema definition.
*
* This method performs a one-time check only, without any form of retry; therefore {@link
* Cluster.Builder#withMaxSchemaAgreementWaitSeconds(int)} does not apply in this case.
*
* @return {@code true} if all hosts agree on the schema; {@code false} if they don't agree, or if
* the check could not be performed (for example, if the control connection is down).
*/
public boolean checkSchemaAgreement() {
try {
return cluster.controlConnection.checkSchemaAgreement();
} catch (Exception e) {
logger.warn("Error while checking schema agreement", e);
return false;
}
}
/**
* Returns the metadata of a keyspace given its name.
*
* @param keyspace the name of the keyspace for which metadata should be returned.
* @return the metadata of the requested keyspace or {@code null} if {@code keyspace} is not a
* known keyspace. Note that the result might be stale or null if metadata was explicitly
* disabled with {@link QueryOptions#setMetadataEnabled(boolean)}.
*/
public KeyspaceMetadata getKeyspace(String keyspace) {
return keyspaces.get(handleId(keyspace));
}
KeyspaceMetadata removeKeyspace(String keyspace) {
KeyspaceMetadata removed = keyspaces.remove(keyspace);
if (tokenMap != null) tokenMap.tokenToHostsByKeyspace.remove(keyspace);
return removed;
}
/**
* Returns a list of all the defined keyspaces.
*
* @return a list of all the defined keyspaces. Note that the result might be stale or empty if
* metadata was explicitly disabled with {@link QueryOptions#setMetadataEnabled(boolean)}.
*/
public List getKeyspaces() {
return new ArrayList(keyspaces.values());
}
/**
* Returns a {@code String} containing CQL queries representing the schema of this cluster.
*
* In other words, this method returns the queries that would allow to recreate the schema of
* this cluster.
*
*
Note that the returned String is formatted to be human readable (for some definition of
* human readable at least).
*
*
It might be stale or empty if metadata was explicitly disabled with {@link
* QueryOptions#setMetadataEnabled(boolean)}.
*
* @return the CQL queries representing this cluster schema as a {code String}.
*/
public String exportSchemaAsString() {
StringBuilder sb = new StringBuilder();
for (KeyspaceMetadata ksm : keyspaces.values()) sb.append(ksm.exportAsString()).append('\n');
return sb.toString();
}
/**
* Creates a tuple type given a list of types.
*
* @param types the types for the tuple type.
* @return the newly created tuple type.
*/
public TupleType newTupleType(DataType... types) {
return newTupleType(Arrays.asList(types));
}
/**
* Creates a tuple type given a list of types.
*
* @param types the types for the tuple type.
* @return the newly created tuple type.
*/
public TupleType newTupleType(List types) {
return new TupleType(
types, cluster.protocolVersion(), cluster.configuration.getCodecRegistry());
}
/**
* Builds a new {@link Token} from its string representation, according to the partitioner
* reported by the Cassandra nodes.
*
* @param tokenStr the string representation.
* @return the token.
* @throws IllegalStateException if the token factory was not initialized. This would typically
* happen if metadata was explicitly disabled with {@link
* QueryOptions#setMetadataEnabled(boolean)} before startup.
*/
public Token newToken(String tokenStr) {
TokenMap current = tokenMap;
if (current == null)
throw new IllegalStateException(
"Token factory not set. This should only happen if metadata was explicitly disabled");
return current.factory.fromString(tokenStr);
}
/**
* Builds a new {@link Token} from a partition key.
*
* @param components the components of the partition key, in their serialized form (obtained with
* {@link TypeCodec#serialize(Object, ProtocolVersion)}).
* @return the token.
* @throws IllegalStateException if the token factory was not initialized. This would typically
* happen if metadata was explicitly disabled with {@link
* QueryOptions#setMetadataEnabled(boolean)} before startup.
*/
public Token newToken(ByteBuffer... components) {
TokenMap current = tokenMap;
if (current == null)
throw new IllegalStateException(
"Token factory not set. This should only happen if metadata was explicitly disabled");
return current.factory.hash(SimpleStatement.compose(components));
}
/**
* Builds a new {@link TokenRange}.
*
* @param start the start token.
* @param end the end token.
* @return the range.
* @throws IllegalStateException if the token factory was not initialized. This would typically
* happen if metadata was explicitly disabled with {@link
* QueryOptions#setMetadataEnabled(boolean)} before startup.
*/
public TokenRange newTokenRange(Token start, Token end) {
TokenMap current = tokenMap;
if (current == null)
throw new IllegalStateException(
"Token factory not set. This should only happen if metadata was explicitly disabled");
return new TokenRange(start, end, current.factory);
}
Token.Factory tokenFactory() {
TokenMap current = tokenMap;
return (current == null) ? null : current.factory;
}
void triggerOnKeyspaceAdded(KeyspaceMetadata keyspace) {
for (SchemaChangeListener listener : cluster.schemaChangeListeners) {
listener.onKeyspaceAdded(keyspace);
}
}
void triggerOnKeyspaceChanged(KeyspaceMetadata current, KeyspaceMetadata previous) {
for (SchemaChangeListener listener : cluster.schemaChangeListeners) {
listener.onKeyspaceChanged(current, previous);
}
}
void triggerOnKeyspaceRemoved(KeyspaceMetadata keyspace) {
for (SchemaChangeListener listener : cluster.schemaChangeListeners) {
listener.onKeyspaceRemoved(keyspace);
}
}
void triggerOnTableAdded(TableMetadata table) {
for (SchemaChangeListener listener : cluster.schemaChangeListeners) {
listener.onTableAdded(table);
}
}
void triggerOnTableChanged(TableMetadata current, TableMetadata previous) {
for (SchemaChangeListener listener : cluster.schemaChangeListeners) {
listener.onTableChanged(current, previous);
}
}
void triggerOnTableRemoved(TableMetadata table) {
for (SchemaChangeListener listener : cluster.schemaChangeListeners) {
listener.onTableRemoved(table);
}
}
void triggerOnUserTypeAdded(UserType type) {
for (SchemaChangeListener listener : cluster.schemaChangeListeners) {
listener.onUserTypeAdded(type);
}
}
void triggerOnUserTypeChanged(UserType current, UserType previous) {
for (SchemaChangeListener listener : cluster.schemaChangeListeners) {
listener.onUserTypeChanged(current, previous);
}
}
void triggerOnUserTypeRemoved(UserType type) {
for (SchemaChangeListener listener : cluster.schemaChangeListeners) {
listener.onUserTypeRemoved(type);
}
}
void triggerOnFunctionAdded(FunctionMetadata function) {
for (SchemaChangeListener listener : cluster.schemaChangeListeners) {
listener.onFunctionAdded(function);
}
}
void triggerOnFunctionChanged(FunctionMetadata current, FunctionMetadata previous) {
for (SchemaChangeListener listener : cluster.schemaChangeListeners) {
listener.onFunctionChanged(current, previous);
}
}
void triggerOnFunctionRemoved(FunctionMetadata function) {
for (SchemaChangeListener listener : cluster.schemaChangeListeners) {
listener.onFunctionRemoved(function);
}
}
void triggerOnAggregateAdded(AggregateMetadata aggregate) {
for (SchemaChangeListener listener : cluster.schemaChangeListeners) {
listener.onAggregateAdded(aggregate);
}
}
void triggerOnAggregateChanged(AggregateMetadata current, AggregateMetadata previous) {
for (SchemaChangeListener listener : cluster.schemaChangeListeners) {
listener.onAggregateChanged(current, previous);
}
}
void triggerOnAggregateRemoved(AggregateMetadata aggregate) {
for (SchemaChangeListener listener : cluster.schemaChangeListeners) {
listener.onAggregateRemoved(aggregate);
}
}
void triggerOnMaterializedViewAdded(MaterializedViewMetadata view) {
for (SchemaChangeListener listener : cluster.schemaChangeListeners) {
listener.onMaterializedViewAdded(view);
}
}
void triggerOnMaterializedViewChanged(
MaterializedViewMetadata current, MaterializedViewMetadata previous) {
for (SchemaChangeListener listener : cluster.schemaChangeListeners) {
listener.onMaterializedViewChanged(current, previous);
}
}
void triggerOnMaterializedViewRemoved(MaterializedViewMetadata view) {
for (SchemaChangeListener listener : cluster.schemaChangeListeners) {
listener.onMaterializedViewRemoved(view);
}
}
private static class TokenMap {
private final Token.Factory factory;
private final Map> primaryToTokens;
private final Map>> tokenToHostsByKeyspace;
private final Map>> hostsToRangesByKeyspace;
private final List ring;
private final Set tokenRanges;
private final Map tokenToPrimary;
private TokenMap(
Token.Factory factory,
List ring,
Set tokenRanges,
Map tokenToPrimary,
Map> primaryToTokens,
Map>> tokenToHostsByKeyspace,
Map>> hostsToRangesByKeyspace) {
this.factory = factory;
this.ring = ring;
this.tokenRanges = tokenRanges;
this.tokenToPrimary = tokenToPrimary;
this.primaryToTokens = primaryToTokens;
this.tokenToHostsByKeyspace = tokenToHostsByKeyspace;
this.hostsToRangesByKeyspace = hostsToRangesByKeyspace;
for (Map.Entry> entry : primaryToTokens.entrySet()) {
Host host = entry.getKey();
host.setTokens(ImmutableSet.copyOf(entry.getValue()));
}
}
private static TokenMap build(
Token.Factory factory,
Map> allTokens,
Collection keyspaces) {
Map tokenToPrimary = new HashMap();
Set allSorted = new TreeSet();
for (Map.Entry> entry : allTokens.entrySet()) {
Host host = entry.getKey();
for (Token t : entry.getValue()) {
try {
allSorted.add(t);
tokenToPrimary.put(t, host);
} catch (IllegalArgumentException e) {
// If we failed parsing that token, skip it
}
}
}
List ring = new ArrayList(allSorted);
Set tokenRanges = makeTokenRanges(ring, factory);
return build(factory, allTokens, keyspaces, ring, tokenRanges, tokenToPrimary);
}
private static TokenMap build(
Token.Factory factory,
Map> allTokens,
Collection keyspaces,
List ring,
Set tokenRanges,
Map tokenToPrimary) {
Set hosts = allTokens.keySet();
Map>> tokenToHosts =
new HashMap>>();
Map>> replStrategyToHosts =
new HashMap>>();
Map>> hostsToRanges =
new HashMap>>();
for (KeyspaceMetadata keyspace : keyspaces) {
ReplicationStrategy strategy = keyspace.replicationStrategy();
Map> ksTokens = replStrategyToHosts.get(strategy);
if (ksTokens == null) {
ksTokens =
(strategy == null)
? makeNonReplicatedMap(tokenToPrimary)
: strategy.computeTokenToReplicaMap(keyspace.getName(), tokenToPrimary, ring);
replStrategyToHosts.put(strategy, ksTokens);
}
tokenToHosts.put(keyspace.getName(), ksTokens);
Map> ksRanges;
if (ring.size() == 1) {
// We forced the single range to ]minToken,minToken], make sure to use that instead of
// relying on the host's token
ImmutableMap.Builder> builder = ImmutableMap.builder();
for (Host host : allTokens.keySet()) builder.put(host, tokenRanges);
ksRanges = builder.build();
} else {
ksRanges = computeHostsToRangesMap(tokenRanges, ksTokens, hosts.size());
}
hostsToRanges.put(keyspace.getName(), ksRanges);
}
return new TokenMap(
factory, ring, tokenRanges, tokenToPrimary, allTokens, tokenToHosts, hostsToRanges);
}
private Set getReplicas(String keyspace, Token token) {
Map> tokenToHosts = tokenToHostsByKeyspace.get(keyspace);
if (tokenToHosts == null) return Collections.emptySet();
// If the token happens to be one of the "primary" tokens, get result directly
Set hosts = tokenToHosts.get(token);
if (hosts != null) return hosts;
// Otherwise, find closest "primary" token on the ring
int i = Collections.binarySearch(ring, token);
if (i < 0) {
i = -i - 1;
if (i >= ring.size()) i = 0;
}
return tokenToHosts.get(ring.get(i));
}
private static Map> makeNonReplicatedMap(Map input) {
Map> output = new HashMap>(input.size());
for (Map.Entry entry : input.entrySet())
output.put(entry.getKey(), ImmutableSet.of(entry.getValue()));
return output;
}
private static Set makeTokenRanges(List ring, Token.Factory factory) {
ImmutableSet.Builder builder = ImmutableSet.builder();
// JAVA-684: if there is only one token, return the range ]minToken, minToken]
if (ring.size() == 1) {
builder.add(new TokenRange(factory.minToken(), factory.minToken(), factory));
} else {
for (int i = 0; i < ring.size(); i++) {
Token start = ring.get(i);
Token end = ring.get((i + 1) % ring.size());
builder.add(new TokenRange(start, end, factory));
}
}
return builder.build();
}
private static Map> computeHostsToRangesMap(
Set tokenRanges, Map> ksTokens, int hostCount) {
Map> builders =
Maps.newHashMapWithExpectedSize(hostCount);
for (TokenRange range : tokenRanges) {
Set replicas = ksTokens.get(range.getEnd());
for (Host host : replicas) {
ImmutableSet.Builder hostRanges = builders.get(host);
if (hostRanges == null) {
hostRanges = ImmutableSet.builder();
builders.put(host, hostRanges);
}
hostRanges.add(range);
}
}
Map> ksRanges = Maps.newHashMapWithExpectedSize(hostCount);
for (Map.Entry> entry : builders.entrySet()) {
ksRanges.put(entry.getKey(), entry.getValue().build());
}
return ksRanges;
}
}
}