blob: 7577f613b5e8aa97b44b748198c4df85aaa7f66e [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.solr.cloud.api.collections;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.cloud.ClusterState;
import org.apache.solr.common.cloud.ZkNodeProps;
import org.apache.solr.common.params.CoreAdminParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.backup.AggregateBackupStats;
import org.apache.solr.core.backup.BackupFilePaths;
import org.apache.solr.core.backup.BackupId;
import org.apache.solr.core.backup.BackupManager;
import org.apache.solr.core.backup.BackupProperties;
import org.apache.solr.core.backup.ShardBackupId;
import org.apache.solr.core.backup.ShardBackupMetadata;
import org.apache.solr.core.backup.repository.BackupRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static org.apache.solr.common.params.CommonParams.NAME;
import static org.apache.solr.core.backup.BackupManager.COLLECTION_NAME_PROP;
import static org.apache.solr.core.backup.BackupManager.START_TIME_PROP;
/**
* An overseer command used to delete files associated with incremental backups.
*
* This assumes use of the incremental backup format, and not the (now deprecated) traditional 'full-snapshot' format.
* The deletion can either delete a specific {@link BackupId}, delete everything except the most recent N backup
* points, or can be used to trigger a "garbage collection" of unused index files in the backup repository.
*/
public class DeleteBackupCmd implements OverseerCollectionMessageHandler.Cmd {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final OverseerCollectionMessageHandler ocmh;
DeleteBackupCmd(OverseerCollectionMessageHandler ocmh) {
this.ocmh = ocmh;
}
@Override
public void call(ClusterState state, ZkNodeProps message, @SuppressWarnings({"rawtypes"}) NamedList results) throws Exception {
String backupLocation = message.getStr(CoreAdminParams.BACKUP_LOCATION);
String backupName = message.getStr(NAME);
String repo = message.getStr(CoreAdminParams.BACKUP_REPOSITORY);
int backupId = message.getInt(CoreAdminParams.BACKUP_ID, -1);
int lastNumBackupPointsToKeep = message.getInt(CoreAdminParams.MAX_NUM_BACKUP_POINTS, -1);
boolean purge = message.getBool(CoreAdminParams.BACKUP_PURGE_UNUSED, false);
if (backupId == -1 && lastNumBackupPointsToKeep == -1 && !purge) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, String.format(Locale.ROOT, "%s, %s or %s param must be provided", CoreAdminParams.BACKUP_ID, CoreAdminParams.MAX_NUM_BACKUP_POINTS,
CoreAdminParams.BACKUP_PURGE_UNUSED));
}
CoreContainer cc = ocmh.overseer.getCoreContainer();
try (BackupRepository repository = cc.newBackupRepository(Optional.ofNullable(repo))) {
URI location = repository.createDirectoryURI(backupLocation);
final URI backupPath = BackupFilePaths.buildExistingBackupLocationURI(repository, location, backupName);
if (repository.exists(repository.resolve(backupPath, BackupManager.TRADITIONAL_BACKUP_PROPS_FILE))) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "The backup name [" + backupName + "] at " +
"location [" + location + "] holds a non-incremental (legacy) backup, but " +
"backup-deletion is only supported on incremental backups");
}
if (purge) {
purge(repository, backupPath, results);
} else if (backupId != -1){
deleteBackupId(repository, backupPath, backupId, results);
} else {
keepNumberOfBackup(repository, backupPath, lastNumBackupPointsToKeep, results);
}
}
}
@SuppressWarnings({"unchecked"})
/**
* Clean up {@code backupPath} by removing all index files, shard-metadata files, and backup property files that are
* unreachable, uncompleted or corrupted.
*/
void purge(BackupRepository repository, URI backupPath, @SuppressWarnings({"rawtypes"}) NamedList result) throws IOException {
PurgeGraph purgeGraph = new PurgeGraph();
purgeGraph.build(repository, backupPath);
BackupFilePaths backupPaths = new BackupFilePaths(repository, backupPath);
repository.delete(backupPaths.getIndexDir(), purgeGraph.indexFileDeletes, true);
repository.delete(backupPaths.getShardBackupMetadataDir(), purgeGraph.shardBackupMetadataDeletes, true);
repository.delete(backupPath, purgeGraph.backupIdDeletes, true);
@SuppressWarnings({"rawtypes"})
NamedList details = new NamedList();
details.add("numBackupIds", purgeGraph.backupIdDeletes.size());
details.add("numShardBackupIds", purgeGraph.shardBackupMetadataDeletes.size());
details.add("numIndexFiles", purgeGraph.indexFileDeletes.size());
result.add("deleted", details);
}
/**
* Keep most recent {@code maxNumBackup} and delete the rest.
*/
void keepNumberOfBackup(BackupRepository repository, URI backupPath,
int maxNumBackup,
@SuppressWarnings({"rawtypes"}) NamedList results) throws Exception {
List<BackupId> backupIds = BackupFilePaths.findAllBackupIdsFromFileListing(repository.listAllOrEmpty(backupPath));
if (backupIds.size() <= maxNumBackup) {
return;
}
Collections.sort(backupIds);
List<BackupId> backupIdDeletes = backupIds.subList(0, backupIds.size() - maxNumBackup);
deleteBackupIds(backupPath, repository, new HashSet<>(backupIdDeletes), results);
}
void deleteBackupIds(URI backupUri, BackupRepository repository,
Set<BackupId> backupIdsDeletes,
@SuppressWarnings({"rawtypes"}) NamedList results) throws IOException {
BackupFilePaths incBackupFiles = new BackupFilePaths(repository, backupUri);
URI shardBackupMetadataDir = incBackupFiles.getShardBackupMetadataDir();
Set<String> referencedIndexFiles = new HashSet<>();
List<ShardBackupId> shardBackupIdFileDeletes = new ArrayList<>();
List<ShardBackupId> shardBackupIds = Arrays.stream(repository.listAllOrEmpty(shardBackupMetadataDir))
.map(sbi -> ShardBackupId.fromShardMetadataFilename(sbi))
.collect(Collectors.toList());
for (ShardBackupId shardBackupId : shardBackupIds) {
final BackupId backupId = shardBackupId.getContainingBackupId();
if (backupIdsDeletes.contains(backupId)) {
shardBackupIdFileDeletes.add(shardBackupId);
} else {
ShardBackupMetadata shardBackupMetadata = ShardBackupMetadata.from(repository, shardBackupMetadataDir, shardBackupId);
if (shardBackupMetadata != null)
referencedIndexFiles.addAll(shardBackupMetadata.listUniqueFileNames());
}
}
Map<BackupId, AggregateBackupStats> backupIdToCollectionBackupPoint = new HashMap<>();
List<String> unusedFiles = new ArrayList<>();
for (ShardBackupId shardBackupIdToDelete : shardBackupIdFileDeletes) {
BackupId backupId = shardBackupIdToDelete.getContainingBackupId();
ShardBackupMetadata shardBackupMetadata = ShardBackupMetadata.from(repository, shardBackupMetadataDir, shardBackupIdToDelete);
if (shardBackupMetadata == null)
continue;
backupIdToCollectionBackupPoint
.putIfAbsent(backupId, new AggregateBackupStats());
backupIdToCollectionBackupPoint.get(backupId).add(shardBackupMetadata);
for (String uniqueIndexFile : shardBackupMetadata.listUniqueFileNames()) {
if (!referencedIndexFiles.contains(uniqueIndexFile)) {
unusedFiles.add(uniqueIndexFile);
}
}
}
repository.delete(incBackupFiles.getShardBackupMetadataDir(),
shardBackupIdFileDeletes.stream().map(ShardBackupId::getBackupMetadataFilename).collect(Collectors.toList()), true);
repository.delete(incBackupFiles.getIndexDir(), unusedFiles, true);
try {
for (BackupId backupId : backupIdsDeletes) {
repository.deleteDirectory(repository.resolveDirectory(backupUri, BackupFilePaths.getZkStateDir(backupId)));
}
} catch (FileNotFoundException e) {
//ignore this
}
//add details to result before deleting backupPropFiles
addResult(backupUri, repository, backupIdsDeletes, backupIdToCollectionBackupPoint, results);
repository.delete(backupUri, backupIdsDeletes.stream().map(id -> BackupFilePaths.getBackupPropsName(id)).collect(Collectors.toList()), true);
}
@SuppressWarnings("unchecked")
private void addResult(URI backupPath, BackupRepository repository,
Set<BackupId> backupIdDeletes,
Map<BackupId, AggregateBackupStats> backupIdToCollectionBackupPoint,
@SuppressWarnings({"rawtypes"}) NamedList results) {
String collectionName = null;
@SuppressWarnings({"rawtypes"})
List<NamedList> shardBackupIdDetails = new ArrayList<>();
results.add("deleted", shardBackupIdDetails);
for (BackupId backupId : backupIdDeletes) {
NamedList<Object> backupIdResult = new NamedList<>();
try {
BackupProperties props = BackupProperties.readFrom(repository, backupPath, BackupFilePaths.getBackupPropsName(backupId));
backupIdResult.add(START_TIME_PROP, props.getStartTime());
if (collectionName == null) {
collectionName = props.getCollection();
results.add(COLLECTION_NAME_PROP, collectionName);
}
} catch (IOException e) {
//prop file not found
}
AggregateBackupStats cbp = backupIdToCollectionBackupPoint.getOrDefault(backupId, new AggregateBackupStats());
backupIdResult.add("backupId", backupId.getId());
backupIdResult.add("size", cbp.getTotalSize());
backupIdResult.add("numFiles", cbp.getNumFiles());
shardBackupIdDetails.add(backupIdResult);
}
}
private void deleteBackupId(BackupRepository repository, URI backupPath,
int bid, @SuppressWarnings({"rawtypes"}) NamedList results) throws Exception {
BackupId backupId = new BackupId(bid);
if (!repository.exists(repository.resolve(backupPath, BackupFilePaths.getBackupPropsName(backupId)))) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Backup ID [" + bid + "] not found; cannot be deleted");
}
deleteBackupIds(backupPath, repository, Collections.singleton(backupId), results);
}
final static class PurgeGraph {
// graph
Map<String, Node> backupIdNodeMap = new HashMap<>();
Map<String, Node> shardBackupMetadataNodeMap = new HashMap<>();
Map<String, Node> indexFileNodeMap = new HashMap<>();
// delete queues
List<String> backupIdDeletes = new ArrayList<>();
List<String> shardBackupMetadataDeletes = new ArrayList<>();
List<String> indexFileDeletes = new ArrayList<>();
public void build(BackupRepository repository, URI backupPath) throws IOException {
BackupFilePaths backupPaths = new BackupFilePaths(repository, backupPath);
buildLogicalGraph(repository, backupPaths);
findDeletableNodes(repository, backupPaths);
}
public void findDeletableNodes(BackupRepository repository, BackupFilePaths backupPaths) {
// mark nodes as existing
visitExistingNodes(repository.listAllOrEmpty(backupPaths.getShardBackupMetadataDir()),
shardBackupMetadataNodeMap, shardBackupMetadataDeletes);
// this may be a long running commands
visitExistingNodes(repository.listAllOrEmpty(backupPaths.getIndexDir()),
indexFileNodeMap, indexFileDeletes);
// TODO Is this propagation logic really necessary?
// The intention seems to be that if some index files are only referenced by a shard-metadata file that
// is itself orphaned, then propagating the "orphaned" status down to each of these index files will allow
// them to be deleted.
//
// But that doesn't seem to hold up under closer inspection.
//
// The graph is populated by following links out from the set of valid backup-id's. All files (shard-
// metadata, or index) that trace back to a valid backup-ID will appear in the graph. If a shard-metadata
// file becomes orphaned, it will be ignored during graph construction and any index files that it alone
// references will not appear in the graph. Since those index files will be unrepresented in the graph, the
// 'visitExistingNodes' calls above should be sufficient to detect them as orphaned.
//
// This all assumes though that propagation is intended to solve the scenario I think it does. If that
// assumption is wrong, then this whole comment is wrong.
// for nodes which are not existing, propagate that information to other nodes
shardBackupMetadataNodeMap.values().forEach(Node::propagateNotExisting);
indexFileNodeMap.values().forEach(Node::propagateNotExisting);
addDeleteNodesToQueue(backupIdNodeMap, backupIdDeletes);
addDeleteNodesToQueue(shardBackupMetadataNodeMap, shardBackupMetadataDeletes);
addDeleteNodesToQueue(indexFileNodeMap, indexFileDeletes);
}
/**
* Visiting files (nodes) actually present in physical layer,
* if it does not present in the {@code nodeMap}, it should be deleted by putting into the {@code deleteQueue}
*/
private void visitExistingNodes(String[] existingNodeKeys, Map<String, Node> nodeMap, List<String> deleteQueue) {
for (String nodeKey : existingNodeKeys) {
Node node = nodeMap.get(nodeKey);
if (node == null) {
deleteQueue.add(nodeKey);
} else {
node.existing = true;
}
}
}
private <T> void addDeleteNodesToQueue(Map<T, Node> tNodeMap, List<T> deleteQueue) {
tNodeMap.forEach((key, value) -> {
if (value.delete) {
deleteQueue.add(key);
}
});
}
Node getBackupIdNode(String backupPropsName) {
return backupIdNodeMap.computeIfAbsent(backupPropsName, bid -> {
Node node = new Node();
node.existing = true;
return node;
});
}
Node getShardBackupIdNode(String shardBackupId) {
return shardBackupMetadataNodeMap.computeIfAbsent(shardBackupId, s -> new Node());
}
Node getIndexFileNode(String indexFile) {
return indexFileNodeMap.computeIfAbsent(indexFile, s -> new IndexFileNode());
}
void addEdge(Node node1, Node node2) {
node1.addNeighbor(node2);
node2.addNeighbor(node1);
}
private void buildLogicalGraph(BackupRepository repository, BackupFilePaths backupPaths) throws IOException {
final URI baseBackupPath = backupPaths.getBackupLocation();
List<BackupId> backupIds = BackupFilePaths.findAllBackupIdsFromFileListing(repository.listAllOrEmpty(baseBackupPath));
for (BackupId backupId : backupIds) {
BackupProperties backupProps = BackupProperties.readFrom(repository, baseBackupPath,
BackupFilePaths.getBackupPropsName(backupId));
Node backupIdNode = getBackupIdNode(BackupFilePaths.getBackupPropsName(backupId));
for (String shardBackupMetadataFilename : backupProps.getAllShardBackupMetadataFiles()) {
Node shardBackupMetadataNode = getShardBackupIdNode(shardBackupMetadataFilename);
addEdge(backupIdNode, shardBackupMetadataNode);
ShardBackupMetadata shardBackupMetadata = ShardBackupMetadata.from(repository, backupPaths.getShardBackupMetadataDir(),
ShardBackupId.fromShardMetadataFilename(shardBackupMetadataFilename));
if (shardBackupMetadata == null)
continue;
for (String indexFile : shardBackupMetadata.listUniqueFileNames()) {
Node indexFileNode = getIndexFileNode(indexFile);
addEdge(indexFileNode, shardBackupMetadataNode);
}
}
}
}
}
//ShardBackupMetadata, BackupId
static class Node {
List<Node> neighbors;
boolean delete = false;
boolean existing = false;
void addNeighbor(Node node) {
if (neighbors == null) {
neighbors = new ArrayList<>();
}
neighbors.add(node);
}
void propagateNotExisting() {
if (existing)
return;
if (neighbors != null)
neighbors.forEach(Node::propagateDelete);
}
void propagateDelete() {
if (delete || !existing)
return;
delete = true;
if (neighbors != null) {
neighbors.forEach(Node::propagateDelete);
}
}
}
//IndexFile
final static class IndexFileNode extends Node {
int refCount = 0;
@Override
void addNeighbor(Node node) {
super.addNeighbor(node);
refCount++;
}
@Override
void propagateDelete() {
if (delete || !existing)
return;
refCount--;
if (refCount == 0) {
delete = true;
}
}
}
}