diff --git a/client-sniffer/build.gradle b/client-sniffer/build.gradle index 40551556731..6542453fa4f 100644 --- a/client-sniffer/build.gradle +++ b/client-sniffer/build.gradle @@ -39,6 +39,7 @@ dependencies { testCompile "org.apache.lucene:lucene-test-framework:${versions.lucene}" testCompile "org.apache.lucene:lucene-core:${versions.lucene}" testCompile "org.apache.lucene:lucene-codecs:${versions.lucene}" + testCompile "org.elasticsearch:securemock:1.2" } //TODO compiling from 1.8 with target 1.7 and source 1.7 is best effort, not enough to ensure we are java 7 compatible diff --git a/client/build.gradle b/client/build.gradle index 43979e84597..6d2b92f0b0c 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -37,6 +37,7 @@ dependencies { testCompile "org.apache.lucene:lucene-test-framework:${versions.lucene}" testCompile "org.apache.lucene:lucene-core:${versions.lucene}" testCompile "org.apache.lucene:lucene-codecs:${versions.lucene}" + testCompile "org.elasticsearch:securemock:1.2" } //TODO compiling from 1.8 with target 1.7 and source 1.7 is best effort, not enough to ensure we are java 7 compatible diff --git a/client/src/test/java/org/elasticsearch/client/CloseableBasicHttpResponse.java b/client/src/test/java/org/elasticsearch/client/CloseableBasicHttpResponse.java new file mode 100644 index 00000000000..904cbe7cfeb --- /dev/null +++ b/client/src/test/java/org/elasticsearch/client/CloseableBasicHttpResponse.java @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.client; + +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.message.BasicHttpResponse; + +import java.io.IOException; + +/** + * Simple {@link CloseableHttpResponse} impl needed to easily create http responses that are closeable given that + * org.apache.http.impl.execchain.HttpResponseProxy is not public. + */ +class CloseableBasicHttpResponse extends BasicHttpResponse implements CloseableHttpResponse { + + public CloseableBasicHttpResponse(StatusLine statusline) { + super(statusline); + } + + @Override + public void close() throws IOException { + //nothing to close + } +} diff --git a/client/src/test/java/org/elasticsearch/client/RestClientMultipleHostsTests.java b/client/src/test/java/org/elasticsearch/client/RestClientMultipleHostsTests.java new file mode 100644 index 00000000000..d78a03509d0 --- /dev/null +++ b/client/src/test/java/org/elasticsearch/client/RestClientMultipleHostsTests.java @@ -0,0 +1,279 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.client; + +import com.carrotsearch.randomizedtesting.generators.RandomInts; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.ProtocolVersion; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicStatusLine; +import org.apache.lucene.util.LuceneTestCase; +import org.junit.Before; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import static org.elasticsearch.client.RestClientTestUtil.randomErrorNoRetryStatusCode; +import static org.elasticsearch.client.RestClientTestUtil.randomErrorRetryStatusCode; +import static org.elasticsearch.client.RestClientTestUtil.randomHttpMethod; +import static org.elasticsearch.client.RestClientTestUtil.randomOkStatusCode; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link RestClient} behaviour against multiple hosts: fail-over, blacklisting etc. + * Relies on a mock http client to intercept requests and return desired responses based on request path. + */ +public class RestClientMultipleHostsTests extends LuceneTestCase { + + private RestClient restClient; + private HttpHost[] httpHosts; + private TrackingFailureListener failureListener; + + @Before + public void createRestClient() throws IOException { + CloseableHttpClient httpClient = mock(CloseableHttpClient.class); + when(httpClient.execute(any(HttpHost.class), any(HttpRequest.class))).thenAnswer(new Answer() { + @Override + public CloseableHttpResponse answer(InvocationOnMock invocationOnMock) throws Throwable { + HttpHost httpHost = (HttpHost) invocationOnMock.getArguments()[0]; + HttpUriRequest request = (HttpUriRequest) invocationOnMock.getArguments()[1]; + //return the desired status code or exception depending on the path + if (request.getURI().getPath().equals("/soe")) { + throw new SocketTimeoutException(httpHost.toString()); + } else if (request.getURI().getPath().equals("/coe")) { + throw new ConnectTimeoutException(httpHost.toString()); + } else if (request.getURI().getPath().equals("/ioe")) { + throw new IOException(httpHost.toString()); + } + int statusCode = Integer.parseInt(request.getURI().getPath().substring(1)); + StatusLine statusLine = new BasicStatusLine(new ProtocolVersion("http", 1, 1), statusCode, ""); + return new CloseableBasicHttpResponse(statusLine); + } + }); + + int numHosts = RandomInts.randomIntBetween(random(), 2, 5); + httpHosts = new HttpHost[numHosts]; + for (int i = 0; i < numHosts; i++) { + httpHosts[i] = new HttpHost("localhost", 9200 + i); + } + restClient = RestClient.builder().setHosts(httpHosts).setHttpClient(httpClient).build(); + failureListener = new TrackingFailureListener(); + restClient.setFailureListener(failureListener); + } + + /** + * Test that + */ + public void testRoundRobinOkStatusCodes() throws Exception { + int numIters = RandomInts.randomIntBetween(random(), 1, 5); + for (int i = 0; i < numIters; i++) { + Set hostsSet = new HashSet<>(); + Collections.addAll(hostsSet, httpHosts); + for (int j = 0; j < httpHosts.length; j++) { + int statusCode = randomOkStatusCode(random()); + try (ElasticsearchResponse response = restClient.performRequest(randomHttpMethod(random()), "/" + statusCode, + Collections.emptyMap(), null)) { + assertThat(response.getStatusLine().getStatusCode(), equalTo(statusCode)); + assertTrue("host not found: " + response.getHost(), hostsSet.remove(response.getHost())); + } + } + assertEquals("every host should have been used but some weren't: " + hostsSet, 0, hostsSet.size()); + } + failureListener.assertNotCalled(); + } + + public void testRoundRobinNoRetryErrors() throws Exception { + int numIters = RandomInts.randomIntBetween(random(), 1, 5); + for (int i = 0; i < numIters; i++) { + Set hostsSet = new HashSet<>(); + Collections.addAll(hostsSet, httpHosts); + for (int j = 0; j < httpHosts.length; j++) { + String method = randomHttpMethod(random()); + int statusCode = randomErrorNoRetryStatusCode(random()); + try (ElasticsearchResponse response = restClient.performRequest(method, "/" + statusCode, + Collections.emptyMap(), null)) { + if (method.equals("HEAD") && statusCode == 404) { + //no exception gets thrown although we got a 404 + assertThat(response.getStatusLine().getStatusCode(), equalTo(404)); + assertThat(response.getStatusLine().getStatusCode(), equalTo(statusCode)); + assertTrue("host not found: " + response.getHost(), hostsSet.remove(response.getHost())); + } else { + fail("request should have failed"); + } + } catch(ElasticsearchResponseException e) { + if (method.equals("HEAD") && statusCode == 404) { + throw e; + } + ElasticsearchResponse response = e.getElasticsearchResponse(); + assertThat(response.getStatusLine().getStatusCode(), equalTo(statusCode)); + assertTrue("host not found: " + response.getHost(), hostsSet.remove(response.getHost())); + assertEquals(0, e.getSuppressed().length); + } + } + assertEquals("every host should have been used but some weren't: " + hostsSet, 0, hostsSet.size()); + } + failureListener.assertNotCalled(); + } + + public void testRoundRobinRetryErrors() throws Exception { + String retryEndpoint = randomErrorRetryEndpoint(); + try { + restClient.performRequest(randomHttpMethod(random()), retryEndpoint, Collections.emptyMap(), null); + fail("request should have failed"); + } catch(ElasticsearchResponseException e) { + Set hostsSet = new HashSet<>(); + Collections.addAll(hostsSet, httpHosts); + //first request causes all the hosts to be blacklisted, the returned exception holds one suppressed exception each + failureListener.assertCalled(httpHosts); + do { + ElasticsearchResponse response = e.getElasticsearchResponse(); + assertThat(response.getStatusLine().getStatusCode(), equalTo(Integer.parseInt(retryEndpoint.substring(1)))); + assertTrue("host [" + response.getHost() + "] not found, most likely used multiple times", + hostsSet.remove(response.getHost())); + if (e.getSuppressed().length > 0) { + assertEquals(1, e.getSuppressed().length); + Throwable suppressed = e.getSuppressed()[0]; + assertThat(suppressed, instanceOf(ElasticsearchResponseException.class)); + e = (ElasticsearchResponseException)suppressed; + } else { + e = null; + } + } while(e != null); + assertEquals("every host should have been used but some weren't: " + hostsSet, 0, hostsSet.size()); + } catch(IOException e) { + Set hostsSet = new HashSet<>(); + Collections.addAll(hostsSet, httpHosts); + //first request causes all the hosts to be blacklisted, the returned exception holds one suppressed exception each + failureListener.assertCalled(httpHosts); + do { + HttpHost httpHost = HttpHost.create(e.getMessage()); + assertTrue("host [" + httpHost + "] not found, most likely used multiple times", hostsSet.remove(httpHost)); + if (e.getSuppressed().length > 0) { + assertEquals(1, e.getSuppressed().length); + Throwable suppressed = e.getSuppressed()[0]; + assertThat(suppressed, instanceOf(IOException.class)); + e = (IOException) suppressed; + } else { + e = null; + } + } while(e != null); + assertEquals("every host should have been used but some weren't: " + hostsSet, 0, hostsSet.size()); + } + + int numIters = RandomInts.randomIntBetween(random(), 2, 5); + for (int i = 1; i <= numIters; i++) { + //check that one different host is resurrected at each new attempt + Set hostsSet = new HashSet<>(); + Collections.addAll(hostsSet, httpHosts); + for (int j = 0; j < httpHosts.length; j++) { + retryEndpoint = randomErrorRetryEndpoint(); + try { + restClient.performRequest(randomHttpMethod(random()), retryEndpoint, Collections.emptyMap(), null); + fail("request should have failed"); + } catch(ElasticsearchResponseException e) { + ElasticsearchResponse response = e.getElasticsearchResponse(); + assertThat(response.getStatusLine().getStatusCode(), equalTo(Integer.parseInt(retryEndpoint.substring(1)))); + assertTrue("host [" + response.getHost() + "] not found, most likely used multiple times", + hostsSet.remove(response.getHost())); + //after the first request, all hosts are blacklisted, a single one gets resurrected each time + failureListener.assertCalled(response.getHost()); + assertEquals(0, e.getSuppressed().length); + } catch(IOException e) { + HttpHost httpHost = HttpHost.create(e.getMessage()); + assertTrue("host [" + httpHost + "] not found, most likely used multiple times", hostsSet.remove(httpHost)); + //after the first request, all hosts are blacklisted, a single one gets resurrected each time + failureListener.assertCalled(httpHost); + assertEquals(0, e.getSuppressed().length); + } + } + assertEquals("every host should have been used but some weren't: " + hostsSet, 0, hostsSet.size()); + if (random().nextBoolean()) { + //mark one host back alive through a successful request and check that all requests after that are sent to it + HttpHost selectedHost = null; + int iters = RandomInts.randomIntBetween(random(), 2, 10); + for (int y = 0; y < iters; y++) { + int statusCode = randomErrorNoRetryStatusCode(random()); + ElasticsearchResponse response; + try (ElasticsearchResponse esResponse = restClient.performRequest(randomHttpMethod(random()), "/" + statusCode, + Collections.emptyMap(), null)) { + response = esResponse; + } + catch(ElasticsearchResponseException e) { + response = e.getElasticsearchResponse(); + } + assertThat(response.getStatusLine().getStatusCode(), equalTo(statusCode)); + if (selectedHost == null) { + selectedHost = response.getHost(); + } else { + assertThat(response.getHost(), equalTo(selectedHost)); + } + } + failureListener.assertNotCalled(); + //let the selected host catch up on number of failures, it gets selected a consecutive number of times as it's the one + //selected to be retried earlier (due to lower number of failures) till all the hosts have the same number of failures + for (int y = 0; y < i + 1; y++) { + retryEndpoint = randomErrorRetryEndpoint(); + try { + restClient.performRequest(randomHttpMethod(random()), retryEndpoint, + Collections.emptyMap(), null); + fail("request should have failed"); + } catch(ElasticsearchResponseException e) { + ElasticsearchResponse response = e.getElasticsearchResponse(); + assertThat(response.getStatusLine().getStatusCode(), equalTo(Integer.parseInt(retryEndpoint.substring(1)))); + assertThat(response.getHost(), equalTo(selectedHost)); + failureListener.assertCalled(selectedHost); + } catch(IOException e) { + HttpHost httpHost = HttpHost.create(e.getMessage()); + assertThat(httpHost, equalTo(selectedHost)); + failureListener.assertCalled(selectedHost); + } + } + } + } + } + + private static String randomErrorRetryEndpoint() { + switch(RandomInts.randomIntBetween(random(), 0, 3)) { + case 0: + return "/" + randomErrorRetryStatusCode(random()); + case 1: + return "/coe"; + case 2: + return "/soe"; + case 3: + return "/ioe"; + } + throw new UnsupportedOperationException(); + } +} diff --git a/client/src/test/java/org/elasticsearch/client/RestClientSingleHostTests.java b/client/src/test/java/org/elasticsearch/client/RestClientSingleHostTests.java new file mode 100644 index 00000000000..c3c08bee628 --- /dev/null +++ b/client/src/test/java/org/elasticsearch/client/RestClientSingleHostTests.java @@ -0,0 +1,421 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.client; + +import com.carrotsearch.randomizedtesting.generators.RandomInts; +import com.carrotsearch.randomizedtesting.generators.RandomStrings; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.ProtocolVersion; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpOptions; +import org.apache.http.client.methods.HttpPatch; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpTrace; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicStatusLine; +import org.apache.http.util.EntityUtils; +import org.apache.lucene.util.LuceneTestCase; +import org.junit.Before; +import org.mockito.ArgumentCaptor; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.elasticsearch.client.RestClientTestUtil.getAllErrorStatusCodes; +import static org.elasticsearch.client.RestClientTestUtil.getHttpMethods; +import static org.elasticsearch.client.RestClientTestUtil.getOkStatusCodes; +import static org.elasticsearch.client.RestClientTestUtil.randomHttpMethod; +import static org.elasticsearch.client.RestClientTestUtil.randomStatusCode; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests for basic functionality of {@link RestClient} against one single host: tests http requests being sent, headers, + * body, different status codes and corresponding responses/exceptions. + * Relies on a mock http client to intercept requests and return desired responses based on request path. + */ +public class RestClientSingleHostTests extends LuceneTestCase { + + private RestClient restClient; + private Header[] defaultHeaders; + private HttpHost httpHost; + private CloseableHttpClient httpClient; + private TrackingFailureListener failureListener; + + @Before + public void createRestClient() throws IOException { + httpClient = mock(CloseableHttpClient.class); + when(httpClient.execute(any(HttpHost.class), any(HttpRequest.class))).thenAnswer(new Answer() { + @Override + public CloseableHttpResponse answer(InvocationOnMock invocationOnMock) throws Throwable { + HttpUriRequest request = (HttpUriRequest) invocationOnMock.getArguments()[1]; + //return the desired status code or exception depending on the path + if (request.getURI().getPath().equals("/soe")) { + throw new SocketTimeoutException(); + } else if (request.getURI().getPath().equals("/coe")) { + throw new ConnectTimeoutException(); + } + int statusCode = Integer.parseInt(request.getURI().getPath().substring(1)); + StatusLine statusLine = new BasicStatusLine(new ProtocolVersion("http", 1, 1), statusCode, ""); + + CloseableHttpResponse httpResponse = new CloseableBasicHttpResponse(statusLine); + //return the same body that was sent + if (request instanceof HttpEntityEnclosingRequest) { + HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity(); + if (entity != null) { + assertTrue("the entity is not repeatable, cannot set it to the response directly", entity.isRepeatable()); + httpResponse.setEntity(entity); + } + } + //return the same headers that were sent + httpResponse.setHeaders(request.getAllHeaders()); + return httpResponse; + } + }); + int numHeaders = RandomInts.randomIntBetween(random(), 0, 3); + defaultHeaders = new Header[numHeaders]; + for (int i = 0; i < numHeaders; i++) { + String headerName = "Header-default" + (random().nextBoolean() ? i : ""); + String headerValue = RandomStrings.randomAsciiOfLengthBetween(random(), 3, 10); + defaultHeaders[i] = new BasicHeader(headerName, headerValue); + } + httpHost = new HttpHost("localhost", 9200); + restClient = RestClient.builder().setHosts(httpHost).setHttpClient(httpClient).setDefaultHeaders(defaultHeaders).build(); + failureListener = new TrackingFailureListener(); + restClient.setFailureListener(failureListener); + } + + /** + * Verifies the content of the {@link HttpRequest} that's internally created and passed through to the http client + */ + public void testInternalHttpRequest() throws Exception { + ArgumentCaptor requestArgumentCaptor = ArgumentCaptor.forClass(HttpUriRequest.class); + int times = 0; + for (String httpMethod : getHttpMethods()) { + HttpUriRequest expectedRequest = performRandomRequest(httpMethod); + verify(httpClient, times(++times)).execute(any(HttpHost.class), requestArgumentCaptor.capture()); + HttpUriRequest actualRequest = requestArgumentCaptor.getValue(); + assertEquals(expectedRequest.getURI(), actualRequest.getURI()); + assertEquals(expectedRequest.getClass(), actualRequest.getClass()); + assertArrayEquals(expectedRequest.getAllHeaders(), actualRequest.getAllHeaders()); + if (expectedRequest instanceof HttpEntityEnclosingRequest) { + HttpEntity expectedEntity = ((HttpEntityEnclosingRequest) expectedRequest).getEntity(); + if (expectedEntity != null) { + HttpEntity actualEntity = ((HttpEntityEnclosingRequest) actualRequest).getEntity(); + assertEquals(EntityUtils.toString(expectedEntity), EntityUtils.toString(actualEntity)); + } + } + } + } + + public void testSetNodes() throws IOException { + try { + restClient.setHosts((HttpHost[]) null); + fail("setHosts should have failed"); + } catch (IllegalArgumentException e) { + assertEquals("hosts must not be null nor empty", e.getMessage()); + } + try { + restClient.setHosts(); + fail("setHosts should have failed"); + } catch (IllegalArgumentException e) { + assertEquals("hosts must not be null nor empty", e.getMessage()); + } + try { + restClient.setHosts((HttpHost) null); + fail("setHosts should have failed"); + } catch (NullPointerException e) { + assertEquals("host cannot be null", e.getMessage()); + } + try { + restClient.setHosts(new HttpHost("localhost", 9200), null, new HttpHost("localhost", 9201)); + fail("setHosts should have failed"); + } catch (NullPointerException e) { + assertEquals("host cannot be null", e.getMessage()); + } + } + + /** + * End to end test for ok status codes + */ + public void testOkStatusCodes() throws Exception { + for (String method : getHttpMethods()) { + for (int okStatusCode : getOkStatusCodes()) { + ElasticsearchResponse response = restClient.performRequest(method, "/" + okStatusCode, + Collections.emptyMap(), null); + assertThat(response.getStatusLine().getStatusCode(), equalTo(okStatusCode)); + } + } + failureListener.assertNotCalled(); + } + + /** + * End to end test for error status codes: they should cause an exception to be thrown, apart from 404 with HEAD requests + */ + public void testErrorStatusCodes() throws Exception { + for (String method : getHttpMethods()) { + //error status codes should cause an exception to be thrown + for (int errorStatusCode : getAllErrorStatusCodes()) { + try (ElasticsearchResponse response = restClient.performRequest(method, "/" + errorStatusCode, + Collections.emptyMap(), null)) { + if (method.equals("HEAD") && errorStatusCode == 404) { + //no exception gets thrown although we got a 404 + assertThat(response.getStatusLine().getStatusCode(), equalTo(errorStatusCode)); + } else { + fail("request should have failed"); + } + } catch(ElasticsearchResponseException e) { + if (method.equals("HEAD") && errorStatusCode == 404) { + throw e; + } + assertThat(e.getElasticsearchResponse().getStatusLine().getStatusCode(), equalTo(errorStatusCode)); + } + if (errorStatusCode <= 500) { + failureListener.assertNotCalled(); + } else { + failureListener.assertCalled(httpHost); + } + } + } + } + + public void testIOExceptions() throws IOException { + for (String method : getHttpMethods()) { + //IOExceptions should be let bubble up + try { + restClient.performRequest(method, "/coe", Collections.emptyMap(), null); + fail("request should have failed"); + } catch(IOException e) { + assertThat(e, instanceOf(ConnectTimeoutException.class)); + } + failureListener.assertCalled(httpHost); + try { + restClient.performRequest(method, "/soe", Collections.emptyMap(), null); + fail("request should have failed"); + } catch(IOException e) { + assertThat(e, instanceOf(SocketTimeoutException.class)); + } + failureListener.assertCalled(httpHost); + } + } + + /** + * End to end test for request and response body. Exercises the mock http client ability to send back + * whatever body it has received. + */ + public void testBody() throws Exception { + String body = "{ \"field\": \"value\" }"; + StringEntity entity = new StringEntity(body); + for (String method : Arrays.asList("DELETE", "GET", "PATCH", "POST", "PUT")) { + for (int okStatusCode : getOkStatusCodes()) { + try (ElasticsearchResponse response = restClient.performRequest(method, "/" + okStatusCode, + Collections.emptyMap(), entity)) { + assertThat(response.getStatusLine().getStatusCode(), equalTo(okStatusCode)); + assertThat(EntityUtils.toString(response.getEntity()), equalTo(body)); + } + } + for (int errorStatusCode : getAllErrorStatusCodes()) { + try { + restClient.performRequest(method, "/" + errorStatusCode, Collections.emptyMap(), entity); + fail("request should have failed"); + } catch(ElasticsearchResponseException e) { + ElasticsearchResponse response = e.getElasticsearchResponse(); + assertThat(response.getStatusLine().getStatusCode(), equalTo(errorStatusCode)); + assertThat(EntityUtils.toString(response.getEntity()), equalTo(body)); + } + } + } + for (String method : Arrays.asList("HEAD", "OPTIONS", "TRACE")) { + try { + restClient.performRequest(method, "/" + randomStatusCode(random()), + Collections.emptyMap(), entity); + fail("request should have failed"); + } catch(UnsupportedOperationException e) { + assertThat(e.getMessage(), equalTo(method + " with body is not supported")); + } + } + } + + public void testNullHeaders() throws Exception { + String method = randomHttpMethod(random()); + int statusCode = randomStatusCode(random()); + try { + restClient.performRequest(method, "/" + statusCode, Collections.emptyMap(), null, (Header[])null); + fail("request should have failed"); + } catch(NullPointerException e) { + assertEquals("request headers must not be null", e.getMessage()); + } + try { + restClient.performRequest(method, "/" + statusCode, Collections.emptyMap(), null, (Header)null); + fail("request should have failed"); + } catch(NullPointerException e) { + assertEquals("request header must not be null", e.getMessage()); + } + } + + public void testNullParams() throws Exception { + String method = randomHttpMethod(random()); + int statusCode = randomStatusCode(random()); + try { + restClient.performRequest(method, "/" + statusCode, null, null); + fail("request should have failed"); + } catch(NullPointerException e) { + assertEquals("params must not be null", e.getMessage()); + } + } + + /** + * End to end test for request and response headers. Exercises the mock http client ability to send back + * whatever headers it has received. + */ + public void testHeaders() throws Exception { + for (String method : getHttpMethods()) { + Map expectedHeaders = new HashMap<>(); + for (Header defaultHeader : defaultHeaders) { + expectedHeaders.put(defaultHeader.getName(), defaultHeader.getValue()); + } + int numHeaders = RandomInts.randomIntBetween(random(), 1, 5); + Header[] headers = new Header[numHeaders]; + for (int i = 0; i < numHeaders; i++) { + String headerName = "Header" + (random().nextBoolean() ? i : ""); + String headerValue = RandomStrings.randomAsciiOfLengthBetween(random(), 3, 10); + headers[i] = new BasicHeader(headerName, headerValue); + expectedHeaders.put(headerName, headerValue); + } + + int statusCode = randomStatusCode(random()); + ElasticsearchResponse esResponse; + try (ElasticsearchResponse response = restClient.performRequest(method, "/" + statusCode, + Collections.emptyMap(), null, headers)) { + esResponse = response; + } catch(ElasticsearchResponseException e) { + esResponse = e.getElasticsearchResponse(); + } + assertThat(esResponse.getStatusLine().getStatusCode(), equalTo(statusCode)); + for (Header responseHeader : esResponse.getHeaders()) { + String headerValue = expectedHeaders.remove(responseHeader.getName()); + assertNotNull("found response header [" + responseHeader.getName() + "] that wasn't originally sent", headerValue); + } + assertEquals("some headers that were sent weren't returned " + expectedHeaders, 0, expectedHeaders.size()); + } + } + + private HttpUriRequest performRandomRequest(String method) throws IOException, URISyntaxException { + String uriAsString = "/" + randomStatusCode(random()); + URIBuilder uriBuilder = new URIBuilder(uriAsString); + Map params = Collections.emptyMap(); + if (random().nextBoolean()) { + int numParams = RandomInts.randomIntBetween(random(), 1, 3); + params = new HashMap<>(numParams); + for (int i = 0; i < numParams; i++) { + String paramKey = "param-" + i; + String paramValue = RandomStrings.randomAsciiOfLengthBetween(random(), 3, 10); + params.put(paramKey, paramValue); + uriBuilder.addParameter(paramKey, paramValue); + } + } + URI uri = uriBuilder.build(); + + HttpUriRequest request; + switch(method) { + case "DELETE": + request = new HttpDeleteWithEntity(uri); + break; + case "GET": + request = new HttpGetWithEntity(uri); + break; + case "HEAD": + request = new HttpHead(uri); + break; + case "OPTIONS": + request = new HttpOptions(uri); + break; + case "PATCH": + request = new HttpPatch(uri); + break; + case "POST": + request = new HttpPost(uri); + break; + case "PUT": + request = new HttpPut(uri); + break; + case "TRACE": + request = new HttpTrace(uri); + break; + default: + throw new UnsupportedOperationException("method not supported: " + method); + } + + HttpEntity entity = null; + if (request instanceof HttpEntityEnclosingRequest && random().nextBoolean()) { + entity = new StringEntity(RandomStrings.randomAsciiOfLengthBetween(random(), 10, 100)); + ((HttpEntityEnclosingRequest) request).setEntity(entity); + } + + Header[] headers = new Header[0]; + for (Header defaultHeader : defaultHeaders) { + //default headers are expected but not sent for each request + request.setHeader(defaultHeader); + } + if (random().nextBoolean()) { + int numHeaders = RandomInts.randomIntBetween(random(), 1, 5); + headers = new Header[numHeaders]; + for (int i = 0; i < numHeaders; i++) { + String headerName = "Header" + (random().nextBoolean() ? i : ""); + String headerValue = RandomStrings.randomAsciiOfLengthBetween(random(), 3, 10); + BasicHeader basicHeader = new BasicHeader(headerName, headerValue); + headers[i] = basicHeader; + request.setHeader(basicHeader); + } + } + + try { + restClient.performRequest(method, uriAsString, params, entity, headers); + } catch(ElasticsearchResponseException e) { + //all good + } + return request; + } +} diff --git a/client/src/test/java/org/elasticsearch/client/RestClientTestUtil.java b/client/src/test/java/org/elasticsearch/client/RestClientTestUtil.java new file mode 100644 index 00000000000..4d4aa00f492 --- /dev/null +++ b/client/src/test/java/org/elasticsearch/client/RestClientTestUtil.java @@ -0,0 +1,84 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.client; + +import com.carrotsearch.randomizedtesting.generators.RandomPicks; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +final class RestClientTestUtil { + + private static final String[] HTTP_METHODS = new String[]{"DELETE", "HEAD", "GET", "OPTIONS", "PATCH", "POST", "PUT", "TRACE"}; + private static final List ALL_STATUS_CODES; + private static final List OK_STATUS_CODES = Arrays.asList(200, 201); + private static final List ALL_ERROR_STATUS_CODES; + private static List ERROR_NO_RETRY_STATUS_CODES = Arrays.asList(400, 401, 403, 404, 405, 500); + private static List ERROR_RETRY_STATUS_CODES = Arrays.asList(502, 503, 504); + + static { + ALL_ERROR_STATUS_CODES = new ArrayList<>(ERROR_RETRY_STATUS_CODES); + ALL_ERROR_STATUS_CODES.addAll(ERROR_NO_RETRY_STATUS_CODES); + ALL_STATUS_CODES = new ArrayList<>(ALL_ERROR_STATUS_CODES); + ALL_STATUS_CODES.addAll(OK_STATUS_CODES); + } + + private RestClientTestUtil() { + + } + + static String[] getHttpMethods() { + return HTTP_METHODS; + } + + static String randomHttpMethod(Random random) { + return RandomPicks.randomFrom(random, HTTP_METHODS); + } + + static int randomStatusCode(Random random) { + return RandomPicks.randomFrom(random, ALL_ERROR_STATUS_CODES); + } + + static int randomOkStatusCode(Random random) { + return RandomPicks.randomFrom(random, OK_STATUS_CODES); + } + + static int randomErrorNoRetryStatusCode(Random random) { + return RandomPicks.randomFrom(random, ERROR_NO_RETRY_STATUS_CODES); + } + + static int randomErrorRetryStatusCode(Random random) { + return RandomPicks.randomFrom(random, ERROR_RETRY_STATUS_CODES); + } + + static List getOkStatusCodes() { + return OK_STATUS_CODES; + } + + static List getAllErrorStatusCodes() { + return ALL_ERROR_STATUS_CODES; + } + + static List getAllStatusCodes() { + return ALL_STATUS_CODES; + } +} diff --git a/client/src/test/java/org/elasticsearch/client/TrackingFailureListener.java b/client/src/test/java/org/elasticsearch/client/TrackingFailureListener.java new file mode 100644 index 00000000000..35842823923 --- /dev/null +++ b/client/src/test/java/org/elasticsearch/client/TrackingFailureListener.java @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.client; + +import org.apache.http.HttpHost; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +/** + * {@link org.elasticsearch.client.RestClient.FailureListener} impl that allows to track when it gets called + */ +class TrackingFailureListener extends RestClient.FailureListener { + private Set hosts = new HashSet<>(); + + @Override + public void onFailure(HttpHost host) throws IOException { + hosts.add(host); + } + + void assertCalled(HttpHost... hosts) { + assertEquals(hosts.length, this.hosts.size()); + assertThat(this.hosts, containsInAnyOrder(hosts)); + this.hosts.clear(); + } + + void assertNotCalled() { + assertEquals(0, hosts.size()); + } +} \ No newline at end of file