blob: 6ea7e3ae77500cc1e2d0c2b9fc5b00c66d0bf111 [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.util;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import com.carrotsearch.randomizedtesting.rules.SystemPropertiesRestoreRule;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.util.ExecutorUtil;
import org.apache.solr.common.util.SolrNamedThreadFactory;
import org.apache.solr.core.PluginInfo;
import org.apache.solr.search.QueryParsing;
import org.apache.solr.util.circuitbreaker.CPUCircuitBreaker;
import org.apache.solr.util.circuitbreaker.CircuitBreaker;
import org.apache.solr.util.circuitbreaker.CircuitBreakerManager;
import org.apache.solr.util.circuitbreaker.MemoryCircuitBreaker;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;
import static org.hamcrest.CoreMatchers.containsString;
@SuppressWarnings({"rawtypes"})
public class TestCircuitBreaker extends SolrTestCaseJ4 {
private final static int NUM_DOCS = 20;
@Rule
public TestRule solrTestRules = RuleChain.outerRule(new SystemPropertiesRestoreRule());
@BeforeClass
public static void setUpClass() throws Exception {
System.setProperty("filterCache.enabled", "false");
System.setProperty("queryResultCache.enabled", "false");
System.setProperty("documentCache.enabled", "true");
initCore("solrconfig-memory-circuitbreaker.xml", "schema.xml");
for (int i = 0 ; i < NUM_DOCS ; i ++) {
assertU(adoc("name", "john smith", "id", "1"));
assertU(adoc("name", "johathon smith", "id", "2"));
assertU(adoc("name", "john percival smith", "id", "3"));
assertU(adoc("id", "1", "title", "this is a title.", "inStock_b1", "true"));
assertU(adoc("id", "2", "title", "this is another title.", "inStock_b1", "true"));
assertU(adoc("id", "3", "title", "Mary had a little lamb.", "inStock_b1", "false"));
//commit inside the loop to get multiple segments to make search as realistic as possible
assertU(commit());
}
}
@Override
public void tearDown() throws Exception {
super.tearDown();
}
@After
public void after() {
h.getCore().getCircuitBreakerManager().deregisterAll();
}
public void testCBAlwaysTrips() {
HashMap<String, String> args = new HashMap<String, String>();
args.put(QueryParsing.DEFTYPE, CircuitBreaker.NAME);
args.put(CommonParams.FL, "id");
removeAllExistingCircuitBreakers();
PluginInfo pluginInfo = h.getCore().getSolrConfig().getPluginInfo(CircuitBreakerManager.class.getName());
CircuitBreaker.CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerManager.buildCBConfig(pluginInfo);
CircuitBreaker circuitBreaker = new MockCircuitBreaker(circuitBreakerConfig);
h.getCore().getCircuitBreakerManager().register(circuitBreaker);
expectThrows(SolrException.class, () -> {
h.query(req("name:\"john smith\""));
});
}
public void testCBFakeMemoryPressure() {
HashMap<String, String> args = new HashMap<String, String>();
args.put(QueryParsing.DEFTYPE, CircuitBreaker.NAME);
args.put(CommonParams.FL, "id");
removeAllExistingCircuitBreakers();
PluginInfo pluginInfo = h.getCore().getSolrConfig().getPluginInfo(CircuitBreakerManager.class.getName());
CircuitBreaker.CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerManager.buildCBConfig(pluginInfo);
CircuitBreaker circuitBreaker = new FakeMemoryPressureCircuitBreaker(circuitBreakerConfig);
h.getCore().getCircuitBreakerManager().register(circuitBreaker);
expectThrows(SolrException.class, () -> {
h.query(req("name:\"john smith\""));
});
}
public void testBuildingMemoryPressure() {
ExecutorService executor = ExecutorUtil.newMDCAwareCachedThreadPool(
new SolrNamedThreadFactory("TestCircuitBreaker"));
HashMap<String, String> args = new HashMap<String, String>();
args.put(QueryParsing.DEFTYPE, CircuitBreaker.NAME);
args.put(CommonParams.FL, "id");
AtomicInteger failureCount = new AtomicInteger();
try {
removeAllExistingCircuitBreakers();
PluginInfo pluginInfo = h.getCore().getSolrConfig().getPluginInfo(CircuitBreakerManager.class.getName());
CircuitBreaker.CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerManager.buildCBConfig(pluginInfo);
CircuitBreaker circuitBreaker = new BuildingUpMemoryPressureCircuitBreaker(circuitBreakerConfig);
h.getCore().getCircuitBreakerManager().register(circuitBreaker);
List<Future<?>> futures = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Future<?> future = executor.submit(() -> {
try {
h.query(req("name:\"john smith\""));
} catch (SolrException e) {
assertThat(e.getMessage(), containsString("Circuit Breakers tripped"));
failureCount.incrementAndGet();
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
});
futures.add(future);
}
for (Future<?> future : futures) {
try {
future.get();
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
executor.shutdown();
try {
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e.getMessage());
}
assertEquals("Number of failed queries is not correct", 1, failureCount.get());
} finally {
if (!executor.isShutdown()) {
executor.shutdown();
}
}
}
public void testFakeCPUCircuitBreaker() {
AtomicInteger failureCount = new AtomicInteger();
ExecutorService executor = ExecutorUtil.newMDCAwareCachedThreadPool(
new SolrNamedThreadFactory("TestCircuitBreaker"));
try {
removeAllExistingCircuitBreakers();
PluginInfo pluginInfo = h.getCore().getSolrConfig().getPluginInfo(CircuitBreakerManager.class.getName());
CircuitBreaker.CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerManager.buildCBConfig(pluginInfo);
CircuitBreaker circuitBreaker = new FakeCPUCircuitBreaker(circuitBreakerConfig);
h.getCore().getCircuitBreakerManager().register(circuitBreaker);
List<Future<?>> futures = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Future<?> future = executor.submit(() -> {
try {
h.query(req("name:\"john smith\""));
} catch (SolrException e) {
assertThat(e.getMessage(), containsString("Circuit Breakers tripped"));
failureCount.incrementAndGet();
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
});
futures.add(future);
}
for (Future<?> future : futures) {
try {
future.get();
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
executor.shutdown();
try {
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e.getMessage());
}
assertEquals("Number of failed queries is not correct",5, failureCount.get());
} finally {
if (!executor.isShutdown()) {
executor.shutdown();
}
}
}
public void testResponseWithCBTiming() {
assertQ(req("q", "*:*", CommonParams.DEBUG_QUERY, "true"),
"//str[@name='rawquerystring']='*:*'",
"//str[@name='querystring']='*:*'",
"//str[@name='parsedquery']='MatchAllDocsQuery(*:*)'",
"//str[@name='parsedquery_toString']='*:*'",
"count(//lst[@name='explain']/*)=3",
"//lst[@name='explain']/str[@name='1']",
"//lst[@name='explain']/str[@name='2']",
"//lst[@name='explain']/str[@name='3']",
"//str[@name='QParser']",
"count(//lst[@name='timing']/*)=4",
"//lst[@name='timing']/double[@name='time']",
"count(//lst[@name='circuitbreaker']/*)>0",
"//lst[@name='circuitbreaker']/double[@name='time']",
"count(//lst[@name='prepare']/*)>0",
"//lst[@name='prepare']/double[@name='time']",
"count(//lst[@name='process']/*)>0",
"//lst[@name='process']/double[@name='time']"
);
}
private void removeAllExistingCircuitBreakers() {
List<CircuitBreaker> registeredCircuitBreakers = h.getCore().getCircuitBreakerManager().getRegisteredCircuitBreakers();
registeredCircuitBreakers.clear();
}
private static class MockCircuitBreaker extends MemoryCircuitBreaker {
public MockCircuitBreaker(CircuitBreakerConfig config) {
super(config);
}
@Override
public boolean isTripped() {
// Always return true
return true;
}
@Override
public String getDebugInfo() {
return "MockCircuitBreaker";
}
}
private static class FakeMemoryPressureCircuitBreaker extends MemoryCircuitBreaker {
public FakeMemoryPressureCircuitBreaker(CircuitBreakerConfig config) {
super(config);
}
@Override
protected long calculateLiveMemoryUsage() {
// Return a number large enough to trigger a pushback from the circuit breaker
return Long.MAX_VALUE;
}
}
private class BuildingUpMemoryPressureCircuitBreaker extends MemoryCircuitBreaker {
private AtomicInteger count;
public BuildingUpMemoryPressureCircuitBreaker(CircuitBreakerConfig config) {
super(config);
this.count = new AtomicInteger(0);
}
@Override
protected long calculateLiveMemoryUsage() {
if (count.getAndIncrement() >= 4) {
return Long.MAX_VALUE;
}
return 5; // Random number guaranteed to not trip the circuit breaker
}
}
private static class FakeCPUCircuitBreaker extends CPUCircuitBreaker {
public FakeCPUCircuitBreaker(CircuitBreakerConfig config) {
super(config);
}
@Override
protected double calculateLiveCPUUsage() {
return 92; // Return a value large enough to trigger the circuit breaker
}
}
}