/*
* 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 java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandles;
import java.net.URL;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.SimpleFSDirectory;
import org.apache.lucene.util.TestUtil;
import org.apache.solr.SolrJettyTestBase;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.embedded.JettyConfig;
import org.apache.solr.client.solrj.embedded.JettySolrRunner;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.util.FileUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@SolrTestCaseJ4.SuppressSSL // Currently unknown why SSL does not work with this test
public class TestReplicationHandlerBackup extends SolrJettyTestBase {
JettySolrRunner masterJetty;
TestReplicationHandler.SolrInstance master = null;
SolrClient masterClient;
private static final String CONF_DIR = "solr" + File.separator + "collection1" + File.separator + "conf"
+ File.separator;
private static String context = "/solr";
boolean addNumberToKeepInRequest = true;
String backupKeepParamName = ReplicationHandler.NUMBER_BACKUPS_TO_KEEP_REQUEST_PARAM;
private static long docsSeed; // see indexDocs()
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static JettySolrRunner createJetty(TestReplicationHandler.SolrInstance instance) throws Exception {
FileUtils.copyFile(new File(SolrTestCaseJ4.TEST_HOME(), "solr.xml"), new File(instance.getHomeDir(), "solr.xml"));
Properties nodeProperties = new Properties();
nodeProperties.setProperty("solr.data.dir", instance.getDataDir());
JettyConfig jettyConfig = JettyConfig.builder().setContext("/solr").setPort(0).build();
JettySolrRunner jetty = new JettySolrRunner(instance.getHomeDir(), nodeProperties, jettyConfig);
jetty.start();
return jetty;
}
private static SolrClient createNewSolrClient(int port) {
try {
// setup the client...
final String baseUrl = buildUrl(port, context);
HttpSolrClient client = getHttpSolrClient(baseUrl);
client.setConnectionTimeout(15000);
client.setSoTimeout(60000);
return client;
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
@Before
public void setUp() throws Exception {
super.setUp();
String configFile = "solrconfig-master1.xml";
if(random().nextBoolean()) {
configFile = "solrconfig-master1-keepOneBackup.xml";
addNumberToKeepInRequest = false;
backupKeepParamName = ReplicationHandler.NUMBER_BACKUPS_TO_KEEP_INIT_PARAM;
}
master = new TestReplicationHandler.SolrInstance(createTempDir("solr-instance").toFile(), "master", null);
master.setUp();
master.copyConfigFile(CONF_DIR + configFile, "solrconfig.xml");
masterJetty = createJetty(master);
masterClient = createNewSolrClient(masterJetty.getLocalPort());
docsSeed = random().nextLong();
}
@Override
@After
public void tearDown() throws Exception {
super.tearDown();
masterClient.close();
masterClient = null;
masterJetty.stop();
masterJetty = null;
master = null;
}
@Test
public void testBackupOnCommit() throws Exception {
//Index
int nDocs = BackupRestoreUtils.indexDocs(masterClient, DEFAULT_TEST_COLLECTION_NAME, docsSeed);
//Confirm if completed
CheckBackupStatus checkBackupStatus = new CheckBackupStatus((HttpSolrClient) masterClient, DEFAULT_TEST_CORENAME);
while (!checkBackupStatus.success) {
checkBackupStatus.fetchStatus();
Thread.sleep(1000);
}
//Validate
try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get(master.getDataDir()), "snapshot*")) {
Path snapDir = stream.iterator().next();
verify(snapDir, nDocs);
}
}
private void verify(Path backup, int nDocs) throws IOException {
try (Directory dir = new SimpleFSDirectory(backup);
IndexReader reader = DirectoryReader.open(dir)) {
IndexSearcher searcher = new IndexSearcher(reader);
TopDocs hits = searcher.search(new MatchAllDocsQuery(), 1);
assertEquals(nDocs, hits.totalHits);
}
}
@Test
public void doTestBackup() throws Exception {
int nDocs = BackupRestoreUtils.indexDocs(masterClient, DEFAULT_TEST_COLLECTION_NAME, docsSeed);
//Confirm if completed
CheckBackupStatus checkBackupStatus = new CheckBackupStatus((HttpSolrClient) masterClient, DEFAULT_TEST_CORENAME);
while (!checkBackupStatus.success) {
checkBackupStatus.fetchStatus();
Thread.sleep(1000);
}
Path[] snapDir = new Path[5]; //One extra for the backup on commit
//First snapshot location
try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get(master.getDataDir()), "snapshot*")) {
snapDir[0] = stream.iterator().next();
}
boolean namedBackup = random().nextBoolean();
String firstBackupTimestamp = null;
String[] backupNames = null;
if (namedBackup) {
backupNames = new String[4];
}
for (int i = 0; i < 4; i++) {
final String backupName = TestUtil.randomSimpleString(random(), 1, 20);
if (!namedBackup) {
if (addNumberToKeepInRequest) {
runBackupCommand(masterJetty, ReplicationHandler.CMD_BACKUP, "&" + backupKeepParamName + "=2");
} else {
runBackupCommand(masterJetty, ReplicationHandler.CMD_BACKUP, "");
}
} else {
runBackupCommand(masterJetty, ReplicationHandler.CMD_BACKUP, "&name=" + backupName);
backupNames[i] = backupName;
}
checkBackupStatus = new CheckBackupStatus((HttpSolrClient) masterClient, DEFAULT_TEST_CORENAME, firstBackupTimestamp);
while (!checkBackupStatus.success) {
checkBackupStatus.fetchStatus();
Thread.sleep(1000);
}
if (i == 0) {
firstBackupTimestamp = checkBackupStatus.backupTimestamp;
}
if (!namedBackup) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get(master.getDataDir()), "snapshot*")) {
snapDir[i+1] = stream.iterator().next();
}
} else {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get(master.getDataDir()), "snapshot." + backupName)) {
snapDir[i+1] = stream.iterator().next();
}
}
verify(snapDir[i+1], nDocs);
}
//Test Deletion of named backup
if (namedBackup) {
testDeleteNamedBackup(backupNames);
} else {
//5 backups got created. 4 explicitly and one because a commit was called.
// Only the last two should still exist.
int count =0;
try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get(master.getDataDir()), "snapshot*")) {
Iterator<Path> iter = stream.iterator();
while (iter.hasNext()) {
iter.next();
count ++;
}
}
//There will be 2 backups, otherwise 1
if (backupKeepParamName.equals(ReplicationHandler.NUMBER_BACKUPS_TO_KEEP_REQUEST_PARAM)) {
assertEquals(2, count);
if (Files.exists(snapDir[0]) || Files.exists(snapDir[1]) || Files.exists(snapDir[2])) {
fail("Backup should have been cleaned up because " + backupKeepParamName + " was set to 2.");
}
} else {
assertEquals(1, count);
if (Files.exists(snapDir[0]) || Files.exists(snapDir[1]) || Files.exists(snapDir[2])
|| Files.exists(snapDir[3])) {
fail("Backup should have been cleaned up because " + backupKeepParamName + " was set to 1.");
}
}
}
}
private void testDeleteNamedBackup(String backupNames[]) throws InterruptedException, IOException {
String lastTimestamp = null;
for (int i = 0; i < 2; i++) {
runBackupCommand(masterJetty, ReplicationHandler.CMD_DELETE_BACKUP, "&name=" +backupNames[i]);
CheckDeleteBackupStatus checkDeleteBackupStatus = new CheckDeleteBackupStatus(backupNames[i], lastTimestamp);
while (true) {
boolean success = checkDeleteBackupStatus.fetchStatus();
if (success) {
lastTimestamp = checkDeleteBackupStatus.lastTimestamp;
if (i == 0) {
Thread.sleep(1000); //make the timestamp change
}
break;
}
Thread.sleep(200);
}
}
}
public static void runBackupCommand(JettySolrRunner masterJetty, String cmd, String params) throws IOException {
String masterUrl = buildUrl(masterJetty.getLocalPort(), context) + "/" + DEFAULT_TEST_CORENAME
+ ReplicationHandler.PATH+"?command=" + cmd + params;
InputStream stream = null;
try {
URL url = new URL(masterUrl);
stream = url.openStream();
stream.close();
} finally {
IOUtils.closeQuietly(stream);
}
}
private class CheckDeleteBackupStatus {
String response = null;
private String backupName;
final Pattern p = Pattern.compile("<str name=\"snapshotDeletedAt\">(.*?)</str>");
String lastTimestamp;
private CheckDeleteBackupStatus(String backupName, String lastTimestamp) {
this.backupName = backupName;
this.lastTimestamp = lastTimestamp;
}
public boolean fetchStatus() throws IOException {
String masterUrl = buildUrl(masterJetty.getLocalPort(), context) + "/" + DEFAULT_TEST_CORENAME + ReplicationHandler.PATH + "?command=" + ReplicationHandler.CMD_DETAILS;
URL url;
InputStream stream = null;
try {
url = new URL(masterUrl);
stream = url.openStream();
response = IOUtils.toString(stream, "UTF-8");
if(response.contains("<str name=\"status\">success</str>")) {
Matcher m = p.matcher(response);
if(m.find() && (lastTimestamp == null || !lastTimestamp.equals(m.group(1)))) {
lastTimestamp = m.group(1);
return true;
}
} else if(response.contains("<str name=\"status\">Unable to delete snapshot: " + backupName + "</str>" )) {
return false;
}
stream.close();
} finally {
IOUtils.closeQuietly(stream);
}
return false;
}
}
}