/*
* Licensed to 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.annotations.VisibleForTesting;
import io.crate.action.FutureActionListener;
import io.crate.analyze.CreateSnapshotAnalyzedStatement;
import io.crate.analyze.DropSnapshotAnalyzedStatement;
import io.crate.analyze.RestoreSnapshotAnalyzedStatement;
import io.crate.exceptions.CreateSnapshotException;
import io.crate.exceptions.TableUnknownException;
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.cluster.snapshots.create.CreateSnapshotRequest;
import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse;
import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest;
import org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotResponse;
import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest;
import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsResponse;
import org.elasticsearch.action.admin.cluster.snapshots.get.TransportGetSnapshotsAction;
import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest;
import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotResponse;
import org.elasticsearch.action.support.IndicesOptions;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.inject.Singleton;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.snapshots.SnapshotInfo;
import org.elasticsearch.snapshots.SnapshotState;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import static io.crate.analyze.SnapshotSettings.IGNORE_UNAVAILABLE;
import static io.crate.analyze.SnapshotSettings.WAIT_FOR_COMPLETION;
@Singleton
public class SnapshotRestoreDDLDispatcher {
private static final Logger LOGGER = Loggers.getLogger(SnapshotRestoreDDLDispatcher.class);
private final TransportActionProvider transportActionProvider;
private final String[] ALL_TEMPLATES = new String[]{"_all"};
@Inject
public SnapshotRestoreDDLDispatcher(TransportActionProvider transportActionProvider) {
this.transportActionProvider = transportActionProvider;
}
public CompletableFuture<Long> dispatch(final DropSnapshotAnalyzedStatement statement) {
final CompletableFuture<Long> future = new CompletableFuture<>();
final String repositoryName = statement.repository();
final String snapshotName = statement.snapshot();
transportActionProvider.transportDeleteSnapshotAction().execute(
new DeleteSnapshotRequest(repositoryName, snapshotName),
new ActionListener<DeleteSnapshotResponse>() {
@Override
public void onResponse(DeleteSnapshotResponse response) {
if (!response.isAcknowledged()) {
LOGGER.info("delete snapshot '{}.{}' not acknowledged", repositoryName, snapshotName);
}
future.complete(1L);
}
@Override
public void onFailure(Exception e) {
future.completeExceptionally(e);
}
}
);
return future;
}
public CompletableFuture<Long> dispatch(final CreateSnapshotAnalyzedStatement statement) {
final CompletableFuture<Long> resultFuture = new CompletableFuture<>();
boolean waitForCompletion = statement.snapshotSettings().getAsBoolean(WAIT_FOR_COMPLETION.name(), WAIT_FOR_COMPLETION.defaultValue());
boolean ignoreUnavailable = statement.snapshotSettings().getAsBoolean(IGNORE_UNAVAILABLE.name(), IGNORE_UNAVAILABLE.defaultValue());
// ignore_unavailable as set by statement
IndicesOptions indicesOptions = IndicesOptions.fromOptions(ignoreUnavailable, true, true, false, IndicesOptions.lenientExpandOpen());
CreateSnapshotRequest request = new CreateSnapshotRequest(statement.snapshot().getRepository(), statement.snapshot().getSnapshotId().getName())
.includeGlobalState(true)
.waitForCompletion(waitForCompletion)
.indices(statement.indices())
.indicesOptions(indicesOptions)
.settings(statement.snapshotSettings());
//noinspection ThrowableResultOfMethodCallIgnored
assert request.validate() == null : "invalid CREATE SNAPSHOT statement";
transportActionProvider.transportCreateSnapshotAction().execute(request, new ActionListener<CreateSnapshotResponse>() {
@Override
public void onResponse(CreateSnapshotResponse createSnapshotResponse) {
SnapshotInfo snapshotInfo = createSnapshotResponse.getSnapshotInfo();
if (snapshotInfo == null) {
// if wait_for_completion is false the snapshotInfo is null
resultFuture.complete(1L);
} else if (snapshotInfo.state() == SnapshotState.FAILED) {
// fail request if snapshot creation failed
String reason = createSnapshotResponse.getSnapshotInfo().reason()
.replaceAll("Index", "Table")
.replaceAll("Indices", "Tables");
resultFuture.completeExceptionally(
new CreateSnapshotException(statement.snapshot(), reason)
);
} else {
resultFuture.complete(1L);
}
}
@Override
public void onFailure(Exception e) {
resultFuture.completeExceptionally(e);
}
});
return resultFuture;
}
public CompletableFuture<Long> dispatch(final RestoreSnapshotAnalyzedStatement analysis) {
boolean waitForCompletion = analysis.settings().getAsBoolean(WAIT_FOR_COMPLETION.name(), WAIT_FOR_COMPLETION.defaultValue());
boolean ignoreUnavailable = analysis.settings().getAsBoolean(IGNORE_UNAVAILABLE.name(), IGNORE_UNAVAILABLE.defaultValue());
// ignore_unavailable as set by statement
IndicesOptions indicesOptions = IndicesOptions.fromOptions(ignoreUnavailable, true, true, false, IndicesOptions.lenientExpandOpen());
FutureActionListener<RestoreSnapshotResponse, Long> listener = new FutureActionListener<>(r -> 1L);
resolveIndexNames(analysis.restoreTables(), ignoreUnavailable, transportActionProvider.transportGetSnapshotsAction(), analysis.repositoryName())
.whenComplete((ResolveIndicesAndTemplatesContext ctx, Throwable t) -> {
if (t == null) {
String[] indexNames = ctx.resolvedIndices().toArray(new String[ctx.resolvedIndices().size()]);
String[] templateNames = analysis.restoreAll() ? ALL_TEMPLATES :
ctx.resolvedTemplates().toArray(new String[ctx.resolvedTemplates().size()]);
RestoreSnapshotRequest request = new RestoreSnapshotRequest(analysis.repositoryName(), analysis.snapshotName())
.indices(indexNames)
.templates(templateNames)
.indicesOptions(indicesOptions)
.settings(analysis.settings())
.waitForCompletion(waitForCompletion)
.includeGlobalState(false)
.includeAliases(true);
transportActionProvider.transportRestoreSnapshotAction().execute(request, listener);
} else {
listener.onFailure((Exception) t);
}
});
return listener;
}
@VisibleForTesting
static CompletableFuture<ResolveIndicesAndTemplatesContext> resolveIndexNames(List<RestoreSnapshotAnalyzedStatement.RestoreTableInfo> restoreTables, boolean ignoreUnavailable,
TransportGetSnapshotsAction getSnapshotsAction, String repositoryName) {
ResolveIndicesAndTemplatesContext resolveIndicesAndTemplatesContext = new ResolveIndicesAndTemplatesContext();
List<RestoreSnapshotAnalyzedStatement.RestoreTableInfo> toResolveFromSnapshot = new ArrayList<>();
for (RestoreSnapshotAnalyzedStatement.RestoreTableInfo table : restoreTables) {
if (table.hasPartitionInfo()) {
resolveIndicesAndTemplatesContext.addIndex(table.partitionName().asIndexName());
resolveIndicesAndTemplatesContext.addTemplate(table.partitionTemplate());
} else if (ignoreUnavailable) {
// If ignoreUnavailable is true, it's cheaper to simply return indexName and the partitioned wildcard instead
// checking if it's a partitioned table or not
resolveIndicesAndTemplatesContext.addIndex(table.tableIdent().indexName());
// For the case its a partitioned table we restore all partitions and the templates
String templateName = table.partitionTemplate();
resolveIndicesAndTemplatesContext.addIndex(templateName + "*");
resolveIndicesAndTemplatesContext.addTemplate(templateName);
} else {
// index name needs to be resolved from snapshot
toResolveFromSnapshot.add(table);
}
}
if (toResolveFromSnapshot.isEmpty()) {
return CompletableFuture.completedFuture(resolveIndicesAndTemplatesContext);
}
final CompletableFuture<ResolveIndicesAndTemplatesContext> f = new CompletableFuture<>();
getSnapshotsAction.execute(
new GetSnapshotsRequest(repositoryName),
new ResolveFromSnapshotActionListener(f, toResolveFromSnapshot, resolveIndicesAndTemplatesContext)
);
return f;
}
@VisibleForTesting
static class ResolveIndicesAndTemplatesContext {
private final Collection<String> resolvedIndices = new HashSet<>();
private final Collection<String> resolvedTemplates = new HashSet<>();
void addIndex(String index) {
resolvedIndices.add(index);
}
void addTemplate(String template) {
resolvedTemplates.add(template);
}
Collection<String> resolvedIndices() {
return resolvedIndices;
}
Collection<String> resolvedTemplates() {
return resolvedTemplates;
}
}
@VisibleForTesting
static class ResolveFromSnapshotActionListener implements ActionListener<GetSnapshotsResponse> {
private final CompletableFuture<ResolveIndicesAndTemplatesContext> returnFuture;
private final ResolveIndicesAndTemplatesContext resolveIndicesAndTemplatesContext;
private final List<RestoreSnapshotAnalyzedStatement.RestoreTableInfo> toResolve;
public ResolveFromSnapshotActionListener(CompletableFuture<ResolveIndicesAndTemplatesContext> returnFuture,
List<RestoreSnapshotAnalyzedStatement.RestoreTableInfo> toResolve,
ResolveIndicesAndTemplatesContext resolveIndicesAndTemplatesContext) {
this.returnFuture = returnFuture;
this.resolveIndicesAndTemplatesContext = resolveIndicesAndTemplatesContext;
this.toResolve = toResolve;
}
@Override
public void onResponse(GetSnapshotsResponse getSnapshotsResponse) {
List<SnapshotInfo> snapshots = getSnapshotsResponse.getSnapshots();
for (RestoreSnapshotAnalyzedStatement.RestoreTableInfo table : toResolve) {
resolveTableFromSnapshot(table, snapshots, resolveIndicesAndTemplatesContext);
}
returnFuture.complete(resolveIndicesAndTemplatesContext);
}
@VisibleForTesting
public static void resolveTableFromSnapshot(RestoreSnapshotAnalyzedStatement.RestoreTableInfo table,
List<SnapshotInfo> snapshots,
ResolveIndicesAndTemplatesContext ctx) throws TableUnknownException {
String name = table.tableIdent().indexName();
for (SnapshotInfo snapshot : snapshots) {
for (String index : snapshot.indices()) {
if (name.equals(index)) {
ctx.addIndex(index);
return;
} else if(isIndexPartitionOfTable(index, table.tableIdent())) {
String templateName = table.partitionTemplate();
// add a partitions wildcard
// to match all partitions if a partitioned table was meant
ctx.addIndex(templateName + "*");
ctx.addTemplate(templateName);
return;
}
}
}
ctx.addTemplate(table.partitionTemplate());
}
private static boolean isIndexPartitionOfTable(String index, TableIdent tableIdent) {
return PartitionName.isPartition(index) &&
PartitionName.fromIndexOrTemplate(index).tableIdent().equals(tableIdent);
}
@Override
public void onFailure(Exception e) {
returnFuture.completeExceptionally(e);
}
}
}