/* * 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.schema; import java.nio.ByteBuffer; import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; import java.util.stream.Collectors; import com.google.common.collect.ImmutableList; import com.google.common.collect.MapDifference; import com.google.common.collect.Sets; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.functions.*; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.KeyspaceNotDefinedException; import org.apache.cassandra.db.Mutation; import org.apache.cassandra.db.SystemKeyspace; import org.apache.cassandra.db.commitlog.CommitLog; import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.UserType; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.exceptions.UnknownTableException; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.locator.LocalStrategy; import org.apache.cassandra.utils.Pair; import org.cliffc.high_scale_lib.NonBlockingHashMap; import static java.lang.String.format; import static com.google.common.collect.Iterables.size; public final class Schema { public static final Schema instance = new Schema(); private volatile Keyspaces keyspaces = Keyspaces.none(); // UUID -> mutable metadata ref map. We have to update these in place every time a table changes. private final Map<TableId, TableMetadataRef> metadataRefs = new NonBlockingHashMap<>(); // (keyspace name, index name) -> mutable metadata ref map. We have to update these in place every time an index changes. private final Map<Pair<String, String>, TableMetadataRef> indexMetadataRefs = new NonBlockingHashMap<>(); // Keyspace objects, one per keyspace. Only one instance should ever exist for any given keyspace. private final Map<String, Keyspace> keyspaceInstances = new NonBlockingHashMap<>(); private volatile UUID version; private final List<SchemaChangeListener> changeListeners = new CopyOnWriteArrayList<>(); /** * Initialize empty schema object and load the hardcoded system tables */ private Schema() { if (DatabaseDescriptor.isDaemonInitialized() || DatabaseDescriptor.isToolInitialized()) { load(SchemaKeyspace.metadata()); load(SystemKeyspace.metadata()); } } /** * Validates that the provided keyspace is not one of the system keyspace. * * @param keyspace the keyspace name to validate. * * @throws InvalidRequestException if {@code keyspace} is the name of a * system keyspace. */ public static void validateKeyspaceNotSystem(String keyspace) { if (SchemaConstants.isSystemKeyspace(keyspace)) throw new InvalidRequestException(format("%s keyspace is not user-modifiable", keyspace)); } /** * load keyspace (keyspace) definitions, but do not initialize the keyspace instances. * Schema version may be updated as the result. */ public void loadFromDisk() { loadFromDisk(true); } /** * Load schema definitions from disk. * * @param updateVersion true if schema version needs to be updated */ public void loadFromDisk(boolean updateVersion) { load(SchemaKeyspace.fetchNonSystemKeyspaces()); if (updateVersion) updateVersion(); } /** * Load up non-system keyspaces * * @param keyspaceDefs The non-system keyspace definitions */ private void load(Iterable<KeyspaceMetadata> keyspaceDefs) { keyspaceDefs.forEach(this::load); } /** * Update (or insert) new keyspace definition * * @param ksm The metadata about keyspace */ synchronized public void load(KeyspaceMetadata ksm) { KeyspaceMetadata previous = keyspaces.getNullable(ksm.name); if (previous == null) loadNew(ksm); else reload(previous, ksm); keyspaces = keyspaces.withAddedOrUpdated(ksm); } private void loadNew(KeyspaceMetadata ksm) { ksm.tablesAndViews() .forEach(metadata -> metadataRefs.put(metadata.id, new TableMetadataRef(metadata))); ksm.tables .indexTables() .forEach((name, metadata) -> indexMetadataRefs.put(Pair.create(ksm.name, name), new TableMetadataRef(metadata))); } private void reload(KeyspaceMetadata previous, KeyspaceMetadata updated) { Keyspace keyspace = getKeyspaceInstance(updated.name); if (keyspace != null) keyspace.setMetadata(updated); MapDifference<TableId, TableMetadata> tablesDiff = previous.tables.diff(updated.tables); MapDifference<TableId, ViewMetadata> viewsDiff = previous.views.diff(updated.views); MapDifference<String, TableMetadata> indexesDiff = previous.tables.indexesDiff(updated.tables); // clean up after removed entries tablesDiff.entriesOnlyOnLeft() .values() .forEach(table -> metadataRefs.remove(table.id)); viewsDiff.entriesOnlyOnLeft() .values() .forEach(view -> metadataRefs.remove(view.metadata.id)); indexesDiff.entriesOnlyOnLeft() .values() .forEach(indexTable -> indexMetadataRefs.remove(Pair.create(indexTable.keyspace, indexTable.indexName().get()))); // load up new entries tablesDiff.entriesOnlyOnRight() .values() .forEach(table -> metadataRefs.put(table.id, new TableMetadataRef(table))); viewsDiff.entriesOnlyOnRight() .values() .forEach(view -> metadataRefs.put(view.metadata.id, new TableMetadataRef(view.metadata))); indexesDiff.entriesOnlyOnRight() .values() .forEach(indexTable -> indexMetadataRefs.put(Pair.create(indexTable.keyspace, indexTable.indexName().get()), new TableMetadataRef(indexTable))); // refresh refs to updated ones tablesDiff.entriesDiffering() .values() .forEach(diff -> metadataRefs.get(diff.rightValue().id).set(diff.rightValue())); viewsDiff.entriesDiffering() .values() .forEach(diff -> metadataRefs.get(diff.rightValue().metadata.id).set(diff.rightValue().metadata)); indexesDiff.entriesDiffering() .values() .stream() .map(MapDifference.ValueDifference::rightValue) .forEach(indexTable -> indexMetadataRefs.get(Pair.create(indexTable.keyspace, indexTable.indexName().get())).set(indexTable)); } public void registerListener(SchemaChangeListener listener) { changeListeners.add(listener); } @SuppressWarnings("unused") public void unregisterListener(SchemaChangeListener listener) { changeListeners.remove(listener); } /** * Get keyspace instance by name * * @param keyspaceName The name of the keyspace * * @return Keyspace object or null if keyspace was not found */ public Keyspace getKeyspaceInstance(String keyspaceName) { return keyspaceInstances.get(keyspaceName); } public ColumnFamilyStore getColumnFamilyStoreInstance(TableId id) { TableMetadata metadata = getTableMetadata(id); if (metadata == null) return null; Keyspace instance = getKeyspaceInstance(metadata.keyspace); if (instance == null) return null; return instance.hasColumnFamilyStore(metadata.id) ? instance.getColumnFamilyStore(metadata.id) : null; } /** * Store given Keyspace instance to the schema * * @param keyspace The Keyspace instance to store * * @throws IllegalArgumentException if Keyspace is already stored */ public void storeKeyspaceInstance(Keyspace keyspace) { if (keyspaceInstances.containsKey(keyspace.getName())) throw new IllegalArgumentException(String.format("Keyspace %s was already initialized.", keyspace.getName())); keyspaceInstances.put(keyspace.getName(), keyspace); } /** * Remove keyspace from schema * * @param keyspaceName The name of the keyspace to remove * * @return removed keyspace instance or null if it wasn't found */ public Keyspace removeKeyspaceInstance(String keyspaceName) { return keyspaceInstances.remove(keyspaceName); } /** * Remove keyspace definition from system * * @param ksm The keyspace definition to remove */ synchronized void unload(KeyspaceMetadata ksm) { keyspaces = keyspaces.without(ksm.name); ksm.tablesAndViews() .forEach(t -> metadataRefs.remove(t.id)); ksm.tables .indexTables() .keySet() .forEach(name -> indexMetadataRefs.remove(Pair.create(ksm.name, name))); } public int getNumberOfTables() { return keyspaces.stream().mapToInt(k -> size(k.tablesAndViews())).sum(); } public ViewMetadata getView(String keyspaceName, String viewName) { assert keyspaceName != null; KeyspaceMetadata ksm = keyspaces.getNullable(keyspaceName); return (ksm == null) ? null : ksm.views.getNullable(viewName); } /** * Get metadata about keyspace by its name * * @param keyspaceName The name of the keyspace * * @return The keyspace metadata or null if it wasn't found */ public KeyspaceMetadata getKeyspaceMetadata(String keyspaceName) { assert keyspaceName != null; return keyspaces.getNullable(keyspaceName); } private Set<String> getNonSystemKeyspacesSet() { return Sets.difference(keyspaces.names(), SchemaConstants.SYSTEM_KEYSPACE_NAMES); } /** * @return collection of the non-system keyspaces (note that this count as system only the * non replicated keyspaces, so keyspace like system_traces which are replicated are actually * returned. See getUserKeyspace() below if you don't want those) */ public ImmutableList<String> getNonSystemKeyspaces() { return ImmutableList.copyOf(getNonSystemKeyspacesSet()); } /** * @return a collection of keyspaces that do not use LocalStrategy for replication */ public List<String> getNonLocalStrategyKeyspaces() { return keyspaces.stream() .filter(keyspace -> keyspace.params.replication.klass != LocalStrategy.class) .map(keyspace -> keyspace.name) .collect(Collectors.toList()); } /** * @return collection of the user defined keyspaces */ public List<String> getUserKeyspaces() { return ImmutableList.copyOf(Sets.difference(getNonSystemKeyspacesSet(), SchemaConstants.REPLICATED_SYSTEM_KEYSPACE_NAMES)); } /** * Get metadata about keyspace inner ColumnFamilies * * @param keyspaceName The name of the keyspace * * @return metadata about ColumnFamilies the belong to the given keyspace */ public Iterable<TableMetadata> getTablesAndViews(String keyspaceName) { assert keyspaceName != null; KeyspaceMetadata ksm = keyspaces.getNullable(keyspaceName); assert ksm != null; return ksm.tablesAndViews(); } /** * @return collection of the all keyspace names registered in the system (system and non-system) */ public Set<String> getKeyspaces() { return keyspaces.names(); } /* TableMetadata/Ref query/control methods */ /** * Given a keyspace name and table/view name, get the table metadata * reference. If the keyspace name or table/view name is not present * this method returns null. * * @return TableMetadataRef object or null if it wasn't found */ public TableMetadataRef getTableMetadataRef(String keyspace, String table) { TableMetadata tm = getTableMetadata(keyspace, table); return tm == null ? null : metadataRefs.get(tm.id); } public TableMetadataRef getIndexTableMetadataRef(String keyspace, String index) { return indexMetadataRefs.get(Pair.create(keyspace, index)); } /** * Get Table metadata by its identifier * * @param id table or view identifier * * @return metadata about Table or View */ public TableMetadataRef getTableMetadataRef(TableId id) { return metadataRefs.get(id); } public TableMetadataRef getTableMetadataRef(Descriptor descriptor) { return getTableMetadataRef(descriptor.ksname, descriptor.cfname); } /** * Given a keyspace name and table name, get the table * meta data. If the keyspace name or table name is not valid * this function returns null. * * @param keyspace The keyspace name * @param table The table name * * @return TableMetadata object or null if it wasn't found */ public TableMetadata getTableMetadata(String keyspace, String table) { assert keyspace != null; assert table != null; KeyspaceMetadata ksm = keyspaces.getNullable(keyspace); return ksm == null ? null : ksm.getTableOrViewNullable(table); } public TableMetadata getTableMetadata(TableId id) { return keyspaces.getTableOrViewNullable(id); } public TableMetadata validateTable(String keyspaceName, String tableName) { if (tableName.isEmpty()) throw new InvalidRequestException("non-empty table is required"); KeyspaceMetadata keyspace = keyspaces.getNullable(keyspaceName); if (keyspace == null) throw new KeyspaceNotDefinedException(format("keyspace %s does not exist", keyspaceName)); TableMetadata metadata = keyspace.getTableOrViewNullable(tableName); if (metadata == null) throw new InvalidRequestException(format("table %s does not exist", tableName)); return metadata; } public TableMetadata getTableMetadata(Descriptor descriptor) { return getTableMetadata(descriptor.ksname, descriptor.cfname); } /** * @throws UnknownTableException if the table couldn't be found in the metadata */ public TableMetadata getExistingTableMetadata(TableId id) throws UnknownTableException { TableMetadata metadata = getTableMetadata(id); if (metadata != null) return metadata; String message = String.format("Couldn't find table with id %s. If a table was just created, this is likely due to the schema" + "not being fully propagated. Please wait for schema agreement on table creation.", id); throw new UnknownTableException(message, id); } /* Function helpers */ /** * Get all function overloads with the specified name * * @param name fully qualified function name * @return an empty list if the keyspace or the function name are not found; * a non-empty collection of {@link Function} otherwise */ public Collection<Function> getFunctions(FunctionName name) { if (!name.hasKeyspace()) throw new IllegalArgumentException(String.format("Function name must be fully qualified: got %s", name)); KeyspaceMetadata ksm = getKeyspaceMetadata(name.keyspace); return ksm == null ? Collections.emptyList() : ksm.functions.get(name); } /** * Find the function with the specified name * * @param name fully qualified function name * @param argTypes function argument types * @return an empty {@link Optional} if the keyspace or the function name are not found; * a non-empty optional of {@link Function} otherwise */ public Optional<Function> findFunction(FunctionName name, List<AbstractType<?>> argTypes) { if (!name.hasKeyspace()) throw new IllegalArgumentException(String.format("Function name must be fully quallified: got %s", name)); KeyspaceMetadata ksm = getKeyspaceMetadata(name.keyspace); return ksm == null ? Optional.empty() : ksm.functions.find(name, argTypes); } /* Version control */ /** * @return current schema version */ public UUID getVersion() { return version; } /** * Read schema from system keyspace and calculate MD5 digest of every row, resulting digest * will be converted into UUID which would act as content-based version of the schema. */ public void updateVersion() { version = SchemaKeyspace.calculateSchemaDigest(); SystemKeyspace.updateSchemaVersion(version); } /* * Like updateVersion, but also announces via gossip */ public void updateVersionAndAnnounce() { updateVersion(); MigrationManager.passiveAnnounce(version); } /** * Clear all KS/CF metadata and reset version. */ public synchronized void clear() { getNonSystemKeyspaces().forEach(k -> unload(getKeyspaceMetadata(k))); updateVersionAndAnnounce(); } /** * Merge remote schema in form of mutations with local and mutate ks/cf metadata objects * (which also involves fs operations on add/drop ks/cf) * * @param mutations the schema changes to apply * * @throws ConfigurationException If one of metadata attributes has invalid value */ synchronized void mergeAndAnnounceVersion(Collection<Mutation> mutations) { merge(mutations); updateVersionAndAnnounce(); } synchronized void merge(Collection<Mutation> mutations) { // only compare the keyspaces affected by this set of schema mutations Set<String> affectedKeyspaces = SchemaKeyspace.affectedKeyspaces(mutations); // fetch the current state of schema for the affected keyspaces only Keyspaces before = keyspaces.filter(k -> affectedKeyspaces.contains(k.name)); // apply the schema mutations SchemaKeyspace.applyChanges(mutations); // apply the schema mutations and fetch the new versions of the altered keyspaces Keyspaces after = SchemaKeyspace.fetchKeyspaces(affectedKeyspaces); MapDifference<String, KeyspaceMetadata> keyspacesDiff = before.diff(after); // dropped keyspaces keyspacesDiff.entriesOnlyOnLeft().values().forEach(this::dropKeyspace); // new keyspaces keyspacesDiff.entriesOnlyOnRight().values().forEach(this::createKeyspace); // updated keyspaces keyspacesDiff.entriesDiffering().entrySet().forEach(diff -> alterKeyspace(diff.getValue().leftValue(), diff.getValue().rightValue())); } private void alterKeyspace(KeyspaceMetadata before, KeyspaceMetadata after) { // calculate the deltas MapDifference<TableId, TableMetadata> tablesDiff = before.tables.diff(after.tables); MapDifference<TableId, ViewMetadata> viewsDiff = before.views.diff(after.views); MapDifference<ByteBuffer, UserType> typesDiff = before.types.diff(after.types); MapDifference<Pair<FunctionName, List<String>>, UDFunction> udfsDiff = before.functions.udfsDiff(after.functions); MapDifference<Pair<FunctionName, List<String>>, UDAggregate> udasDiff = before.functions.udasDiff(after.functions); // drop tables and views viewsDiff.entriesOnlyOnLeft().values().forEach(this::dropView); tablesDiff.entriesOnlyOnLeft().values().forEach(this::dropTable); load(after); // add tables and views tablesDiff.entriesOnlyOnRight().values().forEach(this::createTable); viewsDiff.entriesOnlyOnRight().values().forEach(this::createView); // update tables and views tablesDiff.entriesDiffering().values().forEach(diff -> alterTable(diff.rightValue())); viewsDiff.entriesDiffering().values().forEach(diff -> alterView(diff.rightValue())); // deal with all removed, added, and altered views Keyspace.open(before.name).viewManager.reload(); // notify on everything dropped udasDiff.entriesOnlyOnLeft().values().forEach(this::notifyDropAggregate); udfsDiff.entriesOnlyOnLeft().values().forEach(this::notifyDropFunction); viewsDiff.entriesOnlyOnLeft().values().forEach(this::notifyDropView); tablesDiff.entriesOnlyOnLeft().values().forEach(this::notifyDropTable); typesDiff.entriesOnlyOnLeft().values().forEach(this::notifyDropType); // notify on everything created typesDiff.entriesOnlyOnRight().values().forEach(this::notifyCreateType); tablesDiff.entriesOnlyOnRight().values().forEach(this::notifyCreateTable); viewsDiff.entriesOnlyOnRight().values().forEach(this::notifyCreateView); udfsDiff.entriesOnlyOnRight().values().forEach(this::notifyCreateFunction); udasDiff.entriesOnlyOnRight().values().forEach(this::notifyCreateAggregate); // notify on everything altered if (!before.params.equals(after.params)) notifyAlterKeyspace(after); typesDiff.entriesDiffering().values().forEach(diff -> notifyAlterType(diff.rightValue())); tablesDiff.entriesDiffering().values().forEach(diff -> notifyAlterTable(diff.leftValue(), diff.rightValue())); viewsDiff.entriesDiffering().values().forEach(diff -> notifyAlterView(diff.leftValue(), diff.rightValue())); udfsDiff.entriesDiffering().values().forEach(diff -> notifyAlterFunction(diff.rightValue())); udasDiff.entriesDiffering().values().forEach(diff -> notifyAlterAggregate(diff.rightValue())); } private void createKeyspace(KeyspaceMetadata keyspace) { load(keyspace); Keyspace.open(keyspace.name); notifyCreateKeyspace(keyspace); keyspace.types.forEach(this::notifyCreateType); keyspace.tables.forEach(this::notifyCreateTable); keyspace.views.forEach(this::notifyCreateView); keyspace.functions.udfs().forEach(this::notifyCreateFunction); keyspace.functions.udas().forEach(this::notifyCreateAggregate); } private void dropKeyspace(KeyspaceMetadata keyspace) { keyspace.views.forEach(this::dropView); keyspace.tables.forEach(this::dropTable); // remove the keyspace from the static instances. Keyspace.clear(keyspace.name); unload(keyspace); Keyspace.writeOrder.awaitNewBarrier(); keyspace.functions.udas().forEach(this::notifyDropAggregate); keyspace.functions.udfs().forEach(this::notifyDropFunction); keyspace.views.forEach(this::notifyDropView); keyspace.tables.forEach(this::notifyDropTable); keyspace.types.forEach(this::notifyDropType); notifyDropKeyspace(keyspace); } private void dropView(ViewMetadata metadata) { dropTable(metadata.metadata); } private void dropTable(TableMetadata metadata) { ColumnFamilyStore cfs = Keyspace.open(metadata.keyspace).getColumnFamilyStore(metadata.name); assert cfs != null; // make sure all the indexes are dropped, or else. cfs.indexManager.markAllIndexesRemoved(); CompactionManager.instance.interruptCompactionFor(Collections.singleton(metadata), true); if (DatabaseDescriptor.isAutoSnapshot()) cfs.snapshot(Keyspace.getTimestampedSnapshotNameWithPrefix(cfs.name, ColumnFamilyStore.SNAPSHOT_DROP_PREFIX)); CommitLog.instance.forceRecycleAllSegments(Collections.singleton(metadata.id)); Keyspace.open(metadata.keyspace).dropCf(metadata.id); } private void createTable(TableMetadata table) { Keyspace.open(table.keyspace).initCf(metadataRefs.get(table.id), true); } private void createView(ViewMetadata view) { Keyspace.open(view.keyspace).initCf(metadataRefs.get(view.metadata.id), true); } private void alterTable(TableMetadata updated) { Keyspace.open(updated.keyspace).getColumnFamilyStore(updated.name).reload(); } private void alterView(ViewMetadata updated) { Keyspace.open(updated.keyspace).getColumnFamilyStore(updated.name).reload(); } private void notifyCreateKeyspace(KeyspaceMetadata ksm) { changeListeners.forEach(l -> l.onCreateKeyspace(ksm.name)); } private void notifyCreateTable(TableMetadata metadata) { changeListeners.forEach(l -> l.onCreateTable(metadata.keyspace, metadata.name)); } private void notifyCreateView(ViewMetadata view) { changeListeners.forEach(l -> l.onCreateView(view.keyspace, view.name)); } private void notifyCreateType(UserType ut) { changeListeners.forEach(l -> l.onCreateType(ut.keyspace, ut.getNameAsString())); } private void notifyCreateFunction(UDFunction udf) { changeListeners.forEach(l -> l.onCreateFunction(udf.name().keyspace, udf.name().name, udf.argTypes())); } private void notifyCreateAggregate(UDAggregate udf) { changeListeners.forEach(l -> l.onCreateAggregate(udf.name().keyspace, udf.name().name, udf.argTypes())); } private void notifyAlterKeyspace(KeyspaceMetadata ksm) { changeListeners.forEach(l -> l.onAlterKeyspace(ksm.name)); } private void notifyAlterTable(TableMetadata current, TableMetadata updated) { boolean changeAffectedPreparedStatements = current.changeAffectsPreparedStatements(updated); changeListeners.forEach(l -> l.onAlterTable(updated.keyspace, updated.name, changeAffectedPreparedStatements)); } private void notifyAlterView(ViewMetadata current, ViewMetadata updated) { boolean changeAffectedPreparedStatements = current.metadata.changeAffectsPreparedStatements(updated.metadata); changeListeners.forEach(l ->l.onAlterView(updated.keyspace, updated.name, changeAffectedPreparedStatements)); } private void notifyAlterType(UserType ut) { changeListeners.forEach(l -> l.onAlterType(ut.keyspace, ut.getNameAsString())); } private void notifyAlterFunction(UDFunction udf) { changeListeners.forEach(l -> l.onAlterFunction(udf.name().keyspace, udf.name().name, udf.argTypes())); } private void notifyAlterAggregate(UDAggregate udf) { changeListeners.forEach(l -> l.onAlterAggregate(udf.name().keyspace, udf.name().name, udf.argTypes())); } private void notifyDropKeyspace(KeyspaceMetadata ksm) { changeListeners.forEach(l -> l.onDropKeyspace(ksm.name)); } private void notifyDropTable(TableMetadata metadata) { changeListeners.forEach(l -> l.onDropTable(metadata.keyspace, metadata.name)); } private void notifyDropView(ViewMetadata view) { changeListeners.forEach(l -> l.onDropView(view.keyspace, view.name)); } private void notifyDropType(UserType ut) { changeListeners.forEach(l -> l.onDropType(ut.keyspace, ut.getNameAsString())); } private void notifyDropFunction(UDFunction udf) { changeListeners.forEach(l -> l.onDropFunction(udf.name().keyspace, udf.name().name, udf.argTypes())); } private void notifyDropAggregate(UDAggregate udf) { changeListeners.forEach(l -> l.onDropAggregate(udf.name().keyspace, udf.name().name, udf.argTypes())); } }