/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.cassandra.db.index; import java.nio.ByteBuffer; import java.util.*; import java.util.concurrent.*; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.config.ColumnDefinition; import org.apache.cassandra.config.IndexType; import org.apache.cassandra.db.*; import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.db.filter.ExtendedFilter; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.io.sstable.ReducingKeyIterator; import org.apache.cassandra.io.sstable.SSTableReader; import org.apache.cassandra.utils.FBUtilities; /** * Manages all the indexes associated with a given CFS * Different types of indexes can be created across the same CF */ public class SecondaryIndexManager { private static final Logger logger = LoggerFactory.getLogger(SecondaryIndexManager.class); public static final Updater nullUpdater = new Updater() { public void insert(Column column) { } public void update(Column oldColumn, Column column) { } public void remove(Column current) { } public void updateRowLevelIndexes() {} }; /** * Organizes the indexes by column name */ private final ConcurrentNavigableMap<ByteBuffer, SecondaryIndex> indexesByColumn; /** * Keeps a single instance of a SecondaryIndex for many columns when the index type * has isRowLevelIndex() == true * * This allows updates to happen to an entire row at once */ private final Map<Class<? extends SecondaryIndex>,SecondaryIndex> rowLevelIndexMap; /** * The underlying column family containing the source data for these indexes */ public final ColumnFamilyStore baseCfs; public SecondaryIndexManager(ColumnFamilyStore baseCfs) { indexesByColumn = new ConcurrentSkipListMap<>(); rowLevelIndexMap = new HashMap<>(); this.baseCfs = baseCfs; } /** * Drops and adds new indexes associated with the underlying CF */ public void reload() { // figure out what needs to be added and dropped. // future: if/when we have modifiable settings for secondary indexes, // they'll need to be handled here. Collection<ByteBuffer> indexedColumnNames = indexesByColumn.keySet(); for (ByteBuffer indexedColumn : indexedColumnNames) { ColumnDefinition def = baseCfs.metadata.getColumnDefinition(indexedColumn); if (def == null || def.getIndexType() == null) removeIndexedColumn(indexedColumn); } // TODO: allow all ColumnDefinition type for (ColumnDefinition cdef : baseCfs.metadata.allColumns()) if (cdef.getIndexType() != null && !indexedColumnNames.contains(cdef.name)) addIndexedColumn(cdef); Set<SecondaryIndex> reloadedIndexes = Collections.newSetFromMap(new IdentityHashMap<SecondaryIndex, Boolean>()); for (SecondaryIndex index : indexesByColumn.values()) if (reloadedIndexes.add(index)) index.reload(); } public Set<String> allIndexesNames() { Set<String> names = new HashSet<>(indexesByColumn.size()); for (SecondaryIndex index : indexesByColumn.values()) names.add(index.getIndexName()); return names; } /** * Does a full, blocking rebuild of the indexes specified by columns from the sstables. * Does nothing if columns is empty. * * Caller must acquire and release references to the sstables used here. * * @param sstables the data to build from * @param idxNames the list of columns to index, ordered by comparator */ public void maybeBuildSecondaryIndexes(Collection<SSTableReader> sstables, Set<String> idxNames) { if (idxNames.isEmpty()) return; logger.info(String.format("Submitting index build of %s for data in %s", idxNames, StringUtils.join(sstables, ", "))); SecondaryIndexBuilder builder = new SecondaryIndexBuilder(baseCfs, idxNames, new ReducingKeyIterator(sstables)); Future<?> future = CompactionManager.instance.submitIndexBuild(builder); FBUtilities.waitOnFuture(future); flushIndexesBlocking(); logger.info("Index build of {} complete", idxNames); } public boolean indexes(ByteBuffer name, Collection<SecondaryIndex> indexes) { return !indexFor(name, indexes).isEmpty(); } public List<SecondaryIndex> indexFor(ByteBuffer name, Collection<SecondaryIndex> indexes) { List<SecondaryIndex> matching = null; for (SecondaryIndex index : indexes) { if (index.indexes(name)) { if (matching == null) matching = new ArrayList<>(); matching.add(index); } } return matching == null ? Collections.<SecondaryIndex>emptyList() : matching; } public boolean indexes(Column column) { return indexes(column.name()); } public boolean indexes(ByteBuffer name) { return indexes(name, indexesByColumn.values()); } public List<SecondaryIndex> indexFor(ByteBuffer name) { return indexFor(name, indexesByColumn.values()); } /** * @return true if the indexes can handle the clause. */ public boolean hasIndexFor(List<IndexExpression> clause) { if (clause == null || clause.isEmpty()) return false; // It doesn't seem a clause can have multiple searchers, but since // getIndexSearchersForQuery returns a list ... List<SecondaryIndexSearcher> searchers = getIndexSearchersForQuery(clause); if (searchers.isEmpty()) return false; for (SecondaryIndexSearcher searcher : searchers) if (!searcher.isIndexing(clause)) return false; return true; } /** * Removes a existing index * @param column the indexed column to remove */ public void removeIndexedColumn(ByteBuffer column) { SecondaryIndex index = indexesByColumn.remove(column); if (index == null) return; // Remove this column from from row level index map if (index instanceof PerRowSecondaryIndex) { index.removeColumnDef(column); //If now columns left on this CF remove from row level lookup if (index.getColumnDefs().isEmpty()) rowLevelIndexMap.remove(index.getClass()); } index.removeIndex(column); SystemKeyspace.setIndexRemoved(baseCfs.metadata.ksName, index.getNameForSystemKeyspace(column)); } /** * Adds and builds a index for a column * @param cdef the column definition holding the index data * @return a future which the caller can optionally block on signaling the index is built */ public synchronized Future<?> addIndexedColumn(ColumnDefinition cdef) { if (indexesByColumn.containsKey(cdef.name)) return null; assert cdef.getIndexType() != null; SecondaryIndex index; try { index = SecondaryIndex.createInstance(baseCfs, cdef); } catch (ConfigurationException e) { throw new RuntimeException(e); } // Keep a single instance of the index per-cf for row level indexes // since we want all columns to be under the index if (index instanceof PerRowSecondaryIndex) { SecondaryIndex currentIndex = rowLevelIndexMap.get(index.getClass()); if (currentIndex == null) { rowLevelIndexMap.put(index.getClass(), index); index.init(); } else { index = currentIndex; index.addColumnDef(cdef); logger.info("Creating new index : {}",cdef); } } else { // TODO: We sould do better than throw a RuntimeException if (cdef.getIndexType() == IndexType.CUSTOM && index instanceof AbstractSimplePerColumnSecondaryIndex) throw new RuntimeException("Cannot use a subclass of AbstractSimplePerColumnSecondaryIndex as a CUSTOM index, as they assume they are CFS backed"); index.init(); } // link in indexedColumns. this means that writes will add new data to // the index immediately, // so we don't have to lock everything while we do the build. it's up to // the operator to wait // until the index is actually built before using in queries. indexesByColumn.put(cdef.name, index); // if we're just linking in the index to indexedColumns on an // already-built index post-restart, we're done if (index.isIndexBuilt(cdef.name)) return null; return index.buildIndexAsync(); } /** * * @param column the name of indexes column * @return the index */ public SecondaryIndex getIndexForColumn(ByteBuffer column) { return indexesByColumn.get(column); } /** * Remove the index */ public void invalidate() { for (SecondaryIndex index : indexesByColumn.values()) index.invalidate(); } /** * Flush all indexes to disk */ public void flushIndexesBlocking() { for (SecondaryIndex index : indexesByColumn.values()) index.forceBlockingFlush(); } /** * @return all built indexes (ready to use) */ public List<String> getBuiltIndexes() { List<String> indexList = new ArrayList<>(); for (Map.Entry<ByteBuffer, SecondaryIndex> entry : indexesByColumn.entrySet()) { SecondaryIndex index = entry.getValue(); if (index.isIndexBuilt(entry.getKey())) indexList.add(entry.getValue().getIndexName()); } return indexList; } /** * @return all CFS from indexes which use a backing CFS internally (KEYS) */ public Collection<ColumnFamilyStore> getIndexesBackedByCfs() { ArrayList<ColumnFamilyStore> cfsList = new ArrayList<>(); for (SecondaryIndex index: indexesByColumn.values()) { ColumnFamilyStore cfs = index.getIndexCfs(); if (cfs != null) cfsList.add(cfs); } return cfsList; } /** * @return all indexes which do *not* use a backing CFS internally */ public Collection<SecondaryIndex> getIndexesNotBackedByCfs() { // we use identity map because per row indexes use same instance across many columns Set<SecondaryIndex> indexes = Collections.newSetFromMap(new IdentityHashMap<SecondaryIndex, Boolean>()); for (SecondaryIndex index: indexesByColumn.values()) if (index.getIndexCfs() == null) indexes.add(index); return indexes; } /** * @return all of the secondary indexes without distinction to the (non-)backed by secondary ColumnFamilyStore. */ public Collection<SecondaryIndex> getIndexes() { // we use identity map because per row indexes use same instance across many columns Set<SecondaryIndex> indexes = Collections.newSetFromMap(new IdentityHashMap<SecondaryIndex, Boolean>()); indexes.addAll(indexesByColumn.values()); return indexes; } /** * @return if there are ANY indexes for this table.. */ public boolean hasIndexes() { return !indexesByColumn.isEmpty(); } /** * @return total current ram size of all indexes */ public long getTotalLiveSize() { long total = 0; for (SecondaryIndex index : getIndexes()) total += index.getLiveSize(); return total; } /** * When building an index against existing data, add the given row to the index * * @param key the row key * @param cf the current rows data */ public void indexRow(ByteBuffer key, ColumnFamily cf) { // Update entire row only once per row level index Set<Class<? extends SecondaryIndex>> appliedRowLevelIndexes = null; for (SecondaryIndex index : indexesByColumn.values()) { if (index instanceof PerRowSecondaryIndex) { if (appliedRowLevelIndexes == null) appliedRowLevelIndexes = new HashSet<>(); if (appliedRowLevelIndexes.add(index.getClass())) ((PerRowSecondaryIndex)index).index(key, cf); } else { for (Column column : cf) if (index.indexes(column.name())) ((PerColumnSecondaryIndex) index).insert(key, column); } } } /** * Delete all columns from all indexes for this row. For when cleanup rips a row out entirely. * * @param key the row key * @param indexedColumnsInRow all column names in row */ public void deleteFromIndexes(DecoratedKey key, List<Column> indexedColumnsInRow) { // Update entire row only once per row level index Set<Class<? extends SecondaryIndex>> cleanedRowLevelIndexes = null; for (Column column : indexedColumnsInRow) { SecondaryIndex index = indexesByColumn.get(column.name()); if (index == null) continue; if (index instanceof PerRowSecondaryIndex) { if (cleanedRowLevelIndexes == null) cleanedRowLevelIndexes = new HashSet<>(); if (cleanedRowLevelIndexes.add(index.getClass())) ((PerRowSecondaryIndex)index).delete(key); } else { ((PerColumnSecondaryIndex) index).delete(key.key, column); } } } /** * This helper acts as a closure around the indexManager * and updated cf data to ensure that down in * Memtable's ColumnFamily implementation, the index * can get updated. Note: only a CF backed by AtomicSortedColumns implements * this behaviour fully, other types simply ignore the index updater. */ public Updater updaterFor(DecoratedKey key, ColumnFamily cf) { return (indexesByColumn.isEmpty() && rowLevelIndexMap.isEmpty()) ? nullUpdater : new StandardUpdater(key, cf); } /** * Updated closure with only the modified row key. */ public Updater updaterFor(DecoratedKey key) { return updaterFor(key, null); } /** * Get a list of IndexSearchers from the union of expression index types * @param clause the query clause * @return the searchers needed to query the index */ private List<SecondaryIndexSearcher> getIndexSearchersForQuery(List<IndexExpression> clause) { Map<String, Set<ByteBuffer>> groupByIndexType = new HashMap<>(); //Group columns by type for (IndexExpression ix : clause) { SecondaryIndex index = getIndexForColumn(ix.column); if (index == null) continue; Set<ByteBuffer> columns = groupByIndexType.get(index.getClass().getCanonicalName()); if (columns == null) { columns = new HashSet<>(); groupByIndexType.put(index.getClass().getCanonicalName(), columns); } columns.add(ix.column); } List<SecondaryIndexSearcher> indexSearchers = new ArrayList<>(groupByIndexType.size()); //create searcher per type for (Set<ByteBuffer> column : groupByIndexType.values()) indexSearchers.add(getIndexForColumn(column.iterator().next()).createSecondaryIndexSearcher(column)); return indexSearchers; } /** * Performs a search across a number of column indexes * TODO: add support for querying across index types * * @param filter the column range to restrict to * @return found indexed rows */ public List<Row> search(ExtendedFilter filter) { List<SecondaryIndexSearcher> indexSearchers = getIndexSearchersForQuery(filter.getClause()); if (indexSearchers.isEmpty()) return Collections.emptyList(); //We currently don't support searching across multiple index types if (indexSearchers.size() > 1) throw new RuntimeException("Unable to search across multiple secondary index types"); return indexSearchers.get(0).search(filter); } public Collection<SecondaryIndex> getIndexesByNames(Set<String> idxNames) { List<SecondaryIndex> result = new ArrayList<>(); for (SecondaryIndex index : indexesByColumn.values()) if (idxNames.contains(index.getIndexName())) result.add(index); return result; } public void setIndexBuilt(Set<String> idxNames) { for (SecondaryIndex index : getIndexesByNames(idxNames)) index.setIndexBuilt(); } public void setIndexRemoved(Set<String> idxNames) { for (SecondaryIndex index : getIndexesByNames(idxNames)) index.setIndexRemoved(); } public boolean validate(Column column) { SecondaryIndex index = getIndexForColumn(column.name()); return index == null || index.validate(column); } public static interface Updater { /** called when constructing the index against pre-existing data */ public void insert(Column column); /** called when updating the index from a memtable */ public void update(Column oldColumn, Column column); /** called when lazy-updating the index during compaction (CASSANDRA-2897) */ public void remove(Column current); /** called after memtable updates are complete (CASSANDRA-5397) */ public void updateRowLevelIndexes(); } private class StandardUpdater implements Updater { private final DecoratedKey key; private final ColumnFamily cf; public StandardUpdater(DecoratedKey key, ColumnFamily cf) { this.key = key; this.cf = cf; } public void insert(Column column) { if (column.isMarkedForDelete(System.currentTimeMillis())) return; for (SecondaryIndex index : indexFor(column.name())) if (index instanceof PerColumnSecondaryIndex) ((PerColumnSecondaryIndex) index).insert(key.key, column); } public void update(Column oldColumn, Column column) { if (oldColumn.equals(column)) return; for (SecondaryIndex index : indexFor(column.name())) { if (index instanceof PerColumnSecondaryIndex) { // insert the new value before removing the old one, so we never have a period // where the row is invisible to both queries (the opposite seems preferable); see CASSANDRA-5540 if (!column.isMarkedForDelete(System.currentTimeMillis())) ((PerColumnSecondaryIndex) index).insert(key.key, column); ((PerColumnSecondaryIndex) index).delete(key.key, oldColumn); } } } public void remove(Column column) { if (column.isMarkedForDelete(System.currentTimeMillis())) return; for (SecondaryIndex index : indexFor(column.name())) if (index instanceof PerColumnSecondaryIndex) ((PerColumnSecondaryIndex) index).delete(key.key, column); } public void updateRowLevelIndexes() { for (SecondaryIndex index : rowLevelIndexMap.values()) ((PerRowSecondaryIndex) index).index(key.key, cf); } } }