/**
* 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.commons.io.IOUtils;
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.solr.SolrTestCaseJ4;
import org.apache.solr.TestDistributedSearch;
import org.apache.solr.client.solrj.SolrServer;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.embedded.JettySolrRunner;
import org.apache.solr.client.solrj.impl.CommonsHttpSolrServer;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.util.AbstractSolrTestCase;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.*;
import java.io.*;
import java.net.URL;
/**
* Test for ReplicationHandler
*
* @version $Id: TestReplicationHandler.java 981690 2010-08-02 21:33:19Z yonik $
* @since 1.4
*/
public class TestReplicationHandler extends SolrTestCaseJ4 {
private static final String CONF_DIR = "." + File.separator + "solr" + File.separator + "conf" + File.separator;
private static final String SLAVE_CONFIG = CONF_DIR + "solrconfig-slave.xml";
static JettySolrRunner masterJetty, slaveJetty;
static SolrServer masterClient, slaveClient;
static SolrInstance master = null, slave = null;
static String context = "/solr";
// number of docs to index... decremented for each test case to tell if we accidentally reuse
// index from previous test method
static int nDocs = 500;
@BeforeClass
public static void beforeClass() throws Exception {
master = new SolrInstance("master", null);
master.setUp();
masterJetty = createJetty(master);
masterClient = createNewSolrServer(masterJetty.getLocalPort());
slave = new SolrInstance("slave", masterJetty.getLocalPort());
slave.setUp();
slaveJetty = createJetty(slave);
slaveClient = createNewSolrServer(slaveJetty.getLocalPort());
}
public void clearIndexWithReplication() throws Exception {
NamedList res = query("*:*", masterClient);
SolrDocumentList docs = (SolrDocumentList)res.get("response");
if (docs.getNumFound() != 0) {
masterClient.deleteByQuery("*:*");
masterClient.commit();
// wait for replication to sync
res = rQuery(0, "*:*", slaveClient);
assertEquals(0, ((SolrDocumentList) res.get("response")).getNumFound());
}
}
@AfterClass
public static void afterClass() throws Exception {
masterJetty.stop();
slaveJetty.stop();
master.tearDown();
slave.tearDown();
}
private static JettySolrRunner createJetty(SolrInstance instance) throws Exception {
System.setProperty("solr.solr.home", instance.getHomeDir());
System.setProperty("solr.data.dir", instance.getDataDir());
JettySolrRunner jetty = new JettySolrRunner("/solr", 0);
jetty.start();
return jetty;
}
private static SolrServer createNewSolrServer(int port) {
try {
// setup the server...
String url = "http://localhost:" + port + context;
CommonsHttpSolrServer s = new CommonsHttpSolrServer(url);
s.setDefaultMaxConnectionsPerHost(100);
s.setMaxTotalConnections(100);
return s;
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
int index(SolrServer s, Object... fields) throws Exception {
SolrInputDocument doc = new SolrInputDocument();
for (int i = 0; i < fields.length; i += 2) {
doc.addField((String) (fields[i]), fields[i + 1]);
}
return s.add(doc).getStatus();
}
NamedList query(String query, SolrServer s) throws SolrServerException {
NamedList res = new SimpleOrderedMap();
ModifiableSolrParams params = new ModifiableSolrParams();
params.add("q", query);
QueryResponse qres = s.query(params);
res = qres.getResponse();
return res;
}
/** will sleep up to 30 seconds, looking for expectedDocCount */
private NamedList rQuery(int expectedDocCount, String query, SolrServer server) throws Exception {
int timeSlept = 0;
NamedList res = null;
SolrDocumentList docList = null;
do {
res = query(query, server);
docList = (SolrDocumentList) res.get("response");
timeSlept += 100;
Thread.sleep(100);
} while(docList.getNumFound() != expectedDocCount && timeSlept < 30000);
return res;
}
@Test
public void testReplicateAfterWrite2Slave() throws Exception {
clearIndexWithReplication();
nDocs--;
for (int i = 0; i < nDocs; i++) {
index(masterClient, "id", i, "name", "name = " + i);
}
String masterUrl = "http://localhost:" + masterJetty.getLocalPort() + "/solr/replication?command=disableReplication";
URL url = new URL(masterUrl);
InputStream stream = url.openStream();
try {
stream.close();
} catch (IOException e) {
//e.printStackTrace();
}
masterClient.commit();
NamedList masterQueryRsp = rQuery(nDocs, "*:*", masterClient);
SolrDocumentList masterQueryResult = (SolrDocumentList) masterQueryRsp.get("response");
assertEquals(nDocs, masterQueryResult.getNumFound());
// Make sure that both the index version and index generation on the slave is
// higher than that of the master, just to make the test harder.
index(slaveClient, "id", 551, "name", "name = " + 551);
slaveClient.commit(true, true);
index(slaveClient, "id", 552, "name", "name = " + 552);
slaveClient.commit(true, true);
index(slaveClient, "id", 553, "name", "name = " + 553);
slaveClient.commit(true, true);
index(slaveClient, "id", 554, "name", "name = " + 554);
slaveClient.commit(true, true);
index(slaveClient, "id", 555, "name", "name = " + 555);
slaveClient.commit(true, true);
//this doc is added to slave so it should show an item w/ that result
SolrDocumentList slaveQueryResult = null;
NamedList slaveQueryRsp;
slaveQueryRsp = rQuery(1, "id:555", slaveClient);
slaveQueryResult = (SolrDocumentList) slaveQueryRsp.get("response");
assertEquals(1, slaveQueryResult.getNumFound());
masterUrl = "http://localhost:" + masterJetty.getLocalPort() + "/solr/replication?command=enableReplication";
url = new URL(masterUrl);
stream = url.openStream();
try {
stream.close();
} catch (IOException e) {
//e.printStackTrace();
}
//the slave should have done a full copy of the index so the doc with id:555 should not be there in the slave now
slaveQueryRsp = rQuery(0, "id:555", slaveClient);
slaveQueryResult = (SolrDocumentList) slaveQueryRsp.get("response");
assertEquals(0, slaveQueryResult.getNumFound());
// make sure we replicated the correct index from the master
slaveQueryRsp = rQuery(nDocs, "*:*", slaveClient);
slaveQueryResult = (SolrDocumentList) slaveQueryRsp.get("response");
assertEquals(nDocs, slaveQueryResult.getNumFound());
}
@Test
public void testIndexAndConfigReplication() throws Exception {
clearIndexWithReplication();
nDocs--;
for (int i = 0; i < nDocs; i++)
index(masterClient, "id", i, "name", "name = " + i);
masterClient.commit();
NamedList masterQueryRsp = rQuery(nDocs, "*:*", masterClient);
SolrDocumentList masterQueryResult = (SolrDocumentList) masterQueryRsp.get("response");
assertEquals(nDocs, masterQueryResult.getNumFound());
//get docs from slave and check if number is equal to master
NamedList slaveQueryRsp = rQuery(nDocs, "*:*", slaveClient);
SolrDocumentList slaveQueryResult = (SolrDocumentList) slaveQueryRsp.get("response");
assertEquals(nDocs, slaveQueryResult.getNumFound());
//compare results
String cmp = TestDistributedSearch.compare(masterQueryResult, slaveQueryResult, 0, null);
assertEquals(null, cmp);
//start config files replication test
masterClient.deleteByQuery("*:*");
masterClient.commit();
//change the schema on master
copyFile(new File(CONF_DIR + "schema-replication2.xml"), new File(master.getConfDir(), "schema.xml"));
masterJetty.stop();
masterJetty = createJetty(master);
masterClient = createNewSolrServer(masterJetty.getLocalPort());
copyFile(new File(SLAVE_CONFIG), new File(slave.getConfDir(), "solrconfig.xml"), masterJetty.getLocalPort());
slaveJetty.stop();
slaveJetty = createJetty(slave);
slaveClient = createNewSolrServer(slaveJetty.getLocalPort());
//add a doc with new field and commit on master to trigger snappull from slave.
index(masterClient, "id", "2000", "name", "name = " + 2000, "newname", "newname = " + 2000);
masterClient.commit();
NamedList masterQueryRsp2 = rQuery(1, "*:*", masterClient);
SolrDocumentList masterQueryResult2 = (SolrDocumentList) masterQueryRsp2.get("response");
assertEquals(1, masterQueryResult2.getNumFound());
slaveQueryRsp = rQuery(1, "*:*", slaveClient);
SolrDocument d = ((SolrDocumentList) slaveQueryRsp.get("response")).get(0);
assertEquals("newname = 2000", (String) d.getFieldValue("newname"));
}
@Test
public void testStopPoll() throws Exception {
clearIndexWithReplication();
// Test:
// setup master/slave.
// stop polling on slave, add a doc to master and verify slave hasn't picked it.
nDocs--;
for (int i = 0; i < nDocs; i++)
index(masterClient, "id", i, "name", "name = " + i);
masterClient.commit();
NamedList masterQueryRsp = rQuery(nDocs, "*:*", masterClient);
SolrDocumentList masterQueryResult = (SolrDocumentList) masterQueryRsp.get("response");
assertEquals(nDocs, masterQueryResult.getNumFound());
//get docs from slave and check if number is equal to master
NamedList slaveQueryRsp = rQuery(nDocs, "*:*", slaveClient);
SolrDocumentList slaveQueryResult = (SolrDocumentList) slaveQueryRsp.get("response");
assertEquals(nDocs, slaveQueryResult.getNumFound());
//compare results
String cmp = TestDistributedSearch.compare(masterQueryResult, slaveQueryResult, 0, null);
assertEquals(null, cmp);
// start stop polling test
String slaveURL = "http://localhost:" + slaveJetty.getLocalPort() + "/solr/replication?command=disablepoll";
URL url = new URL(slaveURL);
InputStream stream = url.openStream();
try {
stream.close();
} catch (IOException e) {
//e.printStackTrace();
}
index(masterClient, "id", 501, "name", "name = " + 501);
masterClient.commit();
//get docs from master and check if number is equal to master
masterQueryRsp = rQuery(nDocs+1, "*:*", masterClient);
masterQueryResult = (SolrDocumentList) masterQueryRsp.get("response");
assertEquals(nDocs+1, masterQueryResult.getNumFound());
// NOTE: this test is wierd, we want to verify it DOESNT replicate...
// for now, add a sleep for this.., but the logic is wierd.
Thread.sleep(3000);
//get docs from slave and check if number is not equal to master; polling is disabled
slaveQueryRsp = rQuery(nDocs, "*:*", slaveClient);
slaveQueryResult = (SolrDocumentList) slaveQueryRsp.get("response");
assertEquals(nDocs, slaveQueryResult.getNumFound());
// re-enable replication
slaveURL = "http://localhost:" + slaveJetty.getLocalPort() + "/solr/replication?command=enablepoll";
url = new URL(slaveURL);
stream = url.openStream();
try {
stream.close();
} catch (IOException e) {
//e.printStackTrace();
}
slaveQueryRsp = rQuery(nDocs+1, "*:*", slaveClient);
slaveQueryResult = (SolrDocumentList) slaveQueryRsp.get("response");
assertEquals(nDocs+1, slaveQueryResult.getNumFound());
}
@Test
public void testSnapPullWithMasterUrl() throws Exception {
//change solrconfig on slave
//this has no entry for pollinginterval
copyFile(new File(CONF_DIR + "solrconfig-slave1.xml"), new File(slave.getConfDir(), "solrconfig.xml"), masterJetty.getLocalPort());
slaveJetty.stop();
slaveJetty = createJetty(slave);
slaveClient = createNewSolrServer(slaveJetty.getLocalPort());
masterClient.deleteByQuery("*:*");
nDocs--;
for (int i = 0; i < nDocs; i++)
index(masterClient, "id", i, "name", "name = " + i);
masterClient.commit();
NamedList masterQueryRsp = rQuery(nDocs, "*:*", masterClient);
SolrDocumentList masterQueryResult = (SolrDocumentList) masterQueryRsp.get("response");
assertEquals(nDocs, masterQueryResult.getNumFound());
// snappull
String masterUrl = "http://localhost:" + slaveJetty.getLocalPort() + "/solr/replication?command=fetchindex&masterUrl=";
masterUrl += "http://localhost:" + masterJetty.getLocalPort() + "/solr/replication";
URL url = new URL(masterUrl);
InputStream stream = url.openStream();
try {
stream.close();
} catch (IOException e) {
//e.printStackTrace();
}
//get docs from slave and check if number is equal to master
NamedList slaveQueryRsp = rQuery(nDocs, "*:*", slaveClient);
SolrDocumentList slaveQueryResult = (SolrDocumentList) slaveQueryRsp.get("response");
assertEquals(nDocs, slaveQueryResult.getNumFound());
//compare results
String cmp = TestDistributedSearch.compare(masterQueryResult, slaveQueryResult, 0, null);
assertEquals(null, cmp);
// NOTE: at this point, the slave is not polling any more
// restore it.
copyFile(new File(CONF_DIR + "solrconfig-slave.xml"), new File(slave.getConfDir(), "solrconfig.xml"), masterJetty.getLocalPort());
slaveJetty.stop();
slaveJetty = createJetty(slave);
slaveClient = createNewSolrServer(slaveJetty.getLocalPort());
}
@Test
public void testReplicateAfterStartup() throws Exception {
//stop slave
slaveJetty.stop();
nDocs--;
masterClient.deleteByQuery("*:*");
for (int i = 0; i < nDocs; i++)
index(masterClient, "id", i, "name", "name = " + i);
masterClient.commit();
NamedList masterQueryRsp = rQuery(nDocs, "*:*", masterClient);
SolrDocumentList masterQueryResult = (SolrDocumentList) masterQueryRsp.get("response");
assertEquals(nDocs, masterQueryResult.getNumFound());
//change solrconfig having 'replicateAfter startup' option on master
copyFile(new File(CONF_DIR + "solrconfig-master2.xml"),
new File(master.getConfDir(), "solrconfig.xml"));
masterJetty.stop();
masterJetty = createJetty(master);
masterClient = createNewSolrServer(masterJetty.getLocalPort());
copyFile(new File(SLAVE_CONFIG), new File(slave.getConfDir(), "solrconfig.xml"), masterJetty.getLocalPort());
//start slave
slaveJetty = createJetty(slave);
slaveClient = createNewSolrServer(slaveJetty.getLocalPort());
//get docs from slave and check if number is equal to master
NamedList slaveQueryRsp = rQuery(nDocs, "*:*", slaveClient);
SolrDocumentList slaveQueryResult = (SolrDocumentList) slaveQueryRsp.get("response");
assertEquals(nDocs, slaveQueryResult.getNumFound());
//compare results
String cmp = TestDistributedSearch.compare(masterQueryResult, slaveQueryResult, 0, null);
assertEquals(null, cmp);
// NOTE: the master only replicates after startup now!
// revert that change.
copyFile(new File(CONF_DIR + "solrconfig-master.xml"), new File(master.getConfDir(), "solrconfig.xml"));
masterJetty.stop();
masterJetty = createJetty(master);
masterClient = createNewSolrServer(masterJetty.getLocalPort());
copyFile(new File(SLAVE_CONFIG), new File(slave.getConfDir(), "solrconfig.xml"), masterJetty.getLocalPort());
//start slave
slaveJetty = createJetty(slave);
slaveClient = createNewSolrServer(slaveJetty.getLocalPort());
}
@Test
public void testIndexAndConfigAliasReplication() throws Exception {
clearIndexWithReplication();
nDocs--;
for (int i = 0; i < nDocs; i++)
index(masterClient, "id", i, "name", "name = " + i);
masterClient.commit();
NamedList masterQueryRsp = rQuery(nDocs, "*:*", masterClient);
SolrDocumentList masterQueryResult = (SolrDocumentList) masterQueryRsp.get("response");
assertEquals(nDocs, masterQueryResult.getNumFound());
//get docs from slave and check if number is equal to master
NamedList slaveQueryRsp = rQuery(nDocs, "*:*", slaveClient);
SolrDocumentList slaveQueryResult = (SolrDocumentList) slaveQueryRsp.get("response");
assertEquals(nDocs, slaveQueryResult.getNumFound());
//compare results
String cmp = TestDistributedSearch.compare(masterQueryResult, slaveQueryResult, 0, null);
assertEquals(null, cmp);
//start config files replication test
//clear master index
masterClient.deleteByQuery("*:*");
masterClient.commit();
//change solrconfig on master
copyFile(new File(CONF_DIR + "solrconfig-master1.xml"), new File(master.getConfDir(), "solrconfig.xml"));
//change schema on master
copyFile(new File(CONF_DIR + "schema-replication2.xml"), new File(master.getConfDir(), "schema.xml"));
//keep a copy of the new schema
copyFile(new File(CONF_DIR + "schema-replication2.xml"), new File(master.getConfDir(), "schema-replication2.xml"));
masterJetty.stop();
masterJetty = createJetty(master);
masterClient = createNewSolrServer(masterJetty.getLocalPort());
copyFile(new File(SLAVE_CONFIG), new File(slave.getConfDir(), "solrconfig.xml"), masterJetty.getLocalPort());
slaveJetty.stop();
slaveJetty = createJetty(slave);
slaveClient = createNewSolrServer(slaveJetty.getLocalPort());
//add a doc with new field and commit on master to trigger snappull from slave.
index(masterClient, "id", "2000", "name", "name = " + 2000, "newname", "newname = " + 2000);
masterClient.commit();
NamedList masterQueryRsp2 = rQuery(1, "*:*", masterClient);
SolrDocumentList masterQueryResult2 = (SolrDocumentList) masterQueryRsp2.get("response");
assertEquals(1, masterQueryResult2.getNumFound());
NamedList slaveQueryRsp2 = rQuery(1, "*:*", slaveClient);
SolrDocumentList slaveQueryResult2 = (SolrDocumentList) slaveQueryRsp2.get("response");
assertEquals(1, slaveQueryResult2.getNumFound());
index(slaveClient, "id", "2000", "name", "name = " + 2001, "newname", "newname = " + 2001);
slaveClient.commit();
slaveQueryRsp = rQuery(1, "*:*", slaveClient);
SolrDocument d = ((SolrDocumentList) slaveQueryRsp.get("response")).get(0);
assertEquals("newname = 2001", (String) d.getFieldValue("newname"));
}
@Test
public void testBackup() throws Exception {
masterJetty.stop();
copyFile(new File(CONF_DIR + "solrconfig-master1.xml"), new File(master.getConfDir(), "solrconfig.xml"));
masterJetty = createJetty(master);
masterClient = createNewSolrServer(masterJetty.getLocalPort());
nDocs--;
masterClient.deleteByQuery("*:*");
for (int i = 0; i < nDocs; i++)
index(masterClient, "id", i, "name", "name = " + i);
masterClient.commit();
class BackupThread extends Thread {
volatile String fail = null;
public void run() {
String masterUrl = "http://localhost:" + masterJetty.getLocalPort() + "/solr/replication?command=" + ReplicationHandler.CMD_BACKUP;
URL url;
InputStream stream = null;
try {
url = new URL(masterUrl);
stream = url.openStream();
stream.close();
} catch (Exception e) {
fail = e.getMessage();
} finally {
IOUtils.closeQuietly(stream);
}
};
};
BackupThread backupThread = new BackupThread();
backupThread.start();
File dataDir = new File(master.getDataDir());
class CheckStatus extends Thread {
volatile String fail = null;
volatile String response = null;
volatile boolean success = false;
public void run() {
String masterUrl = "http://localhost:" + masterJetty.getLocalPort() + "/solr/replication?command=" + ReplicationHandler.CMD_DETAILS;
URL url;
InputStream stream = null;
try {
url = new URL(masterUrl);
stream = url.openStream();
response = IOUtils.toString(stream);
if(response.contains("<str name=\"status\">success</str>")) {
success = true;
}
stream.close();
} catch (Exception e) {
fail = e.getMessage();
} finally {
IOUtils.closeQuietly(stream);
}
};
};
int waitCnt = 0;
CheckStatus checkStatus = new CheckStatus();
while(true) {
checkStatus.run();
if(checkStatus.fail != null) {
fail(checkStatus.fail);
}
if(checkStatus.success) {
break;
}
Thread.sleep(200);
if(waitCnt == 10) {
fail("Backup success not detected:" + checkStatus.response);
}
waitCnt++;
}
if(backupThread.fail != null) {
fail(backupThread.fail);
}
File[] files = dataDir.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
if(name.startsWith("snapshot")) {
return true;
}
return false;
}
});
assertEquals(1, files.length);
File snapDir = files[0];
Directory dir = new SimpleFSDirectory(snapDir.getAbsoluteFile());
IndexSearcher searcher = new IndexSearcher(dir, true);
TopDocs hits = searcher.search(new MatchAllDocsQuery(), 1);
assertEquals(nDocs, hits.totalHits);
searcher.close();
dir.close();
}
/* character copy of file using UTF-8 */
private static void copyFile(File src, File dst) throws IOException {
copyFile(src, dst, null);
}
/**
* character copy of file using UTF-8. If port is non-null, will be substituted any time "TEST_PORT" is found.
*/
private static void copyFile(File src, File dst, Integer port) throws IOException {
BufferedReader in = new BufferedReader(new FileReader(src));
Writer out = new FileWriter(dst);
for (String line = in.readLine(); null != line; line = in.readLine()) {
if (null != port)
line = line.replace("TEST_PORT", port.toString());
out.write(line);
}
in.close();
out.close();
}
private static class SolrInstance {
String name;
Integer masterPort;
File homeDir;
File confDir;
File dataDir;
/**
* if masterPort is null, this instance is a master -- otherwise this instance is a slave, and assumes the master is
* on localhost at the specified port.
*/
public SolrInstance(String name, Integer port) {
this.name = name;
this.masterPort = port;
}
public String getHomeDir() {
return homeDir.toString();
}
public String getSchemaFile() {
return CONF_DIR + "schema-replication1.xml";
}
public String getConfDir() {
return confDir.toString();
}
public String getDataDir() {
return dataDir.toString();
}
public String getSolrConfigFile() {
String fname = "";
if (null == masterPort)
fname = CONF_DIR + "solrconfig-master.xml";
else
fname = SLAVE_CONFIG;
return fname;
}
public void setUp() throws Exception {
System.setProperty("solr.test.sys.prop1", "propone");
System.setProperty("solr.test.sys.prop2", "proptwo");
File home = new File(TEMP_DIR,
getClass().getName() + "-" + System.currentTimeMillis());
if (null == masterPort) {
homeDir = new File(home, "master");
dataDir = new File(homeDir, "data");
confDir = new File(homeDir, "conf");
} else {
homeDir = new File(home, "slave");
dataDir = new File(homeDir, "data");
confDir = new File(homeDir, "conf");
}
homeDir.mkdirs();
dataDir.mkdirs();
confDir.mkdirs();
File f = new File(confDir, "solrconfig.xml");
copyFile(new File(getSolrConfigFile()), f, masterPort);
f = new File(confDir, "schema.xml");
copyFile(new File(getSchemaFile()), f);
}
public void tearDown() throws Exception {
AbstractSolrTestCase.recurseDelete(homeDir);
}
}
}