blob: fc632df06d978f4ad280c1b022573a2d44626b94 [file] [log] [blame]
/*
* 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.drill.exec.rpc.user;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.drill.exec.ExecConstants;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import org.apache.calcite.schema.Schema;
import org.apache.calcite.schema.SchemaPlus;
import org.apache.calcite.schema.Table;
import org.apache.calcite.tools.ValidationException;
import org.apache.drill.common.config.DrillConfig;
import org.apache.drill.common.config.DrillProperties;
import org.apache.drill.exec.planner.physical.PlannerSettings;
import org.apache.drill.exec.planner.sql.SchemaUtilities;
import org.apache.drill.exec.planner.sql.handlers.SqlHandlerUtil;
import org.apache.drill.exec.proto.UserBitShared.UserCredentials;
import org.apache.drill.exec.proto.UserProtos.UserProperties;
import org.apache.drill.exec.server.options.OptionManager;
import org.apache.drill.exec.server.options.SessionOptionManager;
import com.google.common.collect.Maps;
import org.apache.drill.exec.store.AbstractSchema;
import org.apache.drill.exec.store.StorageStrategy;
import org.apache.drill.exec.store.dfs.DrillFileSystem;
import org.apache.drill.exec.store.dfs.WorkspaceSchemaFactory;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UserSession implements AutoCloseable {
private static final Logger logger = LoggerFactory.getLogger(UserSession.class);
private boolean supportComplexTypes;
private UserCredentials credentials;
private DrillProperties properties;
private SessionOptionManager sessionOptions;
private final AtomicInteger queryCount;
private final String sessionId;
/** Stores list of temporary tables, key is original table name converted to lower case to achieve case-insensitivity,
* value is generated table name. **/
private final ConcurrentMap<String, String> temporaryTables;
/** Stores list of session temporary locations, key is path to location, value is file system associated with location. **/
private final ConcurrentMap<Path, FileSystem> temporaryLocations;
/** On session close deletes all session temporary locations recursively and clears temporary locations list. */
@Override
public void close() {
for (Map.Entry<Path, FileSystem> entry : temporaryLocations.entrySet()) {
Path path = entry.getKey();
FileSystem fs = entry.getValue();
try {
fs.delete(path, true);
logger.info("Deleted session temporary location [{}] from file system [{}]",
path.toUri().getPath(), fs.getUri());
} catch (Exception e) {
logger.warn("Error during session temporary location [{}] deletion from file system [{}]: [{}]",
path.toUri().getPath(), fs.getUri(), e.getMessage());
}
}
temporaryLocations.clear();
}
/**
* Implementations of this interface are allowed to increment queryCount.
* {@link org.apache.drill.exec.work.user.UserWorker} should have a member that implements the interface.
* No other core class should implement this interface. Test classes may implement (see ControlsInjectionUtil).
*/
public interface QueryCountIncrementer {
void increment(final UserSession session);
}
public static class Builder {
private UserSession userSession;
public static Builder newBuilder() {
return new Builder();
}
public Builder withCredentials(UserCredentials credentials) {
userSession.credentials = credentials;
return this;
}
public Builder withOptionManager(OptionManager systemOptions) {
userSession.sessionOptions = new SessionOptionManager(systemOptions, userSession);
return this;
}
public Builder withUserProperties(UserProperties properties) {
userSession.properties = DrillProperties.createFromProperties(properties, false);
return this;
}
public Builder setSupportComplexTypes(boolean supportComplexTypes) {
userSession.supportComplexTypes = supportComplexTypes;
return this;
}
private boolean canApplyUserProperty() {
final StringBuilder sb = new StringBuilder();
if (userSession.properties.containsKey(DrillProperties.QUOTING_IDENTIFIERS)) {
sb.append(DrillProperties.QUOTING_IDENTIFIERS).append(",");
}
if (userSession.properties.containsKey(DrillProperties.QUERY_TAGS)) {
sb.append(DrillProperties.QUERY_TAGS);
}
if (userSession.sessionOptions == null && sb.length() > 0) {
logger.warn("User property {} can't be installed as a server option without the session option manager",
sb.toString());
return false;
}
return true;
}
public UserSession build() {
if (canApplyUserProperty()) {
if (userSession.properties.containsKey(DrillProperties.QUOTING_IDENTIFIERS)) {
userSession.setSessionOption(PlannerSettings.QUOTING_IDENTIFIERS_KEY,
userSession.properties.getProperty(DrillProperties.QUOTING_IDENTIFIERS));
}
if (userSession.properties.containsKey(DrillProperties.QUERY_TAGS)) {
userSession.setSessionOption(ExecConstants.RM_QUERY_TAGS_KEY,
userSession.properties.getProperty(DrillProperties.QUERY_TAGS));
}
}
UserSession session = userSession;
userSession = null;
return session;
}
Builder() {
userSession = new UserSession();
}
}
private UserSession() {
queryCount = new AtomicInteger(0);
sessionId = UUID.randomUUID().toString();
temporaryTables = Maps.newConcurrentMap();
temporaryLocations = Maps.newConcurrentMap();
properties = DrillProperties.createEmpty();
}
public boolean isSupportComplexTypes() {
return supportComplexTypes;
}
public SessionOptionManager getOptions() {
return sessionOptions;
}
public UserCredentials getCredentials() {
return credentials;
}
/**
* Replace current user credentials with the given user's credentials. Meant to be called only by a
* {@link InboundImpersonationManager impersonation manager}.
*
* @param impersonationManager impersonation manager making this call
* @param newCredentials user credentials to change to
*/
public void replaceUserCredentials(final InboundImpersonationManager impersonationManager,
final UserCredentials newCredentials) {
Preconditions.checkNotNull(impersonationManager, "User credentials can only be replaced by an" +
" impersonation manager.");
credentials = newCredentials;
}
public String getTargetUserName() {
return properties.getProperty(DrillProperties.IMPERSONATION_TARGET);
}
public void incrementQueryCount(final QueryCountIncrementer incrementer) {
assert incrementer != null;
queryCount.incrementAndGet();
}
public int getQueryCount() {
return queryCount.get();
}
/**
* Update the schema path for the session.
* @param newDefaultSchemaPath New default schema path to set. It could be relative to the current default schema or
* absolute schema.
* @param currentDefaultSchema Current default schema.
* @throws ValidationException If the given default schema path is invalid in current schema tree.
*/
public void setDefaultSchemaPath(String newDefaultSchemaPath, SchemaPlus currentDefaultSchema)
throws ValidationException {
final List<String> newDefaultPathAsList = SchemaUtilities.getSchemaPathAsList(newDefaultSchemaPath);
SchemaPlus newDefault;
// First try to find the given schema relative to the current default schema.
newDefault = SchemaUtilities.findSchema(currentDefaultSchema, newDefaultPathAsList);
if (newDefault == null) {
// If we fail to find the schema relative to current default schema, consider the given new default schema path as
// absolute schema path.
newDefault = SchemaUtilities.findSchema(currentDefaultSchema, newDefaultPathAsList);
}
if (newDefault == null) {
SchemaUtilities.throwSchemaNotFoundException(currentDefaultSchema, newDefaultSchemaPath);
}
properties.setProperty(DrillProperties.SCHEMA, SchemaUtilities.getSchemaPath(newDefault));
}
/**
* @return Get current default schema path.
*/
public String getDefaultSchemaPath() {
return properties.getProperty(DrillProperties.SCHEMA, "");
}
/**
* Get default schema from current default schema path and given schema tree.
* @param rootSchema root schema
* @return A {@link org.apache.calcite.schema.SchemaPlus} object.
*/
public SchemaPlus getDefaultSchema(SchemaPlus rootSchema) {
final String defaultSchemaPath = getDefaultSchemaPath();
if (Strings.isNullOrEmpty(defaultSchemaPath)) {
return null;
}
return SchemaUtilities.findSchema(rootSchema, defaultSchemaPath);
}
/**
* Set the option of a session level.
* Note: Option's kind is automatically detected if such option exists.
*
* @param name option name
* @param value option value
*/
public void setSessionOption(String name, String value) {
sessionOptions.setLocalOption(name, value);
}
/**
* @return unique session identifier
*/
public String getSessionId() { return sessionId; }
/**
* Creates and adds session temporary location if absent using schema configuration.
* Before any actions, checks if passed table schema is valid default temporary workspace.
* Generates temporary table name and stores it's original name as key
* and generated name as value in session temporary tables cache.
* Original temporary name is converted to lower case to achieve case-insensitivity.
* If original table name already exists, new name is not regenerated and is reused.
* This can happen if default temporary workspace was changed (file system or location) or
* orphan temporary table name has remained (name was registered but table creation did not succeed).
*
* @param schema table schema
* @param tableName original table name
* @param config drill config
* @return generated temporary table name
* @throws IOException if error during session temporary location creation
*/
public String registerTemporaryTable(AbstractSchema schema, String tableName, DrillConfig config) throws IOException {
addTemporaryLocation(SchemaUtilities.resolveToValidTemporaryWorkspace(schema, config));
String temporaryTableName = new Path(sessionId, UUID.randomUUID().toString()).toUri().getPath();
String oldTemporaryTableName = temporaryTables.putIfAbsent(tableName.toLowerCase(), temporaryTableName);
return oldTemporaryTableName == null ? temporaryTableName : oldTemporaryTableName;
}
/**
* Returns generated temporary table name from the list of session temporary tables, null otherwise.
* Original temporary name is converted to lower case to achieve case-insensitivity.
*
* @param tableName original table name
* @return generated temporary table name
*/
public String resolveTemporaryTableName(String tableName) {
return temporaryTables.get(tableName.toLowerCase());
}
public String getOriginalTableNameFromTemporaryTable(String tableName) {
for (String originalTableName : temporaryTables.keySet()) {
if (temporaryTables.get(originalTableName).equals(tableName)) {
return originalTableName;
}
}
return null;
}
/**
* Checks if passed table is temporary, table name is case-insensitive.
* Before looking for table checks if passed schema is temporary and returns false if not
* since temporary tables are allowed to be created in temporary workspace only.
* If passed workspace is temporary, looks for temporary table.
* First checks if table name is among temporary tables, if not returns false.
* If temporary table named was resolved, checks that temporary table exists on disk,
* to ensure that temporary table actually exists and resolved table name is not orphan
* (for example, in result of unsuccessful temporary table creation).
*
* @param drillSchema table schema
* @param config drill config
* @param tableName original table name
* @return true if temporary table exists in schema, false otherwise
*/
public boolean isTemporaryTable(AbstractSchema drillSchema, DrillConfig config, String tableName) {
if (drillSchema == null || !SchemaUtilities.isTemporaryWorkspace(drillSchema.getFullSchemaName(), config)) {
return false;
}
String temporaryTableName = resolveTemporaryTableName(tableName);
if (temporaryTableName != null) {
Table temporaryTable = SqlHandlerUtil.getTableFromSchema(drillSchema, temporaryTableName);
if (temporaryTable != null && temporaryTable.getJdbcTableType() == Schema.TableType.TABLE) {
return true;
}
}
return false;
}
/**
* Removes temporary table name from the list of session temporary tables.
* Original temporary name is converted to lower case to achieve case-insensitivity.
* Before temporary table drop, checks if passed table schema is valid default temporary workspace.
*
* @param schema table schema
* @param tableName original table name
* @param config drill config
*/
public void removeTemporaryTable(AbstractSchema schema, String tableName, DrillConfig config) {
String temporaryTable = resolveTemporaryTableName(tableName);
if (temporaryTable == null) {
return;
}
SqlHandlerUtil.dropTableFromSchema(SchemaUtilities.resolveToValidTemporaryWorkspace(schema, config), temporaryTable);
temporaryTables.remove(tableName.toLowerCase());
}
/**
* Session temporary tables are stored under temporary workspace location in session folder
* defined by unique session id. These session temporary locations are deleted on session close.
* If default temporary workspace file system or location is changed at runtime,
* new session temporary location will be added with corresponding file system
* to the list of session temporary locations. If location does not exist it will be created and
* {@link StorageStrategy#TEMPORARY} storage rules will be applied to it.
*
* @param temporaryWorkspace temporary workspace
* @throws IOException in case of error during temporary location creation
*/
private void addTemporaryLocation(WorkspaceSchemaFactory.WorkspaceSchema temporaryWorkspace) throws IOException {
DrillFileSystem fs = temporaryWorkspace.getFS();
Path temporaryLocation = new Path(fs.getUri().toString(),
new Path(temporaryWorkspace.getDefaultLocation(), sessionId));
FileSystem fileSystem = temporaryLocations.putIfAbsent(temporaryLocation, fs);
if (fileSystem == null) {
StorageStrategy.TEMPORARY.createPathAndApply(fs, temporaryLocation);
Preconditions.checkArgument(fs.exists(temporaryLocation),
String.format("Temporary location should exist [%s]", temporaryLocation.toUri().getPath()));
}
}
}