blob: 203f050efc5633fc4c5bb4d105506477deb569de [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.client.solrj.impl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.solr.SolrTestCase;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.request.QueryRequest;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.MapSolrParams;
import org.apache.solr.common.util.NamedList;
import org.junit.Test;
/** Test the LBHttp2SolrClient. */
public class LBHttp2SolrClientTest extends SolrTestCase {
/**
* Test method for {@link LBHttp2SolrClient.Builder} that validates that the query param keys
* passed in by the base <code>Http2SolrClient
* </code> instance are used by the LBHttp2SolrClient.
*/
@Test
public void testLBHttp2SolrClientWithTheseParamNamesInTheUrl() {
String url = "http://127.0.0.1:8080";
Set<String> urlParamNames = new HashSet<>(2);
urlParamNames.add("param1");
try (Http2SolrClient http2SolrClient =
new Http2SolrClient.Builder(url).withTheseParamNamesInTheUrl(urlParamNames).build();
LBHttp2SolrClient testClient =
new LBHttp2SolrClient.Builder(http2SolrClient, new LBSolrClient.Endpoint(url))
.build()) {
assertArrayEquals(
"Wrong urlParamNames found in lb client.",
urlParamNames.toArray(),
testClient.getUrlParamNames().toArray());
assertArrayEquals(
"Wrong urlParamNames found in base client.",
urlParamNames.toArray(),
http2SolrClient.getUrlParamNames().toArray());
}
}
@Test
public void testAsyncWithFailures() {
// This demonstrates that the failing endpoint always gets retried, and it is up to the user
// to remove any failing nodes if desired.
LBSolrClient.Endpoint ep1 = new LBSolrClient.Endpoint("http://endpoint.one");
LBSolrClient.Endpoint ep2 = new LBSolrClient.Endpoint("http://endpoint.two");
List<LBSolrClient.Endpoint> endpointList = List.of(ep1, ep2);
Http2SolrClient.Builder b =
new Http2SolrClient.Builder("http://base.url").withConnectionTimeout(10, TimeUnit.SECONDS);
;
try (MockHttp2SolrClient client = new MockHttp2SolrClient("http://base.url", b);
LBHttp2SolrClient testClient = new LBHttp2SolrClient.Builder(client, ep1, ep2).build()) {
for (int j = 0; j < 2; j++) {
// j: first time Endpoint One will retrun error code 500.
// second time Endpoint One will be healthy
String basePathToSucceed;
if (j == 0) {
client.basePathToFail = ep1.getBaseUrl();
basePathToSucceed = ep2.getBaseUrl();
} else {
client.basePathToFail = ep2.getBaseUrl();
basePathToSucceed = ep1.getBaseUrl();
}
for (int i = 0; i < 10; i++) {
// i: we'll try 10 times to see if it behaves the same every time.
QueryRequest queryRequest = new QueryRequest(new MapSolrParams(Map.of("q", "" + i)));
LBSolrClient.Req req = new LBSolrClient.Req(queryRequest, endpointList);
String iterMessage = "iter j/i " + j + "/" + i;
try {
testClient.requestAsync(req).get(1, TimeUnit.MINUTES);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
fail("interrupted");
} catch (TimeoutException | ExecutionException e) {
fail(iterMessage + " Response ended in failure: " + e);
}
if (j == 0) {
// The first endpoint gives an exception, so it retries.
assertEquals(iterMessage, 2, client.lastBasePaths.size());
String failedBasePath = client.lastBasePaths.remove(0);
assertEquals(iterMessage, client.basePathToFail, failedBasePath);
} else {
// The first endpoint does not give the exception, it doesn't retry.
assertEquals(iterMessage, 1, client.lastBasePaths.size());
}
String successBasePath = client.lastBasePaths.remove(0);
assertEquals(iterMessage, basePathToSucceed, successBasePath);
}
}
}
}
@Test
public void testAsync() {
LBSolrClient.Endpoint ep1 = new LBSolrClient.Endpoint("http://endpoint.one");
LBSolrClient.Endpoint ep2 = new LBSolrClient.Endpoint("http://endpoint.two");
List<LBSolrClient.Endpoint> endpointList = List.of(ep1, ep2);
Http2SolrClient.Builder b =
new Http2SolrClient.Builder("http://base.url").withConnectionTimeout(10, TimeUnit.SECONDS);
try (MockHttp2SolrClient client = new MockHttp2SolrClient("http://base.url", b);
LBHttp2SolrClient testClient = new LBHttp2SolrClient.Builder(client, ep1, ep2).build()) {
int limit = 10; // For simplicity use an even limit
List<CompletableFuture<LBSolrClient.Rsp>> responses = new ArrayList<>();
for (int i = 0; i < limit; i++) {
QueryRequest queryRequest = new QueryRequest(new MapSolrParams(Map.of("q", "" + i)));
LBSolrClient.Req req = new LBSolrClient.Req(queryRequest, endpointList);
responses.add(testClient.requestAsync(req));
}
QueryRequest[] queryRequests = new QueryRequest[limit];
int numEndpointOne = 0;
int numEndpointTwo = 0;
for (int i = 0; i < limit; i++) {
SolrRequest<?> lastSolrReq = client.lastSolrRequests.get(i);
assertTrue(lastSolrReq instanceof QueryRequest);
QueryRequest lastQueryReq = (QueryRequest) lastSolrReq;
int index = Integer.parseInt(lastQueryReq.getParams().get("q"));
assertNull("Found same request twice: " + index, queryRequests[index]);
queryRequests[index] = lastQueryReq;
if (lastQueryReq.getBasePath().equals(ep1.toString())) {
numEndpointOne++;
} else if (lastQueryReq.getBasePath().equals(ep2.toString())) {
numEndpointTwo++;
}
LBSolrClient.Rsp lastRsp = null;
try {
lastRsp = responses.get(index).get();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
fail("interrupted");
} catch (ExecutionException ee) {
fail("Response " + index + " ended in failure: " + ee);
}
NamedList<Object> lastResponse = lastRsp.getResponse();
// The Mock will return {"response": index}.
assertEquals("" + index, lastResponse.get("response"));
}
// It is the user's responsibility to shuffle the endpoints when using
// async. LB Http Solr Client always will try the passed-in endpoints
// in order. In this case, endpoint 1 gets all the requests!
assertEquals(limit, numEndpointOne);
assertEquals(0, numEndpointTwo);
assertEquals(limit, client.lastSolrRequests.size());
assertEquals(limit, client.lastCollections.size());
}
}
public static class MockHttp2SolrClient extends Http2SolrClient {
public List<SolrRequest<?>> lastSolrRequests = new ArrayList<>();
public List<String> lastBasePaths = new ArrayList<>();
public List<String> lastCollections = new ArrayList<>();
public String basePathToFail = null;
protected MockHttp2SolrClient(String serverBaseUrl, Builder builder) {
// TODO: Consider creating an interface for Http*SolrClient
// so mocks can Implement, not Extend, and not actually need to
// build an (unused) client
super(serverBaseUrl, builder);
}
@Override
public CompletableFuture<NamedList<Object>> requestAsync(
final SolrRequest<?> solrRequest, String collection) {
CompletableFuture<NamedList<Object>> cf = new CompletableFuture<>();
lastSolrRequests.add(solrRequest);
lastBasePaths.add(solrRequest.getBasePath());
lastCollections.add(collection);
if (solrRequest.getBasePath().equals(basePathToFail)) {
cf.completeExceptionally(
new SolrException(SolrException.ErrorCode.SERVER_ERROR, "We should retry this."));
} else {
cf.complete(generateResponse(solrRequest));
}
return cf;
}
private NamedList<Object> generateResponse(SolrRequest<?> solrRequest) {
String id = solrRequest.getParams().get("q");
return new NamedList<>(Collections.singletonMap("response", id));
}
}
}