/* * 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.facebook.presto.cassandra; import com.facebook.presto.spi.NotFoundException; import com.facebook.presto.spi.SchemaNotFoundException; import com.facebook.presto.spi.SchemaTableName; import com.facebook.presto.spi.TableNotFoundException; import com.google.common.base.Throwables; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableList; import com.google.common.collect.Maps; import com.google.common.util.concurrent.UncheckedExecutionException; import io.airlift.units.Duration; import org.weakref.jmx.Managed; import javax.annotation.concurrent.ThreadSafe; import javax.inject.Inject; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import static com.facebook.presto.cassandra.RetryDriver.retry; import static com.google.common.cache.CacheLoader.asyncReloading; import static java.util.Locale.ENGLISH; import static java.util.Objects.requireNonNull; import static java.util.concurrent.TimeUnit.MILLISECONDS; /** * Cassandra Schema Cache */ @ThreadSafe public class CachingCassandraSchemaProvider { private final String connectorId; private final CassandraSession session; /** * Mapping from an empty string to all schema names. Each schema name is a * mapping from the lower case schema name to the case sensitive schema name. * This mapping is necessary because Presto currently does not properly handle * case sensitive names. */ private final LoadingCache<String, Map<String, String>> schemaNamesCache; /** * Mapping from lower case schema name to all tables in that schema. Each * table name is a mapping from the lower case table name to the case * sensitive table name. This mapping is necessary because Presto currently * does not properly handle case sensitive names. */ private final LoadingCache<String, Map<String, String>> tableNamesCache; private final LoadingCache<SchemaTableName, CassandraTable> tableCache; @Inject public CachingCassandraSchemaProvider( CassandraConnectorId connectorId, CassandraSession session, @ForCassandra ExecutorService executor, CassandraClientConfig cassandraClientConfig) { this(requireNonNull(connectorId, "connectorId is null").toString(), session, executor, requireNonNull(cassandraClientConfig, "cassandraClientConfig is null").getSchemaCacheTtl(), cassandraClientConfig.getSchemaRefreshInterval()); } public CachingCassandraSchemaProvider(String connectorId, CassandraSession session, ExecutorService executor, Duration cacheTtl, Duration refreshInterval) { this.connectorId = requireNonNull(connectorId, "connectorId is null"); this.session = requireNonNull(session, "cassandraSession is null"); requireNonNull(executor, "executor is null"); long expiresAfterWriteMillis = requireNonNull(cacheTtl, "cacheTtl is null").toMillis(); long refreshMills = requireNonNull(refreshInterval, "refreshInterval is null").toMillis(); schemaNamesCache = CacheBuilder.newBuilder() .expireAfterWrite(expiresAfterWriteMillis, MILLISECONDS) .refreshAfterWrite(refreshMills, MILLISECONDS) .build(asyncReloading(new CacheLoader<String, Map<String, String>>() { @Override public Map<String, String> load(String key) throws Exception { return loadAllSchemas(); } }, executor)); tableNamesCache = CacheBuilder.newBuilder() .expireAfterWrite(expiresAfterWriteMillis, MILLISECONDS) .refreshAfterWrite(refreshMills, MILLISECONDS) .build(asyncReloading(new CacheLoader<String, Map<String, String>>() { @Override public Map<String, String> load(String databaseName) throws Exception { return loadAllTables(databaseName); } }, executor)); tableCache = CacheBuilder.newBuilder() .expireAfterWrite(expiresAfterWriteMillis, MILLISECONDS) .refreshAfterWrite(refreshMills, MILLISECONDS) .build(asyncReloading(new CacheLoader<SchemaTableName, CassandraTable>() { @Override public CassandraTable load(SchemaTableName tableName) throws Exception { return loadTable(tableName); } }, executor)); } @Managed public void flushCache() { schemaNamesCache.invalidateAll(); tableNamesCache.invalidateAll(); tableCache.invalidateAll(); } public List<String> getAllSchemas() { return ImmutableList.copyOf(getCacheValue(schemaNamesCache, "", RuntimeException.class).keySet()); } private Map<String, String> loadAllSchemas() throws Exception { return retry() .stopOnIllegalExceptions() .run("getAllSchemas", () -> Maps.uniqueIndex(session.getAllSchemas(), CachingCassandraSchemaProvider::toLowerCase)); } public List<String> getAllTables(String databaseName) throws SchemaNotFoundException { return ImmutableList.copyOf(getCacheValue(tableNamesCache, databaseName, SchemaNotFoundException.class).keySet()); } private Map<String, String> loadAllTables(final String databaseName) throws Exception { return retry().stopOn(NotFoundException.class).stopOnIllegalExceptions() .run("getAllTables", () -> { String caseSensitiveDatabaseName = getCaseSensitiveSchemaName(databaseName); if (caseSensitiveDatabaseName == null) { caseSensitiveDatabaseName = databaseName; } List<String> tables = session.getAllTables(caseSensitiveDatabaseName); Map<String, String> nameMap = Maps.uniqueIndex(tables, CachingCassandraSchemaProvider::toLowerCase); if (tables.isEmpty()) { // Check to see if the database exists session.getSchema(databaseName); } return nameMap; }); } public CassandraTableHandle getTableHandle(SchemaTableName schemaTableName) { requireNonNull(schemaTableName, "schemaTableName is null"); String schemaName = getCaseSensitiveSchemaName(schemaTableName.getSchemaName()); String tableName = getCaseSensitiveTableName(schemaTableName); CassandraTableHandle tableHandle = new CassandraTableHandle(connectorId, schemaName, tableName); return tableHandle; } public String getCaseSensitiveSchemaName(String caseInsensitiveName) { String caseSensitiveSchemaName = getCacheValue(schemaNamesCache, "", RuntimeException.class).get(caseInsensitiveName.toLowerCase(ENGLISH)); return caseSensitiveSchemaName == null ? caseInsensitiveName : caseSensitiveSchemaName; } public String getCaseSensitiveTableName(SchemaTableName schemaTableName) { String caseSensitiveTableName = getCacheValue(tableNamesCache, schemaTableName.getSchemaName(), SchemaNotFoundException.class).get(schemaTableName.getTableName().toLowerCase(ENGLISH)); return caseSensitiveTableName == null ? schemaTableName.getTableName() : caseSensitiveTableName; } public CassandraTable getTable(CassandraTableHandle tableHandle) throws TableNotFoundException { return getCacheValue(tableCache, tableHandle.getSchemaTableName(), TableNotFoundException.class); } public void flushTable(SchemaTableName tableName) { tableCache.invalidate(tableName); tableNamesCache.invalidate(tableName.getSchemaName()); schemaNamesCache.invalidateAll(); } private CassandraTable loadTable(final SchemaTableName tableName) throws Exception { return retry() .stopOn(NotFoundException.class) .stopOnIllegalExceptions() .run("getTable", () -> session.getTable(tableName)); } private static <K, V, E extends Exception> V getCacheValue(LoadingCache<K, V> cache, K key, Class<E> exceptionClass) throws E { try { return cache.get(key); } catch (ExecutionException | UncheckedExecutionException e) { Throwable t = e.getCause(); Throwables.propagateIfInstanceOf(t, exceptionClass); throw Throwables.propagate(t); } } private static String toLowerCase(String value) { return value.toLowerCase(ENGLISH); } }