/* * Licensed to CRATE Technology GmbH ("Crate") under one or more contributor * license agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. Crate 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. * * However, if you have executed another commercial license agreement * with Crate these terms will supersede the license and you may use the * software solely pursuant to the terms of the relevant commercial agreement. */ package io.crate.executor.transport; import com.google.common.base.Joiner; import io.crate.Constants; import io.crate.analyze.CreateTableAnalyzedStatement; import io.crate.exceptions.SQLExceptions; import io.crate.metadata.PartitionName; import io.crate.metadata.TableIdent; import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.indices.alias.Alias; import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; import org.elasticsearch.action.admin.indices.create.CreateIndexResponse; import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; import org.elasticsearch.action.admin.indices.delete.DeleteIndexResponse; import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest; import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateResponse; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.cluster.metadata.AliasOrIndex; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.inject.Singleton; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.indices.IndexAlreadyExistsException; import org.elasticsearch.indices.IndexTemplateAlreadyExistsException; import java.util.Locale; import java.util.concurrent.CompletableFuture; import java.util.SortedMap; @Singleton public class TableCreator { private static final Long SUCCESS_RESULT = 1L; protected static final Logger logger = Loggers.getLogger(TableCreator.class); private final ClusterService clusterService; private final IndexNameExpressionResolver indexNameExpressionResolver; private final TransportActionProvider transportActionProvider; @Inject public TableCreator(ClusterService clusterService, IndexNameExpressionResolver indexNameExpressionResolver, TransportActionProvider transportActionProvider) { this.clusterService = clusterService; this.indexNameExpressionResolver = indexNameExpressionResolver; this.transportActionProvider = transportActionProvider; } public CompletableFuture<Long> create(CreateTableAnalyzedStatement statement) { final CompletableFuture<Long> result = new CompletableFuture<>(); // real work done in createTable() deleteOrphans(new CreateTableResponseListener(result, statement), statement.tableIdent()); return result; } private CreateIndexRequest createIndexRequest(CreateTableAnalyzedStatement statement) { return new CreateIndexRequest(statement.tableIdent().indexName(), settings(statement)) .mapping(Constants.DEFAULT_MAPPING_TYPE, statement.mapping()); } private Settings settings(CreateTableAnalyzedStatement statement) { return statement.tableParameter().settings().getByPrefix("index."); } private PutIndexTemplateRequest createTemplateRequest(CreateTableAnalyzedStatement statement) { return new PutIndexTemplateRequest(statement.templateName()) .mapping(Constants.DEFAULT_MAPPING_TYPE, statement.mapping()) .create(true) .settings(settings(statement)) .template(statement.templatePrefix()) .order(100) .alias(new Alias(statement.tableIdent().indexName())); } private void createTable(final CompletableFuture<Long> result, final CreateTableAnalyzedStatement statement) { if (statement.templateName() != null) { transportActionProvider.transportPutIndexTemplateAction().execute(createTemplateRequest(statement), new ActionListener<PutIndexTemplateResponse>() { @Override public void onResponse(PutIndexTemplateResponse response) { if (!response.isAcknowledged()) { warnNotAcknowledged(String.format(Locale.ENGLISH, "creating table '%s'", statement.tableIdent().fqn())); } result.complete(SUCCESS_RESULT); } @Override public void onFailure(Exception e) { setException(result, e, statement); } }); } else { transportActionProvider.transportCreateIndexAction().execute(createIndexRequest(statement), new ActionListener<CreateIndexResponse>() { @Override public void onResponse(CreateIndexResponse response) { if (!response.isAcknowledged()) { warnNotAcknowledged(String.format(Locale.ENGLISH, "creating table '%s'", statement.tableIdent().fqn())); } result.complete(SUCCESS_RESULT); } @Override public void onFailure(Exception e) { setException(result, e, statement); } }); } } private void setException(CompletableFuture<Long> result, Throwable e, CreateTableAnalyzedStatement statement) { e = SQLExceptions.unwrap(e); String message = e.getMessage(); if ("mapping [default]".equals(message) && e.getCause() != null) { // this is a generic mapping parse exception, // the cause has usually a better more detailed error message result.completeExceptionally(e.getCause()); } else if (statement.ifNotExists() && (e instanceof IndexAlreadyExistsException || (e instanceof IndexTemplateAlreadyExistsException && statement.templateName() != null))) { result.complete(null); } else { result.completeExceptionally(e); } } private void deleteOrphans(final CreateTableResponseListener listener, TableIdent tableIdent) { MetaData metaData = clusterService.state().getMetaData(); String fqn = tableIdent.fqn(); if (metaData.hasAlias(fqn) && isPartition(metaData, fqn)) { logger.debug("Deleting orphaned partitions with alias: {}", fqn); transportActionProvider.transportDeleteIndexAction().execute(new DeleteIndexRequest(fqn), new ActionListener<DeleteIndexResponse>() { @Override public void onResponse(DeleteIndexResponse response) { if (!response.isAcknowledged()) { warnNotAcknowledged("deleting orphaned alias"); } deleteOrphanedPartitions(listener, tableIdent); } @Override public void onFailure(Exception e) { listener.onFailure(e); } }); } else { deleteOrphanedPartitions(listener, tableIdent); } } private static boolean isPartition(MetaData metaData, String fqn) { SortedMap<String, AliasOrIndex> aliasAndIndexLookup = metaData.getAliasAndIndexLookup(); AliasOrIndex aliasOrIndex = aliasAndIndexLookup.get(fqn); return PartitionName.isPartition( aliasOrIndex.getIndices().iterator().next().getIndex().getName()); } /** * if some orphaned partition with the same table name still exist, * delete them beforehand as they would create unwanted and maybe invalid * initial data. * <p> * should never delete partitions of existing partitioned tables */ private void deleteOrphanedPartitions(final CreateTableResponseListener listener, TableIdent tableIdent) { String partitionWildCard = PartitionName.templateName(tableIdent.schema(), tableIdent.name()) + "*"; String[] orphans = indexNameExpressionResolver.concreteIndexNames( clusterService.state(), IndicesOptions.strictExpand(), partitionWildCard); if (orphans.length > 0) { if (logger.isDebugEnabled()) { logger.debug("Deleting orphaned partitions: {}", Joiner.on(", ").join(orphans)); } transportActionProvider.transportDeleteIndexAction().execute(new DeleteIndexRequest(orphans), new ActionListener<DeleteIndexResponse>() { @Override public void onResponse(DeleteIndexResponse response) { if (!response.isAcknowledged()) { warnNotAcknowledged("deleting orphans"); } listener.onResponse(SUCCESS_RESULT); } @Override public void onFailure(Exception e) { listener.onFailure(e); } }); } else { listener.onResponse(SUCCESS_RESULT); } } protected void warnNotAcknowledged(String operationName) { logger.warn("{} was not acknowledged. This could lead to inconsistent state.", operationName); } class CreateTableResponseListener implements ActionListener<Long> { final CompletableFuture<Long> result; final CreateTableAnalyzedStatement statement; public CreateTableResponseListener(CompletableFuture<Long> result, CreateTableAnalyzedStatement statement) { this.result = result; this.statement = statement; } @Override public void onResponse(Long ignored) { createTable(result, statement); } @Override public void onFailure(Exception e) { result.completeExceptionally(e); } } }