All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.datastax.driver.core.Metadata Maven / Gradle / Ivy

/*
 *      Copyright (C) 2012 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.
 */
package com.datastax.driver.core;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Pattern;

import com.google.common.collect.ImmutableSet;
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);

    private final Cluster.Manager cluster;
    volatile String clusterName;
    volatile String partitioner;
    private final ConcurrentMap hosts = new ConcurrentHashMap();
    private final ConcurrentMap keyspaces = new ConcurrentHashMap();
    private volatile TokenMap tokenMap;

    private static final Pattern cqlId = Pattern.compile("\\w+");
    private static final Pattern lowercaseId = Pattern.compile("[a-z][a-z0-9_]*");

    Metadata(Cluster.Manager cluster) {
        this.cluster = cluster;
    }

    // Synchronized to make it easy to detect dropped keyspaces
    synchronized void rebuildSchema(String keyspace, String table, ResultSet ks, ResultSet cfs, ResultSet cols, VersionNumber cassandraVersion) {

        Map> cfDefs = new HashMap>();
        Map>> colsDefs = new HashMap>>();

        // Gather cf defs
        for (Row row : cfs) {
            String ksName = row.getString(KeyspaceMetadata.KS_NAME);
            List l = cfDefs.get(ksName);
            if (l == null) {
                l = new ArrayList();
                cfDefs.put(ksName, l);
            }
            l.add(row);
        }

        // Gather columns per Cf
        for (Row row : cols) {
            String ksName = row.getString(KeyspaceMetadata.KS_NAME);
            String cfName = row.getString(TableMetadata.CF_NAME);
            Map> colsByCf = colsDefs.get(ksName);
            if (colsByCf == null) {
                colsByCf = new HashMap>();
                colsDefs.put(ksName, colsByCf);
            }
            Map l = colsByCf.get(cfName);
            if (l == null) {
                l = new HashMap();
                colsByCf.put(cfName, l);
            }
            ColumnMetadata.Raw c = ColumnMetadata.Raw.fromRow(row, cassandraVersion);
            l.put(c.name, c);
        }

        if (table == null) {
            assert ks != null;
            Set addedKs = new HashSet();
            for (Row ksRow : ks) {
                String ksName = ksRow.getString(KeyspaceMetadata.KS_NAME);
                KeyspaceMetadata ksm = KeyspaceMetadata.build(ksRow);

                if (cfDefs.containsKey(ksName)) {
                    buildTableMetadata(ksm, cfDefs.get(ksName), colsDefs.get(ksName), cassandraVersion);
                }
                addedKs.add(ksName);
                keyspaces.put(ksName, ksm);
            }

            // If keyspace is null, it means we're rebuilding from scratch, so
            // remove anything that was not just added as it means it's a dropped keyspace
            if (keyspace == null) {
                Iterator iter = keyspaces.keySet().iterator();
                while (iter.hasNext()) {
                    if (!addedKs.contains(iter.next()))
                        iter.remove();
                }
            }
        } else {
            assert keyspace != null;
            KeyspaceMetadata ksm = keyspaces.get(keyspace);

            // If we update a keyspace we don't know about, something went
            // wrong. Log an error an schedule a full schema rebuilt.
            if (ksm == null) {
                logger.error(String.format("Asked to rebuild table %s.%s but I don't know keyspace %s", keyspace, table, keyspace));
                cluster.submitSchemaRefresh(null, null);
                return;
            }

            if (cfDefs.containsKey(keyspace))
                buildTableMetadata(ksm, cfDefs.get(keyspace), colsDefs.get(keyspace), cassandraVersion);
        }
    }

    private void buildTableMetadata(KeyspaceMetadata ksm, List cfRows, Map> colsDefs, VersionNumber cassandraVersion) {
        for (Row cfRow : cfRows) {
            String cfName = cfRow.getString(TableMetadata.CF_NAME);
            try {
                Map cols = colsDefs == null ? null : colsDefs.get(cfName);
                if (cols == null || cols.isEmpty()) {
                    if (cassandraVersion.getMajor() >= 2) {
                        // In C* >= 2.0, we should never have no columns metadata because at the very least we should
                        // have the metadata corresponding to the default CQL metadata. So if we don't have any columns,
                        // that can only mean that the table got creating concurrently with our schema queries, and the
                        // query for columns metadata reached the node before the table was persisted while the table
                        // metadata one reached it afterwards. We could make the query to the column metadata sequential
                        // with the table metadata instead of in parallel, but it's probably not worth making it slower
                        // all the time to avoid this race since 1) it's very very uncommon and 2) we can just ignore the
                        // incomplete table here for now and it'll get updated next time with no particular consequence
                        // (if the table creation was concurrent with our querying, we'll get a notifciation later and
                        // will reupdate the schema for it anyway). See JAVA-320 for why we need this.
                        continue;
                    } else {
                        // C* 1.2 don't persists default CQL metadata, so it's possible not to have columns (for thirft
                        // tables). But in that case TableMetadata.build() knows how to handle it.
                        cols = Collections.emptyMap();
                    }
                }
                TableMetadata.build(ksm, cfRow, cols, cassandraVersion);
            } catch (RuntimeException e) {
                // See ControlConnection#refreshSchema for why we'd rather not probably this further
                logger.error(String.format("Error parsing schema for table %s.%s: "
                                           + "Cluster.getMetadata().getKeyspace(\"%s\").getTable(\"%s\") will be missing or incomplete",
                                           ksm.getName(), cfName, ksm.getName(), cfName), e);
            }
        }
    }

    synchronized void rebuildTokenMap(String partitioner, Map> allTokens) {
        if (allTokens.isEmpty())
            return;

        Token.Factory factory = partitioner == null
                              ? (tokenMap == null ? null : tokenMap.factory)
                              : Token.getFactory(partitioner);
        if (factory == null)
            return;

        this.tokenMap = TokenMap.build(factory, allTokens, keyspaces.values());
    }

    Host add(InetSocketAddress address) {
        Host newHost = new Host(address, cluster.convictionPolicyFactory);
        Host previous = hosts.putIfAbsent(address, newHost);
        return previous == null ? newHost : null;
    }

    boolean remove(Host host) {
        return hosts.remove(host.getSocketAddress()) != null;
    }

    Host getHost(InetSocketAddress address) {
        return hosts.get(address);
    }

    // For internal use only
    Collection allHosts() {
        return hosts.values();
    }

    // Deal with case sensitivity for a given keyspace or table id
    static String handleId(String id) {
        // Shouldn't really happen for this method, but no reason to fail here
        if (id == null)
            return null;

        if (cqlId.matcher(id).matches())
            return id.toLowerCase();

        // Check if it's enclosed in quotes. If it is, remove them
        if (id.charAt(0) == '"' && id.charAt(id.length() - 1) == '"')
            return id.substring(1, id.length() - 1);

        // otherwise, just return the id.
        return id;
    }

    // Escape a CQL3 identifier based on its value as read from the schema
    // tables. Because it comes from Cassandra, we could just always quote it,
    // but to get a nicer output we don't do it if it's not necessary.
    static String escapeId(String ident) {
        // we don't need to escape if it's lowercase and match non-quoted CQL3 ids.
        return lowercaseId.matcher(ident).matches() ? ident : quote(ident);
    }

    /**
     * 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 identifier in double quotes, making it case * sensitive. * * @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 '"' + id + '"'; } /** * Returns the set of hosts that are replica for a given partition key. *

* Note that this method is a best effort method. Consumers should not rely * too heavily on the result of this method not being 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 know * by the driver. No strong guarantee is provided on the stalelessness of * this information. It is also not guarantee that the returned set won't * be empty (which is then some form of staleness). */ 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 tokem. *

* Note that this method is a best effort method. Consumers should not rely * too heavily on the result of this method not being stale (or even empty). * * @param keyspace the name of the keyspace to get replicas for. * @param token the token for which to find the set of * replica. * @return the (immutable) set of replicas for {@code token} as know * by the driver. No strong guarantee is provided on the stalelessness of * this information. It is also not guarantee that the returned set won't * be empty (which is then some form of staleness). */ public Set getTokenReplicas(String keyspace, String token) { keyspace = handleId(keyspace); TokenMap current = tokenMap; if (current == null) { return Collections.emptySet(); } else { Set hosts = current.getReplicas(keyspace, current.factory.fromString(token)); 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()); } /** * 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. */ public KeyspaceMetadata getKeyspace(String keyspace) { return keyspaces.get(handleId(keyspace)); } /** * Returns a list of all the defined keyspaces. * * @return a list of all the defined keyspaces. */ 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). * * @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(); } static class TokenMap { private final Token.Factory factory; private final Map>> tokenToHosts; private final List ring; private TokenMap(Token.Factory factory, Map>> tokenToHosts, List ring) { this.factory = factory; this.tokenToHosts = tokenToHosts; this.ring = ring; } public 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 (String tokenStr : entry.getValue()) { try { Token t = factory.fromString(tokenStr); allSorted.add(t); tokenToPrimary.put(t, host); } catch (IllegalArgumentException e) { // If we failed parsing that token, skip it } } } List ring = new ArrayList(allSorted); Map>> tokenToHosts = new HashMap>>(); for (KeyspaceMetadata keyspace : keyspaces) { ReplicationStrategy strategy = keyspace.replicationStrategy(); if (strategy == null) { tokenToHosts.put(keyspace.getName(), makeNonReplicatedMap(tokenToPrimary)); } else { tokenToHosts.put(keyspace.getName(), strategy.computeTokenToReplicaMap(tokenToPrimary, ring)); } } return new TokenMap(factory, tokenToHosts, ring); } private Set getReplicas(String keyspace, Token token) { Map> keyspaceHosts = tokenToHosts.get(keyspace); if (keyspaceHosts == null) return Collections.emptySet(); // Find the primary replica int i = Collections.binarySearch(ring, token); if (i < 0) { i = -i - 1; if (i >= ring.size()) i = 0; } return keyspaceHosts.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; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy