blob: 81f5a1b69e538dd57d9546d9cd7aa0c92d80a9a1 [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.handler;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexCommit;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.lucene.util.TestUtil;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.params.CoreAdminParams;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.handler.admin.CoreAdminHandler;
import org.apache.solr.response.SolrQueryResponse;
import org.junit.After;
import org.junit.Before;
import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Arrays;
@LuceneTestCase.SuppressCodecs({"SimpleText"}) // Backups do checksum validation against a footer value not present in 'SimpleText'
public class TestSnapshotCoreBackup extends SolrTestCaseJ4 {
@Before // unique core per test
public void coreInit() throws Exception {
initCore("solrconfig.xml", "schema.xml");
}
@After // unique core per test
public void coreDestroy() throws Exception {
deleteCore();
}
public void testBackupWithDocsNotSearchable() throws Exception {
//See SOLR-11616 to see when this issue can be triggered
assertU(adoc("id", "1"));
assertU(commit());
assertU(adoc("id", "2"));
assertU(commit("openSearcher", "false"));
assertQ(req("q", "*:*"), "//result[@numFound='1']");
assertQ(req("q", "id:1"), "//result[@numFound='1']");
assertQ(req("q", "id:2"), "//result[@numFound='0']");
//call backup
String location = createTempDir().toFile().getAbsolutePath();
String snapshotName = TestUtil.randomSimpleString(random(), 1, 5);
final CoreContainer cores = h.getCoreContainer();
cores.getAllowPaths().add(Paths.get(location));
try (final CoreAdminHandler admin = new CoreAdminHandler(cores)) {
SolrQueryResponse resp = new SolrQueryResponse();
admin.handleRequestBody(req(CoreAdminParams.ACTION, CoreAdminParams.CoreAdminAction.BACKUPCORE.toString(),
"core", DEFAULT_TEST_COLLECTION_NAME, "name", snapshotName, "location",
location, CoreAdminParams.BACKUP_INCREMENTAL, "false"),
resp);
assertNull("Backup should have succeeded", resp.getException());
simpleBackupCheck(new File(location, "snapshot." + snapshotName), 2);
}
}
public void testBackupBeforeFirstCommit() throws Exception {
// even w/o a user sending any data, the SolrCore initialiation logic should have automatically created
// an "empty" commit point that can be backed up...
final IndexCommit empty = h.getCore().getDeletionPolicy().getLatestCommit();
assertNotNull(empty);
// white box sanity check that the commit point of the "reader" available from SolrIndexSearcher
// matches the commit point that IDPW claims is the "latest"
//
// this is important to ensure that backup/snapshot behavior is consistent with user expection
// when using typical commit + openSearcher
assertEquals(empty, h.getCore().withSearcher(s -> s.getIndexReader().getIndexCommit()));
assertEquals(1L, empty.getGeneration());
assertNotNull(empty.getSegmentsFileName());
final String initialEmptyIndexSegmentFileName = empty.getSegmentsFileName();
final CoreContainer cores = h.getCoreContainer();
final CoreAdminHandler admin = new CoreAdminHandler(cores);
final File backupDir = createTempDir().toFile();
cores.getAllowPaths().add(backupDir.toPath());
{ // first a backup before we've ever done *anything*...
SolrQueryResponse resp = new SolrQueryResponse();
admin.handleRequestBody
(req(CoreAdminParams.ACTION, CoreAdminParams.CoreAdminAction.BACKUPCORE.toString(),
"core", DEFAULT_TEST_COLLECTION_NAME,
"name", "empty_backup1",
"location", backupDir.getAbsolutePath(),
CoreAdminParams.BACKUP_INCREMENTAL, "false"),
resp);
assertNull("Backup should have succeeded", resp.getException());
simpleBackupCheck(new File(backupDir, "snapshot.empty_backup1"),
0, initialEmptyIndexSegmentFileName);
}
{ // Empty (named) snapshot..
SolrQueryResponse resp = new SolrQueryResponse();
admin.handleRequestBody
(req(CoreAdminParams.ACTION, CoreAdminParams.CoreAdminAction.CREATESNAPSHOT.toString(),
"core", DEFAULT_TEST_COLLECTION_NAME,
"commitName", "empty_snapshotA"),
resp);
assertNull("Snapshot A should have succeeded", resp.getException());
}
assertU(adoc("id", "1")); // uncommitted
{ // second backup w/uncommited docs
SolrQueryResponse resp = new SolrQueryResponse();
admin.handleRequestBody
(req(CoreAdminParams.ACTION, CoreAdminParams.CoreAdminAction.BACKUPCORE.toString(),
"core", DEFAULT_TEST_COLLECTION_NAME,
"name", "empty_backup2",
"location", backupDir.getAbsolutePath(),
CoreAdminParams.BACKUP_INCREMENTAL, "false"),
resp);
assertNull("Backup should have succeeded", resp.getException());
simpleBackupCheck(new File(backupDir, "snapshot.empty_backup2"),
0, initialEmptyIndexSegmentFileName);
}
{ // Second empty (named) snapshot..
SolrQueryResponse resp = new SolrQueryResponse();
admin.handleRequestBody
(req(CoreAdminParams.ACTION, CoreAdminParams.CoreAdminAction.CREATESNAPSHOT.toString(),
"core", DEFAULT_TEST_COLLECTION_NAME,
"commitName", "empty_snapshotB"),
resp);
assertNull("Snapshot A should have succeeded", resp.getException());
}
// Committing the doc now should not affect the existing backups or snapshots...
assertU(commit());
for (String name : Arrays.asList("empty_backup1", "empty_backup2")) {
simpleBackupCheck(new File(backupDir, "snapshot." + name ),
0, initialEmptyIndexSegmentFileName);
}
// Make backups from each of the snapshots and check they are still empty as well...
for (String snapName : Arrays.asList("empty_snapshotA", "empty_snapshotB")) {
String name = "empty_backup_from_" + snapName;
SolrQueryResponse resp = new SolrQueryResponse();
admin.handleRequestBody
(req(CoreAdminParams.ACTION, CoreAdminParams.CoreAdminAction.BACKUPCORE.toString(),
"core", DEFAULT_TEST_COLLECTION_NAME,
"name", name,
"commitName", snapName,
"location", backupDir.getAbsolutePath(),
CoreAdminParams.BACKUP_INCREMENTAL, "false"),
resp);
assertNull("Backup "+name+" should have succeeded", resp.getException());
simpleBackupCheck(new File(backupDir, "snapshot." + name),
0, initialEmptyIndexSegmentFileName);
}
admin.close();
}
/**
* Tests that a softCommit does not affect what data is in a backup
*/
public void testBackupAfterSoftCommit() throws Exception {
// sanity check empty index...
assertQ(req("q", "id:42"), "//result[@numFound='0']");
assertQ(req("q", "id:99"), "//result[@numFound='0']");
assertQ(req("q", "*:*"), "//result[@numFound='0']");
// hard commit one doc...
assertU(adoc("id", "99"));
assertU(commit());
assertQ(req("q", "id:99"), "//result[@numFound='1']");
assertQ(req("q", "*:*"), "//result[@numFound='1']");
final IndexCommit oneDocCommit = h.getCore().getDeletionPolicy().getLatestCommit();
assertNotNull(oneDocCommit);
final String oneDocSegmentFile = oneDocCommit.getSegmentsFileName();
final CoreContainer cores = h.getCoreContainer();
final CoreAdminHandler admin = new CoreAdminHandler(cores);
final File backupDir = createTempDir().toFile();
cores.getAllowPaths().add(backupDir.toPath());
{ // take an initial 'backup1a' containing our 1 document
final SolrQueryResponse resp = new SolrQueryResponse();
admin.handleRequestBody
(req(CoreAdminParams.ACTION, CoreAdminParams.CoreAdminAction.BACKUPCORE.toString(),
"core", DEFAULT_TEST_COLLECTION_NAME,
"name", "backup1a",
"location", backupDir.getAbsolutePath(),
CoreAdminParams.BACKUP_INCREMENTAL, "false"),
resp);
assertNull("Backup should have succeeded", resp.getException());
simpleBackupCheck(new File(backupDir, "snapshot.backup1a"),
1, oneDocSegmentFile);
}
{ // and an initial "snapshot1a' that should eventually match
SolrQueryResponse resp = new SolrQueryResponse();
admin.handleRequestBody
(req(CoreAdminParams.ACTION, CoreAdminParams.CoreAdminAction.CREATESNAPSHOT.toString(),
"core", DEFAULT_TEST_COLLECTION_NAME,
"commitName", "snapshot1a"),
resp);
assertNull("Snapshot 1A should have succeeded", resp.getException());
}
// now we add our 2nd doc, and make it searchable, but we do *NOT* hard commit it to the index dir...
assertU(adoc("id", "42"));
assertU(commit("softCommit", "true", "openSearcher", "true"));
assertQ(req("q", "id:99"), "//result[@numFound='1']");
assertQ(req("q", "id:42"), "//result[@numFound='1']");
assertQ(req("q", "*:*"), "//result[@numFound='2']");
{ // we now have an index with two searchable docs, but a new 'backup1b' should still
// be identical to the previous backup...
final SolrQueryResponse resp = new SolrQueryResponse();
admin.handleRequestBody
(req(CoreAdminParams.ACTION, CoreAdminParams.CoreAdminAction.BACKUPCORE.toString(),
"core", DEFAULT_TEST_COLLECTION_NAME,
"name", "backup1b",
"location", backupDir.getAbsolutePath(),
CoreAdminParams.BACKUP_INCREMENTAL, "false"),
resp);
assertNull("Backup should have succeeded", resp.getException());
simpleBackupCheck(new File(backupDir, "snapshot.backup1b"),
1, oneDocSegmentFile);
}
{ // and a second "snapshot1b' should also still be identical
SolrQueryResponse resp = new SolrQueryResponse();
admin.handleRequestBody
(req(CoreAdminParams.ACTION, CoreAdminParams.CoreAdminAction.CREATESNAPSHOT.toString(),
"core", DEFAULT_TEST_COLLECTION_NAME,
"commitName", "snapshot1b"),
resp);
assertNull("Snapshot 1B should have succeeded", resp.getException());
}
// Hard Committing the 2nd doc now should not affect the existing backups or snapshots...
assertU(commit());
for (String name : Arrays.asList("backup1a", "backup1b")) {
simpleBackupCheck(new File(backupDir, "snapshot." + name ),
1, oneDocSegmentFile);
}
{ // But we should be able to confirm both docs appear in a new backup (not based on a previous snapshot)
final SolrQueryResponse resp = new SolrQueryResponse();
admin.handleRequestBody
(req(CoreAdminParams.ACTION, CoreAdminParams.CoreAdminAction.BACKUPCORE.toString(),
"core", DEFAULT_TEST_COLLECTION_NAME,
"name", "backup2",
"location", backupDir.getAbsolutePath(),
CoreAdminParams.BACKUP_INCREMENTAL, "false"),
resp);
assertNull("Backup should have succeeded", resp.getException());
simpleBackupCheck(new File(backupDir, "snapshot.backup2"), 2);
}
// if we go back and create backups from our earlier snapshots they should still only
// have 1 expected doc...
// Make backups from each of the snapshots and check they are still empty as well...
for (String snapName : Arrays.asList("snapshot1a", "snapshot1b")) {
String name = "backup_from_" + snapName;
SolrQueryResponse resp = new SolrQueryResponse();
admin.handleRequestBody
(req(CoreAdminParams.ACTION, CoreAdminParams.CoreAdminAction.BACKUPCORE.toString(),
"core", DEFAULT_TEST_COLLECTION_NAME,
"name", name,
"commitName", snapName,
"location", backupDir.getAbsolutePath(),
CoreAdminParams.BACKUP_INCREMENTAL, "false"),
resp);
assertNull("Backup "+name+" should have succeeded", resp.getException());
simpleBackupCheck(new File(backupDir, "snapshot." + name),
1, oneDocSegmentFile);
}
admin.close();
}
/**
* A simple sanity check that asserts the current weird behavior of DirectoryReader.openIfChanged()
* and demos how 'softCommit' can cause the IndexReader in use by SolrIndexSearcher to missrepresent what
* commit is "current". So Backup code should only ever "trust" the IndexCommit info available from the
* IndexDeletionPolicyWrapper
*
* @see <a href="https://issues.apache.org/jira/browse/LUCENE-9040">LUCENE-9040</a>
* @see <a href="https://issues.apache.org/jira/browse/SOLR-13909">SOLR-13909</a>
*/
public void testDemoWhyBackupCodeShouldNeverUseIndexCommitFromSearcher() throws Exception {
final long EXPECTED_GEN_OF_EMPTY_INDEX = 1L;
// sanity check this is an empty index...
assertQ(req("q", "*:*"), "//result[@numFound='0']");
// sanity check what the searcher/reader of this empty index report about current commit
final IndexCommit empty = h.getCore().withSearcher(s -> {
// sanity check we are empty...
assertEquals(0L, (long) s.getIndexReader().numDocs());
// sanity check this is the initial commit..
final IndexCommit commit = s.getIndexReader().getIndexCommit();
assertEquals(EXPECTED_GEN_OF_EMPTY_INDEX, commit.getGeneration());
return commit;
});
// now let's add & soft commit 1 doc...
assertU(adoc("id", "42"));
assertU(commit("softCommit", "true", "openSearcher", "true"));
// verify it's "searchable" ...
assertQ(req("q", "id:42"), "//result[@numFound='1']");
// sanity check what the searcher/reader of this empty index report about current commit
IndexCommit oneDoc = h.getCore().withSearcher(s -> {
// sanity check this really is the searcher/reader that has the new doc...
assertEquals(1L, (long) s.getIndexReader().numDocs());
final IndexCommit commit = s.getIndexReader().getIndexCommit();
// WTF: how/why does this reader still have the same commit generation as before ? ? ? ? ?
assertEquals("WTF: This Reader (claims) the same generation as our previous pre-softCommif (empty) reader",
EXPECTED_GEN_OF_EMPTY_INDEX, commit.getGeneration());
return commit;
});
assertEquals("WTF: Our two IndexCommits, which we know have different docs, claim to be equals",
empty, oneDoc);
}
/**
* Simple check that the backup exists, is a valid index, and contains the expected number of docs
*/
private static void simpleBackupCheck(final File backup, final int numDocs) throws IOException {
simpleBackupCheck(backup, numDocs, null);
}
/**
* Simple check that the backup exists, is a valid index, and contains the expected number of docs.
* If expectedSegmentsFileName is non null then confirms that file exists in the bakup dir
* <em>and</em> that it is reported as the current segment file when opening a reader on that backup.
*/
private static void simpleBackupCheck(final File backup, final int numDocs,
final String expectedSegmentsFileName) throws IOException {
assertNotNull(backup);
assertTrue("Backup doesn't exist" + backup.toString(), backup.exists());
if (null != expectedSegmentsFileName) {
assertTrue(expectedSegmentsFileName + " doesn't exist in " + backup.toString(),
new File(backup, expectedSegmentsFileName).exists());
}
try (Directory dir = FSDirectory.open(backup.toPath())) {
TestUtil.checkIndex(dir, true, true, true, null);
try (DirectoryReader r = DirectoryReader.open(dir)) {
assertEquals("numDocs in " + backup.toString(),
numDocs, r.numDocs());
if (null != expectedSegmentsFileName) {
assertEquals("segmentsFile of IndexCommit for: " + backup.toString(),
expectedSegmentsFileName, r.getIndexCommit().getSegmentsFileName());
}
}
}
}
}