blob: 37454b50b94fb9ab7e1d4031d6dabf2838cdda88 [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.core.snapshots;
import java.lang.invoke.MethodHandles;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexCommit;
import org.apache.lucene.index.IndexNotFoundException;
import org.apache.lucene.store.SimpleFSDirectory;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.lucene.util.LuceneTestCase.Slow;
import org.apache.lucene.util.TestUtil;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.request.CoreAdminRequest.CreateSnapshot;
import org.apache.solr.client.solrj.request.CoreAdminRequest.DeleteSnapshot;
import org.apache.solr.client.solrj.request.CoreAdminRequest.ListSnapshots;
import org.apache.solr.cloud.SolrCloudTestCase;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.cloud.DocCollection;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.cloud.Slice;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.params.CoreAdminParams.CoreAdminAction;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.snapshots.SolrSnapshotMetaDataManager.SnapshotMetaData;
import org.apache.solr.handler.BackupRestoreUtils;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Tests for index backing up and restoring index snapshots.
*
* These tests use the deprecated "full-snapshot" based backup method. For tests that cover similar snapshot
* functionality incrementally, see {@link org.apache.solr.handler.TestIncrementalCoreBackup}
*/
@LuceneTestCase.SuppressCodecs({"SimpleText"}) // Backups do checksum validation against a footer value not present in 'SimpleText'
@SolrTestCaseJ4.SuppressSSL // Currently unknown why SSL does not work with this test
@Slow
public class TestSolrCoreSnapshots extends SolrCloudTestCase {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static long docsSeed; // see indexDocs()
@BeforeClass
public static void setupClass() throws Exception {
System.setProperty("solr.allowPaths", "*");
useFactory("solr.StandardDirectoryFactory");
configureCluster(1)// nodes
.addConfig("conf1", TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf"))
.configure();
docsSeed = random().nextLong();
}
@AfterClass
public static void teardownClass() throws Exception {
System.clearProperty("test.build.data");
System.clearProperty("test.cache.data");
System.clearProperty("solr.allowPaths");
}
@Test
public void testBackupRestore() throws Exception {
CloudSolrClient solrClient = cluster.getSolrClient();
String collectionName = "SolrCoreSnapshots";
CollectionAdminRequest.Create create = CollectionAdminRequest.createCollection(collectionName, "conf1", 1, 1);
create.process(solrClient);
String location = createTempDir().toFile().getAbsolutePath();
int nDocs = BackupRestoreUtils.indexDocs(cluster.getSolrClient(), collectionName, docsSeed);
DocCollection collectionState = solrClient.getZkStateReader().getClusterState().getCollection(collectionName);
assertEquals(1, collectionState.getActiveSlices().size());
Slice shard = collectionState.getActiveSlices().iterator().next();
assertEquals(1, shard.getReplicas().size());
Replica replica = shard.getReplicas().iterator().next();
String replicaBaseUrl = replica.getBaseUrl();
String coreName = replica.getCoreName();
String backupName = TestUtil.randomSimpleString(random(), 1, 5);
String commitName = TestUtil.randomSimpleString(random(), 1, 5);
String duplicateName = commitName.concat("_duplicate");
try (
SolrClient adminClient = getHttpSolrClient(cluster.getJettySolrRunners().get(0).getBaseUrl().toString());
SolrClient leaderClient = getHttpSolrClient(replica.getCoreUrl())) {
SnapshotMetaData metaData = createSnapshot(adminClient, coreName, commitName);
// Create another snapshot referring to the same index commit to verify the
// reference counting implementation during snapshot deletion.
SnapshotMetaData duplicateCommit = createSnapshot(adminClient, coreName, duplicateName);
assertEquals (metaData.getIndexDirPath(), duplicateCommit.getIndexDirPath());
assertEquals (metaData.getGenerationNumber(), duplicateCommit.getGenerationNumber());
// Delete all documents
leaderClient.deleteByQuery("*:*");
leaderClient.commit();
BackupRestoreUtils.verifyDocs(0, cluster.getSolrClient(), collectionName);
// Verify that the index directory contains at least 2 index commits - one referred by the snapshots
// and the other containing document deletions.
{
List<IndexCommit> commits = listCommits(metaData.getIndexDirPath());
assertTrue(commits.size() >= 2);
}
// Backup the earlier created snapshot.
{
Map<String,String> params = new HashMap<>();
params.put("name", backupName);
params.put("commitName", commitName);
params.put("location", location);
params.put("incremental", "false");
BackupRestoreUtils.runCoreAdminCommand(replicaBaseUrl, coreName, CoreAdminAction.BACKUPCORE.toString(), params);
}
// Restore the backup
{
Map<String,String> params = new HashMap<>();
params.put("name", "snapshot." + backupName);
params.put("location", location);
BackupRestoreUtils.runCoreAdminCommand(replicaBaseUrl, coreName, CoreAdminAction.RESTORECORE.toString(), params);
BackupRestoreUtils.verifyDocs(nDocs, cluster.getSolrClient(), collectionName);
}
// Verify that the old index directory (before restore) contains only those index commits referred by snapshots.
// The IndexWriter (used to cleanup index files) creates an additional commit during closing. Hence we expect 2 commits (instead
// of 1).
{
List<IndexCommit> commits = listCommits(metaData.getIndexDirPath());
assertEquals(2, commits.size());
assertEquals(metaData.getGenerationNumber(), commits.get(0).getGeneration());
}
// Delete first snapshot
deleteSnapshot(adminClient, coreName, commitName);
// Verify that corresponding index files have NOT been deleted (due to reference counting).
assertFalse(listCommits(metaData.getIndexDirPath()).isEmpty());
// Delete second snapshot
deleteSnapshot(adminClient, coreName, duplicateCommit.getName());
// Verify that corresponding index files have been deleted. Ideally this directory should
// be removed immediately. But the current DirectoryFactory impl waits until the
// closing the core (or the directoryFactory) for actual removal. Since the IndexWriter
// (used to cleanup index files) creates an additional commit during closing, we expect a single
// commit (instead of 0).
assertEquals(1, listCommits(duplicateCommit.getIndexDirPath()).size());
}
}
@Test
public void testIndexOptimization() throws Exception {
CloudSolrClient solrClient = cluster.getSolrClient();
String collectionName = "SolrCoreSnapshots_IndexOptimization";
CollectionAdminRequest.Create create = CollectionAdminRequest.createCollection(collectionName, "conf1", 1, 1);
create.process(solrClient);
int nDocs = BackupRestoreUtils.indexDocs(cluster.getSolrClient(), collectionName, docsSeed);
DocCollection collectionState = solrClient.getZkStateReader().getClusterState().getCollection(collectionName);
assertEquals(1, collectionState.getActiveSlices().size());
Slice shard = collectionState.getActiveSlices().iterator().next();
assertEquals(1, shard.getReplicas().size());
Replica replica = shard.getReplicas().iterator().next();
String coreName = replica.getStr(ZkStateReader.CORE_NAME_PROP);
String commitName = TestUtil.randomSimpleString(random(), 1, 5);
try (
SolrClient adminClient = getHttpSolrClient(cluster.getJettySolrRunners().get(0).getBaseUrl().toString());
SolrClient leaderClient = getHttpSolrClient(replica.getCoreUrl())) {
SnapshotMetaData metaData = createSnapshot(adminClient, coreName, commitName);
int numTests = nDocs > 0 ? TestUtil.nextInt(random(), 1, 5) : 1;
for (int attempt=0; attempt<numTests; attempt++) {
//Modify existing index before we call optimize.
if (nDocs > 0) {
//Delete a few docs
int numDeletes = TestUtil.nextInt(random(), 1, nDocs);
for(int i=0; i<numDeletes; i++) {
leaderClient.deleteByQuery("id:" + i);
}
//Add a few more
int moreAdds = TestUtil.nextInt(random(), 1, 100);
for (int i=0; i<moreAdds; i++) {
SolrInputDocument doc = new SolrInputDocument();
doc.addField("id", i + nDocs);
doc.addField("name", "name = " + (i + nDocs));
leaderClient.add(doc);
}
leaderClient.commit();
}
}
// Before invoking optimize command, verify that the index directory contains multiple commits (including the one we snapshotted earlier).
{
Collection<IndexCommit> commits = listCommits(metaData.getIndexDirPath());
// Verify that multiple index commits are stored in this directory.
assertTrue(commits.size() > 0);
// Verify that the snapshot commit is present in this directory.
assertTrue(commits.stream().filter(x -> x.getGeneration() == metaData.getGenerationNumber()).findFirst().isPresent());
}
// Optimize the index.
leaderClient.optimize(true, true, 1);
// After invoking optimize command, verify that the index directory contains multiple commits (including the one we snapshotted earlier).
{
List<IndexCommit> commits = listCommits(metaData.getIndexDirPath());
// Verify that multiple index commits are stored in this directory.
assertTrue(commits.size() > 1);
// Verify that the snapshot commit is present in this directory.
assertTrue(commits.stream().filter(x -> x.getGeneration() == metaData.getGenerationNumber()).findFirst().isPresent());
}
// Delete the snapshot
deleteSnapshot(adminClient, coreName, metaData.getName());
// Add few documents. Without this the optimize command below does not take effect.
{
int moreAdds = TestUtil.nextInt(random(), 1, 100);
for (int i=0; i<moreAdds; i++) {
SolrInputDocument doc = new SolrInputDocument();
doc.addField("id", i + nDocs);
doc.addField("name", "name = " + (i + nDocs));
leaderClient.add(doc);
}
leaderClient.commit();
}
// Optimize the index.
leaderClient.optimize(true, true, 1);
// Verify that the index directory contains only 1 index commit (which is not the same as the snapshotted commit).
Collection<IndexCommit> commits = listCommits(metaData.getIndexDirPath());
assertTrue(commits.size() == 1);
assertFalse(commits.stream().filter(x -> x.getGeneration() == metaData.getGenerationNumber()).findFirst().isPresent());
}
}
private SnapshotMetaData createSnapshot (SolrClient adminClient, String coreName, String commitName) throws Exception {
CreateSnapshot req = new CreateSnapshot(commitName);
req.setCoreName(coreName);
adminClient.request(req);
Collection<SnapshotMetaData> snapshots = listSnapshots(adminClient, coreName);
Optional<SnapshotMetaData> metaData = snapshots.stream().filter(x -> commitName.equals(x.getName())).findFirst();
assertTrue(metaData.isPresent());
return metaData.get();
}
private void deleteSnapshot(SolrClient adminClient, String coreName, String commitName) throws Exception {
DeleteSnapshot req = new DeleteSnapshot(commitName);
req.setCoreName(coreName);
adminClient.request(req);
Collection<SnapshotMetaData> snapshots = listSnapshots(adminClient, coreName);
assertFalse(snapshots.stream().filter(x -> commitName.equals(x.getName())).findFirst().isPresent());
}
private Collection<SnapshotMetaData> listSnapshots(SolrClient adminClient, String coreName) throws Exception {
ListSnapshots req = new ListSnapshots();
req.setCoreName(coreName);
@SuppressWarnings({"rawtypes"})
NamedList resp = adminClient.request(req);
assertTrue( resp.get("snapshots") instanceof NamedList );
@SuppressWarnings({"rawtypes"})
NamedList apiResult = (NamedList) resp.get("snapshots");
List<SnapshotMetaData> result = new ArrayList<>(apiResult.size());
for(int i = 0 ; i < apiResult.size(); i++) {
String commitName = apiResult.getName(i);
String indexDirPath = (String)((NamedList)apiResult.get(commitName)).get("indexDirPath");
long genNumber = Long.parseLong((String)((NamedList)apiResult.get(commitName)).get("generation"));
result.add(new SnapshotMetaData(commitName, indexDirPath, genNumber));
}
return result;
}
private List<IndexCommit> listCommits(String directory) throws Exception {
SimpleFSDirectory dir = new SimpleFSDirectory(Paths.get(directory));
try {
return DirectoryReader.listCommits(dir);
} catch (IndexNotFoundException ex) {
// This can happen when the delete snapshot functionality cleans up the index files (when the directory
// storing these files is not the *current* index directory).
return Collections.emptyList();
}
}
}