blob: dbf6557e50c33084535fada1d75d77f17ded54f4 [file] [log] [blame]
package org.apache.solr;
/**
* 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.
*/
import junit.framework.TestCase;
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.schema.TrieDateField;
import org.apache.solr.util.AbstractSolrTestCase;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.*;
/**
* Helper base class for distributed search test cases
*
* @since solr 1.5
*/
public abstract class BaseDistributedSearchTestCase extends SolrTestCaseJ4 {
public static Random r = random;
protected int shardCount = 4;
/**
* Sub classes can set this flag in their constructor to true if they
* want to fix the number of shards to 'shardCount'
*
* The default is false which means that test will be executed with
* 1, 2, 3, ....shardCount number of shards repeatedly
*/
protected boolean fixShardCount = false;
protected JettySolrRunner controlJetty;
protected List<SolrServer> clients = new ArrayList<SolrServer>();
protected List<JettySolrRunner> jettys = new ArrayList<JettySolrRunner>();
protected String context = "/solr";
protected String shards;
protected File testDir;
protected SolrServer controlClient;
// to stress with higher thread counts and requests, make sure the junit
// xml formatter is not being used (all output will be buffered before
// transformation to xml and cause an OOM exception).
protected int stress = 2;
protected boolean verifyStress = true;
protected int nThreads = 3;
public static int ORDERED = 1;
public static int SKIP = 2;
public static int SKIPVAL = 4;
public static int UNORDERED = 8;
protected int flags;
protected Map<String, Integer> handle = new HashMap<String, Integer>();
protected String id = "id";
public static Logger log = LoggerFactory.getLogger(BaseDistributedSearchTestCase.class);
public static RandVal rint = new RandVal() {
@Override
public Object val() {
return r.nextInt();
}
};
public static RandVal rlong = new RandVal() {
@Override
public Object val() {
return r.nextLong();
}
};
public static RandVal rfloat = new RandVal() {
@Override
public Object val() {
return r.nextFloat();
}
};
public static RandVal rdouble = new RandVal() {
@Override
public Object val() {
return r.nextDouble();
}
};
public static RandVal rdate = new RandDate();
/**
* Perform the actual tests here
*
* @throws Exception on error
*/
public abstract void doTest() throws Exception;
public static String[] fieldNames = new String[]{"n_ti1", "n_f1", "n_tf1", "n_d1", "n_td1", "n_l1", "n_tl1", "n_dt1", "n_tdt1"};
public static RandVal[] randVals = new RandVal[]{rint, rfloat, rfloat, rdouble, rdouble, rlong, rlong, rdate, rdate};
protected String[] getFieldNames() {
return fieldNames;
}
protected RandVal[] getRandValues() {
return randVals;
}
/**
* Subclasses can override this to change a test's solr home
* (default is in test-files)
*/
public String getSolrHome() {
return SolrTestCaseJ4.TEST_HOME();
}
@Override
public void setUp() throws Exception {
SolrTestCaseJ4.resetExceptionIgnores(); // ignore anything with ignore_exception in it
super.setUp();
System.setProperty("solr.test.sys.prop1", "propone");
System.setProperty("solr.test.sys.prop2", "proptwo");
System.setProperty("solr.solr.home", getSolrHome());
testDir = new File(TEMP_DIR,
getClass().getName() + "-" + System.currentTimeMillis());
testDir.mkdirs();
}
@Override
public void tearDown() throws Exception {
destroyServers();
if (!AbstractSolrTestCase.recurseDelete(testDir)) {
System.err.println("!!!! WARNING: best effort to remove " + testDir.getAbsolutePath() + " FAILED !!!!!");
}
super.tearDown();
}
private void createServers(int numShards) throws Exception {
controlJetty = createJetty(testDir, "control");
controlClient = createNewSolrServer(controlJetty.getLocalPort());
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= numShards; i++) {
if (sb.length() > 0) sb.append(',');
JettySolrRunner j = createJetty(testDir, "shard" + i);
jettys.add(j);
clients.add(createNewSolrServer(j.getLocalPort()));
sb.append("localhost:").append(j.getLocalPort()).append(context);
}
shards = sb.toString();
}
protected void destroyServers() throws Exception {
controlJetty.stop();
for (JettySolrRunner jetty : jettys) jetty.stop();
clients.clear();
jettys.clear();
}
public static JettySolrRunner createJetty(File baseDir, String dataDirName) throws Exception {
File subDir = new File(baseDir, dataDirName);
subDir.mkdirs();
System.setProperty("solr.data.dir", subDir.toString());
JettySolrRunner jetty = new JettySolrRunner("/solr", 0);
jetty.start();
return jetty;
}
protected SolrServer createNewSolrServer(int port) {
try {
// setup the server...
String url = "http://localhost:" + port + context;
CommonsHttpSolrServer s = new CommonsHttpSolrServer(url);
s.setConnectionTimeout(100); // 1/10th sec
s.setDefaultMaxConnectionsPerHost(100);
s.setMaxTotalConnections(100);
return s;
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
protected void addFields(SolrInputDocument doc, Object... fields) {
for (int i = 0; i < fields.length; i += 2) {
doc.addField((String) (fields[i]), fields[i + 1]);
}
}// add random fields to the documet before indexing
protected void indexr(Object... fields) throws Exception {
SolrInputDocument doc = new SolrInputDocument();
addFields(doc, fields);
addFields(doc, "rnd_b", true);
addFields(doc, getRandFields(getFieldNames(), getRandValues()));
indexDoc(doc);
}
protected void index(Object... fields) throws Exception {
SolrInputDocument doc = new SolrInputDocument();
addFields(doc, fields);
indexDoc(doc);
}
protected void indexDoc(SolrInputDocument doc) throws IOException, SolrServerException {
controlClient.add(doc);
int which = (doc.getField(id).toString().hashCode() & 0x7fffffff) % clients.size();
SolrServer client = clients.get(which);
client.add(doc);
}
protected void index_specific(int serverNumber, 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]);
}
controlClient.add(doc);
SolrServer client = clients.get(serverNumber);
client.add(doc);
}
protected void del(String q) throws Exception {
controlClient.deleteByQuery(q);
for (SolrServer client : clients) {
client.deleteByQuery(q);
}
}// serial commit...
protected void commit() throws Exception {
controlClient.commit();
for (SolrServer client : clients) client.commit();
}
protected QueryResponse queryServer(ModifiableSolrParams params) throws SolrServerException {
// query a random server
int which = r.nextInt(clients.size());
SolrServer client = clients.get(which);
QueryResponse rsp = client.query(params);
return rsp;
}
protected void query(Object... q) throws Exception {
final ModifiableSolrParams params = new ModifiableSolrParams();
params.add("reqid",Integer.toString(random.nextInt())); // just to help correlate top-level requests w/ sub requests
for (int i = 0; i < q.length; i += 2) {
params.add(q[i].toString(), q[i + 1].toString());
}
params.add("controlClient","true"); // just to enable easier sorting through log files
final QueryResponse controlRsp = controlClient.query(params);
params.remove("controlClient");
// query a random server
params.set("shards", shards);
QueryResponse rsp = queryServer(params);
compareResponses(rsp, controlRsp);
if (stress > 0) {
log.info("starting stress...");
Thread[] threads = new Thread[nThreads];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread() {
@Override
public void run() {
for (int j = 0; j < stress; j++) {
int which = r.nextInt(clients.size());
SolrServer client = clients.get(which);
try {
QueryResponse rsp = client.query(new ModifiableSolrParams(params));
if (verifyStress) {
compareResponses(rsp, controlRsp);
}
} catch (SolrServerException e) {
throw new RuntimeException(e);
}
}
}
};
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
}
}
public static boolean eq(String a, String b) {
return a == b || (a != null && a.equals(b));
}
public static int flags(Map<String, Integer> handle, Object key) {
if (handle == null) return 0;
Integer f = handle.get(key);
return f == null ? 0 : f;
}
public static String compare(NamedList a, NamedList b, int flags, Map<String, Integer> handle) {
boolean ordered = (flags & UNORDERED) == 0;
int posa = 0, posb = 0;
int aSkipped = 0, bSkipped = 0;
for (; ;) {
if (posa >= a.size() || posb >= b.size()) {
break;
}
String namea, nameb;
Object vala, valb = null;
int flagsa, flagsb;
for (; ;) {
namea = a.getName(posa);
vala = a.getVal(posa);
posa++;
flagsa = flags(handle, namea);
if ((flagsa & SKIP) != 0) {
aSkipped++;
continue;
}
break;
}
if (!ordered) posb = 0; // reset if not ordered
while (posb < b.size()) {
nameb = b.getName(posb);
valb = b.getVal(posb);
posb++;
flagsb = flags(handle, nameb);
if ((flagsb & SKIP) != 0) {
bSkipped++;
continue;
}
if (eq(namea, nameb)) {
break;
}
if (ordered) {
return err("." + namea + "!=" + nameb + " (unordered or missing)");
}
// if unordered, continue until we find the right field.
}
// ok, namea and nameb should be equal here already.
if ((flagsa & SKIPVAL) != 0) continue; // keys matching is enough
String cmp = compare(vala, valb, flagsa, handle);
if (cmp != null) return "." + namea + cmp;
}
if (a.size() - aSkipped != b.size() - bSkipped) {
return err(".size()==" + a.size() + "," + b.size() + "skipped=" + aSkipped + "," + bSkipped);
}
return null;
}
public static String compare1(Map a, Map b, int flags, Map<String, Integer> handle) {
String cmp;
for (Object keya : a.keySet()) {
Object vala = a.get(keya);
int flagsa = flags(handle, keya);
if ((flagsa & SKIP) != 0) continue;
if (!b.containsKey(keya)) {
return err("[" + keya + "]==null");
}
if ((flagsa & SKIPVAL) != 0) continue;
Object valb = b.get(keya);
cmp = compare(vala, valb, flagsa, handle);
if (cmp != null) return "[" + keya + "]" + cmp;
}
return null;
}
public static String compare(Map a, Map b, int flags, Map<String, Integer> handle) {
String cmp;
cmp = compare1(a, b, flags, handle);
if (cmp != null) return cmp;
return compare1(b, a, flags, handle);
}
public static String compare(SolrDocument a, SolrDocument b, int flags, Map<String, Integer> handle) {
return compare(a.getFieldValuesMap(), b.getFieldValuesMap(), flags, handle);
}
public static String compare(SolrDocumentList a, SolrDocumentList b, int flags, Map<String, Integer> handle) {
boolean ordered = (flags & UNORDERED) == 0;
String cmp;
int f = flags(handle, "maxScore");
if ((f & SKIPVAL) == 0) {
cmp = compare(a.getMaxScore(), b.getMaxScore(), 0, handle);
if (cmp != null) return ".maxScore" + cmp;
} else {
if (b.getMaxScore() != null) {
if (a.getMaxScore() == null) {
return err(".maxScore missing");
}
}
}
cmp = compare(a.getNumFound(), b.getNumFound(), 0, handle);
if (cmp != null) return ".numFound" + cmp;
cmp = compare(a.getStart(), b.getStart(), 0, handle);
if (cmp != null) return ".start" + cmp;
cmp = compare(a.size(), b.size(), 0, handle);
if (cmp != null) return ".size()" + cmp;
// only for completely ordered results (ties might be in a different order)
if (ordered) {
for (int i = 0; i < a.size(); i++) {
cmp = compare(a.get(i), b.get(i), 0, handle);
if (cmp != null) return "[" + i + "]" + cmp;
}
return null;
}
// unordered case
for (int i = 0; i < a.size(); i++) {
SolrDocument doc = a.get(i);
Object key = doc.getFirstValue("id");
SolrDocument docb = null;
if (key == null) {
// no id field to correlate... must compare ordered
docb = b.get(i);
} else {
for (int j = 0; j < b.size(); j++) {
docb = b.get(j);
if (key.equals(docb.getFirstValue("id"))) break;
}
}
// if (docb == null) return "[id="+key+"]";
cmp = compare(doc, docb, 0, handle);
if (cmp != null) return "[id=" + key + "]" + cmp;
}
return null;
}
public static String compare(Object[] a, Object[] b, int flags, Map<String, Integer> handle) {
if (a.length != b.length) {
return err(".length:" + a.length + "!=" + b.length);
}
for (int i = 0; i < a.length; i++) {
String cmp = compare(a[i], b[i], flags, handle);
if (cmp != null) return "[" + i + "]" + cmp;
}
return null;
}
public static String compare(Object a, Object b, int flags, Map<String, Integer> handle) {
if (a == b) return null;
if (a == null || b == null) return err(":" + a + "!=" + b);
if (a instanceof NamedList && b instanceof NamedList) {
return compare((NamedList) a, (NamedList) b, flags, handle);
}
if (a instanceof SolrDocumentList && b instanceof SolrDocumentList) {
return compare((SolrDocumentList) a, (SolrDocumentList) b, flags, handle);
}
if (a instanceof SolrDocument && b instanceof SolrDocument) {
return compare((SolrDocument) a, (SolrDocument) b, flags, handle);
}
if (a instanceof Map && b instanceof Map) {
return compare((Map) a, (Map) b, flags, handle);
}
if (a instanceof Object[] && b instanceof Object[]) {
return compare((Object[]) a, (Object[]) b, flags, handle);
}
if (a instanceof byte[] && b instanceof byte[]) {
if (!Arrays.equals((byte[]) a, (byte[]) b)) {
return err(":" + a + "!=" + b);
}
return null;
}
if (a instanceof List && b instanceof List) {
return compare(((List) a).toArray(), ((List) b).toArray(), flags, handle);
}
if (!(a.equals(b))) {
return err(":" + a + "!=" + b);
}
return null;
}
/** This method is called for root level comparison errors and can be helpful to set a
* breakpoint in to debug comparison failures.
*/
public static String err(String msg) {
return msg;
}
protected void compareResponses(QueryResponse a, QueryResponse b) {
String cmp;
cmp = compare(a.getResponse(), b.getResponse(), flags, handle);
if (cmp != null) {
log.info("Mismatched responses:\n" + a + "\n" + b);
TestCase.fail(cmp);
}
}
@Test
public void testDistribSearch() throws Exception {
if (fixShardCount) {
createServers(shardCount);
RandVal.uniqueValues = new HashSet(); //reset random values
doTest();
destroyServers();
} else {
for (int nServers = 1; nServers < shardCount; nServers++) {
createServers(nServers);
RandVal.uniqueValues = new HashSet(); //reset random values
doTest();
destroyServers();
}
}
}
public static Object[] getRandFields(String[] fields, RandVal[] randVals) {
Object[] o = new Object[fields.length * 2];
for (int i = 0; i < fields.length; i++) {
o[i * 2] = fields[i];
o[i * 2 + 1] = randVals[i].uval();
}
return o;
}
public static abstract class RandVal {
public static Random r = random;
public static Set uniqueValues = new HashSet();
public abstract Object val();
public Object uval() {
for (; ;) {
Object v = val();
if (uniqueValues.add(v)) return v;
}
}
}
public static class RandDate extends RandVal {
public static TrieDateField df = new TrieDateField();
@Override
public Object val() {
long v = r.nextLong();
Date d = new Date(v);
return df.toExternal(d);
}
}
}