/* * Copyright 2013 Cloudera Inc. * * Licensed 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.client.solrj.retry; import java.io.File; import java.io.IOException; import java.util.concurrent.TimeUnit; import org.apache.solr.SolrTestCaseJ4; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrRequest; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.beans.DocumentObjectBinder; import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer; import org.apache.solr.common.SolrException; import org.apache.solr.common.SolrException.ErrorCode; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.common.util.NamedList; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class RetryingSolrServerTest extends SolrTestCaseJ4 { private SolrClient solrServer; private static final String RESOURCES_DIR = "target" + File.separator + "test-classes"; private static final String DEFAULT_BASE_DIR = "solr"; private static final Logger LOG = LoggerFactory.getLogger(RetryingSolrServerTest.class); @BeforeClass public static void beforeClass() throws Exception { initCore( "solrconfig.xml", "schema.xml", RESOURCES_DIR + File.separator + DEFAULT_BASE_DIR ); } @Before public void setUp() throws Exception { super.setUp(); solrServer = new EmbeddedSolrServer(h.getCoreContainer(), h.getCore().getName()); } @Test public void testGoodRequestNeedsNoRetriesWithNonNullMetricsFacade() throws Exception { testGoodRequestNeedsNoRetries(getMetricsFacade()); } @Test public void testGoodRequestNeedsNoRetriesWithNullMetricsFacade() throws Exception { testGoodRequestNeedsNoRetries(null); } private void testGoodRequestNeedsNoRetries(MetricsFacade metricsFacade) throws Exception { CountingSolrServer countingSolrServer = new CountingSolrServer(solrServer); SolrClient solr = new RetryingSolrServer(countingSolrServer, getNoRetryPolicyFactory(), metricsFacade); SolrQuery query = getDefaultQuery(); solr.query(query); Assert.assertEquals(1, countingSolrServer.getNumRequests()); } @Test public void testNoRetries() throws Exception { SolrQuery query = getDefaultQuery(); solrServer.query(query); FailingSolrServer failingSolrServer = new FailingSolrServer(solrServer); SolrClient solr = new RetryingSolrServer(failingSolrServer, getNoRetryPolicyFactory(), getMetricsFacade()); try { solr.query(query); fail(); } catch (RetriesExhaustedException e) { assertTrue(e.getCause() instanceof FailingSolrServer.InjectedSolrServerException); Assert.assertEquals(1, failingSolrServer.getNumRequests()); Assert.assertEquals(1, failingSolrServer.getNumInjectedFailures()); } } @Test public void testRetryTwice() throws Exception { SolrQuery query = getDefaultQuery(); solrServer.query(query); FailingSolrServer failingSolrServer = new FailingSolrServer(solrServer); SolrClient solr = new RetryingSolrServer(failingSolrServer, getRetryTwicePolicyFactory(), getMetricsFacade()); try { solr.query(query); fail(); } catch (RetriesExhaustedException e) { Assert.assertTrue(e.getCause() instanceof FailingSolrServer.InjectedSolrServerException); Assert.assertEquals(3, failingSolrServer.getNumRequests()); Assert.assertEquals(3, failingSolrServer.getNumInjectedFailures()); LOG.info("RetriesExhaustedException.getMessage(): " + e.getMessage()); } } @Test public void testRetries() throws Exception { new DefaultRetryPolicyFactory(); SolrQuery query = getDefaultQuery(); FailingSolrServer failingSolrServer = new FailingSolrServer(solrServer); SolrClient solr = new RetryingSolrServer( failingSolrServer, new DefaultRetryPolicyFactory(new FlexibleBoundedExponentialBackoffRetry( TimeUnit.MILLISECONDS.toNanos(1), TimeUnit.MILLISECONDS.toNanos(1000), 20, TimeUnit.MINUTES.toNanos(5))), getMetricsFacade() ); Assert.assertNotNull(solr.query(query)); Assert.assertEquals(FailingSolrServer.SUCCESS, failingSolrServer.getNumRequests()); Assert.assertEquals(FailingSolrServer.SUCCESS - 1, failingSolrServer.getNumInjectedFailures()); solr.query(query); Assert.assertEquals(FailingSolrServer.SUCCESS + 1, failingSolrServer.getNumRequests()); Assert.assertEquals(FailingSolrServer.SUCCESS - 1, failingSolrServer.getNumInjectedFailures()); failingSolrServer.reset(); Assert.assertNotNull(solr.query(query)); Assert.assertEquals(FailingSolrServer.SUCCESS, failingSolrServer.getNumRequests()); Assert.assertEquals(FailingSolrServer.SUCCESS - 1, failingSolrServer.getNumInjectedFailures()); solr.query(query); Assert.assertEquals(FailingSolrServer.SUCCESS + 1, failingSolrServer.getNumRequests()); Assert.assertEquals(FailingSolrServer.SUCCESS - 1, failingSolrServer.getNumInjectedFailures()); // verify that after shutdown() is called, requests fail immediately without retries failingSolrServer.reset(); solr.close(); try { solr.query(query); fail(); } catch (RetriesExhaustedException e) { assertTrue(e.getCause() instanceof FailingSolrServer.InjectedSolrServerException); Assert.assertEquals(1, failingSolrServer.getNumRequests()); Assert.assertEquals(1, failingSolrServer.getNumInjectedFailures()); } } @Test public void testRetryOfBadRequest() throws Exception { SolrInputDocument doc = new SolrInputDocument(); // bad doc is missing 'id' field // without RetryingSolrServer try { solrServer.add(doc); fail(); } catch (SolrException e) { assertEquals(ErrorCode.BAD_REQUEST.code, e.code()); } // RetryingSolrServer, retry twice CountingSolrServer countingSolrServer = new CountingSolrServer(solrServer); SolrClient solr = new RetryingSolrServer(countingSolrServer, getRetryTwicePolicyFactory(), getMetricsFacade()); try { solr.add(doc); fail(); } catch (RetriesExhaustedException e) { Assert.assertEquals(3, countingSolrServer.getNumRequests()); Assert.assertTrue(e.getCause() instanceof SolrException); SolrException sex = (SolrException) e.getCause(); Assert.assertEquals(ErrorCode.BAD_REQUEST.code, sex.code()); } } @Test public void testGetUnderlyingSolrServer() throws Exception { RetryingSolrServer retryingSolr = new RetryingSolrServer(solrServer, getNoRetryPolicyFactory(), getMetricsFacade()); assertSame(solrServer, retryingSolr.getUnderlyingSolrServer()); } @Test public void testGetBinder() throws Exception { RetryingSolrServer retryingSolr = new RetryingSolrServer(solrServer, getNoRetryPolicyFactory(), getMetricsFacade()); assertSame(solrServer.getBinder(), retryingSolr.getBinder()); } @Test public void testNormalizeRequestKey() throws Exception { assertEquals("&NOW=<redacted>", normalizeRequestKey("&NOW=1448689151133")); } @Test public void testGetExceptionKey() throws Exception { // matching: assertEquals(" UnknownHostException: <redacted>.foo.bar.com ", getExceptionKey(" UnknownHostException: vb0706.foo.bar.com ")); assertEquals(" collection: collection1 slice: shard<redacted> ", getExceptionKey(" collection: collection1 slice: shard130 ")); assertEquals(" _SHARD<redacted>_REPLICA<redacted> ", getExceptionKey(" _SHARD110_REPLICA3 ")); assertEquals("Xhttps://<redacted>.foo.bar.com:8983y", getExceptionKey("Xhttps://vb0706.foo.bar.com:8983y")); assertEquals("Xhttp://<redacted>.foo.bar.com", getExceptionKey("Xhttp://vb0706.foo.bar.com")); // no port assertEquals("Xhttp://<redacted>.foo.bar.com/", getExceptionKey("Xhttp://vb0706.foo.bar.com/")); // no port assertEquals("Xhttp://<redacted>.foo.bar.com]", getExceptionKey("Xhttp://vb0706.foo.bar.com]")); // no port assertEquals("Xhttp%3A%2F%2F<redacted>.foo.bar.com]", getExceptionKey("Xhttp%3A%2F%2Fvb0706.foo.bar.com]")); // escaped :// assertEquals("ftp://<redacted>.foo.bar:8983/some/path", getExceptionKey("ftp://vb0706.foo.bar:8983/some/path")); // with path assertEquals("Ftp+-.://<redacted>.foo.bar:8983/some/path", getExceptionKey("Ftp+-.://vb0706.foo.bar:8983/some/path")); // scheme with plus, minus and dot assertEquals("insertAndQueryTweets_shard<redacted>_replica<redacted>", getExceptionKey("insertAndQueryTweets_shard30_replica3")); assertEquals("foo bar", getExceptionKey("foo bar")); // whitespace assertEquals("foo bar", getExceptionKey("foo\nbar")); // whitespace assertEquals("foo bar", getExceptionKey("foo\n\tbar")); // whitespace assertEquals("IOException msg", getExceptionKey("java.io.IOException msg")); // omit package name of well known Exception classes // not matching: String str; assertEquals(str = "Xhttps:/vb0706.foo.bar.com:8983y", getExceptionKey(str)); // missing :// assertEquals(str = "://vb0706.foo.bar.com:8983y", getExceptionKey(str)); // missing scheme assertEquals(str = "foo", getExceptionKey(str)); } private String getExceptionKey(final String str) { RetryingSolrServer solr = new RetryingSolrServer(solrServer, getNoRetryPolicyFactory(), getMetricsFacade()); return solr.getExceptionKey(new Throwable() { @Override public String toString() { return str; } }); } private String normalizeRequestKey(String str) { RetryingSolrServer solr = new RetryingSolrServer(solrServer, getNoRetryPolicyFactory(), getMetricsFacade()); return solr.normalizeRequestKey(str); } private RetryPolicyFactory getRetryTwicePolicyFactory() { return new RetryPolicyFactory() { @Override public RetryPolicy getRetryPolicy(Throwable exception, SolrRequest request, SolrClient server, RetryPolicy currentPolicy) { if (currentPolicy == null) { return new FlexibleBoundedExponentialBackoffRetry(0, 0, 2, Long.MAX_VALUE); } else { return currentPolicy; } } }; } private RetryPolicyFactory getNoRetryPolicyFactory() { return new RetryPolicyFactory() { @Override public RetryPolicy getRetryPolicy(Throwable exception, SolrRequest request, SolrClient server, RetryPolicy currentPolicy) { return null; } }; } private SolrQuery getDefaultQuery() { return new SolrQuery("*:*").addFilterQuery("id:choreRunner_12_56"); } private MetricsFacade getMetricsFacade() { // return new CodahaleMetricsFacade(new MetricRegistry(), new TDigestMetricBuilders()); return new MetricsFacade() { @Override public void updateTimer(String name, long duration, TimeUnit unit) { LOG.info("updateTimer name:{}, duration:{}, unit:{}", new Object[]{name, duration, unit}); } @Override public void updateHistogram(String name, long value) { LOG.info("updateHistogram name:{}, value:{}", name, value); } @Override public void markMeter(String name, long increment) { LOG.info("markMeter name:{}, increment:{}", name, increment); } }; } /////////////////////////////////////////////////////////////////////////////// // Nested classes: /////////////////////////////////////////////////////////////////////////////// /** Helper that counts the number of solrj requests */ private static final class CountingSolrServer extends SolrClient { private final SolrClient solrServer; private long numRequests = 0; public CountingSolrServer(SolrClient solrServer) { this.solrServer = solrServer; } public long getNumRequests() { return numRequests; } @Override public NamedList<Object> request(final SolrRequest request, String collection) throws SolrServerException, IOException { numRequests++; return solrServer.request(request, collection); } @Override public DocumentObjectBinder getBinder() { return solrServer.getBinder(); } @Override public void close() { // NOP necessary for testing with EmbeddedSolrServer // solrServer.shutdown(); } } /////////////////////////////////////////////////////////////////////////////// // Nested classes: /////////////////////////////////////////////////////////////////////////////// /** * Test helper that simulates an erratic SolrServer. Injects exceptions on the first N solrj * requests and succeeds on requests thereafter. */ private static final class FailingSolrServer extends SolrClient { private final SolrClient solrServer; private long numRequests = 0; private long numInjectedFailures = 0; public static final int SUCCESS = 5; public FailingSolrServer(SolrClient solrServer) { this.solrServer = solrServer; } public long getNumRequests() { return numRequests; } public long getNumInjectedFailures() { return numInjectedFailures; } public void reset() { numRequests = 0; numInjectedFailures = 0; } @Override public NamedList<Object> request(final SolrRequest request, String collection) throws SolrServerException, IOException { if (++numRequests < SUCCESS) { numInjectedFailures++; if (numRequests % 2 == 0) { throw new InjectedSolrServerException("Injected failure"); } try { throw new InjectedSolrServerException("Nested injected failure"); } catch (InjectedSolrServerException e) { throw new InjectedSolrServerException("Injected failure", e); } } return solrServer.request(request, collection); } @Override public DocumentObjectBinder getBinder() { return solrServer.getBinder(); } @Override public void close() { // NOP necessary for testing with EmbeddedSolrServer // solrServer.shutdown(); } /////////////////////////////////////////////////////////////////////////////// // Nested classes: /////////////////////////////////////////////////////////////////////////////// public static final class InjectedSolrServerException extends SolrServerException { public InjectedSolrServerException(String message) { super(message); } public InjectedSolrServerException(String message, Throwable cause) { super(message, cause); } } } }